mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-02 21:42:52 +00:00
Basic management of shares
This commit is contained in:
parent
de1baf725f
commit
c7d587bea4
15
build.sbt
15
build.sbt
@ -260,6 +260,18 @@ val openapiScalaSettings = Seq(
|
||||
.copy(typeDef =
|
||||
TypeDef("AccountSource", Imports("docspell.common.AccountSource"))
|
||||
)
|
||||
case "itemquery" =>
|
||||
field =>
|
||||
field
|
||||
.copy(typeDef =
|
||||
TypeDef(
|
||||
"ItemQuery",
|
||||
Imports(
|
||||
"docspell.query.ItemQuery",
|
||||
"docspell.restapi.codec.ItemQueryJson._"
|
||||
)
|
||||
)
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
@ -367,6 +379,7 @@ val store = project
|
||||
.settings(testSettingsMUnit)
|
||||
.settings(
|
||||
name := "docspell-store",
|
||||
addCompilerPlugin(Dependencies.kindProjectorPlugin),
|
||||
libraryDependencies ++=
|
||||
Dependencies.doobie ++
|
||||
Dependencies.binny ++
|
||||
@ -472,7 +485,7 @@ val restapi = project
|
||||
openapiSpec := (Compile / resourceDirectory).value / "docspell-openapi.yml",
|
||||
openapiStaticGen := OpenApiDocGenerator.Redoc
|
||||
)
|
||||
.dependsOn(common)
|
||||
.dependsOn(common, query.jvm)
|
||||
|
||||
val joexapi = project
|
||||
.in(file("modules/joexapi"))
|
||||
|
@ -48,6 +48,7 @@ trait BackendApp[F[_]] {
|
||||
def simpleSearch: OSimpleSearch[F]
|
||||
def clientSettings: OClientSettings[F]
|
||||
def totp: OTotp[F]
|
||||
def share: OShare[F]
|
||||
}
|
||||
|
||||
object BackendApp {
|
||||
@ -85,6 +86,7 @@ object BackendApp {
|
||||
customFieldsImpl <- OCustomFields(store)
|
||||
simpleSearchImpl = OSimpleSearch(fulltextImpl, itemSearchImpl)
|
||||
clientSettingsImpl <- OClientSettings(store)
|
||||
shareImpl <- Resource.pure(OShare(store))
|
||||
} yield new BackendApp[F] {
|
||||
val login = loginImpl
|
||||
val signup = signupImpl
|
||||
@ -107,6 +109,7 @@ object BackendApp {
|
||||
val simpleSearch = simpleSearchImpl
|
||||
val clientSettings = clientSettingsImpl
|
||||
val totp = totpImpl
|
||||
val share = shareImpl
|
||||
}
|
||||
|
||||
def apply[F[_]: Async](
|
||||
|
116
modules/backend/src/main/scala/docspell/backend/ops/OShare.scala
Normal file
116
modules/backend/src/main/scala/docspell/backend/ops/OShare.scala
Normal file
@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright 2020 Eike K. & Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package docspell.backend.ops
|
||||
|
||||
import cats.data.OptionT
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.backend.PasswordCrypt
|
||||
import docspell.common._
|
||||
import docspell.query.ItemQuery
|
||||
import docspell.store.Store
|
||||
import docspell.store.records.RShare
|
||||
|
||||
trait OShare[F[_]] {
|
||||
|
||||
def findAll(collective: Ident): F[List[RShare]]
|
||||
|
||||
def delete(id: Ident, collective: Ident): F[Boolean]
|
||||
|
||||
def addNew(share: OShare.NewShare): F[OShare.ChangeResult]
|
||||
|
||||
def findOne(id: Ident, collective: Ident): OptionT[F, RShare]
|
||||
|
||||
def update(
|
||||
id: Ident,
|
||||
share: OShare.NewShare,
|
||||
removePassword: Boolean
|
||||
): F[OShare.ChangeResult]
|
||||
}
|
||||
|
||||
object OShare {
|
||||
|
||||
final case class NewShare(
|
||||
cid: Ident,
|
||||
name: Option[String],
|
||||
query: ItemQuery,
|
||||
enabled: Boolean,
|
||||
password: Option[Password],
|
||||
publishUntil: Timestamp
|
||||
)
|
||||
|
||||
sealed trait ChangeResult
|
||||
object ChangeResult {
|
||||
final case class Success(id: Ident) extends ChangeResult
|
||||
case object PublishUntilInPast extends ChangeResult
|
||||
|
||||
def success(id: Ident): ChangeResult = Success(id)
|
||||
def publishUntilInPast: ChangeResult = PublishUntilInPast
|
||||
}
|
||||
|
||||
def apply[F[_]: Async](store: Store[F]): OShare[F] =
|
||||
new OShare[F] {
|
||||
def findAll(collective: Ident): F[List[RShare]] =
|
||||
store.transact(RShare.findAllByCollective(collective))
|
||||
|
||||
def delete(id: Ident, collective: Ident): F[Boolean] =
|
||||
store.transact(RShare.deleteByIdAndCid(id, collective)).map(_ > 0)
|
||||
|
||||
def addNew(share: NewShare): F[ChangeResult] =
|
||||
for {
|
||||
curTime <- Timestamp.current[F]
|
||||
id <- Ident.randomId[F]
|
||||
pass = share.password.map(PasswordCrypt.crypt)
|
||||
record = RShare(
|
||||
id,
|
||||
share.cid,
|
||||
share.name,
|
||||
share.query,
|
||||
share.enabled,
|
||||
pass,
|
||||
curTime,
|
||||
share.publishUntil,
|
||||
0,
|
||||
None
|
||||
)
|
||||
res <-
|
||||
if (share.publishUntil < curTime) ChangeResult.publishUntilInPast.pure[F]
|
||||
else store.transact(RShare.insert(record)).map(_ => ChangeResult.success(id))
|
||||
} yield res
|
||||
|
||||
def update(
|
||||
id: Ident,
|
||||
share: OShare.NewShare,
|
||||
removePassword: Boolean
|
||||
): F[ChangeResult] =
|
||||
for {
|
||||
curTime <- Timestamp.current[F]
|
||||
record = RShare(
|
||||
id,
|
||||
share.cid,
|
||||
share.name,
|
||||
share.query,
|
||||
share.enabled,
|
||||
share.password.map(PasswordCrypt.crypt),
|
||||
Timestamp.Epoch,
|
||||
share.publishUntil,
|
||||
0,
|
||||
None
|
||||
)
|
||||
res <-
|
||||
if (share.publishUntil < curTime) ChangeResult.publishUntilInPast.pure[F]
|
||||
else
|
||||
store
|
||||
.transact(RShare.updateData(record, removePassword))
|
||||
.map(_ => ChangeResult.success(id))
|
||||
} yield res
|
||||
|
||||
def findOne(id: Ident, collective: Ident): OptionT[F, RShare] =
|
||||
RShare.findOne(id, collective).mapK(store.transform)
|
||||
}
|
||||
}
|
@ -51,6 +51,9 @@ case class Timestamp(value: Instant) {
|
||||
|
||||
def <(other: Timestamp): Boolean =
|
||||
this.value.isBefore(other.value)
|
||||
|
||||
def >(other: Timestamp): Boolean =
|
||||
this.value.isAfter(other.value)
|
||||
}
|
||||
|
||||
object Timestamp {
|
||||
|
@ -1711,6 +1711,96 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/BasicResult"
|
||||
|
||||
/sec/share:
|
||||
get:
|
||||
operationId: "sec-share-get-all"
|
||||
tags: [ Share ]
|
||||
summary: Get a list of shares
|
||||
description: |
|
||||
Return a list of all shares for this collective.
|
||||
security:
|
||||
- authTokenHeader: []
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ShareList"
|
||||
post:
|
||||
operationId: "sec-share-new"
|
||||
tags: [ Share ]
|
||||
summary: Create a new share.
|
||||
description: |
|
||||
Create a new share.
|
||||
security:
|
||||
- authTokenHeader: []
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ShareData"
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/IdResult"
|
||||
/sec/share/{shareId}:
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/shareId"
|
||||
get:
|
||||
operationId: "sec-share-get"
|
||||
tags: [Share]
|
||||
summary: Get details to a single share.
|
||||
description: |
|
||||
Given the id of a share, returns some details about it.
|
||||
security:
|
||||
- authTokenHeader: []
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ShareDetail"
|
||||
put:
|
||||
operationId: "sec-share-update"
|
||||
tags: [ Share ]
|
||||
summary: Update an existing share.
|
||||
description: |
|
||||
Updates an existing share.
|
||||
security:
|
||||
- authTokenHeader: []
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ShareData"
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/BasicResult"
|
||||
delete:
|
||||
operationId: "sec-share-delete-by-id"
|
||||
tags: [ Share ]
|
||||
summary: Delete a share.
|
||||
description: |
|
||||
Deletes a share
|
||||
security:
|
||||
- authTokenHeader: []
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/BasicResult"
|
||||
|
||||
/sec/item/search:
|
||||
get:
|
||||
operationId: "sec-item-search-by-get"
|
||||
@ -4096,6 +4186,83 @@ paths:
|
||||
|
||||
components:
|
||||
schemas:
|
||||
ShareData:
|
||||
description: |
|
||||
Editable data for a share.
|
||||
required:
|
||||
- query
|
||||
- enabled
|
||||
- publishUntil
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
query:
|
||||
type: string
|
||||
format: itemquery
|
||||
enabled:
|
||||
type: boolean
|
||||
password:
|
||||
type: string
|
||||
format: password
|
||||
publishUntil:
|
||||
type: integer
|
||||
format: date-time
|
||||
removePassword:
|
||||
type: boolean
|
||||
description: |
|
||||
For an update request, this can control whether to delete
|
||||
the password. Otherwise if the password is not set, it
|
||||
will not be changed. When adding a new share, this has no
|
||||
effect.
|
||||
|
||||
ShareDetail:
|
||||
description: |
|
||||
Details for an existing share.
|
||||
required:
|
||||
- id
|
||||
- query
|
||||
- enabled
|
||||
- publishAt
|
||||
- publishUntil
|
||||
- password
|
||||
- views
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: ident
|
||||
query:
|
||||
type: string
|
||||
format: itemquery
|
||||
name:
|
||||
type: string
|
||||
enabled:
|
||||
type: boolean
|
||||
publishAt:
|
||||
type: integer
|
||||
format: date-time
|
||||
publishUntil:
|
||||
type: integer
|
||||
format: date-time
|
||||
password:
|
||||
type: boolean
|
||||
views:
|
||||
type: integer
|
||||
format: int32
|
||||
lastAccess:
|
||||
type: integer
|
||||
format: date-time
|
||||
|
||||
ShareList:
|
||||
description: |
|
||||
A list of shares.
|
||||
required:
|
||||
- items
|
||||
properties:
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/ShareDetail"
|
||||
|
||||
DeleteUserData:
|
||||
description: |
|
||||
An excerpt of data that would be deleted when deleting the
|
||||
@ -6121,8 +6288,8 @@ components:
|
||||
type: string
|
||||
IdResult:
|
||||
description: |
|
||||
Some basic result of an operation with an ID as payload. If
|
||||
success if `false` the id is not usable.
|
||||
Some basic result of an operation with an ID as payload, if
|
||||
success is true. If success is `false` the id is not usable.
|
||||
required:
|
||||
- success
|
||||
- message
|
||||
@ -6257,6 +6424,13 @@ components:
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
shareId:
|
||||
name: shareId
|
||||
in: path
|
||||
description: An identifier for a share
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
username:
|
||||
name: username
|
||||
in: path
|
||||
|
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2020 Eike K. & Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package docspell.restapi.codec
|
||||
|
||||
import docspell.query.{ItemQuery, ItemQueryParser}
|
||||
|
||||
import io.circe.{Decoder, Encoder}
|
||||
|
||||
trait ItemQueryJson {
|
||||
|
||||
implicit val itemQueryDecoder: Decoder[ItemQuery] =
|
||||
Decoder.decodeString.emap(str => ItemQueryParser.parse(str).left.map(_.render))
|
||||
|
||||
implicit val itemQueryEncoder: Encoder[ItemQuery] =
|
||||
Encoder.encodeString.contramap(q =>
|
||||
q.raw.getOrElse(ItemQueryParser.unsafeAsString(q.expr))
|
||||
)
|
||||
}
|
||||
|
||||
object ItemQueryJson extends ItemQueryJson
|
@ -94,6 +94,7 @@ object RestServer {
|
||||
"email/send" -> MailSendRoutes(restApp.backend, token),
|
||||
"email/settings" -> MailSettingsRoutes(restApp.backend, token),
|
||||
"email/sent" -> SentMailRoutes(restApp.backend, token),
|
||||
"share" -> ShareRoutes.manage(restApp.backend, token),
|
||||
"usertask/notifydueitems" -> NotifyDueItemsRoutes(cfg, restApp.backend, token),
|
||||
"usertask/scanmailbox" -> ScanMailboxRoutes(restApp.backend, token),
|
||||
"calevent/check" -> CalEventCheckRoutes(),
|
||||
|
@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2020 Eike K. & Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package docspell.restserver.routes
|
||||
|
||||
import cats.data.OptionT
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.backend.BackendApp
|
||||
import docspell.backend.auth.AuthToken
|
||||
import docspell.backend.ops.OShare
|
||||
import docspell.common.Ident
|
||||
import docspell.restapi.model._
|
||||
import docspell.restserver.http4s.ResponseGenerator
|
||||
import docspell.store.records.RShare
|
||||
|
||||
import org.http4s.HttpRoutes
|
||||
import org.http4s.circe.CirceEntityDecoder._
|
||||
import org.http4s.circe.CirceEntityEncoder._
|
||||
import org.http4s.dsl.Http4sDsl
|
||||
|
||||
object ShareRoutes {
|
||||
|
||||
def manage[F[_]: Async](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
|
||||
val dsl = new Http4sDsl[F] with ResponseGenerator[F] {}
|
||||
import dsl._
|
||||
|
||||
HttpRoutes.of {
|
||||
case GET -> Root =>
|
||||
for {
|
||||
all <- backend.share.findAll(user.account.collective)
|
||||
res <- Ok(ShareList(all.map(mkShareDetail)))
|
||||
} yield res
|
||||
|
||||
case req @ POST -> Root =>
|
||||
for {
|
||||
data <- req.as[ShareData]
|
||||
share = mkNewShare(data, user)
|
||||
res <- backend.share.addNew(share)
|
||||
resp <- Ok(mkIdResult(res, "New share created."))
|
||||
} yield resp
|
||||
|
||||
case GET -> Root / Ident(id) =>
|
||||
(for {
|
||||
share <- backend.share.findOne(id, user.account.collective)
|
||||
resp <- OptionT.liftF(Ok(mkShareDetail(share)))
|
||||
} yield resp).getOrElseF(NotFound())
|
||||
|
||||
case req @ PUT -> Root / Ident(id) =>
|
||||
for {
|
||||
data <- req.as[ShareData]
|
||||
share = mkNewShare(data, user)
|
||||
updated <- backend.share.update(id, share, data.removePassword.getOrElse(false))
|
||||
resp <- Ok(mkBasicResult(updated, "Share updated."))
|
||||
} yield resp
|
||||
|
||||
case DELETE -> Root / Ident(id) =>
|
||||
for {
|
||||
del <- backend.share.delete(id, user.account.collective)
|
||||
resp <- Ok(BasicResult(del, if (del) "Share deleted." else "Deleting failed."))
|
||||
} yield resp
|
||||
}
|
||||
}
|
||||
|
||||
def mkNewShare(data: ShareData, user: AuthToken): OShare.NewShare =
|
||||
OShare.NewShare(
|
||||
user.account.collective,
|
||||
data.name,
|
||||
data.query,
|
||||
data.enabled,
|
||||
data.password,
|
||||
data.publishUntil
|
||||
)
|
||||
|
||||
def mkIdResult(r: OShare.ChangeResult, msg: => String): IdResult =
|
||||
r match {
|
||||
case OShare.ChangeResult.Success(id) => IdResult(true, msg, id)
|
||||
case OShare.ChangeResult.PublishUntilInPast =>
|
||||
IdResult(false, "Until date must not be in the past", Ident.unsafe(""))
|
||||
}
|
||||
|
||||
def mkBasicResult(r: OShare.ChangeResult, msg: => String): BasicResult =
|
||||
r match {
|
||||
case OShare.ChangeResult.Success(_) => BasicResult(true, msg)
|
||||
case OShare.ChangeResult.PublishUntilInPast =>
|
||||
BasicResult(false, "Until date must not be in the past")
|
||||
}
|
||||
|
||||
def mkShareDetail(r: RShare): ShareDetail =
|
||||
ShareDetail(
|
||||
r.id,
|
||||
r.query,
|
||||
r.name,
|
||||
r.enabled,
|
||||
r.publishAt,
|
||||
r.publishUntil,
|
||||
r.password.isDefined,
|
||||
r.views,
|
||||
r.lastAccess
|
||||
)
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
CREATE TABLE "item_share" (
|
||||
"id" varchar(254) not null primary key,
|
||||
"cid" varchar(254) not null,
|
||||
"name" varchar(254),
|
||||
"query" varchar(2000) not null,
|
||||
"enabled" boolean not null,
|
||||
"pass" varchar(254),
|
||||
"publish_at" timestamp not null,
|
||||
"publish_until" timestamp not null,
|
||||
"views" int not null,
|
||||
"last_access" timestamp,
|
||||
foreign key ("cid") references "collective"("cid") on delete cascade
|
||||
)
|
@ -0,0 +1,13 @@
|
||||
CREATE TABLE `item_share` (
|
||||
`id` varchar(254) not null primary key,
|
||||
`cid` varchar(254) not null,
|
||||
`name` varchar(254),
|
||||
`query` varchar(2000) not null,
|
||||
`enabled` boolean not null,
|
||||
`pass` varchar(254),
|
||||
`publish_at` timestamp not null,
|
||||
`publish_until` timestamp not null,
|
||||
`views` int not null,
|
||||
`last_access` timestamp,
|
||||
foreign key (`cid`) references `collective`(`cid`) on delete cascade
|
||||
)
|
@ -0,0 +1,13 @@
|
||||
CREATE TABLE "item_share" (
|
||||
"id" varchar(254) not null primary key,
|
||||
"cid" varchar(254) not null,
|
||||
"name" varchar(254),
|
||||
"query" varchar(2000) not null,
|
||||
"enabled" boolean not null,
|
||||
"pass" varchar(254),
|
||||
"publish_at" timestamp not null,
|
||||
"publish_until" timestamp not null,
|
||||
"views" int not null,
|
||||
"last_access" timestamp,
|
||||
foreign key ("cid") references "collective"("cid") on delete cascade
|
||||
)
|
@ -9,6 +9,7 @@ package docspell.store
|
||||
import scala.concurrent.ExecutionContext
|
||||
|
||||
import cats.effect._
|
||||
import cats.~>
|
||||
import fs2._
|
||||
|
||||
import docspell.store.file.FileStore
|
||||
@ -19,6 +20,7 @@ import doobie._
|
||||
import doobie.hikari.HikariTransactor
|
||||
|
||||
trait Store[F[_]] {
|
||||
def transform: ConnectionIO ~> F
|
||||
|
||||
def transact[A](prg: ConnectionIO[A]): F[A]
|
||||
|
||||
|
@ -6,8 +6,10 @@
|
||||
|
||||
package docspell.store.impl
|
||||
|
||||
import cats.arrow.FunctionK
|
||||
import cats.effect.Async
|
||||
import cats.implicits._
|
||||
import cats.~>
|
||||
|
||||
import docspell.store.file.FileStore
|
||||
import docspell.store.migrate.FlywayMigrate
|
||||
@ -22,6 +24,9 @@ final class StoreImpl[F[_]: Async](
|
||||
xa: Transactor[F]
|
||||
) extends Store[F] {
|
||||
|
||||
def transform: ConnectionIO ~> F =
|
||||
FunctionK.lift(transact)
|
||||
|
||||
def migrate: F[Int] =
|
||||
FlywayMigrate.run[F](jdbc).map(_.migrationsExecuted)
|
||||
|
||||
|
@ -6,20 +6,25 @@
|
||||
|
||||
package docspell.store.records
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
import cats.data.{NonEmptyList, OptionT}
|
||||
|
||||
import docspell.common._
|
||||
import docspell.query.ItemQuery
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
|
||||
final case class RShare(
|
||||
id: Ident,
|
||||
cid: Ident,
|
||||
name: Option[String],
|
||||
query: ItemQuery,
|
||||
enabled: Boolean,
|
||||
password: Option[Password],
|
||||
publishedAt: Timestamp,
|
||||
publishedUntil: Timestamp,
|
||||
publishAt: Timestamp,
|
||||
publishUntil: Timestamp,
|
||||
views: Int,
|
||||
lastAccess: Option[Timestamp]
|
||||
) {}
|
||||
@ -31,11 +36,12 @@ object RShare {
|
||||
|
||||
val id = Column[Ident]("id", this)
|
||||
val cid = Column[Ident]("cid", this)
|
||||
val name = Column[String]("name", this)
|
||||
val query = Column[ItemQuery]("query", this)
|
||||
val enabled = Column[Boolean]("enabled", this)
|
||||
val password = Column[Password]("password", this)
|
||||
val publishedAt = Column[Timestamp]("published_at", this)
|
||||
val publishedUntil = Column[Timestamp]("published_until", this)
|
||||
val password = Column[Password]("pass", this)
|
||||
val publishedAt = Column[Timestamp]("publish_at", this)
|
||||
val publishedUntil = Column[Timestamp]("publish_until", this)
|
||||
val views = Column[Int]("views", this)
|
||||
val lastAccess = Column[Timestamp]("last_access", this)
|
||||
|
||||
@ -43,6 +49,7 @@ object RShare {
|
||||
NonEmptyList.of(
|
||||
id,
|
||||
cid,
|
||||
name,
|
||||
query,
|
||||
enabled,
|
||||
password,
|
||||
@ -56,4 +63,47 @@ object RShare {
|
||||
val T: Table = Table(None)
|
||||
def as(alias: String): Table = Table(Some(alias))
|
||||
|
||||
def insert(r: RShare): ConnectionIO[Int] =
|
||||
DML.insert(
|
||||
T,
|
||||
T.all,
|
||||
fr"${r.id},${r.cid},${r.name},${r.query},${r.enabled},${r.password},${r.publishAt},${r.publishUntil},${r.views},${r.lastAccess}"
|
||||
)
|
||||
|
||||
def incAccess(id: Ident): ConnectionIO[Int] =
|
||||
for {
|
||||
curTime <- Timestamp.current[ConnectionIO]
|
||||
n <- DML.update(
|
||||
T,
|
||||
T.id === id,
|
||||
DML.set(T.views.increment(1), T.lastAccess.setTo(curTime))
|
||||
)
|
||||
} yield n
|
||||
|
||||
def updateData(r: RShare, removePassword: Boolean): ConnectionIO[Int] =
|
||||
DML.update(
|
||||
T,
|
||||
T.id === r.id && T.cid === r.cid,
|
||||
DML.set(
|
||||
T.name.setTo(r.name),
|
||||
T.query.setTo(r.query),
|
||||
T.enabled.setTo(r.enabled),
|
||||
T.publishedUntil.setTo(r.publishUntil)
|
||||
) ++ (if (r.password.isDefined || removePassword)
|
||||
List(T.password.setTo(r.password))
|
||||
else Nil)
|
||||
)
|
||||
|
||||
def findOne(id: Ident, cid: Ident): OptionT[ConnectionIO, RShare] =
|
||||
OptionT(
|
||||
Select(select(T.all), from(T), T.id === id && T.cid === cid).build
|
||||
.query[RShare]
|
||||
.option
|
||||
)
|
||||
|
||||
def findAllByCollective(cid: Ident): ConnectionIO[List[RShare]] =
|
||||
Select(select(T.all), from(T), T.cid === cid).build.query[RShare].to[List]
|
||||
|
||||
def deleteByIdAndCid(id: Ident, cid: Ident): ConnectionIO[Int] =
|
||||
DML.delete(T, T.id === id && T.cid === cid)
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ module Api exposing
|
||||
, addCorrOrg
|
||||
, addCorrPerson
|
||||
, addMember
|
||||
, addShare
|
||||
, addTag
|
||||
, addTagsMultiple
|
||||
, attachmentPreviewURL
|
||||
@ -40,6 +41,7 @@ module Api exposing
|
||||
, deleteOrg
|
||||
, deletePerson
|
||||
, deleteScanMailbox
|
||||
, deleteShare
|
||||
, deleteSource
|
||||
, deleteTag
|
||||
, deleteUser
|
||||
@ -72,6 +74,8 @@ module Api exposing
|
||||
, getPersonsLight
|
||||
, getScanMailbox
|
||||
, getSentMails
|
||||
, getShare
|
||||
, getShares
|
||||
, getSources
|
||||
, getTagCloud
|
||||
, getTags
|
||||
@ -147,6 +151,7 @@ module Api exposing
|
||||
, unconfirmMultiple
|
||||
, updateNotifyDueItems
|
||||
, updateScanMailbox
|
||||
, updateShare
|
||||
, upload
|
||||
, uploadAmend
|
||||
, uploadSingle
|
||||
@ -215,6 +220,9 @@ import Api.Model.ScanMailboxSettingsList exposing (ScanMailboxSettingsList)
|
||||
import Api.Model.SearchStats exposing (SearchStats)
|
||||
import Api.Model.SecondFactor exposing (SecondFactor)
|
||||
import Api.Model.SentMails exposing (SentMails)
|
||||
import Api.Model.ShareData exposing (ShareData)
|
||||
import Api.Model.ShareDetail exposing (ShareDetail)
|
||||
import Api.Model.ShareList exposing (ShareList)
|
||||
import Api.Model.SimpleMail exposing (SimpleMail)
|
||||
import Api.Model.SourceAndTags exposing (SourceAndTags)
|
||||
import Api.Model.SourceList exposing (SourceList)
|
||||
@ -2206,6 +2214,57 @@ disableOtp flags otp receive =
|
||||
|
||||
|
||||
|
||||
--- Share
|
||||
|
||||
|
||||
getShares : Flags -> (Result Http.Error ShareList -> msg) -> Cmd msg
|
||||
getShares flags receive =
|
||||
Http2.authGet
|
||||
{ url = flags.config.baseUrl ++ "/api/v1/sec/share"
|
||||
, account = getAccount flags
|
||||
, expect = Http.expectJson receive Api.Model.ShareList.decoder
|
||||
}
|
||||
|
||||
|
||||
getShare : Flags -> String -> (Result Http.Error ShareDetail -> msg) -> Cmd msg
|
||||
getShare flags id receive =
|
||||
Http2.authGet
|
||||
{ url = flags.config.baseUrl ++ "/api/v1/sec/share/" ++ id
|
||||
, account = getAccount flags
|
||||
, expect = Http.expectJson receive Api.Model.ShareDetail.decoder
|
||||
}
|
||||
|
||||
|
||||
addShare : Flags -> ShareData -> (Result Http.Error IdResult -> msg) -> Cmd msg
|
||||
addShare flags share receive =
|
||||
Http2.authPost
|
||||
{ url = flags.config.baseUrl ++ "/api/v1/sec/share"
|
||||
, account = getAccount flags
|
||||
, body = Http.jsonBody (Api.Model.ShareData.encode share)
|
||||
, expect = Http.expectJson receive Api.Model.IdResult.decoder
|
||||
}
|
||||
|
||||
|
||||
updateShare : Flags -> String -> ShareData -> (Result Http.Error BasicResult -> msg) -> Cmd msg
|
||||
updateShare flags id share receive =
|
||||
Http2.authPut
|
||||
{ url = flags.config.baseUrl ++ "/api/v1/sec/share/" ++ id
|
||||
, account = getAccount flags
|
||||
, body = Http.jsonBody (Api.Model.ShareData.encode share)
|
||||
, expect = Http.expectJson receive Api.Model.BasicResult.decoder
|
||||
}
|
||||
|
||||
|
||||
deleteShare : Flags -> String -> (Result Http.Error BasicResult -> msg) -> Cmd msg
|
||||
deleteShare flags id receive =
|
||||
Http2.authDelete
|
||||
{ url = flags.config.baseUrl ++ "/api/v1/sec/share/" ++ id
|
||||
, account = getAccount flags
|
||||
, expect = Http.expectJson receive Api.Model.BasicResult.decoder
|
||||
}
|
||||
|
||||
|
||||
|
||||
--- Helper
|
||||
|
||||
|
||||
|
@ -37,7 +37,7 @@ init =
|
||||
|
||||
emptyModel : DatePicker
|
||||
emptyModel =
|
||||
DatePicker.initFromDate (Date.fromCalendarDate 2019 Aug 21)
|
||||
DatePicker.initFromDate (Date.fromCalendarDate 2021 Oct 31)
|
||||
|
||||
|
||||
defaultSettings : Settings
|
||||
|
282
modules/webapp/src/main/elm/Comp/ShareForm.elm
Normal file
282
modules/webapp/src/main/elm/Comp/ShareForm.elm
Normal file
@ -0,0 +1,282 @@
|
||||
{-
|
||||
Copyright 2020 Eike K. & Contributors
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-}
|
||||
|
||||
|
||||
module Comp.ShareForm exposing (Model, Msg, getShare, init, setShare, update, view)
|
||||
|
||||
import Api.Model.ShareData exposing (ShareData)
|
||||
import Api.Model.ShareDetail exposing (ShareDetail)
|
||||
import Comp.Basic as B
|
||||
import Comp.DatePicker
|
||||
import Comp.PasswordInput
|
||||
import Data.Flags exposing (Flags)
|
||||
import DatePicker exposing (DatePicker)
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events exposing (onCheck, onInput)
|
||||
import Messages.Comp.ShareForm exposing (Texts)
|
||||
import Styles as S
|
||||
import Util.Maybe
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ share : ShareDetail
|
||||
, name : Maybe String
|
||||
, query : String
|
||||
, enabled : Bool
|
||||
, passwordModel : Comp.PasswordInput.Model
|
||||
, password : Maybe String
|
||||
, passwordSet : Bool
|
||||
, clearPassword : Bool
|
||||
, untilModel : DatePicker
|
||||
, untilDate : Maybe Int
|
||||
}
|
||||
|
||||
|
||||
init : ( Model, Cmd Msg )
|
||||
init =
|
||||
let
|
||||
( dp, dpc ) =
|
||||
Comp.DatePicker.init
|
||||
in
|
||||
( { share = Api.Model.ShareDetail.empty
|
||||
, name = Nothing
|
||||
, query = ""
|
||||
, enabled = False
|
||||
, passwordModel = Comp.PasswordInput.init
|
||||
, password = Nothing
|
||||
, passwordSet = False
|
||||
, clearPassword = False
|
||||
, untilModel = dp
|
||||
, untilDate = Nothing
|
||||
}
|
||||
, Cmd.map UntilDateMsg dpc
|
||||
)
|
||||
|
||||
|
||||
isValid : Model -> Bool
|
||||
isValid model =
|
||||
model.query /= "" && model.untilDate /= Nothing
|
||||
|
||||
|
||||
type Msg
|
||||
= SetName String
|
||||
| SetQuery String
|
||||
| SetShare ShareDetail
|
||||
| ToggleEnabled
|
||||
| ToggleClearPassword
|
||||
| PasswordMsg Comp.PasswordInput.Msg
|
||||
| UntilDateMsg Comp.DatePicker.Msg
|
||||
|
||||
|
||||
setShare : ShareDetail -> Msg
|
||||
setShare share =
|
||||
SetShare share
|
||||
|
||||
|
||||
getShare : Model -> Maybe ( String, ShareData )
|
||||
getShare model =
|
||||
if isValid model then
|
||||
Just
|
||||
( model.share.id
|
||||
, { name = model.name
|
||||
, query = model.query
|
||||
, enabled = model.enabled
|
||||
, password = model.password
|
||||
, removePassword =
|
||||
if model.share.id == "" then
|
||||
Nothing
|
||||
|
||||
else
|
||||
Just model.clearPassword
|
||||
, publishUntil = Maybe.withDefault 0 model.untilDate
|
||||
}
|
||||
)
|
||||
|
||||
else
|
||||
Nothing
|
||||
|
||||
|
||||
update : Flags -> Msg -> Model -> ( Model, Cmd Msg )
|
||||
update _ msg model =
|
||||
case msg of
|
||||
SetShare s ->
|
||||
( { model
|
||||
| share = s
|
||||
, name = s.name
|
||||
, query = s.query
|
||||
, enabled = s.enabled
|
||||
, password = Nothing
|
||||
, passwordSet = s.password
|
||||
, clearPassword = False
|
||||
, untilDate =
|
||||
if s.publishUntil > 0 then
|
||||
Just s.publishUntil
|
||||
|
||||
else
|
||||
Nothing
|
||||
}
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
SetName n ->
|
||||
( { model | name = Util.Maybe.fromString n }, Cmd.none )
|
||||
|
||||
SetQuery n ->
|
||||
( { model | query = n }, Cmd.none )
|
||||
|
||||
ToggleEnabled ->
|
||||
( { model | enabled = not model.enabled }, Cmd.none )
|
||||
|
||||
ToggleClearPassword ->
|
||||
( { model | clearPassword = not model.clearPassword }, Cmd.none )
|
||||
|
||||
PasswordMsg lm ->
|
||||
let
|
||||
( pm, pw ) =
|
||||
Comp.PasswordInput.update lm model.passwordModel
|
||||
in
|
||||
( { model
|
||||
| passwordModel = pm
|
||||
, password = pw
|
||||
}
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
UntilDateMsg lm ->
|
||||
let
|
||||
( dp, event ) =
|
||||
Comp.DatePicker.updateDefault lm model.untilModel
|
||||
|
||||
nextDate =
|
||||
case event of
|
||||
DatePicker.Picked date ->
|
||||
Just (Comp.DatePicker.endOfDay date)
|
||||
|
||||
_ ->
|
||||
Nothing
|
||||
in
|
||||
( { model | untilModel = dp, untilDate = nextDate }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
|
||||
|
||||
--- View
|
||||
|
||||
|
||||
view : Texts -> Model -> Html Msg
|
||||
view texts model =
|
||||
div
|
||||
[ class "flex flex-col" ]
|
||||
[ div [ class "mb-4" ]
|
||||
[ label
|
||||
[ for "sharename"
|
||||
, class S.inputLabel
|
||||
]
|
||||
[ text texts.basics.name
|
||||
]
|
||||
, input
|
||||
[ type_ "text"
|
||||
, onInput SetName
|
||||
, placeholder texts.basics.name
|
||||
, value <| Maybe.withDefault "" model.name
|
||||
, id "sharename"
|
||||
, class S.textInput
|
||||
]
|
||||
[]
|
||||
]
|
||||
, div [ class "mb-4" ]
|
||||
[ label
|
||||
[ for "sharequery"
|
||||
, class S.inputLabel
|
||||
]
|
||||
[ text texts.queryLabel
|
||||
, B.inputRequired
|
||||
]
|
||||
, input
|
||||
[ type_ "text"
|
||||
, onInput SetQuery
|
||||
, placeholder texts.queryLabel
|
||||
, value model.query
|
||||
, id "sharequery"
|
||||
, class S.textInput
|
||||
, classList
|
||||
[ ( S.inputErrorBorder
|
||||
, not (isValid model)
|
||||
)
|
||||
]
|
||||
]
|
||||
[]
|
||||
]
|
||||
, div [ class "mb-4" ]
|
||||
[ label
|
||||
[ class "inline-flex items-center"
|
||||
, for "source-enabled"
|
||||
]
|
||||
[ input
|
||||
[ type_ "checkbox"
|
||||
, onCheck (\_ -> ToggleEnabled)
|
||||
, checked model.enabled
|
||||
, class S.checkboxInput
|
||||
, id "source-enabled"
|
||||
]
|
||||
[]
|
||||
, span [ class "ml-2" ]
|
||||
[ text texts.enabled
|
||||
]
|
||||
]
|
||||
]
|
||||
, div [ class "mb-4" ]
|
||||
[ label
|
||||
[ class S.inputLabel
|
||||
]
|
||||
[ text texts.password
|
||||
]
|
||||
, Html.map PasswordMsg
|
||||
(Comp.PasswordInput.view2
|
||||
{ placeholder = texts.password }
|
||||
model.password
|
||||
False
|
||||
model.passwordModel
|
||||
)
|
||||
, div
|
||||
[ class "mb-2"
|
||||
, classList [ ( "hidden", not model.passwordSet ) ]
|
||||
]
|
||||
[ label
|
||||
[ class "inline-flex items-center"
|
||||
, for "clear-password"
|
||||
]
|
||||
[ input
|
||||
[ type_ "checkbox"
|
||||
, onCheck (\_ -> ToggleClearPassword)
|
||||
, checked model.clearPassword
|
||||
, class S.checkboxInput
|
||||
, id "clear-password"
|
||||
]
|
||||
[]
|
||||
, span [ class "ml-2" ]
|
||||
[ text texts.clearPassword
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
, div [ class "mb-2 max-w-sm" ]
|
||||
[ label [ class S.inputLabel ]
|
||||
[ text texts.publishUntil
|
||||
, B.inputRequired
|
||||
]
|
||||
, div [ class "relative" ]
|
||||
[ Html.map UntilDateMsg
|
||||
(Comp.DatePicker.viewTimeDefault
|
||||
model.untilDate
|
||||
model.untilModel
|
||||
)
|
||||
, i [ class S.dateInputIcon, class "fa fa-calendar" ] []
|
||||
]
|
||||
]
|
||||
]
|
349
modules/webapp/src/main/elm/Comp/ShareManage.elm
Normal file
349
modules/webapp/src/main/elm/Comp/ShareManage.elm
Normal file
@ -0,0 +1,349 @@
|
||||
{-
|
||||
Copyright 2020 Eike K. & Contributors
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-}
|
||||
|
||||
|
||||
module Comp.ShareManage exposing (Model, Msg, init, loadShares, update, view)
|
||||
|
||||
import Api
|
||||
import Api.Model.BasicResult exposing (BasicResult)
|
||||
import Api.Model.IdResult exposing (IdResult)
|
||||
import Api.Model.ShareDetail exposing (ShareDetail)
|
||||
import Api.Model.ShareList exposing (ShareList)
|
||||
import Comp.Basic as B
|
||||
import Comp.ItemDetail.Model exposing (Msg(..))
|
||||
import Comp.MenuBar as MB
|
||||
import Comp.ShareForm
|
||||
import Comp.ShareTable
|
||||
import Data.Flags exposing (Flags)
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events exposing (onClick)
|
||||
import Http
|
||||
import Messages.Comp.ShareManage exposing (Texts)
|
||||
import Styles as S
|
||||
|
||||
|
||||
type FormError
|
||||
= FormErrorNone
|
||||
| FormErrorHttp Http.Error
|
||||
| FormErrorInvalid
|
||||
| FormErrorSubmit String
|
||||
|
||||
|
||||
type ViewMode
|
||||
= Table
|
||||
| Form
|
||||
|
||||
|
||||
type DeleteConfirm
|
||||
= DeleteConfirmOff
|
||||
| DeleteConfirmOn
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ viewMode : ViewMode
|
||||
, shares : List ShareDetail
|
||||
, formModel : Comp.ShareForm.Model
|
||||
, loading : Bool
|
||||
, formError : FormError
|
||||
, deleteConfirm : DeleteConfirm
|
||||
}
|
||||
|
||||
|
||||
init : ( Model, Cmd Msg )
|
||||
init =
|
||||
let
|
||||
( fm, fc ) =
|
||||
Comp.ShareForm.init
|
||||
in
|
||||
( { viewMode = Table
|
||||
, shares = []
|
||||
, formModel = fm
|
||||
, loading = False
|
||||
, formError = FormErrorNone
|
||||
, deleteConfirm = DeleteConfirmOff
|
||||
}
|
||||
, Cmd.map FormMsg fc
|
||||
)
|
||||
|
||||
|
||||
type Msg
|
||||
= LoadShares
|
||||
| TableMsg Comp.ShareTable.Msg
|
||||
| FormMsg Comp.ShareForm.Msg
|
||||
| InitNewShare
|
||||
| SetViewMode ViewMode
|
||||
| Submit
|
||||
| RequestDelete
|
||||
| CancelDelete
|
||||
| DeleteShareNow String
|
||||
| LoadSharesResp (Result Http.Error ShareList)
|
||||
| AddShareResp (Result Http.Error IdResult)
|
||||
| UpdateShareResp (Result Http.Error BasicResult)
|
||||
| GetShareResp (Result Http.Error ShareDetail)
|
||||
| DeleteShareResp (Result Http.Error BasicResult)
|
||||
|
||||
|
||||
loadShares : Msg
|
||||
loadShares =
|
||||
LoadShares
|
||||
|
||||
|
||||
|
||||
--- update
|
||||
|
||||
|
||||
update : Flags -> Msg -> Model -> ( Model, Cmd Msg )
|
||||
update flags msg model =
|
||||
case msg of
|
||||
InitNewShare ->
|
||||
let
|
||||
nm =
|
||||
{ model | viewMode = Form, formError = FormErrorNone }
|
||||
|
||||
share =
|
||||
Api.Model.ShareDetail.empty
|
||||
in
|
||||
update flags (FormMsg (Comp.ShareForm.setShare share)) nm
|
||||
|
||||
SetViewMode vm ->
|
||||
( { model | viewMode = vm, formError = FormErrorNone }
|
||||
, if vm == Table then
|
||||
Api.getShares flags LoadSharesResp
|
||||
|
||||
else
|
||||
Cmd.none
|
||||
)
|
||||
|
||||
FormMsg lm ->
|
||||
let
|
||||
( fm, fc ) =
|
||||
Comp.ShareForm.update flags lm model.formModel
|
||||
in
|
||||
( { model | formModel = fm }, Cmd.map FormMsg fc )
|
||||
|
||||
TableMsg lm ->
|
||||
let
|
||||
action =
|
||||
Comp.ShareTable.update lm
|
||||
|
||||
nextModel =
|
||||
{ model | viewMode = Form, formError = FormErrorNone }
|
||||
in
|
||||
case action of
|
||||
Comp.ShareTable.Edit share ->
|
||||
update flags (FormMsg <| Comp.ShareForm.setShare share) nextModel
|
||||
|
||||
RequestDelete ->
|
||||
( { model | deleteConfirm = DeleteConfirmOn }, Cmd.none )
|
||||
|
||||
CancelDelete ->
|
||||
( { model | deleteConfirm = DeleteConfirmOff }, Cmd.none )
|
||||
|
||||
DeleteShareNow id ->
|
||||
( { model | deleteConfirm = DeleteConfirmOff, loading = True }
|
||||
, Api.deleteShare flags id DeleteShareResp
|
||||
)
|
||||
|
||||
LoadShares ->
|
||||
( { model | loading = True }, Api.getShares flags LoadSharesResp )
|
||||
|
||||
LoadSharesResp (Ok list) ->
|
||||
( { model | loading = False, shares = list.items, formError = FormErrorNone }, Cmd.none )
|
||||
|
||||
LoadSharesResp (Err err) ->
|
||||
( { model | loading = False, formError = FormErrorHttp err }, Cmd.none )
|
||||
|
||||
Submit ->
|
||||
case Comp.ShareForm.getShare model.formModel of
|
||||
Just ( id, data ) ->
|
||||
if id == "" then
|
||||
( { model | loading = True }, Api.addShare flags data AddShareResp )
|
||||
|
||||
else
|
||||
( { model | loading = True }, Api.updateShare flags id data UpdateShareResp )
|
||||
|
||||
Nothing ->
|
||||
( { model | formError = FormErrorInvalid }, Cmd.none )
|
||||
|
||||
AddShareResp (Ok res) ->
|
||||
if res.success then
|
||||
( model, Api.getShare flags res.id GetShareResp )
|
||||
|
||||
else
|
||||
( { model | loading = False, formError = FormErrorSubmit res.message }, Cmd.none )
|
||||
|
||||
AddShareResp (Err err) ->
|
||||
( { model | loading = False, formError = FormErrorHttp err }, Cmd.none )
|
||||
|
||||
UpdateShareResp (Ok res) ->
|
||||
if res.success then
|
||||
( model, Api.getShare flags model.formModel.share.id GetShareResp )
|
||||
|
||||
else
|
||||
( { model | loading = False, formError = FormErrorSubmit res.message }, Cmd.none )
|
||||
|
||||
UpdateShareResp (Err err) ->
|
||||
( { model | loading = False, formError = FormErrorHttp err }, Cmd.none )
|
||||
|
||||
GetShareResp (Ok share) ->
|
||||
let
|
||||
nextModel =
|
||||
{ model | formError = FormErrorNone, loading = False }
|
||||
in
|
||||
update flags (FormMsg <| Comp.ShareForm.setShare share) nextModel
|
||||
|
||||
GetShareResp (Err err) ->
|
||||
( { model | formError = FormErrorHttp err }, Cmd.none )
|
||||
|
||||
DeleteShareResp (Ok res) ->
|
||||
if res.success then
|
||||
update flags (SetViewMode Table) { model | loading = False }
|
||||
|
||||
else
|
||||
( { model | formError = FormErrorSubmit res.message, loading = False }, Cmd.none )
|
||||
|
||||
DeleteShareResp (Err err) ->
|
||||
( { model | formError = FormErrorHttp err, loading = False }, Cmd.none )
|
||||
|
||||
|
||||
|
||||
--- view
|
||||
|
||||
|
||||
view : Texts -> Flags -> Model -> Html Msg
|
||||
view texts _ model =
|
||||
if model.viewMode == Table then
|
||||
viewTable texts model
|
||||
|
||||
else
|
||||
viewForm texts model
|
||||
|
||||
|
||||
viewTable : Texts -> Model -> Html Msg
|
||||
viewTable texts model =
|
||||
div [ class "flex flex-col" ]
|
||||
[ MB.view
|
||||
{ start =
|
||||
[]
|
||||
, end =
|
||||
[ MB.PrimaryButton
|
||||
{ tagger = InitNewShare
|
||||
, title = texts.createNewShare
|
||||
, icon = Just "fa fa-plus"
|
||||
, label = texts.newShare
|
||||
}
|
||||
]
|
||||
, rootClasses = "mb-4"
|
||||
}
|
||||
, Html.map TableMsg (Comp.ShareTable.view texts.shareTable model.shares)
|
||||
, B.loadingDimmer
|
||||
{ label = ""
|
||||
, active = model.loading
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
viewForm : Texts -> Model -> Html Msg
|
||||
viewForm texts model =
|
||||
let
|
||||
newShare =
|
||||
model.formModel.share.id == ""
|
||||
in
|
||||
Html.form [ class "relative" ]
|
||||
[ if newShare then
|
||||
h1 [ class S.header2 ]
|
||||
[ text texts.createNewShare
|
||||
]
|
||||
|
||||
else
|
||||
h1 [ class S.header2 ]
|
||||
[ text <| Maybe.withDefault texts.noName model.formModel.share.name
|
||||
, div [ class "opacity-50 text-sm" ]
|
||||
[ text "Id: "
|
||||
, text model.formModel.share.id
|
||||
]
|
||||
]
|
||||
, MB.view
|
||||
{ start =
|
||||
[ MB.PrimaryButton
|
||||
{ tagger = Submit
|
||||
, title = "Submit this form"
|
||||
, icon = Just "fa fa-save"
|
||||
, label = texts.basics.submit
|
||||
}
|
||||
, MB.SecondaryButton
|
||||
{ tagger = SetViewMode Table
|
||||
, title = texts.basics.backToList
|
||||
, icon = Just "fa fa-arrow-left"
|
||||
, label = texts.basics.cancel
|
||||
}
|
||||
]
|
||||
, end =
|
||||
if not newShare then
|
||||
[ MB.DeleteButton
|
||||
{ tagger = RequestDelete
|
||||
, title = texts.deleteThisShare
|
||||
, icon = Just "fa fa-trash"
|
||||
, label = texts.basics.delete
|
||||
}
|
||||
]
|
||||
|
||||
else
|
||||
[]
|
||||
, rootClasses = "mb-4"
|
||||
}
|
||||
, div
|
||||
[ classList
|
||||
[ ( "hidden", model.formError == FormErrorNone )
|
||||
]
|
||||
, class "my-2"
|
||||
, class S.errorMessage
|
||||
]
|
||||
[ case model.formError of
|
||||
FormErrorNone ->
|
||||
text ""
|
||||
|
||||
FormErrorHttp err ->
|
||||
text (texts.httpError err)
|
||||
|
||||
FormErrorInvalid ->
|
||||
text texts.correctFormErrors
|
||||
|
||||
FormErrorSubmit m ->
|
||||
text m
|
||||
]
|
||||
, Html.map FormMsg (Comp.ShareForm.view texts.shareForm model.formModel)
|
||||
, B.loadingDimmer
|
||||
{ active = model.loading
|
||||
, label = texts.basics.loading
|
||||
}
|
||||
, B.contentDimmer
|
||||
(model.deleteConfirm == DeleteConfirmOn)
|
||||
(div [ class "flex flex-col" ]
|
||||
[ div [ class "text-lg" ]
|
||||
[ i [ class "fa fa-info-circle mr-2" ] []
|
||||
, text texts.reallyDeleteShare
|
||||
]
|
||||
, div [ class "mt-4 flex flex-row items-center" ]
|
||||
[ B.deleteButton
|
||||
{ label = texts.basics.yes
|
||||
, icon = "fa fa-check"
|
||||
, disabled = False
|
||||
, handler = onClick (DeleteShareNow model.formModel.share.id)
|
||||
, attrs = [ href "#" ]
|
||||
}
|
||||
, B.secondaryButton
|
||||
{ label = texts.basics.no
|
||||
, icon = "fa fa-times"
|
||||
, disabled = False
|
||||
, handler = onClick CancelDelete
|
||||
, attrs = [ href "#", class "ml-2" ]
|
||||
}
|
||||
]
|
||||
]
|
||||
)
|
||||
]
|
87
modules/webapp/src/main/elm/Comp/ShareTable.elm
Normal file
87
modules/webapp/src/main/elm/Comp/ShareTable.elm
Normal file
@ -0,0 +1,87 @@
|
||||
{-
|
||||
Copyright 2020 Eike K. & Contributors
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-}
|
||||
|
||||
|
||||
module Comp.ShareTable exposing
|
||||
( Msg(..)
|
||||
, SelectAction(..)
|
||||
, update
|
||||
, view
|
||||
)
|
||||
|
||||
import Api.Model.ShareDetail exposing (ShareDetail)
|
||||
import Comp.Basic as B
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Messages.Comp.ShareTable exposing (Texts)
|
||||
import Styles as S
|
||||
import Util.Html
|
||||
import Util.String
|
||||
|
||||
|
||||
type Msg
|
||||
= Select ShareDetail
|
||||
|
||||
|
||||
type SelectAction
|
||||
= Edit ShareDetail
|
||||
|
||||
|
||||
update : Msg -> SelectAction
|
||||
update msg =
|
||||
case msg of
|
||||
Select share ->
|
||||
Edit share
|
||||
|
||||
|
||||
|
||||
--- View
|
||||
|
||||
|
||||
view : Texts -> List ShareDetail -> Html Msg
|
||||
view texts shares =
|
||||
table [ class S.tableMain ]
|
||||
[ thead []
|
||||
[ tr []
|
||||
[ th [ class "" ] []
|
||||
, th [ class "text-left" ]
|
||||
[ text texts.basics.id
|
||||
]
|
||||
, th [ class "text-left" ]
|
||||
[ text texts.basics.name
|
||||
]
|
||||
, th [ class "text-center" ]
|
||||
[ text texts.enabled
|
||||
]
|
||||
, th [ class "text-center" ]
|
||||
[ text texts.publishUntil
|
||||
]
|
||||
]
|
||||
]
|
||||
, tbody []
|
||||
(List.map (renderShareLine texts) shares)
|
||||
]
|
||||
|
||||
|
||||
renderShareLine : Texts -> ShareDetail -> Html Msg
|
||||
renderShareLine texts share =
|
||||
tr
|
||||
[ class S.tableRow
|
||||
]
|
||||
[ B.editLinkTableCell texts.basics.edit (Select share)
|
||||
, td [ class "text-left py-4 md:py-2" ]
|
||||
[ text (Util.String.ellipsis 8 share.id)
|
||||
]
|
||||
, td [ class "text-left py-4 md:py-2" ]
|
||||
[ text (Maybe.withDefault "-" share.name)
|
||||
]
|
||||
, td [ class "w-px px-2 text-center" ]
|
||||
[ Util.Html.checkbox2 share.enabled
|
||||
]
|
||||
, td [ class "hidden sm:table-cell text-center" ]
|
||||
[ texts.formatDateTime share.publishUntil |> text
|
||||
]
|
||||
]
|
@ -58,11 +58,11 @@ module Data.Icons exposing
|
||||
, personIcon2
|
||||
, search
|
||||
, searchIcon
|
||||
, share
|
||||
, shareIcon
|
||||
, showQr
|
||||
, showQrIcon
|
||||
, source
|
||||
, source2
|
||||
, sourceIcon
|
||||
, sourceIcon2
|
||||
, tag
|
||||
, tag2
|
||||
@ -79,9 +79,14 @@ import Html exposing (Html, i)
|
||||
import Html.Attributes exposing (class)
|
||||
|
||||
|
||||
source : String
|
||||
source =
|
||||
"upload icon"
|
||||
share : String
|
||||
share =
|
||||
"fa fa-share-alt"
|
||||
|
||||
|
||||
shareIcon : String -> Html msg
|
||||
shareIcon classes =
|
||||
i [ class (classes ++ " " ++ share) ] []
|
||||
|
||||
|
||||
source2 : String
|
||||
@ -89,11 +94,6 @@ source2 =
|
||||
"fa fa-upload"
|
||||
|
||||
|
||||
sourceIcon : String -> Html msg
|
||||
sourceIcon classes =
|
||||
i [ class (source ++ " " ++ classes) ] []
|
||||
|
||||
|
||||
sourceIcon2 : String -> Html msg
|
||||
sourceIcon2 classes =
|
||||
i [ class (source2 ++ " " ++ classes) ] []
|
||||
|
46
modules/webapp/src/main/elm/Messages/Comp/ShareForm.elm
Normal file
46
modules/webapp/src/main/elm/Messages/Comp/ShareForm.elm
Normal file
@ -0,0 +1,46 @@
|
||||
{-
|
||||
Copyright 2020 Eike K. & Contributors
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-}
|
||||
|
||||
|
||||
module Messages.Comp.ShareForm exposing
|
||||
( Texts
|
||||
, de
|
||||
, gb
|
||||
)
|
||||
|
||||
import Messages.Basics
|
||||
|
||||
|
||||
type alias Texts =
|
||||
{ basics : Messages.Basics.Texts
|
||||
, queryLabel : String
|
||||
, enabled : String
|
||||
, password : String
|
||||
, publishUntil : String
|
||||
, clearPassword : String
|
||||
}
|
||||
|
||||
|
||||
gb : Texts
|
||||
gb =
|
||||
{ basics = Messages.Basics.gb
|
||||
, queryLabel = "Query"
|
||||
, enabled = "Enabled"
|
||||
, password = "Password"
|
||||
, publishUntil = "Publish Until"
|
||||
, clearPassword = "Remove password"
|
||||
}
|
||||
|
||||
|
||||
de : Texts
|
||||
de =
|
||||
{ basics = Messages.Basics.de
|
||||
, queryLabel = "Abfrage"
|
||||
, enabled = "Aktiv"
|
||||
, password = "Passwort"
|
||||
, publishUntil = "Publiziert bis"
|
||||
, clearPassword = "Passwort entfernen"
|
||||
}
|
74
modules/webapp/src/main/elm/Messages/Comp/ShareManage.elm
Normal file
74
modules/webapp/src/main/elm/Messages/Comp/ShareManage.elm
Normal file
@ -0,0 +1,74 @@
|
||||
{-
|
||||
Copyright 2020 Eike K. & Contributors
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-}
|
||||
|
||||
|
||||
module Messages.Comp.ShareManage exposing
|
||||
( Texts
|
||||
, de
|
||||
, gb
|
||||
)
|
||||
|
||||
import Http
|
||||
import Messages.Basics
|
||||
import Messages.Comp.HttpError
|
||||
import Messages.Comp.ShareForm
|
||||
import Messages.Comp.ShareTable
|
||||
|
||||
|
||||
type alias Texts =
|
||||
{ basics : Messages.Basics.Texts
|
||||
, shareTable : Messages.Comp.ShareTable.Texts
|
||||
, shareForm : Messages.Comp.ShareForm.Texts
|
||||
, httpError : Http.Error -> String
|
||||
, newShare : String
|
||||
, copyToClipboard : String
|
||||
, openInNewTab : String
|
||||
, publicUrl : String
|
||||
, reallyDeleteShare : String
|
||||
, createNewShare : String
|
||||
, deleteThisShare : String
|
||||
, errorGeneratingQR : String
|
||||
, correctFormErrors : String
|
||||
, noName : String
|
||||
}
|
||||
|
||||
|
||||
gb : Texts
|
||||
gb =
|
||||
{ basics = Messages.Basics.gb
|
||||
, httpError = Messages.Comp.HttpError.gb
|
||||
, shareTable = Messages.Comp.ShareTable.gb
|
||||
, shareForm = Messages.Comp.ShareForm.gb
|
||||
, newShare = "New share"
|
||||
, copyToClipboard = "Copy to clipboard"
|
||||
, openInNewTab = "Open in new tab/window"
|
||||
, publicUrl = "Public URL"
|
||||
, reallyDeleteShare = "Really delete this share?"
|
||||
, createNewShare = "Create new share"
|
||||
, deleteThisShare = "Delete this share"
|
||||
, errorGeneratingQR = "Error generating QR Code"
|
||||
, correctFormErrors = "Please correct the errors in the form."
|
||||
, noName = "No Name"
|
||||
}
|
||||
|
||||
|
||||
de : Texts
|
||||
de =
|
||||
{ basics = Messages.Basics.de
|
||||
, shareTable = Messages.Comp.ShareTable.de
|
||||
, shareForm = Messages.Comp.ShareForm.de
|
||||
, httpError = Messages.Comp.HttpError.de
|
||||
, newShare = "Neue Freigabe"
|
||||
, copyToClipboard = "In die Zwischenablage kopieren"
|
||||
, openInNewTab = "Im neuen Tab/Fenster öffnen"
|
||||
, publicUrl = "Öffentliche URL"
|
||||
, reallyDeleteShare = "Diese Freigabe wirklich entfernen?"
|
||||
, createNewShare = "Neue Freigabe erstellen"
|
||||
, deleteThisShare = "Freigabe löschen"
|
||||
, errorGeneratingQR = "Fehler beim Generieren des QR-Code"
|
||||
, correctFormErrors = "Bitte korrigiere die Fehler im Formular."
|
||||
, noName = "Ohne Name"
|
||||
}
|
42
modules/webapp/src/main/elm/Messages/Comp/ShareTable.elm
Normal file
42
modules/webapp/src/main/elm/Messages/Comp/ShareTable.elm
Normal file
@ -0,0 +1,42 @@
|
||||
{-
|
||||
Copyright 2020 Eike K. & Contributors
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-}
|
||||
|
||||
|
||||
module Messages.Comp.ShareTable exposing
|
||||
( Texts
|
||||
, de
|
||||
, gb
|
||||
)
|
||||
|
||||
import Messages.Basics
|
||||
import Messages.DateFormat as DF
|
||||
import Messages.UiLanguage
|
||||
|
||||
|
||||
type alias Texts =
|
||||
{ basics : Messages.Basics.Texts
|
||||
, formatDateTime : Int -> String
|
||||
, enabled : String
|
||||
, publishUntil : String
|
||||
}
|
||||
|
||||
|
||||
gb : Texts
|
||||
gb =
|
||||
{ basics = Messages.Basics.gb
|
||||
, formatDateTime = DF.formatDateTimeLong Messages.UiLanguage.English
|
||||
, enabled = "Enabled"
|
||||
, publishUntil = "Publish Until"
|
||||
}
|
||||
|
||||
|
||||
de : Texts
|
||||
de =
|
||||
{ basics = Messages.Basics.de
|
||||
, formatDateTime = DF.formatDateTimeLong Messages.UiLanguage.German
|
||||
, enabled = "Aktiv"
|
||||
, publishUntil = "Publiziert bis"
|
||||
}
|
@ -15,6 +15,7 @@ import Http
|
||||
import Messages.Basics
|
||||
import Messages.Comp.CollectiveSettingsForm
|
||||
import Messages.Comp.HttpError
|
||||
import Messages.Comp.ShareManage
|
||||
import Messages.Comp.SourceManage
|
||||
import Messages.Comp.UserManage
|
||||
|
||||
@ -24,12 +25,14 @@ type alias Texts =
|
||||
, userManage : Messages.Comp.UserManage.Texts
|
||||
, collectiveSettingsForm : Messages.Comp.CollectiveSettingsForm.Texts
|
||||
, sourceManage : Messages.Comp.SourceManage.Texts
|
||||
, shareManage : Messages.Comp.ShareManage.Texts
|
||||
, httpError : Http.Error -> String
|
||||
, collectiveSettings : String
|
||||
, insights : String
|
||||
, sources : String
|
||||
, settings : String
|
||||
, users : String
|
||||
, shares : String
|
||||
, user : String
|
||||
, collective : String
|
||||
, size : String
|
||||
@ -44,12 +47,14 @@ gb =
|
||||
, userManage = Messages.Comp.UserManage.gb
|
||||
, collectiveSettingsForm = Messages.Comp.CollectiveSettingsForm.gb
|
||||
, sourceManage = Messages.Comp.SourceManage.gb
|
||||
, shareManage = Messages.Comp.ShareManage.gb
|
||||
, httpError = Messages.Comp.HttpError.gb
|
||||
, collectiveSettings = "Collective Settings"
|
||||
, insights = "Insights"
|
||||
, sources = "Sources"
|
||||
, settings = "Settings"
|
||||
, users = "Users"
|
||||
, shares = "Shares"
|
||||
, user = "User"
|
||||
, collective = "Collective"
|
||||
, size = "Size"
|
||||
@ -64,12 +69,14 @@ de =
|
||||
, userManage = Messages.Comp.UserManage.de
|
||||
, collectiveSettingsForm = Messages.Comp.CollectiveSettingsForm.de
|
||||
, sourceManage = Messages.Comp.SourceManage.de
|
||||
, shareManage = Messages.Comp.ShareManage.de
|
||||
, httpError = Messages.Comp.HttpError.de
|
||||
, collectiveSettings = "Kollektiveinstellungen"
|
||||
, insights = "Statistiken"
|
||||
, sources = "Quellen"
|
||||
, settings = "Einstellungen"
|
||||
, users = "Benutzer"
|
||||
, shares = "Freigaben"
|
||||
, user = "Benutzer"
|
||||
, collective = "Kollektiv"
|
||||
, size = "Größe"
|
||||
|
@ -17,6 +17,7 @@ import Api.Model.BasicResult exposing (BasicResult)
|
||||
import Api.Model.CollectiveSettings exposing (CollectiveSettings)
|
||||
import Api.Model.ItemInsights exposing (ItemInsights)
|
||||
import Comp.CollectiveSettingsForm
|
||||
import Comp.ShareManage
|
||||
import Comp.SourceManage
|
||||
import Comp.UserManage
|
||||
import Data.Flags exposing (Flags)
|
||||
@ -28,6 +29,7 @@ type alias Model =
|
||||
, sourceModel : Comp.SourceManage.Model
|
||||
, userModel : Comp.UserManage.Model
|
||||
, settingsModel : Comp.CollectiveSettingsForm.Model
|
||||
, shareModel : Comp.ShareManage.Model
|
||||
, insights : ItemInsights
|
||||
, formState : FormState
|
||||
}
|
||||
@ -48,10 +50,14 @@ init flags =
|
||||
|
||||
( cm, cc ) =
|
||||
Comp.CollectiveSettingsForm.init flags Api.Model.CollectiveSettings.empty
|
||||
|
||||
( shm, shc ) =
|
||||
Comp.ShareManage.init
|
||||
in
|
||||
( { currentTab = Just InsightsTab
|
||||
, sourceModel = sm
|
||||
, userModel = Comp.UserManage.emptyModel
|
||||
, shareModel = shm
|
||||
, settingsModel = cm
|
||||
, insights = Api.Model.ItemInsights.empty
|
||||
, formState = InitialState
|
||||
@ -59,6 +65,7 @@ init flags =
|
||||
, Cmd.batch
|
||||
[ Cmd.map SourceMsg sc
|
||||
, Cmd.map SettingsFormMsg cc
|
||||
, Cmd.map ShareMsg shc
|
||||
]
|
||||
)
|
||||
|
||||
@ -68,6 +75,7 @@ type Tab
|
||||
| UserTab
|
||||
| InsightsTab
|
||||
| SettingsTab
|
||||
| ShareTab
|
||||
|
||||
|
||||
type Msg
|
||||
@ -79,3 +87,4 @@ type Msg
|
||||
| GetInsightsResp (Result Http.Error ItemInsights)
|
||||
| CollectiveSettingsResp (Result Http.Error CollectiveSettings)
|
||||
| SubmitResp (Result Http.Error BasicResult)
|
||||
| ShareMsg Comp.ShareManage.Msg
|
||||
|
@ -9,6 +9,7 @@ module Page.CollectiveSettings.Update exposing (update)
|
||||
|
||||
import Api
|
||||
import Comp.CollectiveSettingsForm
|
||||
import Comp.ShareManage
|
||||
import Comp.SourceManage
|
||||
import Comp.UserManage
|
||||
import Data.Flags exposing (Flags)
|
||||
@ -36,6 +37,9 @@ update flags msg model =
|
||||
SettingsTab ->
|
||||
update flags Init m
|
||||
|
||||
ShareTab ->
|
||||
update flags (ShareMsg Comp.ShareManage.loadShares) m
|
||||
|
||||
SourceMsg m ->
|
||||
let
|
||||
( m2, c2 ) =
|
||||
@ -43,6 +47,13 @@ update flags msg model =
|
||||
in
|
||||
( { model | sourceModel = m2 }, Cmd.map SourceMsg c2 )
|
||||
|
||||
ShareMsg lm ->
|
||||
let
|
||||
( sm, sc ) =
|
||||
Comp.ShareManage.update flags lm model.shareModel
|
||||
in
|
||||
( { model | shareModel = sm }, Cmd.map ShareMsg sc )
|
||||
|
||||
UserMsg m ->
|
||||
let
|
||||
( m2, c2 ) =
|
||||
|
@ -10,6 +10,7 @@ module Page.CollectiveSettings.View2 exposing (viewContent, viewSidebar)
|
||||
import Api.Model.TagCount exposing (TagCount)
|
||||
import Comp.Basic as B
|
||||
import Comp.CollectiveSettingsForm
|
||||
import Comp.ShareManage
|
||||
import Comp.SourceManage
|
||||
import Comp.UserManage
|
||||
import Data.Flags exposing (Flags)
|
||||
@ -60,6 +61,17 @@ viewSidebar texts visible _ _ model =
|
||||
[ class "ml-3" ]
|
||||
[ text texts.sources ]
|
||||
]
|
||||
, a
|
||||
[ href "#"
|
||||
, onClick (SetTab ShareTab)
|
||||
, class S.sidebarLink
|
||||
, menuEntryActive model ShareTab
|
||||
]
|
||||
[ Icons.shareIcon ""
|
||||
, span
|
||||
[ class "ml-3" ]
|
||||
[ text texts.shares ]
|
||||
]
|
||||
, a
|
||||
[ href "#"
|
||||
, onClick (SetTab SettingsTab)
|
||||
@ -105,6 +117,9 @@ viewContent texts flags settings model =
|
||||
Just SourceTab ->
|
||||
viewSources texts flags settings model
|
||||
|
||||
Just ShareTab ->
|
||||
viewShares texts flags model
|
||||
|
||||
Nothing ->
|
||||
[]
|
||||
)
|
||||
@ -230,6 +245,21 @@ viewSources texts flags settings model =
|
||||
]
|
||||
|
||||
|
||||
viewShares : Texts -> Flags -> Model -> List (Html Msg)
|
||||
viewShares texts flags model =
|
||||
[ h1
|
||||
[ class S.header1
|
||||
, class "inline-flex items-center"
|
||||
]
|
||||
[ Icons.shareIcon ""
|
||||
, div [ class "ml-3" ]
|
||||
[ text texts.shares
|
||||
]
|
||||
]
|
||||
, Html.map ShareMsg (Comp.ShareManage.view texts.shareManage flags model.shareModel)
|
||||
]
|
||||
|
||||
|
||||
viewUsers : Texts -> UiSettings -> Model -> List (Html Msg)
|
||||
viewUsers texts settings model =
|
||||
[ h1
|
||||
|
Loading…
x
Reference in New Issue
Block a user