mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-03-30 21:05:08 +00:00
commit
f33aa969d8
@ -598,3 +598,4 @@ addCommandAlias("make-zip", ";restserver/universal:packageBin ;joex/universal:pa
|
||||
addCommandAlias("make-deb", ";restserver/debian:packageBin ;joex/debian:packageBin")
|
||||
addCommandAlias("make-tools", ";root/toolsPackage")
|
||||
addCommandAlias("make-pkg", ";clean ;make ;make-zip ;make-deb ;make-tools")
|
||||
addCommandAlias("reformatAll", ";project root ;scalafix ;scalafmtAll")
|
||||
|
@ -35,6 +35,7 @@ trait BackendApp[F[_]] {
|
||||
def mail: OMail[F]
|
||||
def joex: OJoex[F]
|
||||
def userTask: OUserTask[F]
|
||||
def folder: OFolder[F]
|
||||
}
|
||||
|
||||
object BackendApp {
|
||||
@ -67,6 +68,7 @@ object BackendApp {
|
||||
JavaMailEmil(blocker, Settings.defaultSettings.copy(debug = cfg.mailDebug))
|
||||
mailImpl <- OMail(store, javaEmil)
|
||||
userTaskImpl <- OUserTask(utStore, queue, joexImpl)
|
||||
folderImpl <- OFolder(store)
|
||||
} yield new BackendApp[F] {
|
||||
val login: Login[F] = loginImpl
|
||||
val signup: OSignup[F] = signupImpl
|
||||
@ -84,6 +86,7 @@ object BackendApp {
|
||||
val mail = mailImpl
|
||||
val joex = joexImpl
|
||||
val userTask = userTaskImpl
|
||||
val folder = folderImpl
|
||||
}
|
||||
|
||||
def apply[F[_]: ConcurrentEffect: ContextShift](
|
||||
|
@ -0,0 +1,110 @@
|
||||
package docspell.backend.ops
|
||||
|
||||
import cats.effect._
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.queries.QFolder
|
||||
import docspell.store.records.{RFolder, RUser}
|
||||
import docspell.store.{AddResult, Store}
|
||||
|
||||
trait OFolder[F[_]] {
|
||||
|
||||
def findAll(
|
||||
account: AccountId,
|
||||
ownerLogin: Option[Ident],
|
||||
nameQuery: Option[String]
|
||||
): F[Vector[OFolder.FolderItem]]
|
||||
|
||||
def findById(id: Ident, account: AccountId): F[Option[OFolder.FolderDetail]]
|
||||
|
||||
/** Adds a new folder. If `login` is non-empty, the `folder.user`
|
||||
* property is ignored and the user-id is determined by the given
|
||||
* login name.
|
||||
*/
|
||||
def add(folder: RFolder, login: Option[Ident]): F[AddResult]
|
||||
|
||||
def changeName(
|
||||
folder: Ident,
|
||||
account: AccountId,
|
||||
name: String
|
||||
): F[OFolder.FolderChangeResult]
|
||||
|
||||
def addMember(
|
||||
folder: Ident,
|
||||
account: AccountId,
|
||||
member: Ident
|
||||
): F[OFolder.FolderChangeResult]
|
||||
|
||||
def removeMember(
|
||||
folder: Ident,
|
||||
account: AccountId,
|
||||
member: Ident
|
||||
): F[OFolder.FolderChangeResult]
|
||||
|
||||
def delete(id: Ident, account: AccountId): F[OFolder.FolderChangeResult]
|
||||
}
|
||||
|
||||
object OFolder {
|
||||
|
||||
type FolderChangeResult = QFolder.FolderChangeResult
|
||||
val FolderChangeResult = QFolder.FolderChangeResult
|
||||
|
||||
type FolderItem = QFolder.FolderItem
|
||||
val FolderItem = QFolder.FolderItem
|
||||
|
||||
type FolderDetail = QFolder.FolderDetail
|
||||
val FolderDetail = QFolder.FolderDetail
|
||||
|
||||
def apply[F[_]: Effect](store: Store[F]): Resource[F, OFolder[F]] =
|
||||
Resource.pure[F, OFolder[F]](new OFolder[F] {
|
||||
def findAll(
|
||||
account: AccountId,
|
||||
ownerLogin: Option[Ident],
|
||||
nameQuery: Option[String]
|
||||
): F[Vector[FolderItem]] =
|
||||
store.transact(QFolder.findAll(account, None, ownerLogin, nameQuery))
|
||||
|
||||
def findById(id: Ident, account: AccountId): F[Option[FolderDetail]] =
|
||||
store.transact(QFolder.findById(id, account))
|
||||
|
||||
def add(folder: RFolder, login: Option[Ident]): F[AddResult] = {
|
||||
val insert = login match {
|
||||
case Some(n) =>
|
||||
for {
|
||||
user <- RUser.findByAccount(AccountId(folder.collectiveId, n))
|
||||
s = user.map(u => folder.copy(owner = u.uid)).getOrElse(folder)
|
||||
n <- RFolder.insert(s)
|
||||
} yield n
|
||||
|
||||
case None =>
|
||||
RFolder.insert(folder)
|
||||
}
|
||||
val exists = RFolder.existsByName(folder.collectiveId, folder.name)
|
||||
store.add(insert, exists)
|
||||
}
|
||||
|
||||
def changeName(
|
||||
folder: Ident,
|
||||
account: AccountId,
|
||||
name: String
|
||||
): F[FolderChangeResult] =
|
||||
store.transact(QFolder.changeName(folder, account, name))
|
||||
|
||||
def addMember(
|
||||
folder: Ident,
|
||||
account: AccountId,
|
||||
member: Ident
|
||||
): F[FolderChangeResult] =
|
||||
store.transact(QFolder.addMember(folder, account, member))
|
||||
|
||||
def removeMember(
|
||||
folder: Ident,
|
||||
account: AccountId,
|
||||
member: Ident
|
||||
): F[FolderChangeResult] =
|
||||
store.transact(QFolder.removeMember(folder, account, member))
|
||||
|
||||
def delete(id: Ident, account: AccountId): F[FolderChangeResult] =
|
||||
store.transact(QFolder.delete(id, account))
|
||||
})
|
||||
}
|
@ -9,7 +9,7 @@ import docspell.backend.ops.OItemSearch._
|
||||
import docspell.common._
|
||||
import docspell.ftsclient._
|
||||
import docspell.store.Store
|
||||
import docspell.store.queries.QItem
|
||||
import docspell.store.queries.{QFolder, QItem}
|
||||
import docspell.store.queue.JobQueue
|
||||
import docspell.store.records.RJob
|
||||
|
||||
@ -30,7 +30,7 @@ trait OFulltext[F[_]] {
|
||||
|
||||
def findIndexOnly(
|
||||
fts: OFulltext.FtsInput,
|
||||
collective: Ident,
|
||||
account: AccountId,
|
||||
batch: Batch
|
||||
): F[Vector[OFulltext.FtsItemWithTags]]
|
||||
|
||||
@ -94,27 +94,29 @@ object OFulltext {
|
||||
|
||||
def findIndexOnly(
|
||||
ftsQ: OFulltext.FtsInput,
|
||||
collective: Ident,
|
||||
account: AccountId,
|
||||
batch: Batch
|
||||
): F[Vector[OFulltext.FtsItemWithTags]] = {
|
||||
val fq = FtsQuery(
|
||||
ftsQ.query,
|
||||
collective,
|
||||
account.collective,
|
||||
Set.empty,
|
||||
Set.empty,
|
||||
batch.limit,
|
||||
batch.offset,
|
||||
FtsQuery.HighlightSetting(ftsQ.highlightPre, ftsQ.highlightPost)
|
||||
)
|
||||
for {
|
||||
ftsR <- fts.search(fq)
|
||||
folders <- store.transact(QFolder.getMemberFolders(account))
|
||||
ftsR <- fts.search(fq.withFolders(folders))
|
||||
ftsItems = ftsR.results.groupBy(_.itemId)
|
||||
select = ftsR.results.map(r => QItem.SelectedItem(r.itemId, r.score)).toSet
|
||||
itemsWithTags <-
|
||||
store
|
||||
.transact(
|
||||
QItem.findItemsWithTags(
|
||||
collective,
|
||||
QItem.findSelectedItems(QItem.Query.empty(collective), select)
|
||||
account.collective,
|
||||
QItem.findSelectedItems(QItem.Query.empty(account), select)
|
||||
)
|
||||
)
|
||||
.take(batch.limit.toLong)
|
||||
@ -182,7 +184,8 @@ object OFulltext {
|
||||
val sqlResult = search(q, batch)
|
||||
val fq = FtsQuery(
|
||||
ftsQ.query,
|
||||
q.collective,
|
||||
q.account.collective,
|
||||
Set.empty,
|
||||
Set.empty,
|
||||
0,
|
||||
0,
|
||||
|
@ -24,6 +24,8 @@ trait OItem[F[_]] {
|
||||
|
||||
def setDirection(item: Ident, direction: Direction, collective: Ident): F[AddResult]
|
||||
|
||||
def setFolder(item: Ident, folder: Option[Ident], collective: Ident): F[AddResult]
|
||||
|
||||
def setCorrOrg(item: Ident, org: Option[Ident], collective: Ident): F[AddResult]
|
||||
|
||||
def addCorrOrg(item: Ident, org: OOrganization.OrgAndContacts): F[AddResult]
|
||||
@ -131,6 +133,19 @@ object OItem {
|
||||
.attempt
|
||||
.map(AddResult.fromUpdate)
|
||||
|
||||
def setFolder(
|
||||
item: Ident,
|
||||
folder: Option[Ident],
|
||||
collective: Ident
|
||||
): F[AddResult] =
|
||||
store
|
||||
.transact(RItem.updateFolder(item, collective, folder))
|
||||
.attempt
|
||||
.map(AddResult.fromUpdate)
|
||||
.flatTap(
|
||||
onSuccessIgnoreError(fts.updateFolder(logger, item, collective, folder))
|
||||
)
|
||||
|
||||
def setCorrOrg(item: Ident, org: Option[Ident], collective: Ident): F[AddResult] =
|
||||
store
|
||||
.transact(RItem.updateCorrOrg(item, collective, org))
|
||||
|
@ -107,7 +107,7 @@ object OItemSearch {
|
||||
val search = QItem.findItems(q, batch)
|
||||
store
|
||||
.transact(
|
||||
QItem.findItemsWithTags(q.collective, search).take(batch.limit.toLong)
|
||||
QItem.findItemsWithTags(q.account.collective, search).take(batch.limit.toLong)
|
||||
)
|
||||
.compile
|
||||
.toVector
|
||||
|
@ -58,6 +58,7 @@ object OUpload {
|
||||
case class UploadMeta(
|
||||
direction: Option[Direction],
|
||||
sourceAbbrev: String,
|
||||
folderId: Option[Ident],
|
||||
validFileTypes: Seq[MimeType]
|
||||
)
|
||||
|
||||
@ -123,6 +124,7 @@ object OUpload {
|
||||
lang.getOrElse(Language.German),
|
||||
data.meta.direction,
|
||||
data.meta.sourceAbbrev,
|
||||
data.meta.folderId,
|
||||
data.meta.validFileTypes
|
||||
)
|
||||
args =
|
||||
@ -147,7 +149,10 @@ object OUpload {
|
||||
(for {
|
||||
src <- OptionT(store.transact(RSource.find(sourceId)))
|
||||
updata = data.copy(
|
||||
meta = data.meta.copy(sourceAbbrev = src.abbrev),
|
||||
meta = data.meta.copy(
|
||||
sourceAbbrev = src.abbrev,
|
||||
folderId = data.meta.folderId.orElse(src.folderId)
|
||||
),
|
||||
priority = src.priority
|
||||
)
|
||||
accId = AccountId(src.cid, src.sid)
|
||||
|
@ -36,6 +36,7 @@ object ProcessItemArgs {
|
||||
language: Language,
|
||||
direction: Option[Direction],
|
||||
sourceAbbrev: String,
|
||||
folderId: Option[Ident],
|
||||
validFileTypes: Seq[MimeType]
|
||||
)
|
||||
|
||||
|
@ -27,7 +27,9 @@ case class ScanMailboxArgs(
|
||||
// delete the after submitting (only if targetFolder is None)
|
||||
deleteMail: Boolean,
|
||||
// set the direction when submitting
|
||||
direction: Option[Direction]
|
||||
direction: Option[Direction],
|
||||
// set a folder for items
|
||||
itemFolder: Option[Ident]
|
||||
)
|
||||
|
||||
object ScanMailboxArgs {
|
||||
|
@ -17,11 +17,12 @@ import org.log4s.getLogger
|
||||
*/
|
||||
trait FtsClient[F[_]] {
|
||||
|
||||
/** Initialization tasks. This is called exactly once and then never
|
||||
/** Initialization tasks. This is called exactly once at the very
|
||||
* beginning when initializing the full-text index and then never
|
||||
* again (except when re-indexing everything). It may be used to
|
||||
* setup the database.
|
||||
*/
|
||||
def initialize: F[Unit]
|
||||
def initialize: List[FtsMigration[F]]
|
||||
|
||||
/** Run a full-text search. */
|
||||
def search(q: FtsQuery): F[FtsResult]
|
||||
@ -57,7 +58,7 @@ trait FtsClient[F[_]] {
|
||||
collective: Ident,
|
||||
name: String
|
||||
): F[Unit] =
|
||||
updateIndex(logger, TextData.item(itemId, collective, Some(name), None))
|
||||
updateIndex(logger, TextData.item(itemId, collective, None, Some(name), None))
|
||||
|
||||
def updateItemNotes(
|
||||
logger: Logger[F],
|
||||
@ -67,7 +68,7 @@ trait FtsClient[F[_]] {
|
||||
): F[Unit] =
|
||||
updateIndex(
|
||||
logger,
|
||||
TextData.item(itemId, collective, None, Some(notes.getOrElse("")))
|
||||
TextData.item(itemId, collective, None, None, Some(notes.getOrElse("")))
|
||||
)
|
||||
|
||||
def updateAttachmentName(
|
||||
@ -83,12 +84,20 @@ trait FtsClient[F[_]] {
|
||||
itemId,
|
||||
attachId,
|
||||
collective,
|
||||
None,
|
||||
Language.English,
|
||||
Some(name.getOrElse("")),
|
||||
None
|
||||
)
|
||||
)
|
||||
|
||||
def updateFolder(
|
||||
logger: Logger[F],
|
||||
itemId: Ident,
|
||||
collective: Ident,
|
||||
folder: Option[Ident]
|
||||
): F[Unit]
|
||||
|
||||
def removeItem(logger: Logger[F], itemId: Ident): F[Unit]
|
||||
|
||||
def removeAttachment(logger: Logger[F], attachId: Ident): F[Unit]
|
||||
@ -107,8 +116,8 @@ object FtsClient {
|
||||
new FtsClient[F] {
|
||||
private[this] val logger = Logger.log4s[F](getLogger)
|
||||
|
||||
def initialize: F[Unit] =
|
||||
logger.info("Full-text search is disabled!")
|
||||
def initialize: List[FtsMigration[F]] =
|
||||
Nil
|
||||
|
||||
def search(q: FtsQuery): F[FtsResult] =
|
||||
logger.warn("Full-text search is disabled!") *> FtsResult.empty.pure[F]
|
||||
@ -116,6 +125,14 @@ object FtsClient {
|
||||
def updateIndex(logger: Logger[F], data: Stream[F, TextData]): F[Unit] =
|
||||
logger.warn("Full-text search is disabled!")
|
||||
|
||||
def updateFolder(
|
||||
logger: Logger[F],
|
||||
itemId: Ident,
|
||||
collective: Ident,
|
||||
folder: Option[Ident]
|
||||
): F[Unit] =
|
||||
logger.warn("Full-text search is disabled!")
|
||||
|
||||
def indexData(logger: Logger[F], data: Stream[F, TextData]): F[Unit] =
|
||||
logger.warn("Full-text search is disabled!")
|
||||
|
||||
|
@ -0,0 +1,24 @@
|
||||
package docspell.ftsclient
|
||||
|
||||
import docspell.common._
|
||||
|
||||
final case class FtsMigration[F[_]](
|
||||
version: Int,
|
||||
engine: Ident,
|
||||
description: String,
|
||||
task: F[FtsMigration.Result]
|
||||
)
|
||||
|
||||
object FtsMigration {
|
||||
|
||||
sealed trait Result
|
||||
object Result {
|
||||
case object WorkDone extends Result
|
||||
case object ReIndexAll extends Result
|
||||
case object IndexAll extends Result
|
||||
|
||||
def workDone: Result = WorkDone
|
||||
def reIndexAll: Result = ReIndexAll
|
||||
def indexAll: Result = IndexAll
|
||||
}
|
||||
}
|
@ -10,11 +10,16 @@ import docspell.common._
|
||||
* Searches must only look for given collective and in the given list
|
||||
* of item ids, if it is non-empty. If the item set is empty, then
|
||||
* don't restrict the result in this way.
|
||||
*
|
||||
* The set of folders must be used to restrict the results only to
|
||||
* items that have one of the folders set or no folder set. If the
|
||||
* set is empty, the restriction does not apply.
|
||||
*/
|
||||
final case class FtsQuery(
|
||||
q: String,
|
||||
collective: Ident,
|
||||
items: Set[Ident],
|
||||
folders: Set[Ident],
|
||||
limit: Int,
|
||||
offset: Int,
|
||||
highlight: FtsQuery.HighlightSetting
|
||||
@ -22,6 +27,9 @@ final case class FtsQuery(
|
||||
|
||||
def nextPage: FtsQuery =
|
||||
copy(offset = limit + offset)
|
||||
|
||||
def withFolders(fs: Set[Ident]): FtsQuery =
|
||||
copy(folders = fs)
|
||||
}
|
||||
|
||||
object FtsQuery {
|
||||
|
@ -10,6 +10,8 @@ sealed trait TextData {
|
||||
|
||||
def collective: Ident
|
||||
|
||||
def folder: Option[Ident]
|
||||
|
||||
final def fold[A](f: TextData.Attachment => A, g: TextData.Item => A): A =
|
||||
this match {
|
||||
case a: TextData.Attachment => f(a)
|
||||
@ -23,6 +25,7 @@ object TextData {
|
||||
item: Ident,
|
||||
attachId: Ident,
|
||||
collective: Ident,
|
||||
folder: Option[Ident],
|
||||
lang: Language,
|
||||
name: Option[String],
|
||||
text: Option[String]
|
||||
@ -36,15 +39,17 @@ object TextData {
|
||||
item: Ident,
|
||||
attachId: Ident,
|
||||
collective: Ident,
|
||||
folder: Option[Ident],
|
||||
lang: Language,
|
||||
name: Option[String],
|
||||
text: Option[String]
|
||||
): TextData =
|
||||
Attachment(item, attachId, collective, lang, name, text)
|
||||
Attachment(item, attachId, collective, folder, lang, name, text)
|
||||
|
||||
final case class Item(
|
||||
item: Ident,
|
||||
collective: Ident,
|
||||
folder: Option[Ident],
|
||||
name: Option[String],
|
||||
notes: Option[String]
|
||||
) extends TextData {
|
||||
@ -56,8 +61,9 @@ object TextData {
|
||||
def item(
|
||||
item: Ident,
|
||||
collective: Ident,
|
||||
folder: Option[Ident],
|
||||
name: Option[String],
|
||||
notes: Option[String]
|
||||
): TextData =
|
||||
Item(item, collective, name, notes)
|
||||
Item(item, collective, folder, name, notes)
|
||||
}
|
||||
|
@ -0,0 +1,9 @@
|
||||
package docspell.ftssolr
|
||||
|
||||
import docspell.common._
|
||||
|
||||
final case class DocIdResult(ids: List[Ident]) {
|
||||
|
||||
def toSetFolder(folder: Option[Ident]): List[SetFolder] =
|
||||
ids.map(id => SetFolder(id, folder))
|
||||
}
|
@ -25,6 +25,7 @@ object Field {
|
||||
val content_en = Field("content_en")
|
||||
val itemName = Field("itemName")
|
||||
val itemNotes = Field("itemNotes")
|
||||
val folderId = Field("folder")
|
||||
|
||||
def contentField(lang: Language): Field =
|
||||
lang match {
|
||||
|
@ -1,5 +1,7 @@
|
||||
package docspell.ftssolr
|
||||
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common._
|
||||
import docspell.ftsclient._
|
||||
|
||||
@ -21,6 +23,7 @@ trait JsonCodec {
|
||||
(Field.id.name, enc(td.id)),
|
||||
(Field.itemId.name, enc(td.item)),
|
||||
(Field.collectiveId.name, enc(td.collective)),
|
||||
(Field.folderId.name, td.folder.getOrElse(Ident.unsafe("")).asJson),
|
||||
(Field.attachmentId.name, enc(td.attachId)),
|
||||
(Field.attachmentName.name, Json.fromString(td.name.getOrElse(""))),
|
||||
(Field.discriminator.name, Json.fromString("attachment"))
|
||||
@ -37,6 +40,7 @@ trait JsonCodec {
|
||||
(Field.id.name, enc(td.id)),
|
||||
(Field.itemId.name, enc(td.item)),
|
||||
(Field.collectiveId.name, enc(td.collective)),
|
||||
(Field.folderId.name, td.folder.getOrElse(Ident.unsafe("")).asJson),
|
||||
(Field.itemName.name, Json.fromString(td.name.getOrElse(""))),
|
||||
(Field.itemNotes.name, Json.fromString(td.notes.getOrElse(""))),
|
||||
(Field.discriminator.name, Json.fromString("item"))
|
||||
@ -49,6 +53,18 @@ trait JsonCodec {
|
||||
): Encoder[TextData] =
|
||||
Encoder(_.fold(ae.apply, ie.apply))
|
||||
|
||||
implicit def docIdResultsDecoder: Decoder[DocIdResult] =
|
||||
new Decoder[DocIdResult] {
|
||||
final def apply(c: HCursor): Decoder.Result[DocIdResult] =
|
||||
c.downField("response")
|
||||
.downField("docs")
|
||||
.values
|
||||
.getOrElse(Nil)
|
||||
.toList
|
||||
.traverse(_.hcursor.get[Ident](Field.id.name))
|
||||
.map(DocIdResult.apply)
|
||||
}
|
||||
|
||||
implicit def ftsResultDecoder: Decoder[FtsResult] =
|
||||
new Decoder[FtsResult] {
|
||||
final def apply(c: HCursor): Decoder.Result[FtsResult] =
|
||||
@ -89,6 +105,12 @@ trait JsonCodec {
|
||||
} yield md
|
||||
}
|
||||
|
||||
implicit def decodeEverythingToUnit: Decoder[Unit] =
|
||||
new Decoder[Unit] {
|
||||
final def apply(c: HCursor): Decoder.Result[Unit] =
|
||||
Right(())
|
||||
}
|
||||
|
||||
implicit def identKeyEncoder: KeyEncoder[Ident] =
|
||||
new KeyEncoder[Ident] {
|
||||
override def apply(ident: Ident): String = ident.id
|
||||
@ -129,9 +151,24 @@ trait JsonCodec {
|
||||
}
|
||||
}
|
||||
|
||||
implicit def textDataEncoder: Encoder[SetFields] =
|
||||
implicit def setTextDataFieldsEncoder: Encoder[SetFields] =
|
||||
Encoder(_.td.fold(setAttachmentEncoder.apply, setItemEncoder.apply))
|
||||
|
||||
implicit def setFolderEncoder(implicit
|
||||
enc: Encoder[Option[Ident]]
|
||||
): Encoder[SetFolder] =
|
||||
new Encoder[SetFolder] {
|
||||
final def apply(td: SetFolder): Json =
|
||||
Json.fromFields(
|
||||
List(
|
||||
(Field.id.name, td.docId.asJson),
|
||||
(
|
||||
Field.folderId.name,
|
||||
Map("set" -> td.folder.asJson).asJson
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object JsonCodec extends JsonCodec
|
||||
|
@ -40,16 +40,26 @@ object QueryData {
|
||||
fields: List[Field],
|
||||
fq: FtsQuery
|
||||
): QueryData = {
|
||||
val q = sanitize(fq.q)
|
||||
val extQ = search.map(f => s"${f.name}:($q)").mkString(" OR ")
|
||||
val items = fq.items.map(_.id).mkString(" ")
|
||||
val collQ = s"""${Field.collectiveId.name}:"${fq.collective.id}""""
|
||||
val filterQ = fq.items match {
|
||||
case s if s.isEmpty =>
|
||||
collQ
|
||||
case _ =>
|
||||
(collQ :: List(s"""${Field.itemId.name}:($items)""")).mkString(" AND ")
|
||||
}
|
||||
val q = sanitize(fq.q)
|
||||
val extQ = search.map(f => s"${f.name}:($q)").mkString(" OR ")
|
||||
val items = fq.items.map(_.id).mkString(" ")
|
||||
val folders = fq.folders.map(_.id).mkString(" ")
|
||||
val filterQ = List(
|
||||
s"""${Field.collectiveId.name}:"${fq.collective.id}"""",
|
||||
fq.items match {
|
||||
case s if s.isEmpty =>
|
||||
""
|
||||
case _ =>
|
||||
s"""${Field.itemId.name}:($items)"""
|
||||
},
|
||||
fq.folders match {
|
||||
case s if s.isEmpty =>
|
||||
""
|
||||
case _ =>
|
||||
s"""${Field.folderId.name}:($folders) OR (*:* NOT ${Field.folderId.name}:*)"""
|
||||
}
|
||||
).filterNot(_.isEmpty).map(t => s"($t)").mkString(" AND ")
|
||||
|
||||
QueryData(
|
||||
extQ,
|
||||
filterQ,
|
||||
|
@ -0,0 +1,5 @@
|
||||
package docspell.ftssolr
|
||||
|
||||
import docspell.common._
|
||||
|
||||
final case class SetFolder(docId: Ident, folder: Option[Ident])
|
@ -17,7 +17,7 @@ final class SolrFtsClient[F[_]: Effect](
|
||||
solrQuery: SolrQuery[F]
|
||||
) extends FtsClient[F] {
|
||||
|
||||
def initialize: F[Unit] =
|
||||
def initialize: List[FtsMigration[F]] =
|
||||
solrSetup.setupSchema
|
||||
|
||||
def search(q: FtsQuery): F[FtsResult] =
|
||||
@ -29,6 +29,17 @@ final class SolrFtsClient[F[_]: Effect](
|
||||
def updateIndex(logger: Logger[F], data: Stream[F, TextData]): F[Unit] =
|
||||
modifyIndex(logger, data)(solrUpdate.update)
|
||||
|
||||
def updateFolder(
|
||||
logger: Logger[F],
|
||||
itemId: Ident,
|
||||
collective: Ident,
|
||||
folder: Option[Ident]
|
||||
): F[Unit] =
|
||||
logger.debug(
|
||||
s"Update folder in solr index for coll/item ${collective.id}/${itemId.id}"
|
||||
) *>
|
||||
solrUpdate.updateFolder(itemId, collective, folder)
|
||||
|
||||
def modifyIndex(logger: Logger[F], data: Stream[F, TextData])(
|
||||
f: List[TextData] => F[Unit]
|
||||
): F[Unit] =
|
||||
|
@ -4,6 +4,7 @@ import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common._
|
||||
import docspell.ftsclient.FtsMigration
|
||||
|
||||
import _root_.io.circe._
|
||||
import _root_.io.circe.generic.semiauto._
|
||||
@ -15,21 +16,48 @@ import org.http4s.client.dsl.Http4sClientDsl
|
||||
|
||||
trait SolrSetup[F[_]] {
|
||||
|
||||
def setupSchema: F[Unit]
|
||||
def setupSchema: List[FtsMigration[F]]
|
||||
|
||||
}
|
||||
|
||||
object SolrSetup {
|
||||
private val solrEngine = Ident.unsafe("solr")
|
||||
|
||||
def apply[F[_]: ConcurrentEffect](cfg: SolrConfig, client: Client[F]): SolrSetup[F] = {
|
||||
val dsl = new Http4sClientDsl[F] {}
|
||||
import dsl._
|
||||
|
||||
new SolrSetup[F] {
|
||||
|
||||
val url = (Uri.unsafeFromString(cfg.url.asString) / "schema")
|
||||
.withQueryParam("commitWithin", cfg.commitWithin.toString)
|
||||
|
||||
def setupSchema: F[Unit] = {
|
||||
def setupSchema: List[FtsMigration[F]] =
|
||||
List(
|
||||
FtsMigration[F](
|
||||
1,
|
||||
solrEngine,
|
||||
"Initialize",
|
||||
setupCoreSchema.map(_ => FtsMigration.Result.workDone)
|
||||
),
|
||||
FtsMigration[F](
|
||||
3,
|
||||
solrEngine,
|
||||
"Add folder field",
|
||||
addFolderField.map(_ => FtsMigration.Result.workDone)
|
||||
),
|
||||
FtsMigration[F](
|
||||
4,
|
||||
solrEngine,
|
||||
"Index all from database",
|
||||
FtsMigration.Result.indexAll.pure[F]
|
||||
)
|
||||
)
|
||||
|
||||
def addFolderField: F[Unit] =
|
||||
addStringField(Field.folderId)
|
||||
|
||||
def setupCoreSchema: F[Unit] = {
|
||||
val cmds0 =
|
||||
List(
|
||||
Field.id,
|
||||
|
@ -1,13 +1,16 @@
|
||||
package docspell.ftssolr
|
||||
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common._
|
||||
import docspell.ftsclient._
|
||||
import docspell.ftssolr.JsonCodec._
|
||||
|
||||
import _root_.io.circe._
|
||||
import _root_.io.circe.syntax._
|
||||
import org.http4s._
|
||||
import org.http4s.circe.CirceEntityDecoder._
|
||||
import org.http4s.circe._
|
||||
import org.http4s.client.Client
|
||||
import org.http4s.client.dsl.Http4sClientDsl
|
||||
@ -18,6 +21,8 @@ trait SolrUpdate[F[_]] {
|
||||
|
||||
def update(tds: List[TextData]): F[Unit]
|
||||
|
||||
def updateFolder(itemId: Ident, collective: Ident, folder: Option[Ident]): F[Unit]
|
||||
|
||||
def delete(q: String, commitWithin: Option[Int]): F[Unit]
|
||||
}
|
||||
|
||||
@ -43,6 +48,29 @@ object SolrUpdate {
|
||||
client.expect[Unit](req)
|
||||
}
|
||||
|
||||
def updateFolder(
|
||||
itemId: Ident,
|
||||
collective: Ident,
|
||||
folder: Option[Ident]
|
||||
): F[Unit] = {
|
||||
val queryUrl = Uri.unsafeFromString(cfg.url.asString) / "query"
|
||||
val q = QueryData(
|
||||
"*:*",
|
||||
s"${Field.itemId.name}:${itemId.id} AND ${Field.collectiveId.name}:${collective.id}",
|
||||
Int.MaxValue,
|
||||
0,
|
||||
List(Field.id),
|
||||
Map.empty
|
||||
)
|
||||
val searchReq = Method.POST(q.asJson, queryUrl)
|
||||
for {
|
||||
docIds <- client.expect[DocIdResult](searchReq)
|
||||
sets = docIds.toSetFolder(folder)
|
||||
req = Method.POST(sets.asJson, url)
|
||||
_ <- client.expect[Unit](req)
|
||||
} yield ()
|
||||
}
|
||||
|
||||
def delete(q: String, commitWithin: Option[Int]): F[Unit] = {
|
||||
val uri = commitWithin match {
|
||||
case Some(n) =>
|
||||
|
@ -84,6 +84,7 @@ object JoexAppImpl {
|
||||
joex <- OJoex(client, store)
|
||||
upload <- OUpload(store, queue, cfg.files, joex)
|
||||
fts <- createFtsClient(cfg)(httpClient)
|
||||
itemOps <- OItem(store, fts)
|
||||
javaEmil =
|
||||
JavaMailEmil(blocker, Settings.defaultSettings.copy(debug = cfg.mailDebug))
|
||||
sch <- SchedulerBuilder(cfg.scheduler, blocker, store)
|
||||
@ -91,7 +92,7 @@ object JoexAppImpl {
|
||||
.withTask(
|
||||
JobTask.json(
|
||||
ProcessItemArgs.taskName,
|
||||
ItemHandler.newItem[F](cfg, fts),
|
||||
ItemHandler.newItem[F](cfg, itemOps, fts),
|
||||
ItemHandler.onCancel[F]
|
||||
)
|
||||
)
|
||||
|
@ -1,9 +1,8 @@
|
||||
package docspell.joex.fts
|
||||
|
||||
import cats._
|
||||
import cats.data.{Kleisli, NonEmptyList}
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
import cats.{ApplicativeError, FlatMap, Semigroup}
|
||||
|
||||
import docspell.common._
|
||||
import docspell.ftsclient._
|
||||
@ -15,6 +14,19 @@ object FtsWork {
|
||||
def apply[F[_]](f: FtsContext[F] => F[Unit]): FtsWork[F] =
|
||||
Kleisli(f)
|
||||
|
||||
def allInitializeTasks[F[_]: Monad]: FtsWork[F] =
|
||||
FtsWork[F](_ => ().pure[F]).tap[FtsContext[F]].flatMap { ctx =>
|
||||
NonEmptyList.fromList(ctx.fts.initialize.map(fm => from[F](fm.task))) match {
|
||||
case Some(nel) =>
|
||||
nel.reduce(semigroup[F])
|
||||
case None =>
|
||||
FtsWork[F](_ => ().pure[F])
|
||||
}
|
||||
}
|
||||
|
||||
def from[F[_]: FlatMap: Applicative](t: F[FtsMigration.Result]): FtsWork[F] =
|
||||
Kleisli.liftF(t).flatMap(transformResult[F])
|
||||
|
||||
def all[F[_]: FlatMap](
|
||||
m0: FtsWork[F],
|
||||
mn: FtsWork[F]*
|
||||
@ -24,14 +36,25 @@ object FtsWork {
|
||||
implicit def semigroup[F[_]: FlatMap]: Semigroup[FtsWork[F]] =
|
||||
Semigroup.instance((mt1, mt2) => mt1.flatMap(_ => mt2))
|
||||
|
||||
private def transformResult[F[_]: Applicative: FlatMap](
|
||||
r: FtsMigration.Result
|
||||
): FtsWork[F] =
|
||||
r match {
|
||||
case FtsMigration.Result.WorkDone =>
|
||||
Kleisli.pure(())
|
||||
|
||||
case FtsMigration.Result.IndexAll =>
|
||||
insertAll[F](None)
|
||||
|
||||
case FtsMigration.Result.ReIndexAll =>
|
||||
clearIndex[F](None) >> insertAll[F](None)
|
||||
}
|
||||
|
||||
// some tasks
|
||||
|
||||
def log[F[_]](f: Logger[F] => F[Unit]): FtsWork[F] =
|
||||
FtsWork(ctx => f(ctx.logger))
|
||||
|
||||
def initialize[F[_]]: FtsWork[F] =
|
||||
FtsWork(_.fts.initialize)
|
||||
|
||||
def clearIndex[F[_]](coll: Option[Ident]): FtsWork[F] =
|
||||
coll match {
|
||||
case Some(cid) =>
|
||||
@ -40,7 +63,7 @@ object FtsWork {
|
||||
FtsWork(ctx => ctx.fts.clearAll(ctx.logger))
|
||||
}
|
||||
|
||||
def insertAll[F[_]: Effect](coll: Option[Ident]): FtsWork[F] =
|
||||
def insertAll[F[_]: FlatMap](coll: Option[Ident]): FtsWork[F] =
|
||||
FtsWork
|
||||
.all(
|
||||
FtsWork(ctx =>
|
||||
@ -57,6 +80,7 @@ object FtsWork {
|
||||
caa.item,
|
||||
caa.id,
|
||||
caa.collective,
|
||||
caa.folder,
|
||||
caa.lang,
|
||||
caa.name,
|
||||
caa.content
|
||||
@ -69,7 +93,9 @@ object FtsWork {
|
||||
ctx.logger,
|
||||
ctx.store
|
||||
.transact(QItem.allNameAndNotes(coll, ctx.cfg.migration.indexAllChunk * 5))
|
||||
.map(nn => TextData.item(nn.id, nn.collective, Option(nn.name), nn.notes))
|
||||
.map(nn =>
|
||||
TextData.item(nn.id, nn.collective, nn.folder, Option(nn.name), nn.notes)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -1,9 +1,9 @@
|
||||
package docspell.joex.fts
|
||||
|
||||
import cats.Traverse
|
||||
import cats.data.{Kleisli, OptionT}
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
import cats.{Applicative, FlatMap, Traverse}
|
||||
|
||||
import docspell.common._
|
||||
import docspell.ftsclient._
|
||||
@ -20,6 +20,9 @@ case class Migration[F[_]](
|
||||
|
||||
object Migration {
|
||||
|
||||
def from[F[_]: Applicative: FlatMap](fm: FtsMigration[F]): Migration[F] =
|
||||
Migration(fm.version, fm.engine, fm.description, FtsWork.from(fm.task))
|
||||
|
||||
def apply[F[_]: Effect](
|
||||
cfg: Config.FullTextSearch,
|
||||
fts: FtsClient[F],
|
||||
|
@ -21,7 +21,7 @@ object MigrationTask {
|
||||
.flatMap(_ =>
|
||||
Task(ctx =>
|
||||
Migration[F](cfg, fts, ctx.store, ctx.logger)
|
||||
.run(migrationTasks[F])
|
||||
.run(migrationTasks[F](fts))
|
||||
)
|
||||
)
|
||||
|
||||
@ -44,11 +44,7 @@ object MigrationTask {
|
||||
Some(DocspellSystem.migrationTaskTracker)
|
||||
)
|
||||
|
||||
private val solrEngine = Ident.unsafe("solr")
|
||||
def migrationTasks[F[_]: Effect]: List[Migration[F]] =
|
||||
List(
|
||||
Migration[F](1, solrEngine, "initialize", FtsWork.initialize[F]),
|
||||
Migration[F](2, solrEngine, "Index all from database", FtsWork.insertAll[F](None))
|
||||
)
|
||||
def migrationTasks[F[_]: Effect](fts: FtsClient[F]): List[Migration[F]] =
|
||||
fts.initialize.map(fm => Migration.from(fm))
|
||||
|
||||
}
|
||||
|
@ -21,13 +21,7 @@ object ReIndexTask {
|
||||
Task
|
||||
.log[F, Args](_.info(s"Running full-text re-index now"))
|
||||
.flatMap(_ =>
|
||||
Task(ctx =>
|
||||
(clearData[F](ctx.args.collective) ++
|
||||
FtsWork.log[F](_.info("Inserting data from database")) ++
|
||||
FtsWork.insertAll[F](
|
||||
ctx.args.collective
|
||||
)).forContext(cfg, fts).run(ctx)
|
||||
)
|
||||
Task(ctx => clearData[F](ctx.args.collective).forContext(cfg, fts).run(ctx))
|
||||
)
|
||||
|
||||
def onCancel[F[_]: Sync]: Task[F, Args, Unit] =
|
||||
@ -41,7 +35,9 @@ object ReIndexTask {
|
||||
.clearIndex(collective)
|
||||
.recoverWith(
|
||||
FtsWork.log[F](_.info("Clearing data failed. Continue re-indexing."))
|
||||
)
|
||||
) ++
|
||||
FtsWork.log[F](_.info("Inserting data from database")) ++
|
||||
FtsWork.insertAll[F](collective)
|
||||
|
||||
case None =>
|
||||
FtsWork
|
||||
@ -50,6 +46,6 @@ object ReIndexTask {
|
||||
FtsWork.log[F](_.info("Clearing data failed. Continue re-indexing."))
|
||||
) ++
|
||||
FtsWork.log[F](_.info("Running index initialize")) ++
|
||||
FtsWork.initialize[F]
|
||||
FtsWork.allInitializeTasks[F]
|
||||
})
|
||||
}
|
||||
|
@ -71,7 +71,7 @@ object NotifyDueItemsTask {
|
||||
now <- Timestamp.current[F]
|
||||
q =
|
||||
QItem.Query
|
||||
.empty(ctx.args.account.collective)
|
||||
.empty(ctx.args.account)
|
||||
.copy(
|
||||
states = ItemState.validStates.toList,
|
||||
tagsInclude = ctx.args.tagsInclude,
|
||||
|
@ -251,7 +251,6 @@ object ExtractArchive {
|
||||
this
|
||||
case Some(nel) =>
|
||||
val sorted = nel.sorted
|
||||
println(s"---------------------------- $sorted ")
|
||||
val offset = sorted.head.first
|
||||
val pos =
|
||||
sorted.zipWithIndex.map({ case (p, i) => p.id -> (i + offset) }).toList.toMap
|
||||
|
@ -5,6 +5,7 @@ import cats.effect._
|
||||
import cats.implicits._
|
||||
import fs2.Stream
|
||||
|
||||
import docspell.backend.ops.OItem
|
||||
import docspell.common.{ItemState, ProcessItemArgs}
|
||||
import docspell.ftsclient.FtsClient
|
||||
import docspell.joex.Config
|
||||
@ -27,11 +28,12 @@ object ItemHandler {
|
||||
|
||||
def newItem[F[_]: ConcurrentEffect: ContextShift](
|
||||
cfg: Config,
|
||||
itemOps: OItem[F],
|
||||
fts: FtsClient[F]
|
||||
): Task[F, Args, Unit] =
|
||||
CreateItem[F]
|
||||
.flatMap(itemStateTask(ItemState.Processing))
|
||||
.flatMap(safeProcess[F](cfg, fts))
|
||||
.flatMap(safeProcess[F](cfg, itemOps, fts))
|
||||
.map(_ => ())
|
||||
|
||||
def itemStateTask[F[_]: Sync, A](
|
||||
@ -48,11 +50,12 @@ object ItemHandler {
|
||||
|
||||
def safeProcess[F[_]: ConcurrentEffect: ContextShift](
|
||||
cfg: Config,
|
||||
itemOps: OItem[F],
|
||||
fts: FtsClient[F]
|
||||
)(data: ItemData): Task[F, Args, ItemData] =
|
||||
isLastRetry[F].flatMap {
|
||||
case true =>
|
||||
ProcessItem[F](cfg, fts)(data).attempt.flatMap({
|
||||
ProcessItem[F](cfg, itemOps, fts)(data).attempt.flatMap({
|
||||
case Right(d) =>
|
||||
Task.pure(d)
|
||||
case Left(ex) =>
|
||||
@ -62,7 +65,7 @@ object ItemHandler {
|
||||
.andThen(_ => Sync[F].raiseError(ex))
|
||||
})
|
||||
case false =>
|
||||
ProcessItem[F](cfg, fts)(data).flatMap(itemStateTask(ItemState.Created))
|
||||
ProcessItem[F](cfg, itemOps, fts)(data).flatMap(itemStateTask(ItemState.Created))
|
||||
}
|
||||
|
||||
private def markItemCreated[F[_]: Sync]: Task[F, Args, Boolean] =
|
||||
|
@ -2,6 +2,7 @@ package docspell.joex.process
|
||||
|
||||
import cats.effect._
|
||||
|
||||
import docspell.backend.ops.OItem
|
||||
import docspell.common.ProcessItemArgs
|
||||
import docspell.ftsclient.FtsClient
|
||||
import docspell.joex.Config
|
||||
@ -11,6 +12,7 @@ object ProcessItem {
|
||||
|
||||
def apply[F[_]: ConcurrentEffect: ContextShift](
|
||||
cfg: Config,
|
||||
itemOps: OItem[F],
|
||||
fts: FtsClient[F]
|
||||
)(item: ItemData): Task[F, ProcessItemArgs, ItemData] =
|
||||
ExtractArchive(item)
|
||||
@ -22,6 +24,7 @@ object ProcessItem {
|
||||
.flatMap(analysisOnly[F](cfg))
|
||||
.flatMap(Task.setProgress(80))
|
||||
.flatMap(LinkProposal[F])
|
||||
.flatMap(SetGivenData[F](itemOps))
|
||||
.flatMap(Task.setProgress(99))
|
||||
|
||||
def analysisOnly[F[_]: Sync](
|
||||
|
@ -0,0 +1,35 @@
|
||||
package docspell.joex.process
|
||||
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.backend.ops.OItem
|
||||
import docspell.common._
|
||||
import docspell.joex.scheduler.Task
|
||||
|
||||
object SetGivenData {
|
||||
|
||||
def apply[F[_]: Sync](
|
||||
ops: OItem[F]
|
||||
)(data: ItemData): Task[F, ProcessItemArgs, ItemData] =
|
||||
if (data.item.state.isValid)
|
||||
Task
|
||||
.log[F, ProcessItemArgs](_.debug(s"Not setting data on existing item"))
|
||||
.map(_ => data)
|
||||
else
|
||||
Task { ctx =>
|
||||
val itemId = data.item.id
|
||||
val folderId = ctx.args.meta.folderId
|
||||
val collective = ctx.args.meta.collective
|
||||
for {
|
||||
_ <- ctx.logger.info("Starting setting given data")
|
||||
_ <- ctx.logger.debug(s"Set item folder: '${folderId.map(_.id)}'")
|
||||
e <- ops.setFolder(itemId, folderId, collective).attempt
|
||||
_ <- e.fold(
|
||||
ex => ctx.logger.warn(s"Error setting folder: ${ex.getMessage}"),
|
||||
_ => ().pure[F]
|
||||
)
|
||||
} yield data
|
||||
}
|
||||
|
||||
}
|
@ -33,8 +33,13 @@ object TextExtraction {
|
||||
)
|
||||
_ <- ctx.logger.debug("Storing extracted texts")
|
||||
_ <- txt.toList.traverse(rm => ctx.store.transact(RAttachmentMeta.upsert(rm._1)))
|
||||
idxItem =
|
||||
TextData.item(item.item.id, ctx.args.meta.collective, item.item.name.some, None)
|
||||
idxItem = TextData.item(
|
||||
item.item.id,
|
||||
ctx.args.meta.collective,
|
||||
None, //folder
|
||||
item.item.name.some,
|
||||
None
|
||||
)
|
||||
_ <- fts.indexData(ctx.logger, (idxItem +: txt.map(_._2)).toSeq: _*)
|
||||
dur <- start
|
||||
_ <- ctx.logger.info(s"Text extraction finished in ${dur.formatExact}")
|
||||
@ -55,6 +60,7 @@ object TextExtraction {
|
||||
item.item.id,
|
||||
ra.id,
|
||||
collective,
|
||||
None, //folder
|
||||
lang,
|
||||
ra.name,
|
||||
rm.content
|
||||
|
@ -143,7 +143,7 @@ object ScanMailboxTask {
|
||||
folder <- requireFolder(a)(name)
|
||||
search <- searchMails(a)(folder)
|
||||
headers <- Kleisli.liftF(filterMessageIds(search.mails))
|
||||
_ <- headers.traverse(handleOne(a, upload))
|
||||
_ <- headers.traverse(handleOne(ctx.args, a, upload))
|
||||
} yield ScanResult(name, search.mails.size, search.count - search.mails.size)
|
||||
|
||||
def requireFolder[C](a: Access[F, C])(name: String): MailOp[F, C, MailFolder] =
|
||||
@ -239,7 +239,9 @@ object ScanMailboxTask {
|
||||
MailOp.pure(())
|
||||
}
|
||||
|
||||
def submitMail(upload: OUpload[F])(mail: Mail[F]): F[OUpload.UploadResult] = {
|
||||
def submitMail(upload: OUpload[F], args: Args)(
|
||||
mail: Mail[F]
|
||||
): F[OUpload.UploadResult] = {
|
||||
val file = OUpload.File(
|
||||
Some(mail.header.subject + ".eml"),
|
||||
Some(MimeType.emls.head),
|
||||
@ -251,6 +253,7 @@ object ScanMailboxTask {
|
||||
meta = OUpload.UploadMeta(
|
||||
Some(dir),
|
||||
s"mailbox-${ctx.args.account.user.id}",
|
||||
args.itemFolder,
|
||||
Seq.empty
|
||||
)
|
||||
data = OUpload.UploadData(
|
||||
@ -264,14 +267,14 @@ object ScanMailboxTask {
|
||||
} yield res
|
||||
}
|
||||
|
||||
def handleOne[C](a: Access[F, C], upload: OUpload[F])(
|
||||
def handleOne[C](args: Args, a: Access[F, C], upload: OUpload[F])(
|
||||
mh: MailHeader
|
||||
): MailOp[F, C, Unit] =
|
||||
for {
|
||||
mail <- a.loadMail(mh)
|
||||
res <- mail match {
|
||||
case Some(m) =>
|
||||
Kleisli.liftF(submitMail(upload)(m).attempt)
|
||||
Kleisli.liftF(submitMail(upload, args)(m).attempt)
|
||||
case None =>
|
||||
MailOp.pure[F, C, Either[Throwable, OUpload.UploadResult]](
|
||||
Either.left(new Exception(s"Mail not found"))
|
||||
|
@ -144,6 +144,7 @@ structure:
|
||||
```
|
||||
{ multiple: Bool
|
||||
, direction: Maybe String
|
||||
, folder: Maybe String
|
||||
}
|
||||
```
|
||||
|
||||
@ -156,6 +157,11 @@ Furthermore, the direction of the document (one of `incoming` or
|
||||
`outgoing`) can be given. It is optional, it can be left out or
|
||||
`null`.
|
||||
|
||||
A `folder` id can be specified. Each item created by this request will
|
||||
be placed into this folder. Errors are logged (for example, the folder
|
||||
may have been deleted before the task is executed) and the item is
|
||||
then not put into any folder.
|
||||
|
||||
This kind of request is very common and most programming languages
|
||||
have support for this. For example, here is another curl command
|
||||
uploading two files with meta data:
|
||||
|
@ -795,6 +795,140 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/BasicResult"
|
||||
/sec/folder:
|
||||
get:
|
||||
tags: [ Folder ]
|
||||
summary: Get a list of folders.
|
||||
description: |
|
||||
Return a list of folders for the current collective.
|
||||
|
||||
All folders are returned, including those not owned by the
|
||||
current user.
|
||||
|
||||
It is possible to restrict the results by a substring match of
|
||||
the name.
|
||||
security:
|
||||
- authTokenHeader: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/q"
|
||||
- $ref: "#/components/parameters/owning"
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/FolderList"
|
||||
post:
|
||||
tags: [ Folder ]
|
||||
summary: Create a new folder
|
||||
description: |
|
||||
Create a new folder owned by the current user. If a folder with
|
||||
the same name already exists, an error is thrown.
|
||||
security:
|
||||
- authTokenHeader: []
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/NewFolder"
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/IdResult"
|
||||
/sec/folder/{id}:
|
||||
get:
|
||||
tags: [ Folder ]
|
||||
summary: Get folder details.
|
||||
description: |
|
||||
Return details about a folder.
|
||||
security:
|
||||
- authTokenHeader: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/id"
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/FolderDetail"
|
||||
put:
|
||||
tags: [ Folder ]
|
||||
summary: Change the name of a folder
|
||||
description: |
|
||||
Changes the name of a folder. The new name must not exists.
|
||||
security:
|
||||
- authTokenHeader: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/id"
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/NewFolder"
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/BasicResult"
|
||||
delete:
|
||||
tags: [ Folder ]
|
||||
summary: Delete a folder by its id.
|
||||
description: |
|
||||
Deletes a folder.
|
||||
security:
|
||||
- authTokenHeader: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/id"
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/BasicResult"
|
||||
/sec/folder/{id}/member/{userId}:
|
||||
put:
|
||||
tags: [ Folder ]
|
||||
summary: Add a member to this folder
|
||||
description: |
|
||||
Adds a member to this folder (identified by `id`).
|
||||
security:
|
||||
- authTokenHeader: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/id"
|
||||
- $ref: "#/components/parameters/userId"
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/BasicResult"
|
||||
delete:
|
||||
tags: [ Folder ]
|
||||
summary: Removes a member from this folder.
|
||||
description: |
|
||||
Removes a member from this folder.
|
||||
security:
|
||||
- authTokenHeader: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/id"
|
||||
- $ref: "#/components/parameters/userId"
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/BasicResult"
|
||||
|
||||
/sec/collective:
|
||||
get:
|
||||
tags: [ Collective ]
|
||||
@ -850,7 +984,7 @@ paths:
|
||||
summary: Get some insights regarding your items.
|
||||
description: |
|
||||
Returns some information about how many items there are, how
|
||||
much space they occupy etc.
|
||||
much folder they occupy etc.
|
||||
security:
|
||||
- authTokenHeader: []
|
||||
responses:
|
||||
@ -1231,6 +1365,31 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/BasicResult"
|
||||
/sec/item/{id}/folder:
|
||||
put:
|
||||
tags: [ Item ]
|
||||
summary: Set a folder for this item.
|
||||
description: |
|
||||
Updates the folder property for this item to "place" the item
|
||||
into a folder. If the request contains an empty object or an
|
||||
`id` property of `null`, the item is moved into the "public"
|
||||
or "root" folder.
|
||||
security:
|
||||
- authTokenHeader: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/id"
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/OptionalId"
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/BasicResult"
|
||||
/sec/item/{id}/corrOrg:
|
||||
put:
|
||||
tags: [ Item ]
|
||||
@ -2358,6 +2517,90 @@ paths:
|
||||
|
||||
components:
|
||||
schemas:
|
||||
FolderList:
|
||||
description: |
|
||||
A list of folders with their member counts.
|
||||
required:
|
||||
- items
|
||||
properties:
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/FolderItem"
|
||||
FolderItem:
|
||||
description: |
|
||||
An item in a folder list.
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- owner
|
||||
- created
|
||||
- isMember
|
||||
- memberCount
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: ident
|
||||
name:
|
||||
type: string
|
||||
owner:
|
||||
$ref: "#/components/schemas/IdName"
|
||||
created:
|
||||
type: integer
|
||||
format: date-time
|
||||
isMember:
|
||||
type: boolean
|
||||
memberCount:
|
||||
type: integer
|
||||
format: int32
|
||||
NewFolder:
|
||||
description: |
|
||||
Data required to create a new folder.
|
||||
required:
|
||||
- name
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
FolderDetail:
|
||||
description: |
|
||||
Details about a folder.
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- owner
|
||||
- created
|
||||
- isMember
|
||||
- memberCount
|
||||
- members
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: ident
|
||||
name:
|
||||
type: string
|
||||
owner:
|
||||
$ref: "#/components/schemas/IdName"
|
||||
created:
|
||||
type: integer
|
||||
format: date-time
|
||||
isMember:
|
||||
type: boolean
|
||||
memberCount:
|
||||
type: integer
|
||||
format: int32
|
||||
members:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/IdName"
|
||||
FolderMember:
|
||||
description: |
|
||||
Information to add or remove a folder member.
|
||||
required:
|
||||
- userId
|
||||
properties:
|
||||
userId:
|
||||
type: string
|
||||
format: ident
|
||||
ItemFtsSearch:
|
||||
description: |
|
||||
Query description for a full-text only search.
|
||||
@ -2451,6 +2694,13 @@ components:
|
||||
The direction to apply to items resulting from importing
|
||||
mails. If not set, the value is guessed based on the from
|
||||
and to mail headers and your address book.
|
||||
itemFolder:
|
||||
type: string
|
||||
format: ident
|
||||
description: |
|
||||
The folder id that is applied to items resulting from
|
||||
importing mails. If the folder id is not valid when the
|
||||
task executes, items have no folder set.
|
||||
ImapSettingsList:
|
||||
description: |
|
||||
A list of user email settings.
|
||||
@ -2949,6 +3199,8 @@ components:
|
||||
$ref: "#/components/schemas/IdName"
|
||||
inReplyTo:
|
||||
$ref: "#/components/schemas/IdName"
|
||||
folder:
|
||||
$ref: "#/components/schemas/IdName"
|
||||
dueDate:
|
||||
type: integer
|
||||
format: date-time
|
||||
@ -3153,11 +3405,15 @@ components:
|
||||
description: |
|
||||
A user of a collective.
|
||||
required:
|
||||
- id
|
||||
- login
|
||||
- state
|
||||
- loginCount
|
||||
- created
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: ident
|
||||
login:
|
||||
type: string
|
||||
format: ident
|
||||
@ -3188,9 +3444,15 @@ components:
|
||||
Meta information for an item upload. The user can specify some
|
||||
structured information with a binary file.
|
||||
|
||||
Additional metadata is not required. However, you have to
|
||||
specifiy whether the corresponding files should become one
|
||||
single item or if an item is created for each file.
|
||||
Additional metadata is not required. However, if there is some
|
||||
specified, you have to specifiy whether the corresponding
|
||||
files should become one single item or if an item is created
|
||||
for each file.
|
||||
|
||||
A direction can be given, `Incoming` is used if not specified.
|
||||
|
||||
A folderId can be given, the item is placed into this folder
|
||||
after creation.
|
||||
required:
|
||||
- multiple
|
||||
properties:
|
||||
@ -3200,6 +3462,9 @@ components:
|
||||
direction:
|
||||
type: string
|
||||
format: direction
|
||||
folder:
|
||||
type: string
|
||||
format: ident
|
||||
Collective:
|
||||
description: |
|
||||
Information about a collective.
|
||||
@ -3270,6 +3535,9 @@ components:
|
||||
priority:
|
||||
type: string
|
||||
format: priority
|
||||
folder:
|
||||
type: string
|
||||
format: ident
|
||||
created:
|
||||
description: DateTime
|
||||
type: integer
|
||||
@ -3527,6 +3795,9 @@ components:
|
||||
concEquip:
|
||||
type: string
|
||||
format: ident
|
||||
folder:
|
||||
type: string
|
||||
format: ident
|
||||
dateFrom:
|
||||
type: integer
|
||||
format: date-time
|
||||
@ -3580,6 +3851,8 @@ components:
|
||||
$ref: "#/components/schemas/IdName"
|
||||
concEquip:
|
||||
$ref: "#/components/schemas/IdName"
|
||||
folder:
|
||||
$ref: "#/components/schemas/IdName"
|
||||
fileCount:
|
||||
type: integer
|
||||
format: int32
|
||||
@ -3633,6 +3906,22 @@ components:
|
||||
type: boolean
|
||||
message:
|
||||
type: string
|
||||
IdResult:
|
||||
description: |
|
||||
Some basic result of an operation with an ID as payload. If
|
||||
success if `false` the id is not usable.
|
||||
required:
|
||||
- success
|
||||
- message
|
||||
- id
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
message:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
format: ident
|
||||
Tag:
|
||||
description: |
|
||||
A tag used to annotate items. A tag may have a category which
|
||||
@ -3739,6 +4028,13 @@ components:
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
userId:
|
||||
name: userId
|
||||
in: path
|
||||
description: An identifier
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
itemId:
|
||||
name: itemId
|
||||
in: path
|
||||
@ -3753,6 +4049,13 @@ components:
|
||||
required: false
|
||||
schema:
|
||||
type: boolean
|
||||
owning:
|
||||
name: full
|
||||
in: query
|
||||
description: Whether to get owning folders
|
||||
required: false
|
||||
schema:
|
||||
type: boolean
|
||||
checksum:
|
||||
name: checksum
|
||||
in: path
|
||||
|
@ -81,7 +81,8 @@ object RestServer {
|
||||
"usertask/notifydueitems" -> NotifyDueItemsRoutes(cfg, restApp.backend, token),
|
||||
"usertask/scanmailbox" -> ScanMailboxRoutes(restApp.backend, token),
|
||||
"calevent/check" -> CalEventCheckRoutes(),
|
||||
"fts" -> FullTextIndexRoutes.secured(cfg, restApp.backend, token)
|
||||
"fts" -> FullTextIndexRoutes.secured(cfg, restApp.backend, token),
|
||||
"folder" -> FolderRoutes(restApp.backend, token)
|
||||
)
|
||||
|
||||
def openRoutes[F[_]: Effect](cfg: Config, restApp: RestApp[F]): HttpRoutes[F] =
|
||||
|
@ -15,8 +15,8 @@ import docspell.common.syntax.all._
|
||||
import docspell.ftsclient.FtsResult
|
||||
import docspell.restapi.model._
|
||||
import docspell.restserver.conv.Conversions._
|
||||
import docspell.store.AddResult
|
||||
import docspell.store.records._
|
||||
import docspell.store.{AddResult, UpdateResult}
|
||||
|
||||
import bitpeace.FileMeta
|
||||
import org.http4s.headers.`Content-Type`
|
||||
@ -85,6 +85,7 @@ trait Conversions {
|
||||
data.concPerson.map(p => IdName(p.pid, p.name)),
|
||||
data.concEquip.map(e => IdName(e.eid, e.name)),
|
||||
data.inReplyTo.map(mkIdName),
|
||||
data.folder.map(mkIdName),
|
||||
data.item.dueDate,
|
||||
data.item.notes,
|
||||
data.attachments.map((mkAttachment(data) _).tupled).toList,
|
||||
@ -109,9 +110,9 @@ trait Conversions {
|
||||
|
||||
// item list
|
||||
|
||||
def mkQuery(m: ItemSearch, coll: Ident): OItemSearch.Query =
|
||||
def mkQuery(m: ItemSearch, account: AccountId): OItemSearch.Query =
|
||||
OItemSearch.Query(
|
||||
coll,
|
||||
account,
|
||||
m.name,
|
||||
if (m.inbox) Seq(ItemState.Created)
|
||||
else ItemState.validStates.toList,
|
||||
@ -120,6 +121,7 @@ trait Conversions {
|
||||
m.corrOrg,
|
||||
m.concPerson,
|
||||
m.concEquip,
|
||||
m.folder,
|
||||
m.tagsInclude.map(Ident.unsafe),
|
||||
m.tagsExclude.map(Ident.unsafe),
|
||||
m.dateFrom,
|
||||
@ -192,6 +194,7 @@ trait Conversions {
|
||||
i.corrPerson.map(mkIdName),
|
||||
i.concPerson.map(mkIdName),
|
||||
i.concEquip.map(mkIdName),
|
||||
i.folder.map(mkIdName),
|
||||
i.fileCount,
|
||||
Nil,
|
||||
Nil
|
||||
@ -284,9 +287,11 @@ trait Conversions {
|
||||
.find(_.name.exists(_.equalsIgnoreCase("meta")))
|
||||
.map(p => parseMeta(p.body))
|
||||
.map(fm =>
|
||||
fm.map(m => (m.multiple, UploadMeta(m.direction, "webapp", validFileTypes)))
|
||||
fm.map(m =>
|
||||
(m.multiple, UploadMeta(m.direction, "webapp", m.folder, validFileTypes))
|
||||
)
|
||||
)
|
||||
.getOrElse((true, UploadMeta(None, "webapp", validFileTypes)).pure[F])
|
||||
.getOrElse((true, UploadMeta(None, "webapp", None, validFileTypes)).pure[F])
|
||||
|
||||
val files = mp.parts
|
||||
.filter(p => p.name.forall(s => !s.equalsIgnoreCase("meta")))
|
||||
@ -431,7 +436,16 @@ trait Conversions {
|
||||
|
||||
// users
|
||||
def mkUser(ru: RUser): User =
|
||||
User(ru.login, ru.state, None, ru.email, ru.lastLogin, ru.loginCount, ru.created)
|
||||
User(
|
||||
ru.uid,
|
||||
ru.login,
|
||||
ru.state,
|
||||
None,
|
||||
ru.email,
|
||||
ru.lastLogin,
|
||||
ru.loginCount,
|
||||
ru.created
|
||||
)
|
||||
|
||||
def newUser[F[_]: Sync](u: User, cid: Ident): F[RUser] =
|
||||
timeId.map {
|
||||
@ -451,7 +465,7 @@ trait Conversions {
|
||||
|
||||
def changeUser(u: User, cid: Ident): RUser =
|
||||
RUser(
|
||||
Ident.unsafe(""),
|
||||
u.id,
|
||||
u.login,
|
||||
cid,
|
||||
u.password.getOrElse(Password.empty),
|
||||
@ -479,12 +493,21 @@ trait Conversions {
|
||||
// sources
|
||||
|
||||
def mkSource(s: RSource): Source =
|
||||
Source(s.sid, s.abbrev, s.description, s.counter, s.enabled, s.priority, s.created)
|
||||
Source(
|
||||
s.sid,
|
||||
s.abbrev,
|
||||
s.description,
|
||||
s.counter,
|
||||
s.enabled,
|
||||
s.priority,
|
||||
s.folderId,
|
||||
s.created
|
||||
)
|
||||
|
||||
def newSource[F[_]: Sync](s: Source, cid: Ident): F[RSource] =
|
||||
timeId.map({
|
||||
case (id, now) =>
|
||||
RSource(id, cid, s.abbrev, s.description, 0, s.enabled, s.priority, now)
|
||||
RSource(id, cid, s.abbrev, s.description, 0, s.enabled, s.priority, now, s.folder)
|
||||
})
|
||||
|
||||
def changeSource[F[_]: Sync](s: Source, coll: Ident): RSource =
|
||||
@ -496,7 +519,8 @@ trait Conversions {
|
||||
s.counter,
|
||||
s.enabled,
|
||||
s.priority,
|
||||
s.created
|
||||
s.created,
|
||||
s.folder
|
||||
)
|
||||
|
||||
// equipment
|
||||
@ -528,6 +552,14 @@ trait Conversions {
|
||||
BasicResult(true, "The job has been removed from the queue.")
|
||||
}
|
||||
|
||||
def idResult(ar: AddResult, id: Ident, successMsg: String): IdResult =
|
||||
ar match {
|
||||
case AddResult.Success => IdResult(true, successMsg, id)
|
||||
case AddResult.EntityExists(msg) => IdResult(false, msg, Ident.unsafe(""))
|
||||
case AddResult.Failure(ex) =>
|
||||
IdResult(false, s"Internal error: ${ex.getMessage}", Ident.unsafe(""))
|
||||
}
|
||||
|
||||
def basicResult(ar: AddResult, successMsg: String): BasicResult =
|
||||
ar match {
|
||||
case AddResult.Success => BasicResult(true, successMsg)
|
||||
@ -536,6 +568,14 @@ trait Conversions {
|
||||
BasicResult(false, s"Internal error: ${ex.getMessage}")
|
||||
}
|
||||
|
||||
def basicResult(ar: UpdateResult, successMsg: String): BasicResult =
|
||||
ar match {
|
||||
case UpdateResult.Success => BasicResult(true, successMsg)
|
||||
case UpdateResult.NotFound => BasicResult(false, "Not found")
|
||||
case UpdateResult.Failure(ex) =>
|
||||
BasicResult(false, s"Internal error: ${ex.getMessage}")
|
||||
}
|
||||
|
||||
def basicResult(ur: OUpload.UploadResult): BasicResult =
|
||||
ur match {
|
||||
case UploadResult.Success => BasicResult(true, "Files submitted.")
|
||||
|
@ -24,6 +24,8 @@ object QueryParam {
|
||||
|
||||
object FullOpt extends OptionalQueryParamDecoderMatcher[Boolean]("full")
|
||||
|
||||
object OwningOpt extends OptionalQueryParamDecoderMatcher[Boolean]("owning")
|
||||
|
||||
object ContactKindOpt extends OptionalQueryParamDecoderMatcher[ContactKind]("kind")
|
||||
|
||||
object QueryOpt extends OptionalQueryParamDecoderMatcher[QueryString]("q")
|
||||
|
@ -0,0 +1,113 @@
|
||||
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.OFolder
|
||||
import docspell.common._
|
||||
import docspell.restapi.model._
|
||||
import docspell.restserver.conv.Conversions
|
||||
import docspell.restserver.http4s._
|
||||
import docspell.store.records.RFolder
|
||||
|
||||
import org.http4s.HttpRoutes
|
||||
import org.http4s.circe.CirceEntityDecoder._
|
||||
import org.http4s.circe.CirceEntityEncoder._
|
||||
import org.http4s.dsl.Http4sDsl
|
||||
|
||||
object FolderRoutes {
|
||||
|
||||
def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
|
||||
val dsl = new Http4sDsl[F] with ResponseGenerator[F] {}
|
||||
import dsl._
|
||||
|
||||
HttpRoutes.of {
|
||||
case GET -> Root :? QueryParam.QueryOpt(q) :? QueryParam.OwningOpt(owning) =>
|
||||
val login =
|
||||
owning.filter(identity).map(_ => user.account.user)
|
||||
for {
|
||||
all <- backend.folder.findAll(user.account, login, q.map(_.q))
|
||||
resp <- Ok(FolderList(all.map(mkFolder).toList))
|
||||
} yield resp
|
||||
|
||||
case req @ POST -> Root =>
|
||||
for {
|
||||
data <- req.as[NewFolder]
|
||||
nfolder <- newFolder(data, user.account)
|
||||
res <- backend.folder.add(nfolder, Some(user.account.user))
|
||||
resp <-
|
||||
Ok(Conversions.idResult(res, nfolder.id, "Folder successfully created."))
|
||||
} yield resp
|
||||
|
||||
case GET -> Root / Ident(id) =>
|
||||
(for {
|
||||
folder <- OptionT(backend.folder.findById(id, user.account))
|
||||
resp <- OptionT.liftF(Ok(mkFolderDetail(folder)))
|
||||
} yield resp).getOrElseF(NotFound())
|
||||
|
||||
case req @ PUT -> Root / Ident(id) =>
|
||||
for {
|
||||
data <- req.as[NewFolder]
|
||||
res <- backend.folder.changeName(id, user.account, data.name)
|
||||
resp <- Ok(mkFolderChangeResult(res))
|
||||
} yield resp
|
||||
|
||||
case DELETE -> Root / Ident(id) =>
|
||||
for {
|
||||
res <- backend.folder.delete(id, user.account)
|
||||
resp <- Ok(mkFolderChangeResult(res))
|
||||
} yield resp
|
||||
|
||||
case PUT -> Root / Ident(id) / "member" / Ident(userId) =>
|
||||
for {
|
||||
res <- backend.folder.addMember(id, user.account, userId)
|
||||
resp <- Ok(mkFolderChangeResult(res))
|
||||
} yield resp
|
||||
|
||||
case DELETE -> Root / Ident(id) / "member" / Ident(userId) =>
|
||||
for {
|
||||
res <- backend.folder.removeMember(id, user.account, userId)
|
||||
resp <- Ok(mkFolderChangeResult(res))
|
||||
} yield resp
|
||||
}
|
||||
}
|
||||
|
||||
private def newFolder[F[_]: Sync](ns: NewFolder, account: AccountId): F[RFolder] =
|
||||
RFolder.newFolder(ns.name, account)
|
||||
|
||||
private def mkFolder(item: OFolder.FolderItem): FolderItem =
|
||||
FolderItem(
|
||||
item.id,
|
||||
item.name,
|
||||
Conversions.mkIdName(item.owner),
|
||||
item.created,
|
||||
item.member,
|
||||
item.memberCount
|
||||
)
|
||||
|
||||
private def mkFolderDetail(item: OFolder.FolderDetail): FolderDetail =
|
||||
FolderDetail(
|
||||
item.id,
|
||||
item.name,
|
||||
Conversions.mkIdName(item.owner),
|
||||
item.created,
|
||||
item.member,
|
||||
item.memberCount,
|
||||
item.members.map(Conversions.mkIdName)
|
||||
)
|
||||
|
||||
private def mkFolderChangeResult(r: OFolder.FolderChangeResult): BasicResult =
|
||||
r match {
|
||||
case OFolder.FolderChangeResult.Success =>
|
||||
BasicResult(true, "Successfully changed folder.")
|
||||
case OFolder.FolderChangeResult.NotFound =>
|
||||
BasicResult(false, "Folder or user not found.")
|
||||
case OFolder.FolderChangeResult.Forbidden =>
|
||||
BasicResult(false, "Not allowed to edit folder.")
|
||||
case OFolder.FolderChangeResult.Exists =>
|
||||
BasicResult(false, "The member already exists.")
|
||||
}
|
||||
}
|
@ -35,7 +35,7 @@ object ItemRoutes {
|
||||
for {
|
||||
mask <- req.as[ItemSearch]
|
||||
_ <- logger.ftrace(s"Got search mask: $mask")
|
||||
query = Conversions.mkQuery(mask, user.account.collective)
|
||||
query = Conversions.mkQuery(mask, user.account)
|
||||
_ <- logger.ftrace(s"Running query: $query")
|
||||
resp <- mask.fullText match {
|
||||
case Some(fq) if cfg.fullTextSearch.enabled =>
|
||||
@ -62,7 +62,7 @@ object ItemRoutes {
|
||||
for {
|
||||
mask <- req.as[ItemSearch]
|
||||
_ <- logger.ftrace(s"Got search mask: $mask")
|
||||
query = Conversions.mkQuery(mask, user.account.collective)
|
||||
query = Conversions.mkQuery(mask, user.account)
|
||||
_ <- logger.ftrace(s"Running query: $query")
|
||||
resp <- mask.fullText match {
|
||||
case Some(fq) if cfg.fullTextSearch.enabled =>
|
||||
@ -94,7 +94,7 @@ object ItemRoutes {
|
||||
for {
|
||||
items <- backend.fulltext.findIndexOnly(
|
||||
ftsIn,
|
||||
user.account.collective,
|
||||
user.account,
|
||||
Batch(mask.offset, mask.limit).restrictLimitTo(cfg.maxItemPageSize)
|
||||
)
|
||||
ok <- Ok(Conversions.mkItemListWithTagsFtsPlain(items))
|
||||
@ -149,6 +149,13 @@ object ItemRoutes {
|
||||
resp <- Ok(Conversions.basicResult(res, "Direction updated"))
|
||||
} yield resp
|
||||
|
||||
case req @ PUT -> Root / Ident(id) / "folder" =>
|
||||
for {
|
||||
idref <- req.as[OptionalId]
|
||||
res <- backend.item.setFolder(id, idref.id, user.account.collective)
|
||||
resp <- Ok(Conversions.basicResult(res, "Folder updated"))
|
||||
} yield resp
|
||||
|
||||
case req @ PUT -> Root / Ident(id) / "corrOrg" =>
|
||||
for {
|
||||
idref <- req.as[OptionalId]
|
||||
|
@ -112,7 +112,8 @@ object ScanMailboxRoutes {
|
||||
settings.receivedSinceHours.map(_.toLong).map(Duration.hours),
|
||||
settings.targetFolder,
|
||||
settings.deleteMail,
|
||||
settings.direction
|
||||
settings.direction,
|
||||
settings.itemFolder
|
||||
)
|
||||
)
|
||||
)
|
||||
@ -139,6 +140,7 @@ object ScanMailboxRoutes {
|
||||
task.args.receivedSince.map(_.hours.toInt),
|
||||
task.args.targetFolder,
|
||||
task.args.deleteMail,
|
||||
task.args.direction
|
||||
task.args.direction,
|
||||
task.args.itemFolder
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,34 @@
|
||||
CREATE TABLE `folder` (
|
||||
`id` varchar(254) not null primary key,
|
||||
`name` varchar(254) not null,
|
||||
`cid` varchar(254) not null,
|
||||
`owner` varchar(254) not null,
|
||||
`created` timestamp not null,
|
||||
unique (`name`, `cid`),
|
||||
foreign key (`cid`) references `collective`(`cid`),
|
||||
foreign key (`owner`) references `user_`(`uid`)
|
||||
);
|
||||
|
||||
CREATE TABLE `folder_member` (
|
||||
`id` varchar(254) not null primary key,
|
||||
`folder_id` varchar(254) not null,
|
||||
`user_id` varchar(254) not null,
|
||||
`created` timestamp not null,
|
||||
unique (`folder_id`, `user_id`),
|
||||
foreign key (`folder_id`) references `folder`(`id`),
|
||||
foreign key (`user_id`) references `user_`(`uid`)
|
||||
);
|
||||
|
||||
ALTER TABLE `item`
|
||||
ADD COLUMN `folder_id` varchar(254) NULL;
|
||||
|
||||
ALTER TABLE `item`
|
||||
ADD FOREIGN KEY (`folder_id`)
|
||||
REFERENCES `folder`(`id`);
|
||||
|
||||
ALTER TABLE `source`
|
||||
ADD COLUMN `folder_id` varchar(254) NULL;
|
||||
|
||||
ALTER TABLE `source`
|
||||
ADD FOREIGN KEY (`folder_id`)
|
||||
REFERENCES `folder`(`id`);
|
@ -0,0 +1,34 @@
|
||||
CREATE TABLE "folder" (
|
||||
"id" varchar(254) not null primary key,
|
||||
"name" varchar(254) not null,
|
||||
"cid" varchar(254) not null,
|
||||
"owner" varchar(254) not null,
|
||||
"created" timestamp not null,
|
||||
unique ("name", "cid"),
|
||||
foreign key ("cid") references "collective"("cid"),
|
||||
foreign key ("owner") references "user_"("uid")
|
||||
);
|
||||
|
||||
CREATE TABLE "folder_member" (
|
||||
"id" varchar(254) not null primary key,
|
||||
"folder_id" varchar(254) not null,
|
||||
"user_id" varchar(254) not null,
|
||||
"created" timestamp not null,
|
||||
unique ("folder_id", "user_id"),
|
||||
foreign key ("folder_id") references "folder"("id"),
|
||||
foreign key ("user_id") references "user_"("uid")
|
||||
);
|
||||
|
||||
ALTER TABLE "item"
|
||||
ADD COLUMN "folder_id" varchar(254) NULL;
|
||||
|
||||
ALTER TABLE "item"
|
||||
ADD FOREIGN KEY ("folder_id")
|
||||
REFERENCES "folder"("id");
|
||||
|
||||
ALTER TABLE "source"
|
||||
ADD COLUMN "folder_id" varchar(254) NULL;
|
||||
|
||||
ALTER TABLE "source"
|
||||
ADD FOREIGN KEY ("folder_id")
|
||||
REFERENCES "folder"("id");
|
@ -0,0 +1,29 @@
|
||||
package docspell.store
|
||||
|
||||
import cats.ApplicativeError
|
||||
import cats.implicits._
|
||||
|
||||
sealed trait UpdateResult
|
||||
|
||||
object UpdateResult {
|
||||
|
||||
case object Success extends UpdateResult
|
||||
case object NotFound extends UpdateResult
|
||||
final case class Failure(ex: Throwable) extends UpdateResult
|
||||
|
||||
def success: UpdateResult = Success
|
||||
def notFound: UpdateResult = NotFound
|
||||
def failure(ex: Throwable): UpdateResult = Failure(ex)
|
||||
|
||||
def fromUpdateRows(n: Int): UpdateResult =
|
||||
if (n > 0) success
|
||||
else notFound
|
||||
|
||||
def fromUpdate[F[_]](
|
||||
fn: F[Int]
|
||||
)(implicit ev: ApplicativeError[F, Throwable]): F[UpdateResult] =
|
||||
fn.attempt.map {
|
||||
case Right(n) => fromUpdateRows(n)
|
||||
case Left(ex) => failure(ex)
|
||||
}
|
||||
}
|
@ -145,6 +145,7 @@ object QAttachment {
|
||||
id: Ident,
|
||||
item: Ident,
|
||||
collective: Ident,
|
||||
folder: Option[Ident],
|
||||
lang: Language,
|
||||
name: Option[String],
|
||||
content: Option[String]
|
||||
@ -160,10 +161,11 @@ object QAttachment {
|
||||
val mContent = RAttachmentMeta.Columns.content.prefix("m")
|
||||
val iId = RItem.Columns.id.prefix("i")
|
||||
val iColl = RItem.Columns.cid.prefix("i")
|
||||
val iFolder = RItem.Columns.folder.prefix("i")
|
||||
val cId = RCollective.Columns.id.prefix("c")
|
||||
val cLang = RCollective.Columns.language.prefix("c")
|
||||
|
||||
val cols = Seq(aId, aItem, iColl, cLang, aName, mContent)
|
||||
val cols = Seq(aId, aItem, iColl, iFolder, cLang, aName, mContent)
|
||||
val from = RAttachment.table ++ fr"a INNER JOIN" ++
|
||||
RAttachmentMeta.table ++ fr"m ON" ++ aId.is(mId) ++
|
||||
fr"INNER JOIN" ++ RItem.table ++ fr"i ON" ++ iId.is(aItem) ++
|
||||
|
@ -0,0 +1,279 @@
|
||||
package docspell.store.queries
|
||||
|
||||
import cats.data.OptionT
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.records._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
|
||||
object QFolder {
|
||||
|
||||
final case class FolderItem(
|
||||
id: Ident,
|
||||
name: String,
|
||||
owner: IdRef,
|
||||
created: Timestamp,
|
||||
member: Boolean,
|
||||
memberCount: Int
|
||||
) {
|
||||
def withMembers(members: List[IdRef]): FolderDetail =
|
||||
FolderDetail(id, name, owner, created, member, memberCount, members)
|
||||
}
|
||||
|
||||
final case class FolderDetail(
|
||||
id: Ident,
|
||||
name: String,
|
||||
owner: IdRef,
|
||||
created: Timestamp,
|
||||
member: Boolean,
|
||||
memberCount: Int,
|
||||
members: List[IdRef]
|
||||
)
|
||||
|
||||
sealed trait FolderChangeResult
|
||||
object FolderChangeResult {
|
||||
case object Success extends FolderChangeResult
|
||||
def success: FolderChangeResult = Success
|
||||
case object NotFound extends FolderChangeResult
|
||||
def notFound: FolderChangeResult = NotFound
|
||||
case object Forbidden extends FolderChangeResult
|
||||
def forbidden: FolderChangeResult = Forbidden
|
||||
case object Exists extends FolderChangeResult
|
||||
def exists: FolderChangeResult = Exists
|
||||
}
|
||||
|
||||
def delete(id: Ident, account: AccountId): ConnectionIO[FolderChangeResult] = {
|
||||
def tryDelete =
|
||||
for {
|
||||
_ <- RItem.removeFolder(id)
|
||||
_ <- RSource.removeFolder(id)
|
||||
_ <- RFolderMember.deleteAll(id)
|
||||
_ <- RFolder.delete(id)
|
||||
} yield FolderChangeResult.success
|
||||
|
||||
(for {
|
||||
uid <- OptionT(findUserId(account))
|
||||
folder <- OptionT(RFolder.findById(id))
|
||||
res <- OptionT.liftF(
|
||||
if (folder.owner == uid) tryDelete
|
||||
else FolderChangeResult.forbidden.pure[ConnectionIO]
|
||||
)
|
||||
} yield res).getOrElse(FolderChangeResult.notFound)
|
||||
}
|
||||
|
||||
def changeName(
|
||||
folder: Ident,
|
||||
account: AccountId,
|
||||
name: String
|
||||
): ConnectionIO[FolderChangeResult] = {
|
||||
def tryUpdate(ns: RFolder): ConnectionIO[FolderChangeResult] =
|
||||
for {
|
||||
n <- RFolder.update(ns)
|
||||
res =
|
||||
if (n == 0) FolderChangeResult.notFound
|
||||
else FolderChangeResult.Success
|
||||
} yield res
|
||||
|
||||
(for {
|
||||
uid <- OptionT(findUserId(account))
|
||||
folder <- OptionT(RFolder.findById(folder))
|
||||
res <- OptionT.liftF(
|
||||
if (folder.owner == uid) tryUpdate(folder.copy(name = name))
|
||||
else FolderChangeResult.forbidden.pure[ConnectionIO]
|
||||
)
|
||||
} yield res).getOrElse(FolderChangeResult.notFound)
|
||||
}
|
||||
|
||||
def removeMember(
|
||||
folder: Ident,
|
||||
account: AccountId,
|
||||
member: Ident
|
||||
): ConnectionIO[FolderChangeResult] = {
|
||||
def tryRemove: ConnectionIO[FolderChangeResult] =
|
||||
for {
|
||||
n <- RFolderMember.delete(member, folder)
|
||||
res =
|
||||
if (n == 0) FolderChangeResult.notFound
|
||||
else FolderChangeResult.Success
|
||||
} yield res
|
||||
|
||||
(for {
|
||||
uid <- OptionT(findUserId(account))
|
||||
folder <- OptionT(RFolder.findById(folder))
|
||||
res <- OptionT.liftF(
|
||||
if (folder.owner == uid) tryRemove
|
||||
else FolderChangeResult.forbidden.pure[ConnectionIO]
|
||||
)
|
||||
} yield res).getOrElse(FolderChangeResult.notFound)
|
||||
}
|
||||
|
||||
def addMember(
|
||||
folder: Ident,
|
||||
account: AccountId,
|
||||
member: Ident
|
||||
): ConnectionIO[FolderChangeResult] = {
|
||||
def tryAdd: ConnectionIO[FolderChangeResult] =
|
||||
for {
|
||||
spm <- RFolderMember.findByUserId(member, folder)
|
||||
mem <- RFolderMember.newMember[ConnectionIO](folder, member)
|
||||
res <-
|
||||
if (spm.isDefined) FolderChangeResult.exists.pure[ConnectionIO]
|
||||
else RFolderMember.insert(mem).map(_ => FolderChangeResult.Success)
|
||||
} yield res
|
||||
|
||||
(for {
|
||||
uid <- OptionT(findUserId(account))
|
||||
folder <- OptionT(RFolder.findById(folder))
|
||||
res <- OptionT.liftF(
|
||||
if (folder.owner == uid) tryAdd
|
||||
else FolderChangeResult.forbidden.pure[ConnectionIO]
|
||||
)
|
||||
} yield res).getOrElse(FolderChangeResult.notFound)
|
||||
}
|
||||
|
||||
def findById(id: Ident, account: AccountId): ConnectionIO[Option[FolderDetail]] = {
|
||||
val mUserId = RFolderMember.Columns.user.prefix("m")
|
||||
val mFolderId = RFolderMember.Columns.folder.prefix("m")
|
||||
val uId = RUser.Columns.uid.prefix("u")
|
||||
val uLogin = RUser.Columns.login.prefix("u")
|
||||
val sColl = RFolder.Columns.collective.prefix("s")
|
||||
val sId = RFolder.Columns.id.prefix("s")
|
||||
|
||||
val from = RFolderMember.table ++ fr"m INNER JOIN" ++
|
||||
RUser.table ++ fr"u ON" ++ mUserId.is(uId) ++ fr"INNER JOIN" ++
|
||||
RFolder.table ++ fr"s ON" ++ mFolderId.is(sId)
|
||||
|
||||
val memberQ = selectSimple(
|
||||
Seq(uId, uLogin),
|
||||
from,
|
||||
and(mFolderId.is(id), sColl.is(account.collective))
|
||||
).query[IdRef].to[Vector]
|
||||
|
||||
(for {
|
||||
folder <- OptionT(findAll(account, Some(id), None, None).map(_.headOption))
|
||||
memb <- OptionT.liftF(memberQ)
|
||||
} yield folder.withMembers(memb.toList)).value
|
||||
}
|
||||
|
||||
def findAll(
|
||||
account: AccountId,
|
||||
idQ: Option[Ident],
|
||||
ownerLogin: Option[Ident],
|
||||
nameQ: Option[String]
|
||||
): ConnectionIO[Vector[FolderItem]] = {
|
||||
// with memberlogin as
|
||||
// (select m.folder_id,u.login
|
||||
// from folder_member m
|
||||
// inner join user_ u on u.uid = m.user_id
|
||||
// inner join folder s on s.id = m.folder_id
|
||||
// where s.cid = 'eike'
|
||||
// union all
|
||||
// select s.id,u.login
|
||||
// from folder s
|
||||
// inner join user_ u on u.uid = s.owner
|
||||
// where s.cid = 'eike')
|
||||
// select s.id
|
||||
// ,s.name
|
||||
// ,s.owner
|
||||
// ,u.login
|
||||
// ,s.created
|
||||
// ,(select count(*) > 0 from memberlogin where folder_id = s.id and login = 'eike') as member
|
||||
// ,(select count(*) - 1 from memberlogin where folder_id = s.id) as member_count
|
||||
// from folder s
|
||||
// inner join user_ u on u.uid = s.owner
|
||||
// where s.cid = 'eike';
|
||||
|
||||
val uId = RUser.Columns.uid.prefix("u")
|
||||
val uLogin = RUser.Columns.login.prefix("u")
|
||||
val sId = RFolder.Columns.id.prefix("s")
|
||||
val sOwner = RFolder.Columns.owner.prefix("s")
|
||||
val sName = RFolder.Columns.name.prefix("s")
|
||||
val sColl = RFolder.Columns.collective.prefix("s")
|
||||
val mUser = RFolderMember.Columns.user.prefix("m")
|
||||
val mFolder = RFolderMember.Columns.folder.prefix("m")
|
||||
|
||||
//CTE
|
||||
val cte: Fragment = {
|
||||
val from1 = RFolderMember.table ++ fr"m INNER JOIN" ++
|
||||
RUser.table ++ fr"u ON" ++ uId.is(mUser) ++ fr"INNER JOIN" ++
|
||||
RFolder.table ++ fr"s ON" ++ sId.is(mFolder)
|
||||
|
||||
val from2 = RFolder.table ++ fr"s INNER JOIN" ++
|
||||
RUser.table ++ fr"u ON" ++ uId.is(sOwner)
|
||||
|
||||
withCTE(
|
||||
"memberlogin" ->
|
||||
(selectSimple(Seq(mFolder, uLogin), from1, sColl.is(account.collective)) ++
|
||||
fr"UNION ALL" ++
|
||||
selectSimple(Seq(sId, uLogin), from2, sColl.is(account.collective)))
|
||||
)
|
||||
}
|
||||
|
||||
val isMember =
|
||||
fr"SELECT COUNT(*) > 0 FROM memberlogin WHERE" ++ mFolder.prefix("").is(sId) ++
|
||||
fr"AND" ++ uLogin.prefix("").is(account.user)
|
||||
|
||||
val memberCount =
|
||||
fr"SELECT COUNT(*) - 1 FROM memberlogin WHERE" ++ mFolder.prefix("").is(sId)
|
||||
|
||||
//Query
|
||||
val cols = Seq(
|
||||
sId.f,
|
||||
sName.f,
|
||||
sOwner.f,
|
||||
uLogin.f,
|
||||
RFolder.Columns.created.prefix("s").f,
|
||||
fr"(" ++ isMember ++ fr") as mem",
|
||||
fr"(" ++ memberCount ++ fr") as cnt"
|
||||
)
|
||||
|
||||
val from = RFolder.table ++ fr"s INNER JOIN" ++
|
||||
RUser.table ++ fr"u ON" ++ uId.is(sOwner)
|
||||
|
||||
val where =
|
||||
sColl.is(account.collective) :: idQ.toList
|
||||
.map(id => sId.is(id)) ::: nameQ.toList.map(q =>
|
||||
sName.lowerLike(s"%${q.toLowerCase}%")
|
||||
) ::: ownerLogin.toList.map(login => uLogin.is(login))
|
||||
|
||||
(cte ++ selectSimple(commas(cols), from, and(where) ++ orderBy(sName.asc)))
|
||||
.query[FolderItem]
|
||||
.to[Vector]
|
||||
}
|
||||
|
||||
/** Select all folder_id where the given account is member or owner. */
|
||||
def findMemberFolderIds(account: AccountId): Fragment = {
|
||||
val fId = RFolder.Columns.id.prefix("f")
|
||||
val fOwner = RFolder.Columns.owner.prefix("f")
|
||||
val fColl = RFolder.Columns.collective.prefix("f")
|
||||
val uId = RUser.Columns.uid.prefix("u")
|
||||
val uLogin = RUser.Columns.login.prefix("u")
|
||||
val mFolder = RFolderMember.Columns.folder.prefix("m")
|
||||
val mUser = RFolderMember.Columns.user.prefix("m")
|
||||
|
||||
selectSimple(
|
||||
Seq(fId),
|
||||
RFolder.table ++ fr"f INNER JOIN" ++ RUser.table ++ fr"u ON" ++ fOwner.is(uId),
|
||||
and(fColl.is(account.collective), uLogin.is(account.user))
|
||||
) ++
|
||||
fr"UNION ALL" ++
|
||||
selectSimple(
|
||||
Seq(mFolder),
|
||||
RFolderMember.table ++ fr"m INNER JOIN" ++ RFolder.table ++ fr"f ON" ++ fId.is(
|
||||
mFolder
|
||||
) ++
|
||||
fr"INNER JOIN" ++ RUser.table ++ fr"u ON" ++ uId.is(mUser),
|
||||
and(fColl.is(account.collective), uLogin.is(account.user))
|
||||
)
|
||||
}
|
||||
|
||||
def getMemberFolders(account: AccountId): ConnectionIO[Set[Ident]] =
|
||||
findMemberFolderIds(account).query[Ident].to[Set]
|
||||
|
||||
private def findUserId(account: AccountId): ConnectionIO[Option[Ident]] =
|
||||
RUser.findByAccount(account).map(_.map(_.uid))
|
||||
}
|
@ -66,6 +66,7 @@ object QItem {
|
||||
concPerson: Option[RPerson],
|
||||
concEquip: Option[REquipment],
|
||||
inReplyTo: Option[IdRef],
|
||||
folder: Option[IdRef],
|
||||
tags: Vector[RTag],
|
||||
attachments: Vector[(RAttachment, FileMeta)],
|
||||
sources: Vector[(RAttachmentSource, FileMeta)],
|
||||
@ -83,10 +84,11 @@ object QItem {
|
||||
val P1C = RPerson.Columns.all.map(_.prefix("p1"))
|
||||
val EC = REquipment.Columns.all.map(_.prefix("e"))
|
||||
val ICC = List(RItem.Columns.id, RItem.Columns.name).map(_.prefix("ref"))
|
||||
val FC = List(RFolder.Columns.id, RFolder.Columns.name).map(_.prefix("f"))
|
||||
|
||||
val cq =
|
||||
selectSimple(
|
||||
IC ++ OC ++ P0C ++ P1C ++ EC ++ ICC,
|
||||
IC ++ OC ++ P0C ++ P1C ++ EC ++ ICC ++ FC,
|
||||
RItem.table ++ fr"i",
|
||||
Fragment.empty
|
||||
) ++
|
||||
@ -105,6 +107,9 @@ object QItem {
|
||||
fr"LEFT JOIN" ++ RItem.table ++ fr"ref ON" ++ RItem.Columns.inReplyTo
|
||||
.prefix("i")
|
||||
.is(RItem.Columns.id.prefix("ref")) ++
|
||||
fr"LEFT JOIN" ++ RFolder.table ++ fr"f ON" ++ RItem.Columns.folder
|
||||
.prefix("i")
|
||||
.is(RFolder.Columns.id.prefix("f")) ++
|
||||
fr"WHERE" ++ RItem.Columns.id.prefix("i").is(id)
|
||||
|
||||
val q = cq
|
||||
@ -115,6 +120,7 @@ object QItem {
|
||||
Option[RPerson],
|
||||
Option[RPerson],
|
||||
Option[REquipment],
|
||||
Option[IdRef],
|
||||
Option[IdRef]
|
||||
)
|
||||
]
|
||||
@ -132,7 +138,7 @@ object QItem {
|
||||
arch <- archives
|
||||
ts <- tags
|
||||
} yield data.map(d =>
|
||||
ItemData(d._1, d._2, d._3, d._4, d._5, d._6, ts, att, srcs, arch)
|
||||
ItemData(d._1, d._2, d._3, d._4, d._5, d._6, d._7, ts, att, srcs, arch)
|
||||
)
|
||||
}
|
||||
|
||||
@ -149,11 +155,12 @@ object QItem {
|
||||
corrOrg: Option[IdRef],
|
||||
corrPerson: Option[IdRef],
|
||||
concPerson: Option[IdRef],
|
||||
concEquip: Option[IdRef]
|
||||
concEquip: Option[IdRef],
|
||||
folder: Option[IdRef]
|
||||
)
|
||||
|
||||
case class Query(
|
||||
collective: Ident,
|
||||
account: AccountId,
|
||||
name: Option[String],
|
||||
states: Seq[ItemState],
|
||||
direction: Option[Direction],
|
||||
@ -161,6 +168,7 @@ object QItem {
|
||||
corrOrg: Option[Ident],
|
||||
concPerson: Option[Ident],
|
||||
concEquip: Option[Ident],
|
||||
folder: Option[Ident],
|
||||
tagsInclude: List[Ident],
|
||||
tagsExclude: List[Ident],
|
||||
dateFrom: Option[Timestamp],
|
||||
@ -173,9 +181,9 @@ object QItem {
|
||||
)
|
||||
|
||||
object Query {
|
||||
def empty(collective: Ident): Query =
|
||||
def empty(account: AccountId): Query =
|
||||
Query(
|
||||
collective,
|
||||
account,
|
||||
None,
|
||||
Seq.empty,
|
||||
None,
|
||||
@ -183,6 +191,7 @@ object QItem {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Nil,
|
||||
Nil,
|
||||
None,
|
||||
@ -227,10 +236,12 @@ object QItem {
|
||||
val PC = RPerson.Columns
|
||||
val OC = ROrganization.Columns
|
||||
val EC = REquipment.Columns
|
||||
val FC = RFolder.Columns
|
||||
val itemCols = IC.all
|
||||
val personCols = List(RPerson.Columns.pid, RPerson.Columns.name)
|
||||
val orgCols = List(ROrganization.Columns.oid, ROrganization.Columns.name)
|
||||
val equipCols = List(REquipment.Columns.eid, REquipment.Columns.name)
|
||||
val personCols = List(PC.pid, PC.name)
|
||||
val orgCols = List(OC.oid, OC.name)
|
||||
val equipCols = List(EC.eid, EC.name)
|
||||
val folderCols = List(FC.id, FC.name)
|
||||
|
||||
val finalCols = commas(
|
||||
Seq(
|
||||
@ -251,6 +262,8 @@ object QItem {
|
||||
PC.name.prefix("p1").f,
|
||||
EC.eid.prefix("e1").f,
|
||||
EC.name.prefix("e1").f,
|
||||
FC.id.prefix("f1").f,
|
||||
FC.name.prefix("f1").f,
|
||||
q.orderAsc match {
|
||||
case Some(co) =>
|
||||
coalesce(co(IC).prefix("i").f, IC.created.prefix("i").f)
|
||||
@ -260,21 +273,27 @@ object QItem {
|
||||
) ++ moreCols
|
||||
)
|
||||
|
||||
val withItem = selectSimple(itemCols, RItem.table, IC.cid.is(q.collective))
|
||||
val withPerson = selectSimple(personCols, RPerson.table, PC.cid.is(q.collective))
|
||||
val withOrgs = selectSimple(orgCols, ROrganization.table, OC.cid.is(q.collective))
|
||||
val withEquips = selectSimple(equipCols, REquipment.table, EC.cid.is(q.collective))
|
||||
val withItem = selectSimple(itemCols, RItem.table, IC.cid.is(q.account.collective))
|
||||
val withPerson =
|
||||
selectSimple(personCols, RPerson.table, PC.cid.is(q.account.collective))
|
||||
val withOrgs =
|
||||
selectSimple(orgCols, ROrganization.table, OC.cid.is(q.account.collective))
|
||||
val withEquips =
|
||||
selectSimple(equipCols, REquipment.table, EC.cid.is(q.account.collective))
|
||||
val withFolder =
|
||||
selectSimple(folderCols, RFolder.table, FC.collective.is(q.account.collective))
|
||||
val withAttach = fr"SELECT COUNT(" ++ AC.id.f ++ fr") as num, " ++ AC.itemId.f ++
|
||||
fr"from" ++ RAttachment.table ++ fr"GROUP BY (" ++ AC.itemId.f ++ fr")"
|
||||
|
||||
val selectKW = if (distinct) fr"SELECT DISTINCT" else fr"SELECT"
|
||||
val query = withCTE(
|
||||
withCTE(
|
||||
(Seq(
|
||||
"items" -> withItem,
|
||||
"persons" -> withPerson,
|
||||
"orgs" -> withOrgs,
|
||||
"equips" -> withEquips,
|
||||
"attachs" -> withAttach
|
||||
"attachs" -> withAttach,
|
||||
"folders" -> withFolder
|
||||
) ++ ctes): _*
|
||||
) ++
|
||||
selectKW ++ finalCols ++ fr" FROM items i" ++
|
||||
@ -282,8 +301,10 @@ object QItem {
|
||||
fr"LEFT JOIN persons p0 ON" ++ IC.corrPerson.prefix("i").is(PC.pid.prefix("p0")) ++
|
||||
fr"LEFT JOIN orgs o0 ON" ++ IC.corrOrg.prefix("i").is(OC.oid.prefix("o0")) ++
|
||||
fr"LEFT JOIN persons p1 ON" ++ IC.concPerson.prefix("i").is(PC.pid.prefix("p1")) ++
|
||||
fr"LEFT JOIN equips e1 ON" ++ IC.concEquipment.prefix("i").is(EC.eid.prefix("e1"))
|
||||
query
|
||||
fr"LEFT JOIN equips e1 ON" ++ IC.concEquipment
|
||||
.prefix("i")
|
||||
.is(EC.eid.prefix("e1")) ++
|
||||
fr"LEFT JOIN folders f1 ON" ++ IC.folder.prefix("i").is(FC.id.prefix("f1"))
|
||||
}
|
||||
|
||||
def findItems(q: Query, batch: Batch): Stream[ConnectionIO, ListItem] = {
|
||||
@ -315,10 +336,11 @@ object QItem {
|
||||
RTagItem.Columns.tagId.isOneOf(q.tagsExclude)
|
||||
)
|
||||
|
||||
val iFolder = IC.folder.prefix("i")
|
||||
val name = q.name.map(_.toLowerCase).map(queryWildcard)
|
||||
val allNames = q.allNames.map(_.toLowerCase).map(queryWildcard)
|
||||
val cond = and(
|
||||
IC.cid.prefix("i").is(q.collective),
|
||||
IC.cid.prefix("i").is(q.account.collective),
|
||||
IC.state.prefix("i").isOneOf(q.states),
|
||||
IC.incoming.prefix("i").isOrDiscard(q.direction),
|
||||
name
|
||||
@ -340,6 +362,7 @@ object QItem {
|
||||
ROrganization.Columns.oid.prefix("o0").isOrDiscard(q.corrOrg),
|
||||
RPerson.Columns.pid.prefix("p1").isOrDiscard(q.concPerson),
|
||||
REquipment.Columns.eid.prefix("e1").isOrDiscard(q.concEquip),
|
||||
RFolder.Columns.id.prefix("f1").isOrDiscard(q.folder),
|
||||
if (q.tagsInclude.isEmpty) Fragment.empty
|
||||
else
|
||||
IC.id.prefix("i") ++ sql" IN (" ++ tagSelectsIncl
|
||||
@ -365,7 +388,8 @@ object QItem {
|
||||
.map(nel => IC.id.prefix("i").isIn(nel))
|
||||
.getOrElse(IC.id.prefix("i").is(""))
|
||||
)
|
||||
.getOrElse(Fragment.empty)
|
||||
.getOrElse(Fragment.empty),
|
||||
or(iFolder.isNull, iFolder.isIn(QFolder.findMemberFolderIds(q.account)))
|
||||
)
|
||||
|
||||
val order = q.orderAsc match {
|
||||
@ -456,7 +480,10 @@ object QItem {
|
||||
n <- store.transact(RItem.deleteByIdAndCollective(itemId, collective))
|
||||
} yield tn + rn + n + mn
|
||||
|
||||
private def findByFileIdsQuery(fileMetaIds: NonEmptyList[Ident], limit: Option[Int]) = {
|
||||
private def findByFileIdsQuery(
|
||||
fileMetaIds: NonEmptyList[Ident],
|
||||
limit: Option[Int]
|
||||
): Fragment = {
|
||||
val IC = RItem.Columns.all.map(_.prefix("i"))
|
||||
val aItem = RAttachment.Columns.itemId.prefix("a")
|
||||
val aId = RAttachment.Columns.id.prefix("a")
|
||||
@ -558,6 +585,7 @@ object QItem {
|
||||
final case class NameAndNotes(
|
||||
id: Ident,
|
||||
collective: Ident,
|
||||
folder: Option[Ident],
|
||||
name: String,
|
||||
notes: Option[String]
|
||||
)
|
||||
@ -565,12 +593,13 @@ object QItem {
|
||||
coll: Option[Ident],
|
||||
chunkSize: Int
|
||||
): Stream[ConnectionIO, NameAndNotes] = {
|
||||
val iId = RItem.Columns.id
|
||||
val iColl = RItem.Columns.cid
|
||||
val iName = RItem.Columns.name
|
||||
val iNotes = RItem.Columns.notes
|
||||
val iId = RItem.Columns.id
|
||||
val iColl = RItem.Columns.cid
|
||||
val iName = RItem.Columns.name
|
||||
val iFolder = RItem.Columns.folder
|
||||
val iNotes = RItem.Columns.notes
|
||||
|
||||
val cols = Seq(iId, iColl, iName, iNotes)
|
||||
val cols = Seq(iId, iColl, iFolder, iName, iNotes)
|
||||
val where = coll.map(cid => iColl.is(cid)).getOrElse(Fragment.empty)
|
||||
selectSimple(cols, RItem.table, where)
|
||||
.query[NameAndNotes]
|
||||
|
@ -0,0 +1,86 @@
|
||||
package docspell.store.records
|
||||
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Column
|
||||
import docspell.store.impl.Implicits._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
|
||||
case class RFolder(
|
||||
id: Ident,
|
||||
name: String,
|
||||
collectiveId: Ident,
|
||||
owner: Ident,
|
||||
created: Timestamp
|
||||
)
|
||||
|
||||
object RFolder {
|
||||
|
||||
def newFolder[F[_]: Sync](name: String, account: AccountId): F[RFolder] =
|
||||
for {
|
||||
nId <- Ident.randomId[F]
|
||||
now <- Timestamp.current[F]
|
||||
} yield RFolder(nId, name, account.collective, account.user, now)
|
||||
|
||||
val table = fr"folder"
|
||||
|
||||
object Columns {
|
||||
|
||||
val id = Column("id")
|
||||
val name = Column("name")
|
||||
val collective = Column("cid")
|
||||
val owner = Column("owner")
|
||||
val created = Column("created")
|
||||
|
||||
val all = List(id, name, collective, owner, created)
|
||||
}
|
||||
|
||||
import Columns._
|
||||
|
||||
def insert(value: RFolder): ConnectionIO[Int] = {
|
||||
val sql = insertRow(
|
||||
table,
|
||||
all,
|
||||
fr"${value.id},${value.name},${value.collectiveId},${value.owner},${value.created}"
|
||||
)
|
||||
sql.update.run
|
||||
}
|
||||
|
||||
def update(v: RFolder): ConnectionIO[Int] =
|
||||
updateRow(
|
||||
table,
|
||||
and(id.is(v.id), collective.is(v.collectiveId), owner.is(v.owner)),
|
||||
name.setTo(v.name)
|
||||
).update.run
|
||||
|
||||
def existsByName(coll: Ident, folderName: String): ConnectionIO[Boolean] =
|
||||
selectCount(id, table, and(collective.is(coll), name.is(folderName)))
|
||||
.query[Int]
|
||||
.unique
|
||||
.map(_ > 0)
|
||||
|
||||
def findById(folderId: Ident): ConnectionIO[Option[RFolder]] = {
|
||||
val sql = selectSimple(all, table, id.is(folderId))
|
||||
sql.query[RFolder].option
|
||||
}
|
||||
|
||||
def findAll(
|
||||
coll: Ident,
|
||||
nameQ: Option[String],
|
||||
order: Columns.type => Column
|
||||
): ConnectionIO[Vector[RFolder]] = {
|
||||
val q = Seq(collective.is(coll)) ++ (nameQ match {
|
||||
case Some(str) => Seq(name.lowerLike(s"%${str.toLowerCase}%"))
|
||||
case None => Seq.empty
|
||||
})
|
||||
val sql = selectSimple(all, table, and(q)) ++ orderBy(order(Columns).f)
|
||||
sql.query[RFolder].to[Vector]
|
||||
}
|
||||
|
||||
def delete(folderId: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(table, id.is(folderId)).update.run
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package docspell.store.records
|
||||
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Column
|
||||
import docspell.store.impl.Implicits._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
|
||||
case class RFolderMember(
|
||||
id: Ident,
|
||||
folderId: Ident,
|
||||
userId: Ident,
|
||||
created: Timestamp
|
||||
)
|
||||
|
||||
object RFolderMember {
|
||||
|
||||
def newMember[F[_]: Sync](folder: Ident, user: Ident): F[RFolderMember] =
|
||||
for {
|
||||
nId <- Ident.randomId[F]
|
||||
now <- Timestamp.current[F]
|
||||
} yield RFolderMember(nId, folder, user, now)
|
||||
|
||||
val table = fr"folder_member"
|
||||
|
||||
object Columns {
|
||||
|
||||
val id = Column("id")
|
||||
val folder = Column("folder_id")
|
||||
val user = Column("user_id")
|
||||
val created = Column("created")
|
||||
|
||||
val all = List(id, folder, user, created)
|
||||
}
|
||||
|
||||
import Columns._
|
||||
|
||||
def insert(value: RFolderMember): ConnectionIO[Int] = {
|
||||
val sql = insertRow(
|
||||
table,
|
||||
all,
|
||||
fr"${value.id},${value.folderId},${value.userId},${value.created}"
|
||||
)
|
||||
sql.update.run
|
||||
}
|
||||
|
||||
def findByUserId(userId: Ident, folderId: Ident): ConnectionIO[Option[RFolderMember]] =
|
||||
selectSimple(all, table, and(folder.is(folderId), user.is(userId)))
|
||||
.query[RFolderMember]
|
||||
.option
|
||||
|
||||
def delete(userId: Ident, folderId: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(table, and(folder.is(folderId), user.is(userId))).update.run
|
||||
|
||||
def deleteAll(folderId: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(table, folder.is(folderId)).update.run
|
||||
}
|
@ -27,7 +27,8 @@ case class RItem(
|
||||
dueDate: Option[Timestamp],
|
||||
created: Timestamp,
|
||||
updated: Timestamp,
|
||||
notes: Option[String]
|
||||
notes: Option[String],
|
||||
folderId: Option[Ident]
|
||||
) {}
|
||||
|
||||
object RItem {
|
||||
@ -58,6 +59,7 @@ object RItem {
|
||||
None,
|
||||
now,
|
||||
now,
|
||||
None,
|
||||
None
|
||||
)
|
||||
|
||||
@ -80,6 +82,7 @@ object RItem {
|
||||
val created = Column("created")
|
||||
val updated = Column("updated")
|
||||
val notes = Column("notes")
|
||||
val folder = Column("folder_id")
|
||||
val all = List(
|
||||
id,
|
||||
cid,
|
||||
@ -96,7 +99,8 @@ object RItem {
|
||||
dueDate,
|
||||
created,
|
||||
updated,
|
||||
notes
|
||||
notes,
|
||||
folder
|
||||
)
|
||||
}
|
||||
import Columns._
|
||||
@ -107,7 +111,7 @@ object RItem {
|
||||
all,
|
||||
fr"${v.id},${v.cid},${v.name},${v.itemDate},${v.source},${v.direction},${v.state}," ++
|
||||
fr"${v.corrOrg},${v.corrPerson},${v.concPerson},${v.concEquipment},${v.inReplyTo},${v.dueDate}," ++
|
||||
fr"${v.created},${v.updated},${v.notes}"
|
||||
fr"${v.created},${v.updated},${v.notes},${v.folderId}"
|
||||
).update.run
|
||||
|
||||
def getCollective(itemId: Ident): ConnectionIO[Option[Ident]] =
|
||||
@ -239,7 +243,21 @@ object RItem {
|
||||
n <- updateRow(
|
||||
table,
|
||||
and(cid.is(coll), concEquipment.is(Some(currentEquip))),
|
||||
commas(concPerson.setTo(None: Option[Ident]), updated.setTo(t))
|
||||
commas(concEquipment.setTo(None: Option[Ident]), updated.setTo(t))
|
||||
).update.run
|
||||
} yield n
|
||||
|
||||
def updateFolder(
|
||||
itemId: Ident,
|
||||
coll: Ident,
|
||||
folderId: Option[Ident]
|
||||
): ConnectionIO[Int] =
|
||||
for {
|
||||
t <- currentTime
|
||||
n <- updateRow(
|
||||
table,
|
||||
and(cid.is(coll), id.is(itemId)),
|
||||
commas(folder.setTo(folderId), updated.setTo(t))
|
||||
).update.run
|
||||
} yield n
|
||||
|
||||
@ -295,4 +313,9 @@ object RItem {
|
||||
|
||||
def findByIdAndCollective(itemId: Ident, coll: Ident): ConnectionIO[Option[RItem]] =
|
||||
selectSimple(all, table, and(id.is(itemId), cid.is(coll))).query[RItem].option
|
||||
|
||||
def removeFolder(folderId: Ident): ConnectionIO[Int] = {
|
||||
val empty: Option[Ident] = None
|
||||
updateRow(table, folder.is(folderId), folder.setTo(empty)).update.run
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,8 @@ case class RSource(
|
||||
counter: Int,
|
||||
enabled: Boolean,
|
||||
priority: Priority,
|
||||
created: Timestamp
|
||||
created: Timestamp,
|
||||
folderId: Option[Ident]
|
||||
) {}
|
||||
|
||||
object RSource {
|
||||
@ -32,8 +33,10 @@ object RSource {
|
||||
val enabled = Column("enabled")
|
||||
val priority = Column("priority")
|
||||
val created = Column("created")
|
||||
val folder = Column("folder_id")
|
||||
|
||||
val all = List(sid, cid, abbrev, description, counter, enabled, priority, created)
|
||||
val all =
|
||||
List(sid, cid, abbrev, description, counter, enabled, priority, created, folder)
|
||||
}
|
||||
|
||||
import Columns._
|
||||
@ -42,7 +45,7 @@ object RSource {
|
||||
val sql = insertRow(
|
||||
table,
|
||||
all,
|
||||
fr"${v.sid},${v.cid},${v.abbrev},${v.description},${v.counter},${v.enabled},${v.priority},${v.created}"
|
||||
fr"${v.sid},${v.cid},${v.abbrev},${v.description},${v.counter},${v.enabled},${v.priority},${v.created},${v.folderId}"
|
||||
)
|
||||
sql.update.run
|
||||
}
|
||||
@ -56,7 +59,8 @@ object RSource {
|
||||
abbrev.setTo(v.abbrev),
|
||||
description.setTo(v.description),
|
||||
enabled.setTo(v.enabled),
|
||||
priority.setTo(v.priority)
|
||||
priority.setTo(v.priority),
|
||||
folder.setTo(v.folderId)
|
||||
)
|
||||
)
|
||||
sql.update.run
|
||||
@ -97,4 +101,9 @@ object RSource {
|
||||
|
||||
def delete(sourceId: Ident, coll: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(table, and(sid.is(sourceId), cid.is(coll))).update.run
|
||||
|
||||
def removeFolder(folderId: Ident): ConnectionIO[Int] = {
|
||||
val empty: Option[Ident] = None
|
||||
updateRow(table, folder.is(folderId), folder.setTo(empty)).update.run
|
||||
}
|
||||
}
|
||||
|
@ -3,16 +3,20 @@ module Api exposing
|
||||
, addConcPerson
|
||||
, addCorrOrg
|
||||
, addCorrPerson
|
||||
, addMember
|
||||
, addTag
|
||||
, cancelJob
|
||||
, changeFolderName
|
||||
, changePassword
|
||||
, checkCalEvent
|
||||
, createImapSettings
|
||||
, createMailSettings
|
||||
, createNewFolder
|
||||
, createNotifyDueItems
|
||||
, createScanMailbox
|
||||
, deleteAttachment
|
||||
, deleteEquip
|
||||
, deleteFolder
|
||||
, deleteImapSettings
|
||||
, deleteItem
|
||||
, deleteMailSettings
|
||||
@ -28,6 +32,8 @@ module Api exposing
|
||||
, getCollectiveSettings
|
||||
, getContacts
|
||||
, getEquipments
|
||||
, getFolderDetail
|
||||
, getFolders
|
||||
, getImapSettings
|
||||
, getInsights
|
||||
, getItemProposals
|
||||
@ -61,6 +67,7 @@ module Api exposing
|
||||
, putUser
|
||||
, refreshSession
|
||||
, register
|
||||
, removeMember
|
||||
, sendMail
|
||||
, setAttachmentName
|
||||
, setCollectiveSettings
|
||||
@ -70,6 +77,7 @@ module Api exposing
|
||||
, setCorrOrg
|
||||
, setCorrPerson
|
||||
, setDirection
|
||||
, setFolder
|
||||
, setItemDate
|
||||
, setItemDueDate
|
||||
, setItemName
|
||||
@ -101,7 +109,10 @@ import Api.Model.EmailSettings exposing (EmailSettings)
|
||||
import Api.Model.EmailSettingsList exposing (EmailSettingsList)
|
||||
import Api.Model.Equipment exposing (Equipment)
|
||||
import Api.Model.EquipmentList exposing (EquipmentList)
|
||||
import Api.Model.FolderDetail exposing (FolderDetail)
|
||||
import Api.Model.FolderList exposing (FolderList)
|
||||
import Api.Model.GenInvite exposing (GenInvite)
|
||||
import Api.Model.IdResult exposing (IdResult)
|
||||
import Api.Model.ImapSettings exposing (ImapSettings)
|
||||
import Api.Model.ImapSettingsList exposing (ImapSettingsList)
|
||||
import Api.Model.InviteResult exposing (InviteResult)
|
||||
@ -114,6 +125,7 @@ import Api.Model.ItemSearch exposing (ItemSearch)
|
||||
import Api.Model.ItemUploadMeta exposing (ItemUploadMeta)
|
||||
import Api.Model.JobQueueState exposing (JobQueueState)
|
||||
import Api.Model.MoveAttachment exposing (MoveAttachment)
|
||||
import Api.Model.NewFolder exposing (NewFolder)
|
||||
import Api.Model.NotificationSettings exposing (NotificationSettings)
|
||||
import Api.Model.NotificationSettingsList exposing (NotificationSettingsList)
|
||||
import Api.Model.OptionalDate exposing (OptionalDate)
|
||||
@ -150,6 +162,85 @@ import Util.Http as Http2
|
||||
|
||||
|
||||
|
||||
--- Folders
|
||||
|
||||
|
||||
deleteFolder : Flags -> String -> (Result Http.Error BasicResult -> msg) -> Cmd msg
|
||||
deleteFolder flags id receive =
|
||||
Http2.authDelete
|
||||
{ url = flags.config.baseUrl ++ "/api/v1/sec/folder/" ++ id
|
||||
, account = getAccount flags
|
||||
, expect = Http.expectJson receive Api.Model.BasicResult.decoder
|
||||
}
|
||||
|
||||
|
||||
removeMember : Flags -> String -> String -> (Result Http.Error BasicResult -> msg) -> Cmd msg
|
||||
removeMember flags id user receive =
|
||||
Http2.authDelete
|
||||
{ url = flags.config.baseUrl ++ "/api/v1/sec/folder/" ++ id ++ "/member/" ++ user
|
||||
, account = getAccount flags
|
||||
, expect = Http.expectJson receive Api.Model.BasicResult.decoder
|
||||
}
|
||||
|
||||
|
||||
addMember : Flags -> String -> String -> (Result Http.Error BasicResult -> msg) -> Cmd msg
|
||||
addMember flags id user receive =
|
||||
Http2.authPut
|
||||
{ url = flags.config.baseUrl ++ "/api/v1/sec/folder/" ++ id ++ "/member/" ++ user
|
||||
, account = getAccount flags
|
||||
, body = Http.emptyBody
|
||||
, expect = Http.expectJson receive Api.Model.BasicResult.decoder
|
||||
}
|
||||
|
||||
|
||||
changeFolderName : Flags -> String -> NewFolder -> (Result Http.Error BasicResult -> msg) -> Cmd msg
|
||||
changeFolderName flags id ns receive =
|
||||
Http2.authPut
|
||||
{ url = flags.config.baseUrl ++ "/api/v1/sec/folder/" ++ id
|
||||
, account = getAccount flags
|
||||
, body = Http.jsonBody (Api.Model.NewFolder.encode ns)
|
||||
, expect = Http.expectJson receive Api.Model.BasicResult.decoder
|
||||
}
|
||||
|
||||
|
||||
createNewFolder : Flags -> NewFolder -> (Result Http.Error IdResult -> msg) -> Cmd msg
|
||||
createNewFolder flags ns receive =
|
||||
Http2.authPost
|
||||
{ url = flags.config.baseUrl ++ "/api/v1/sec/folder"
|
||||
, account = getAccount flags
|
||||
, body = Http.jsonBody (Api.Model.NewFolder.encode ns)
|
||||
, expect = Http.expectJson receive Api.Model.IdResult.decoder
|
||||
}
|
||||
|
||||
|
||||
getFolderDetail : Flags -> String -> (Result Http.Error FolderDetail -> msg) -> Cmd msg
|
||||
getFolderDetail flags id receive =
|
||||
Http2.authGet
|
||||
{ url = flags.config.baseUrl ++ "/api/v1/sec/folder/" ++ id
|
||||
, account = getAccount flags
|
||||
, expect = Http.expectJson receive Api.Model.FolderDetail.decoder
|
||||
}
|
||||
|
||||
|
||||
getFolders : Flags -> String -> Bool -> (Result Http.Error FolderList -> msg) -> Cmd msg
|
||||
getFolders flags query owningOnly receive =
|
||||
Http2.authGet
|
||||
{ url =
|
||||
flags.config.baseUrl
|
||||
++ "/api/v1/sec/folder?q="
|
||||
++ Url.percentEncode query
|
||||
++ (if owningOnly then
|
||||
"&owning=true"
|
||||
|
||||
else
|
||||
""
|
||||
)
|
||||
, account = getAccount flags
|
||||
, expect = Http.expectJson receive Api.Model.FolderList.decoder
|
||||
}
|
||||
|
||||
|
||||
|
||||
--- Full-Text
|
||||
|
||||
|
||||
@ -1172,6 +1263,16 @@ setDirection flags item dir receive =
|
||||
}
|
||||
|
||||
|
||||
setFolder : Flags -> String -> OptionalId -> (Result Http.Error BasicResult -> msg) -> Cmd msg
|
||||
setFolder flags item id receive =
|
||||
Http2.authPut
|
||||
{ url = flags.config.baseUrl ++ "/api/v1/sec/item/" ++ item ++ "/folder"
|
||||
, account = getAccount flags
|
||||
, body = Http.jsonBody (Api.Model.OptionalId.encode id)
|
||||
, expect = Http.expectJson receive Api.Model.BasicResult.decoder
|
||||
}
|
||||
|
||||
|
||||
setCorrOrg : Flags -> String -> OptionalId -> (Result Http.Error BasicResult -> msg) -> Cmd msg
|
||||
setCorrOrg flags item id receive =
|
||||
Http2.authPut
|
||||
|
@ -57,6 +57,12 @@ init key url flags settings =
|
||||
|
||||
( um, uc ) =
|
||||
Page.UserSettings.Data.init flags settings
|
||||
|
||||
( mdm, mdc ) =
|
||||
Page.ManageData.Data.init flags
|
||||
|
||||
( csm, csc ) =
|
||||
Page.CollectiveSettings.Data.init flags
|
||||
in
|
||||
( { flags = flags
|
||||
, key = key
|
||||
@ -64,8 +70,8 @@ init key url flags settings =
|
||||
, version = Api.Model.VersionInfo.empty
|
||||
, homeModel = Page.Home.Data.init flags
|
||||
, loginModel = Page.Login.Data.emptyModel
|
||||
, manageDataModel = Page.ManageData.Data.emptyModel
|
||||
, collSettingsModel = Page.CollectiveSettings.Data.emptyModel
|
||||
, manageDataModel = mdm
|
||||
, collSettingsModel = csm
|
||||
, userSettingsModel = um
|
||||
, queueModel = Page.Queue.Data.emptyModel
|
||||
, registerModel = Page.Register.Data.emptyModel
|
||||
@ -76,7 +82,11 @@ init key url flags settings =
|
||||
, subs = Sub.none
|
||||
, uiSettings = settings
|
||||
}
|
||||
, Cmd.map UserSettingsMsg uc
|
||||
, Cmd.batch
|
||||
[ Cmd.map UserSettingsMsg uc
|
||||
, Cmd.map ManageDataMsg mdc
|
||||
, Cmd.map CollSettingsMsg csc
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
|
@ -160,7 +160,11 @@ viewCollectiveSettings model =
|
||||
|
||||
viewManageData : Model -> Html Msg
|
||||
viewManageData model =
|
||||
Html.map ManageDataMsg (Page.ManageData.View.view model.uiSettings model.manageDataModel)
|
||||
Html.map ManageDataMsg
|
||||
(Page.ManageData.View.view model.flags
|
||||
model.uiSettings
|
||||
model.manageDataModel
|
||||
)
|
||||
|
||||
|
||||
viewLogin : Model -> Html Msg
|
||||
|
@ -49,7 +49,7 @@ emptyModel =
|
||||
, city = ""
|
||||
, country =
|
||||
Comp.Dropdown.makeSingleList
|
||||
{ makeOption = \c -> { value = c.code, text = c.label }
|
||||
{ makeOption = \c -> { value = c.code, text = c.label, additional = "" }
|
||||
, placeholder = "Select Country"
|
||||
, options = countries
|
||||
, selected = Nothing
|
||||
|
@ -43,6 +43,7 @@ init settings =
|
||||
\l ->
|
||||
{ value = Data.Language.toIso3 l
|
||||
, text = Data.Language.toName l
|
||||
, additional = ""
|
||||
}
|
||||
, placeholder = ""
|
||||
, options = Data.Language.all
|
||||
|
@ -32,6 +32,7 @@ emptyModel =
|
||||
\ct ->
|
||||
{ value = Data.ContactType.toString ct
|
||||
, text = Data.ContactType.toString ct
|
||||
, additional = ""
|
||||
}
|
||||
, placeholder = ""
|
||||
, options = Data.ContactType.all
|
||||
|
@ -8,6 +8,8 @@ module Comp.Dropdown exposing
|
||||
, makeMultiple
|
||||
, makeSingle
|
||||
, makeSingleList
|
||||
, mkOption
|
||||
, setMkOption
|
||||
, update
|
||||
, view
|
||||
)
|
||||
@ -27,9 +29,15 @@ import Util.List
|
||||
type alias Option =
|
||||
{ value : String
|
||||
, text : String
|
||||
, additional : String
|
||||
}
|
||||
|
||||
|
||||
mkOption : String -> String -> Option
|
||||
mkOption value text =
|
||||
Option value text ""
|
||||
|
||||
|
||||
type alias Item a =
|
||||
{ value : a
|
||||
, option : Option
|
||||
@ -63,6 +71,11 @@ type alias Model a =
|
||||
}
|
||||
|
||||
|
||||
setMkOption : (a -> Option) -> Model a -> Model a
|
||||
setMkOption mkopt model =
|
||||
{ model | makeOption = mkopt }
|
||||
|
||||
|
||||
makeModel :
|
||||
{ multiple : Bool
|
||||
, searchable : Int -> Bool
|
||||
@ -508,4 +521,7 @@ renderOption item =
|
||||
, onClick (AddItem item)
|
||||
]
|
||||
[ text item.option.text
|
||||
, span [ class "small-info right-float" ]
|
||||
[ text item.option.additional
|
||||
]
|
||||
]
|
||||
|
@ -51,7 +51,12 @@ emptyModel =
|
||||
, replyTo = Nothing
|
||||
, sslType =
|
||||
Comp.Dropdown.makeSingleList
|
||||
{ makeOption = \s -> { value = Data.SSLType.toString s, text = Data.SSLType.label s }
|
||||
{ makeOption =
|
||||
\s ->
|
||||
{ value = Data.SSLType.toString s
|
||||
, text = Data.SSLType.label s
|
||||
, additional = ""
|
||||
}
|
||||
, placeholder = ""
|
||||
, options = Data.SSLType.all
|
||||
, selected = Just Data.SSLType.None
|
||||
@ -74,7 +79,12 @@ init ems =
|
||||
, replyTo = ems.replyTo
|
||||
, sslType =
|
||||
Comp.Dropdown.makeSingleList
|
||||
{ makeOption = \s -> { value = Data.SSLType.toString s, text = Data.SSLType.label s }
|
||||
{ makeOption =
|
||||
\s ->
|
||||
{ value = Data.SSLType.toString s
|
||||
, text = Data.SSLType.label s
|
||||
, additional = ""
|
||||
}
|
||||
, placeholder = ""
|
||||
, options = Data.SSLType.all
|
||||
, selected =
|
||||
|
418
modules/webapp/src/main/elm/Comp/FolderDetail.elm
Normal file
418
modules/webapp/src/main/elm/Comp/FolderDetail.elm
Normal file
@ -0,0 +1,418 @@
|
||||
module Comp.FolderDetail exposing
|
||||
( Model
|
||||
, Msg
|
||||
, init
|
||||
, initEmpty
|
||||
, update
|
||||
, view
|
||||
)
|
||||
|
||||
import Api
|
||||
import Api.Model.BasicResult exposing (BasicResult)
|
||||
import Api.Model.FolderDetail exposing (FolderDetail)
|
||||
import Api.Model.IdName exposing (IdName)
|
||||
import Api.Model.IdResult exposing (IdResult)
|
||||
import Api.Model.NewFolder exposing (NewFolder)
|
||||
import Api.Model.User exposing (User)
|
||||
import Api.Model.UserList exposing (UserList)
|
||||
import Comp.FixedDropdown
|
||||
import Comp.YesNoDimmer
|
||||
import Data.Flags exposing (Flags)
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events exposing (onClick, onInput)
|
||||
import Http
|
||||
import Util.Http
|
||||
import Util.Maybe
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ result : Maybe BasicResult
|
||||
, folder : FolderDetail
|
||||
, name : Maybe String
|
||||
, members : List IdName
|
||||
, users : List User
|
||||
, memberDropdown : Comp.FixedDropdown.Model IdName
|
||||
, selectedMember : Maybe IdName
|
||||
, loading : Bool
|
||||
, deleteDimmer : Comp.YesNoDimmer.Model
|
||||
}
|
||||
|
||||
|
||||
type Msg
|
||||
= SetName String
|
||||
| MemberDropdownMsg (Comp.FixedDropdown.Msg IdName)
|
||||
| SaveName
|
||||
| NewFolderResp (Result Http.Error IdResult)
|
||||
| ChangeFolderResp (Result Http.Error BasicResult)
|
||||
| ChangeNameResp (Result Http.Error BasicResult)
|
||||
| FolderDetailResp (Result Http.Error FolderDetail)
|
||||
| AddMember
|
||||
| RemoveMember IdName
|
||||
| RequestDelete
|
||||
| DeleteMsg Comp.YesNoDimmer.Msg
|
||||
| DeleteResp (Result Http.Error BasicResult)
|
||||
| GoBack
|
||||
|
||||
|
||||
init : List User -> FolderDetail -> Model
|
||||
init users folder =
|
||||
{ result = Nothing
|
||||
, folder = folder
|
||||
, name = Util.Maybe.fromString folder.name
|
||||
, members = folder.members
|
||||
, users = users
|
||||
, memberDropdown =
|
||||
Comp.FixedDropdown.initMap .name
|
||||
(makeOptions users folder)
|
||||
, selectedMember = Nothing
|
||||
, loading = False
|
||||
, deleteDimmer = Comp.YesNoDimmer.emptyModel
|
||||
}
|
||||
|
||||
|
||||
initEmpty : List User -> Model
|
||||
initEmpty users =
|
||||
init users Api.Model.FolderDetail.empty
|
||||
|
||||
|
||||
makeOptions : List User -> FolderDetail -> List IdName
|
||||
makeOptions users folder =
|
||||
let
|
||||
toIdName u =
|
||||
IdName u.id u.login
|
||||
|
||||
notMember idn =
|
||||
List.member idn (folder.owner :: folder.members) |> not
|
||||
in
|
||||
List.map toIdName users
|
||||
|> List.filter notMember
|
||||
|
||||
|
||||
|
||||
--- Update
|
||||
|
||||
|
||||
update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Bool )
|
||||
update flags msg model =
|
||||
case msg of
|
||||
GoBack ->
|
||||
( model, Cmd.none, True )
|
||||
|
||||
SetName str ->
|
||||
( { model | name = Util.Maybe.fromString str }
|
||||
, Cmd.none
|
||||
, False
|
||||
)
|
||||
|
||||
MemberDropdownMsg lmsg ->
|
||||
let
|
||||
( mm, sel ) =
|
||||
Comp.FixedDropdown.update lmsg model.memberDropdown
|
||||
in
|
||||
( { model
|
||||
| memberDropdown = mm
|
||||
, selectedMember =
|
||||
case sel of
|
||||
Just _ ->
|
||||
sel
|
||||
|
||||
Nothing ->
|
||||
model.selectedMember
|
||||
}
|
||||
, Cmd.none
|
||||
, False
|
||||
)
|
||||
|
||||
SaveName ->
|
||||
case model.name of
|
||||
Just name ->
|
||||
let
|
||||
cmd =
|
||||
if model.folder.id == "" then
|
||||
Api.createNewFolder flags (NewFolder name) NewFolderResp
|
||||
|
||||
else
|
||||
Api.changeFolderName flags
|
||||
model.folder.id
|
||||
(NewFolder name)
|
||||
ChangeNameResp
|
||||
in
|
||||
( { model
|
||||
| loading = True
|
||||
, result = Nothing
|
||||
}
|
||||
, cmd
|
||||
, False
|
||||
)
|
||||
|
||||
Nothing ->
|
||||
( model, Cmd.none, False )
|
||||
|
||||
NewFolderResp (Ok ir) ->
|
||||
if ir.success then
|
||||
( model, Api.getFolderDetail flags ir.id FolderDetailResp, False )
|
||||
|
||||
else
|
||||
( { model
|
||||
| loading = False
|
||||
, result = Just (BasicResult ir.success ir.message)
|
||||
}
|
||||
, Cmd.none
|
||||
, False
|
||||
)
|
||||
|
||||
NewFolderResp (Err err) ->
|
||||
( { model
|
||||
| loading = False
|
||||
, result = Just (BasicResult False (Util.Http.errorToString err))
|
||||
}
|
||||
, Cmd.none
|
||||
, False
|
||||
)
|
||||
|
||||
ChangeFolderResp (Ok r) ->
|
||||
if r.success then
|
||||
( model
|
||||
, Api.getFolderDetail flags model.folder.id FolderDetailResp
|
||||
, False
|
||||
)
|
||||
|
||||
else
|
||||
( { model | loading = False, result = Just r }
|
||||
, Cmd.none
|
||||
, False
|
||||
)
|
||||
|
||||
ChangeFolderResp (Err err) ->
|
||||
( { model
|
||||
| loading = False
|
||||
, result = Just (BasicResult False (Util.Http.errorToString err))
|
||||
}
|
||||
, Cmd.none
|
||||
, False
|
||||
)
|
||||
|
||||
ChangeNameResp (Ok r) ->
|
||||
let
|
||||
model_ =
|
||||
{ model | result = Just r, loading = False }
|
||||
in
|
||||
( model_, Cmd.none, False )
|
||||
|
||||
ChangeNameResp (Err err) ->
|
||||
( { model
|
||||
| result = Just (BasicResult False (Util.Http.errorToString err))
|
||||
, loading = False
|
||||
}
|
||||
, Cmd.none
|
||||
, False
|
||||
)
|
||||
|
||||
FolderDetailResp (Ok sd) ->
|
||||
( init model.users sd, Cmd.none, False )
|
||||
|
||||
FolderDetailResp (Err err) ->
|
||||
( { model
|
||||
| loading = False
|
||||
, result = Just (BasicResult False (Util.Http.errorToString err))
|
||||
}
|
||||
, Cmd.none
|
||||
, False
|
||||
)
|
||||
|
||||
AddMember ->
|
||||
case model.selectedMember of
|
||||
Just mem ->
|
||||
( { model | loading = True }
|
||||
, Api.addMember flags model.folder.id mem.id ChangeFolderResp
|
||||
, False
|
||||
)
|
||||
|
||||
Nothing ->
|
||||
( model, Cmd.none, False )
|
||||
|
||||
RemoveMember idname ->
|
||||
( { model | loading = True }
|
||||
, Api.removeMember flags model.folder.id idname.id ChangeFolderResp
|
||||
, False
|
||||
)
|
||||
|
||||
RequestDelete ->
|
||||
let
|
||||
( dm, _ ) =
|
||||
Comp.YesNoDimmer.update Comp.YesNoDimmer.activate model.deleteDimmer
|
||||
in
|
||||
( { model | deleteDimmer = dm }, Cmd.none, False )
|
||||
|
||||
DeleteMsg lm ->
|
||||
let
|
||||
( dm, flag ) =
|
||||
Comp.YesNoDimmer.update lm model.deleteDimmer
|
||||
|
||||
cmd =
|
||||
if flag then
|
||||
Api.deleteFolder flags model.folder.id DeleteResp
|
||||
|
||||
else
|
||||
Cmd.none
|
||||
in
|
||||
( { model | deleteDimmer = dm }, cmd, False )
|
||||
|
||||
DeleteResp (Ok r) ->
|
||||
( { model | result = Just r }, Cmd.none, r.success )
|
||||
|
||||
DeleteResp (Err err) ->
|
||||
( { model | result = Just (BasicResult False (Util.Http.errorToString err)) }
|
||||
, Cmd.none
|
||||
, False
|
||||
)
|
||||
|
||||
|
||||
|
||||
--- View
|
||||
|
||||
|
||||
view : Flags -> Model -> Html Msg
|
||||
view flags model =
|
||||
let
|
||||
isOwner =
|
||||
Maybe.map .user flags.account
|
||||
|> Maybe.map ((==) model.folder.owner.name)
|
||||
|> Maybe.withDefault False
|
||||
in
|
||||
div []
|
||||
([ Html.map DeleteMsg (Comp.YesNoDimmer.view model.deleteDimmer)
|
||||
, if model.folder.id == "" then
|
||||
div []
|
||||
[ text "Create a new folder. You are automatically set as owner of this new folder."
|
||||
]
|
||||
|
||||
else
|
||||
div []
|
||||
[ text "Modify this folder by changing the name or add/remove members."
|
||||
]
|
||||
, if model.folder.id /= "" && not isOwner then
|
||||
div [ class "ui info message" ]
|
||||
[ text "You are not the owner of this folder and therefore are not allowed to edit it."
|
||||
]
|
||||
|
||||
else
|
||||
div [] []
|
||||
, div
|
||||
[ classList
|
||||
[ ( "ui message", True )
|
||||
, ( "invisible hidden", model.result == Nothing )
|
||||
, ( "error", Maybe.map .success model.result == Just False )
|
||||
, ( "success", Maybe.map .success model.result == Just True )
|
||||
]
|
||||
]
|
||||
[ Maybe.map .message model.result
|
||||
|> Maybe.withDefault ""
|
||||
|> text
|
||||
]
|
||||
, div [ class "ui header" ]
|
||||
[ text "Owner"
|
||||
]
|
||||
, div [ class "" ]
|
||||
[ text model.folder.owner.name
|
||||
]
|
||||
, div [ class "ui header" ]
|
||||
[ text "Name"
|
||||
]
|
||||
, div [ class "ui action input" ]
|
||||
[ input
|
||||
[ type_ "text"
|
||||
, onInput SetName
|
||||
, Maybe.withDefault "" model.name
|
||||
|> value
|
||||
]
|
||||
[]
|
||||
, button
|
||||
[ class "ui icon button"
|
||||
, onClick SaveName
|
||||
]
|
||||
[ i [ class "save icon" ] []
|
||||
]
|
||||
]
|
||||
]
|
||||
++ viewMembers model
|
||||
++ viewButtons model
|
||||
)
|
||||
|
||||
|
||||
viewButtons : Model -> List (Html Msg)
|
||||
viewButtons model =
|
||||
[ div [ class "ui divider" ] []
|
||||
, button
|
||||
[ class "ui button"
|
||||
, onClick GoBack
|
||||
]
|
||||
[ text "Back"
|
||||
]
|
||||
, button
|
||||
[ classList
|
||||
[ ( "ui red button", True )
|
||||
, ( "invisible hidden", model.folder.id == "" )
|
||||
]
|
||||
, onClick RequestDelete
|
||||
]
|
||||
[ text "Delete"
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
viewMembers : Model -> List (Html Msg)
|
||||
viewMembers model =
|
||||
if model.folder.id == "" then
|
||||
[]
|
||||
|
||||
else
|
||||
[ div [ class "ui header" ]
|
||||
[ text "Members"
|
||||
]
|
||||
, div [ class "ui form" ]
|
||||
[ div [ class "inline field" ]
|
||||
[ Html.map MemberDropdownMsg
|
||||
(Comp.FixedDropdown.view
|
||||
(Maybe.map makeItem model.selectedMember)
|
||||
model.memberDropdown
|
||||
)
|
||||
, button
|
||||
[ class "ui primary button"
|
||||
, title "Add a new member"
|
||||
, onClick AddMember
|
||||
]
|
||||
[ text "Add"
|
||||
]
|
||||
]
|
||||
]
|
||||
, div
|
||||
[ class "ui list"
|
||||
]
|
||||
(List.map viewMember model.members)
|
||||
]
|
||||
|
||||
|
||||
makeItem : IdName -> Comp.FixedDropdown.Item IdName
|
||||
makeItem idn =
|
||||
Comp.FixedDropdown.Item idn idn.name
|
||||
|
||||
|
||||
viewMember : IdName -> Html Msg
|
||||
viewMember member =
|
||||
div
|
||||
[ class "item"
|
||||
]
|
||||
[ a
|
||||
[ class "link icon"
|
||||
, href "#"
|
||||
, title "Remove this member"
|
||||
, onClick (RemoveMember member)
|
||||
]
|
||||
[ i [ class "red trash icon" ] []
|
||||
]
|
||||
, span []
|
||||
[ text member.name
|
||||
]
|
||||
]
|
237
modules/webapp/src/main/elm/Comp/FolderManage.elm
Normal file
237
modules/webapp/src/main/elm/Comp/FolderManage.elm
Normal file
@ -0,0 +1,237 @@
|
||||
module Comp.FolderManage exposing
|
||||
( Model
|
||||
, Msg
|
||||
, empty
|
||||
, init
|
||||
, update
|
||||
, view
|
||||
)
|
||||
|
||||
import Api
|
||||
import Api.Model.FolderDetail exposing (FolderDetail)
|
||||
import Api.Model.FolderItem exposing (FolderItem)
|
||||
import Api.Model.FolderList exposing (FolderList)
|
||||
import Api.Model.User exposing (User)
|
||||
import Api.Model.UserList exposing (UserList)
|
||||
import Comp.FolderDetail
|
||||
import Comp.FolderTable
|
||||
import Data.Flags exposing (Flags)
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events exposing (onCheck, onClick, onInput)
|
||||
import Http
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ tableModel : Comp.FolderTable.Model
|
||||
, detailModel : Maybe Comp.FolderDetail.Model
|
||||
, folders : List FolderItem
|
||||
, users : List User
|
||||
, query : String
|
||||
, owningOnly : Bool
|
||||
, loading : Bool
|
||||
}
|
||||
|
||||
|
||||
type Msg
|
||||
= TableMsg Comp.FolderTable.Msg
|
||||
| DetailMsg Comp.FolderDetail.Msg
|
||||
| UserListResp (Result Http.Error UserList)
|
||||
| FolderListResp (Result Http.Error FolderList)
|
||||
| FolderDetailResp (Result Http.Error FolderDetail)
|
||||
| SetQuery String
|
||||
| InitNewFolder
|
||||
| ToggleOwningOnly
|
||||
|
||||
|
||||
empty : Model
|
||||
empty =
|
||||
{ tableModel = Comp.FolderTable.init
|
||||
, detailModel = Nothing
|
||||
, folders = []
|
||||
, users = []
|
||||
, query = ""
|
||||
, owningOnly = True
|
||||
, loading = False
|
||||
}
|
||||
|
||||
|
||||
init : Flags -> ( Model, Cmd Msg )
|
||||
init flags =
|
||||
( empty
|
||||
, Cmd.batch
|
||||
[ Api.getUsers flags UserListResp
|
||||
, Api.getFolders flags empty.query empty.owningOnly FolderListResp
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
|
||||
--- Update
|
||||
|
||||
|
||||
update : Flags -> Msg -> Model -> ( Model, Cmd Msg )
|
||||
update flags msg model =
|
||||
case msg of
|
||||
TableMsg lm ->
|
||||
let
|
||||
( tm, action ) =
|
||||
Comp.FolderTable.update lm model.tableModel
|
||||
|
||||
cmd =
|
||||
case action of
|
||||
Comp.FolderTable.EditAction item ->
|
||||
Api.getFolderDetail flags item.id FolderDetailResp
|
||||
|
||||
Comp.FolderTable.NoAction ->
|
||||
Cmd.none
|
||||
in
|
||||
( { model | tableModel = tm }, cmd )
|
||||
|
||||
DetailMsg lm ->
|
||||
case model.detailModel of
|
||||
Just detail ->
|
||||
let
|
||||
( dm, dc, back ) =
|
||||
Comp.FolderDetail.update flags lm detail
|
||||
|
||||
cmd =
|
||||
if back then
|
||||
Api.getFolders flags model.query model.owningOnly FolderListResp
|
||||
|
||||
else
|
||||
Cmd.none
|
||||
in
|
||||
( { model
|
||||
| detailModel =
|
||||
if back then
|
||||
Nothing
|
||||
|
||||
else
|
||||
Just dm
|
||||
}
|
||||
, Cmd.batch
|
||||
[ Cmd.map DetailMsg dc
|
||||
, cmd
|
||||
]
|
||||
)
|
||||
|
||||
Nothing ->
|
||||
( model, Cmd.none )
|
||||
|
||||
SetQuery str ->
|
||||
( { model | query = str }
|
||||
, Api.getFolders flags str model.owningOnly FolderListResp
|
||||
)
|
||||
|
||||
ToggleOwningOnly ->
|
||||
let
|
||||
newOwning =
|
||||
not model.owningOnly
|
||||
in
|
||||
( { model | owningOnly = newOwning }
|
||||
, Api.getFolders flags model.query newOwning FolderListResp
|
||||
)
|
||||
|
||||
UserListResp (Ok ul) ->
|
||||
( { model | users = ul.items }, Cmd.none )
|
||||
|
||||
UserListResp (Err err) ->
|
||||
( model, Cmd.none )
|
||||
|
||||
FolderListResp (Ok sl) ->
|
||||
( { model | folders = sl.items }, Cmd.none )
|
||||
|
||||
FolderListResp (Err err) ->
|
||||
( model, Cmd.none )
|
||||
|
||||
FolderDetailResp (Ok sd) ->
|
||||
( { model | detailModel = Comp.FolderDetail.init model.users sd |> Just }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
FolderDetailResp (Err err) ->
|
||||
( model, Cmd.none )
|
||||
|
||||
InitNewFolder ->
|
||||
let
|
||||
sd =
|
||||
Comp.FolderDetail.initEmpty model.users
|
||||
in
|
||||
( { model | detailModel = Just sd }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
|
||||
|
||||
--- View
|
||||
|
||||
|
||||
view : Flags -> Model -> Html Msg
|
||||
view flags model =
|
||||
case model.detailModel of
|
||||
Just dm ->
|
||||
viewDetail flags dm
|
||||
|
||||
Nothing ->
|
||||
viewTable model
|
||||
|
||||
|
||||
viewDetail : Flags -> Comp.FolderDetail.Model -> Html Msg
|
||||
viewDetail flags detailModel =
|
||||
div []
|
||||
[ Html.map DetailMsg (Comp.FolderDetail.view flags detailModel)
|
||||
]
|
||||
|
||||
|
||||
viewTable : Model -> Html Msg
|
||||
viewTable model =
|
||||
div []
|
||||
[ div [ class "ui secondary menu" ]
|
||||
[ div [ class "horizontally fitted item" ]
|
||||
[ div [ class "ui icon input" ]
|
||||
[ input
|
||||
[ type_ "text"
|
||||
, onInput SetQuery
|
||||
, value model.query
|
||||
, placeholder "Search…"
|
||||
]
|
||||
[]
|
||||
, i [ class "ui search icon" ]
|
||||
[]
|
||||
]
|
||||
]
|
||||
, div [ class "item" ]
|
||||
[ div [ class "ui checkbox" ]
|
||||
[ input
|
||||
[ type_ "checkbox"
|
||||
, onCheck (\_ -> ToggleOwningOnly)
|
||||
, checked model.owningOnly
|
||||
]
|
||||
[]
|
||||
, label [] [ text "Show owning folders only" ]
|
||||
]
|
||||
]
|
||||
, div [ class "right menu" ]
|
||||
[ div [ class "item" ]
|
||||
[ a
|
||||
[ class "ui primary button"
|
||||
, href "#"
|
||||
, onClick InitNewFolder
|
||||
]
|
||||
[ i [ class "plus icon" ] []
|
||||
, text "New Folder"
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
, Html.map TableMsg (Comp.FolderTable.view model.tableModel model.folders)
|
||||
, div
|
||||
[ classList
|
||||
[ ( "ui dimmer", True )
|
||||
, ( "active", model.loading )
|
||||
]
|
||||
]
|
||||
[ div [ class "ui loader" ] []
|
||||
]
|
||||
]
|
91
modules/webapp/src/main/elm/Comp/FolderTable.elm
Normal file
91
modules/webapp/src/main/elm/Comp/FolderTable.elm
Normal file
@ -0,0 +1,91 @@
|
||||
module Comp.FolderTable exposing
|
||||
( Action(..)
|
||||
, Model
|
||||
, Msg
|
||||
, init
|
||||
, update
|
||||
, view
|
||||
)
|
||||
|
||||
import Api.Model.FolderItem exposing (FolderItem)
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events exposing (onClick)
|
||||
import Util.Html
|
||||
import Util.Time
|
||||
|
||||
|
||||
type alias Model =
|
||||
{}
|
||||
|
||||
|
||||
type Msg
|
||||
= EditItem FolderItem
|
||||
|
||||
|
||||
type Action
|
||||
= NoAction
|
||||
| EditAction FolderItem
|
||||
|
||||
|
||||
init : Model
|
||||
init =
|
||||
{}
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Action )
|
||||
update msg model =
|
||||
case msg of
|
||||
EditItem item ->
|
||||
( model, EditAction item )
|
||||
|
||||
|
||||
view : Model -> List FolderItem -> Html Msg
|
||||
view _ items =
|
||||
div []
|
||||
[ table [ class "ui very basic center aligned table" ]
|
||||
[ thead []
|
||||
[ th [ class "collapsing" ] []
|
||||
, th [] [ text "Name" ]
|
||||
, th [] [ text "Owner" ]
|
||||
, th [] [ text "Owner or Member" ]
|
||||
, th [] [ text "#Member" ]
|
||||
, th [] [ text "Created" ]
|
||||
]
|
||||
, tbody []
|
||||
(List.map viewItem items)
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
viewItem : FolderItem -> Html Msg
|
||||
viewItem item =
|
||||
tr []
|
||||
[ td [ class "collapsing" ]
|
||||
[ a
|
||||
[ href "#"
|
||||
, class "ui basic small blue label"
|
||||
, onClick (EditItem item)
|
||||
]
|
||||
[ i [ class "edit icon" ] []
|
||||
, text "Edit"
|
||||
]
|
||||
]
|
||||
, td []
|
||||
[ text item.name
|
||||
]
|
||||
, td []
|
||||
[ text item.owner.name
|
||||
]
|
||||
, td []
|
||||
[ Util.Html.checkbox item.isMember
|
||||
]
|
||||
, td []
|
||||
[ String.fromInt item.memberCount
|
||||
|> text
|
||||
]
|
||||
, td []
|
||||
[ Util.Time.formatDateShort item.created
|
||||
|> text
|
||||
]
|
||||
]
|
@ -47,7 +47,12 @@ emptyModel =
|
||||
, password = Nothing
|
||||
, sslType =
|
||||
Comp.Dropdown.makeSingleList
|
||||
{ makeOption = \s -> { value = Data.SSLType.toString s, text = Data.SSLType.label s }
|
||||
{ makeOption =
|
||||
\s ->
|
||||
{ value = Data.SSLType.toString s
|
||||
, text = Data.SSLType.label s
|
||||
, additional = ""
|
||||
}
|
||||
, placeholder = ""
|
||||
, options = Data.SSLType.all
|
||||
, selected = Just Data.SSLType.None
|
||||
@ -68,7 +73,12 @@ init ems =
|
||||
, password = ems.imapPassword
|
||||
, sslType =
|
||||
Comp.Dropdown.makeSingleList
|
||||
{ makeOption = \s -> { value = Data.SSLType.toString s, text = Data.SSLType.label s }
|
||||
{ makeOption =
|
||||
\s ->
|
||||
{ value = Data.SSLType.toString s
|
||||
, text = Data.SSLType.label s
|
||||
, additional = ""
|
||||
}
|
||||
, placeholder = ""
|
||||
, options = Data.SSLType.all
|
||||
, selected =
|
||||
|
@ -21,7 +21,6 @@ import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events exposing (onClick)
|
||||
import Markdown
|
||||
import Ports
|
||||
import Util.List
|
||||
import Util.String
|
||||
import Util.Time
|
||||
@ -125,6 +124,10 @@ viewItem settings item =
|
||||
|> List.intersperse ", "
|
||||
|> String.concat
|
||||
|
||||
folder =
|
||||
Maybe.map .name item.folder
|
||||
|> Maybe.withDefault ""
|
||||
|
||||
dueDate =
|
||||
Maybe.map Util.Time.formatDateShort item.dueDate
|
||||
|> Maybe.withDefault ""
|
||||
@ -212,6 +215,14 @@ viewItem settings item =
|
||||
, text " "
|
||||
, Util.String.withDefault "-" conc |> text
|
||||
]
|
||||
, div
|
||||
[ class "item"
|
||||
, title "Folder"
|
||||
]
|
||||
[ Icons.folderIcon ""
|
||||
, text " "
|
||||
, Util.String.withDefault "-" folder |> text
|
||||
]
|
||||
]
|
||||
, div [ class "right floated meta" ]
|
||||
[ div [ class "ui horizontal list" ]
|
||||
|
@ -11,6 +11,8 @@ import Api.Model.Attachment exposing (Attachment)
|
||||
import Api.Model.BasicResult exposing (BasicResult)
|
||||
import Api.Model.DirectionValue exposing (DirectionValue)
|
||||
import Api.Model.EquipmentList exposing (EquipmentList)
|
||||
import Api.Model.FolderItem exposing (FolderItem)
|
||||
import Api.Model.FolderList exposing (FolderList)
|
||||
import Api.Model.IdName exposing (IdName)
|
||||
import Api.Model.ItemDetail exposing (ItemDetail)
|
||||
import Api.Model.ItemProposals exposing (ItemProposals)
|
||||
@ -52,6 +54,7 @@ import Page exposing (Page(..))
|
||||
import Ports
|
||||
import Set exposing (Set)
|
||||
import Util.File exposing (makeFileId)
|
||||
import Util.Folder exposing (mkFolderOption)
|
||||
import Util.Http
|
||||
import Util.List
|
||||
import Util.Maybe
|
||||
@ -71,6 +74,8 @@ type alias Model =
|
||||
, corrPersonModel : Comp.Dropdown.Model IdName
|
||||
, concPersonModel : Comp.Dropdown.Model IdName
|
||||
, concEquipModel : Comp.Dropdown.Model IdName
|
||||
, folderModel : Comp.Dropdown.Model IdName
|
||||
, allFolders : List FolderItem
|
||||
, nameModel : String
|
||||
, notesModel : Maybe String
|
||||
, notesField : NotesField
|
||||
@ -140,6 +145,7 @@ emptyModel =
|
||||
\entry ->
|
||||
{ value = Data.Direction.toString entry
|
||||
, text = Data.Direction.toString entry
|
||||
, additional = ""
|
||||
}
|
||||
, options = Data.Direction.all
|
||||
, placeholder = "Choose a direction…"
|
||||
@ -147,24 +153,30 @@ emptyModel =
|
||||
}
|
||||
, corrOrgModel =
|
||||
Comp.Dropdown.makeSingle
|
||||
{ makeOption = \e -> { value = e.id, text = e.name }
|
||||
{ makeOption = \e -> { value = e.id, text = e.name, additional = "" }
|
||||
, placeholder = ""
|
||||
}
|
||||
, corrPersonModel =
|
||||
Comp.Dropdown.makeSingle
|
||||
{ makeOption = \e -> { value = e.id, text = e.name }
|
||||
{ makeOption = \e -> { value = e.id, text = e.name, additional = "" }
|
||||
, placeholder = ""
|
||||
}
|
||||
, concPersonModel =
|
||||
Comp.Dropdown.makeSingle
|
||||
{ makeOption = \e -> { value = e.id, text = e.name }
|
||||
{ makeOption = \e -> { value = e.id, text = e.name, additional = "" }
|
||||
, placeholder = ""
|
||||
}
|
||||
, concEquipModel =
|
||||
Comp.Dropdown.makeSingle
|
||||
{ makeOption = \e -> { value = e.id, text = e.name }
|
||||
{ makeOption = \e -> { value = e.id, text = e.name, additional = "" }
|
||||
, placeholder = ""
|
||||
}
|
||||
, folderModel =
|
||||
Comp.Dropdown.makeSingle
|
||||
{ makeOption = \e -> { value = e.id, text = e.name, additional = "" }
|
||||
, placeholder = ""
|
||||
}
|
||||
, allFolders = []
|
||||
, nameModel = ""
|
||||
, notesModel = Nothing
|
||||
, notesField = ViewNotes
|
||||
@ -268,6 +280,8 @@ type Msg
|
||||
| EditAttachNameSet String
|
||||
| EditAttachNameSubmit
|
||||
| EditAttachNameResp (Result Http.Error BasicResult)
|
||||
| GetFolderResp (Result Http.Error FolderList)
|
||||
| FolderDropdownMsg (Comp.Dropdown.Msg IdName)
|
||||
|
||||
|
||||
|
||||
@ -281,6 +295,7 @@ getOptions flags =
|
||||
, Api.getOrgLight flags GetOrgResp
|
||||
, Api.getPersonsLight flags GetPersonResp
|
||||
, Api.getEquipments flags "" GetEquipResp
|
||||
, Api.getFolders flags "" False GetFolderResp
|
||||
]
|
||||
|
||||
|
||||
@ -310,6 +325,16 @@ setDirection flags model =
|
||||
Cmd.none
|
||||
|
||||
|
||||
setFolder : Flags -> Model -> Maybe IdName -> Cmd Msg
|
||||
setFolder flags model mref =
|
||||
let
|
||||
idref =
|
||||
Maybe.map .id mref
|
||||
|> OptionalId
|
||||
in
|
||||
Api.setFolder flags model.item.id idref SaveResp
|
||||
|
||||
|
||||
setCorrOrg : Flags -> Model -> Maybe IdName -> Cmd Msg
|
||||
setCorrOrg flags model mref =
|
||||
let
|
||||
@ -523,6 +548,20 @@ update key flags next msg model =
|
||||
( m7, c7, s7 ) =
|
||||
update key flags next AddFilesReset m6
|
||||
|
||||
( m8, c8, s8 ) =
|
||||
update key
|
||||
flags
|
||||
next
|
||||
(FolderDropdownMsg
|
||||
(Comp.Dropdown.SetSelection
|
||||
(item.folder
|
||||
|> Maybe.map List.singleton
|
||||
|> Maybe.withDefault []
|
||||
)
|
||||
)
|
||||
)
|
||||
m7
|
||||
|
||||
proposalCmd =
|
||||
if item.state == "created" then
|
||||
Api.getItemProposals flags item.id GetProposalResp
|
||||
@ -530,7 +569,7 @@ update key flags next msg model =
|
||||
else
|
||||
Cmd.none
|
||||
in
|
||||
( { m7
|
||||
( { m8
|
||||
| item = item
|
||||
, nameModel = item.name
|
||||
, notesModel = item.notes
|
||||
@ -548,11 +587,12 @@ update key flags next msg model =
|
||||
, c5
|
||||
, c6
|
||||
, c7
|
||||
, c8
|
||||
, getOptions flags
|
||||
, proposalCmd
|
||||
, Api.getSentMails flags item.id SentMailsResp
|
||||
]
|
||||
, Sub.batch [ s1, s2, s3, s4, s5, s6, s7 ]
|
||||
, Sub.batch [ s1, s2, s3, s4, s5, s6, s7, s8 ]
|
||||
)
|
||||
|
||||
SetActiveAttachment pos ->
|
||||
@ -575,6 +615,26 @@ update key flags next msg model =
|
||||
else
|
||||
noSub ( model, Api.itemDetail flags model.item.id GetItemResp )
|
||||
|
||||
FolderDropdownMsg m ->
|
||||
let
|
||||
( m2, c2 ) =
|
||||
Comp.Dropdown.update m model.folderModel
|
||||
|
||||
newModel =
|
||||
{ model | folderModel = m2 }
|
||||
|
||||
idref =
|
||||
Comp.Dropdown.getSelected m2 |> List.head
|
||||
|
||||
save =
|
||||
if isDropdownChangeMsg m then
|
||||
setFolder flags newModel idref
|
||||
|
||||
else
|
||||
Cmd.none
|
||||
in
|
||||
noSub ( newModel, Cmd.batch [ save, Cmd.map FolderDropdownMsg c2 ] )
|
||||
|
||||
TagDropdownMsg m ->
|
||||
let
|
||||
( m2, c2 ) =
|
||||
@ -827,6 +887,30 @@ update key flags next msg model =
|
||||
SetDueDateSuggestion date ->
|
||||
noSub ( model, setDueDate flags model (Just date) )
|
||||
|
||||
GetFolderResp (Ok fs) ->
|
||||
let
|
||||
model_ =
|
||||
{ model
|
||||
| allFolders = fs.items
|
||||
, folderModel =
|
||||
Comp.Dropdown.setMkOption
|
||||
(mkFolderOption flags fs.items)
|
||||
model.folderModel
|
||||
}
|
||||
|
||||
mkIdName fitem =
|
||||
IdName fitem.id fitem.name
|
||||
|
||||
opts =
|
||||
fs.items
|
||||
|> List.map mkIdName
|
||||
|> Comp.Dropdown.SetOptions
|
||||
in
|
||||
update key flags next (FolderDropdownMsg opts) model_
|
||||
|
||||
GetFolderResp (Err _) ->
|
||||
noSub ( model, Cmd.none )
|
||||
|
||||
GetTagsResp (Ok tags) ->
|
||||
let
|
||||
tagList =
|
||||
@ -1382,29 +1466,33 @@ update key flags next msg model =
|
||||
noSub ( { model | attachRename = Nothing }, Cmd.none )
|
||||
|
||||
EditAttachNameResp (Ok res) ->
|
||||
case model.attachRename of
|
||||
Just m ->
|
||||
let
|
||||
changeName a =
|
||||
if a.id == m.id then
|
||||
{ a | name = Util.Maybe.fromString m.newName }
|
||||
if res.success then
|
||||
case model.attachRename of
|
||||
Just m ->
|
||||
let
|
||||
changeName a =
|
||||
if a.id == m.id then
|
||||
{ a | name = Util.Maybe.fromString m.newName }
|
||||
|
||||
else
|
||||
a
|
||||
else
|
||||
a
|
||||
|
||||
changeItem i =
|
||||
{ i | attachments = List.map changeName i.attachments }
|
||||
in
|
||||
noSub
|
||||
( { model
|
||||
| attachRename = Nothing
|
||||
, item = changeItem model.item
|
||||
}
|
||||
, Cmd.none
|
||||
)
|
||||
changeItem i =
|
||||
{ i | attachments = List.map changeName i.attachments }
|
||||
in
|
||||
noSub
|
||||
( { model
|
||||
| attachRename = Nothing
|
||||
, item = changeItem model.item
|
||||
}
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
Nothing ->
|
||||
noSub ( model, Cmd.none )
|
||||
Nothing ->
|
||||
noSub ( model, Cmd.none )
|
||||
|
||||
else
|
||||
noSub ( model, Cmd.none )
|
||||
|
||||
EditAttachNameResp (Err _) ->
|
||||
noSub ( model, Cmd.none )
|
||||
@ -1939,6 +2027,17 @@ renderItemInfo settings model =
|
||||
|> text
|
||||
]
|
||||
|
||||
itemfolder =
|
||||
div
|
||||
[ class "item"
|
||||
, title "Folder"
|
||||
]
|
||||
[ Icons.folderIcon ""
|
||||
, Maybe.map .name model.item.folder
|
||||
|> Maybe.withDefault "-"
|
||||
|> text
|
||||
]
|
||||
|
||||
src =
|
||||
div
|
||||
[ class "item"
|
||||
@ -1972,6 +2071,7 @@ renderItemInfo settings model =
|
||||
[ date
|
||||
, corr
|
||||
, conc
|
||||
, itemfolder
|
||||
, src
|
||||
]
|
||||
(if Util.Maybe.isEmpty model.item.dueDate then
|
||||
@ -2061,7 +2161,7 @@ renderEditForm settings model =
|
||||
]
|
||||
in
|
||||
div [ class "ui attached segment" ]
|
||||
[ div [ class "ui form" ]
|
||||
[ div [ class "ui form warning" ]
|
||||
[ div [ class "field" ]
|
||||
[ label []
|
||||
[ Icons.tagsIcon "grey"
|
||||
@ -2082,6 +2182,25 @@ renderEditForm settings model =
|
||||
]
|
||||
]
|
||||
]
|
||||
, div [ class "field" ]
|
||||
[ label []
|
||||
[ Icons.folderIcon "grey"
|
||||
, text "Folder"
|
||||
]
|
||||
, Html.map FolderDropdownMsg (Comp.Dropdown.view settings model.folderModel)
|
||||
, div
|
||||
[ classList
|
||||
[ ( "ui warning message", True )
|
||||
, ( "hidden", isFolderMember model )
|
||||
]
|
||||
]
|
||||
[ Markdown.toHtml [] """
|
||||
You are **not a member** of this folder. This item will be **hidden**
|
||||
from any search now. Use a folder where you are a member of to make this
|
||||
item visible. This message will disappear then.
|
||||
"""
|
||||
]
|
||||
]
|
||||
, div [ class "field" ]
|
||||
[ label []
|
||||
[ Icons.directionIcon "grey"
|
||||
@ -2407,3 +2526,14 @@ renderEditAttachmentName model attach =
|
||||
|
||||
Nothing ->
|
||||
span [ class "invisible hidden" ] []
|
||||
|
||||
|
||||
isFolderMember : Model -> Bool
|
||||
isFolderMember model =
|
||||
let
|
||||
selected =
|
||||
Comp.Dropdown.getSelected model.folderModel
|
||||
|> List.head
|
||||
|> Maybe.map .id
|
||||
in
|
||||
Util.Folder.isFolderMember model.allFolders selected
|
||||
|
@ -61,7 +61,7 @@ emptyModel : Model
|
||||
emptyModel =
|
||||
{ connectionModel =
|
||||
Comp.Dropdown.makeSingle
|
||||
{ makeOption = \a -> { value = a, text = a }
|
||||
{ makeOption = \a -> { value = a, text = a, additional = "" }
|
||||
, placeholder = "Select connection..."
|
||||
}
|
||||
, subject = ""
|
||||
@ -124,7 +124,7 @@ update flags msg model =
|
||||
|
||||
cm =
|
||||
Comp.Dropdown.makeSingleList
|
||||
{ makeOption = \a -> { value = a, text = a }
|
||||
{ makeOption = \a -> { value = a, text = a, additional = "" }
|
||||
, placeholder = "Select Connection..."
|
||||
, options = names
|
||||
, selected = List.head names
|
||||
|
@ -139,7 +139,7 @@ init flags =
|
||||
( { settings = Api.Model.NotificationSettings.empty
|
||||
, connectionModel =
|
||||
Comp.Dropdown.makeSingle
|
||||
{ makeOption = \a -> { value = a, text = a }
|
||||
{ makeOption = \a -> { value = a, text = a, additional = "" }
|
||||
, placeholder = "Select connection..."
|
||||
}
|
||||
, tagInclModel = Util.Tag.makeDropdownModel
|
||||
@ -290,7 +290,7 @@ update flags msg model =
|
||||
|
||||
cm =
|
||||
Comp.Dropdown.makeSingleList
|
||||
{ makeOption = \a -> { value = a, text = a }
|
||||
{ makeOption = \a -> { value = a, text = a, additional = "" }
|
||||
, placeholder = "Select Connection..."
|
||||
, options = names
|
||||
, selected = List.head names
|
||||
|
@ -10,10 +10,13 @@ module Comp.ScanMailboxForm exposing
|
||||
|
||||
import Api
|
||||
import Api.Model.BasicResult exposing (BasicResult)
|
||||
import Api.Model.FolderItem exposing (FolderItem)
|
||||
import Api.Model.FolderList exposing (FolderList)
|
||||
import Api.Model.IdName exposing (IdName)
|
||||
import Api.Model.ImapSettingsList exposing (ImapSettingsList)
|
||||
import Api.Model.ScanMailboxSettings exposing (ScanMailboxSettings)
|
||||
import Comp.CalEventInput
|
||||
import Comp.Dropdown
|
||||
import Comp.Dropdown exposing (isDropdownChangeMsg)
|
||||
import Comp.IntField
|
||||
import Comp.StringListInput
|
||||
import Comp.YesNoDimmer
|
||||
@ -26,9 +29,12 @@ import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events exposing (onCheck, onClick, onInput)
|
||||
import Http
|
||||
import Markdown
|
||||
import Util.Folder exposing (mkFolderOption)
|
||||
import Util.Http
|
||||
import Util.List
|
||||
import Util.Maybe
|
||||
import Util.Update
|
||||
|
||||
|
||||
type alias Model =
|
||||
@ -47,6 +53,9 @@ type alias Model =
|
||||
, formMsg : Maybe BasicResult
|
||||
, loading : Int
|
||||
, yesNoDelete : Comp.YesNoDimmer.Model
|
||||
, folderModel : Comp.Dropdown.Model IdName
|
||||
, allFolders : List FolderItem
|
||||
, itemFolderId : Maybe String
|
||||
}
|
||||
|
||||
|
||||
@ -73,6 +82,8 @@ type Msg
|
||||
| FoldersMsg Comp.StringListInput.Msg
|
||||
| DirectionMsg (Maybe Direction)
|
||||
| YesNoDeleteMsg Comp.YesNoDimmer.Msg
|
||||
| GetFolderResp (Result Http.Error FolderList)
|
||||
| FolderDropdownMsg (Comp.Dropdown.Msg IdName)
|
||||
|
||||
|
||||
initWith : Flags -> ScanMailboxSettings -> ( Model, Cmd Msg )
|
||||
@ -108,11 +119,13 @@ initWith flags s =
|
||||
, scheduleModel = sm
|
||||
, formMsg = Nothing
|
||||
, yesNoDelete = Comp.YesNoDimmer.emptyModel
|
||||
, itemFolderId = s.itemFolder
|
||||
}
|
||||
, Cmd.batch
|
||||
[ Api.getImapSettings flags "" ConnResp
|
||||
, nc
|
||||
, Cmd.map CalEventMsg sc
|
||||
, Api.getFolders flags "" False GetFolderResp
|
||||
]
|
||||
)
|
||||
|
||||
@ -129,7 +142,7 @@ init flags =
|
||||
( { settings = Api.Model.ScanMailboxSettings.empty
|
||||
, connectionModel =
|
||||
Comp.Dropdown.makeSingle
|
||||
{ makeOption = \a -> { value = a, text = a }
|
||||
{ makeOption = \a -> { value = a, text = a, additional = "" }
|
||||
, placeholder = "Select connection..."
|
||||
}
|
||||
, enabled = False
|
||||
@ -143,12 +156,20 @@ init flags =
|
||||
, schedule = initialSchedule
|
||||
, scheduleModel = sm
|
||||
, formMsg = Nothing
|
||||
, loading = 1
|
||||
, loading = 2
|
||||
, yesNoDelete = Comp.YesNoDimmer.emptyModel
|
||||
, folderModel =
|
||||
Comp.Dropdown.makeSingle
|
||||
{ makeOption = \e -> { value = e.id, text = e.name, additional = "" }
|
||||
, placeholder = ""
|
||||
}
|
||||
, allFolders = []
|
||||
, itemFolderId = Nothing
|
||||
}
|
||||
, Cmd.batch
|
||||
[ Api.getImapSettings flags "" ConnResp
|
||||
, Cmd.map CalEventMsg sc
|
||||
, Api.getFolders flags "" False GetFolderResp
|
||||
]
|
||||
)
|
||||
|
||||
@ -186,6 +207,7 @@ makeSettings model =
|
||||
, folders = folders
|
||||
, direction = Maybe.map Data.Direction.toString model.direction
|
||||
, schedule = Data.CalEvent.makeEvent timer
|
||||
, itemFolder = model.itemFolderId
|
||||
}
|
||||
in
|
||||
Data.Validated.map3 make
|
||||
@ -260,7 +282,7 @@ update flags msg model =
|
||||
|
||||
cm =
|
||||
Comp.Dropdown.makeSingleList
|
||||
{ makeOption = \a -> { value = a, text = a }
|
||||
{ makeOption = \a -> { value = a, text = a, additional = "" }
|
||||
, placeholder = "Select Connection..."
|
||||
, options = names
|
||||
, selected = List.head names
|
||||
@ -402,6 +424,84 @@ update flags msg model =
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
GetFolderResp (Ok fs) ->
|
||||
let
|
||||
model_ =
|
||||
{ model
|
||||
| allFolders = fs.items
|
||||
, loading = model.loading - 1
|
||||
, folderModel =
|
||||
Comp.Dropdown.setMkOption
|
||||
(mkFolderOption flags fs.items)
|
||||
model.folderModel
|
||||
}
|
||||
|
||||
mkIdName fitem =
|
||||
IdName fitem.id fitem.name
|
||||
|
||||
opts =
|
||||
fs.items
|
||||
|> List.map mkIdName
|
||||
|> Comp.Dropdown.SetOptions
|
||||
|
||||
mkIdNameFromId id =
|
||||
List.filterMap
|
||||
(\f ->
|
||||
if f.id == id then
|
||||
Just (IdName id f.name)
|
||||
|
||||
else
|
||||
Nothing
|
||||
)
|
||||
fs.items
|
||||
|
||||
sel =
|
||||
case Maybe.map mkIdNameFromId model.itemFolderId of
|
||||
Just idref ->
|
||||
idref
|
||||
|
||||
Nothing ->
|
||||
[]
|
||||
|
||||
removeAction ( a, _, c ) =
|
||||
( a, c )
|
||||
|
||||
addNoAction ( a, b ) =
|
||||
( a, NoAction, b )
|
||||
in
|
||||
Util.Update.andThen1
|
||||
[ update flags (FolderDropdownMsg opts) >> removeAction
|
||||
, update flags (FolderDropdownMsg (Comp.Dropdown.SetSelection sel)) >> removeAction
|
||||
]
|
||||
model_
|
||||
|> addNoAction
|
||||
|
||||
GetFolderResp (Err _) ->
|
||||
( { model | loading = model.loading - 1 }
|
||||
, NoAction
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
FolderDropdownMsg m ->
|
||||
let
|
||||
( m2, c2 ) =
|
||||
Comp.Dropdown.update m model.folderModel
|
||||
|
||||
newModel =
|
||||
{ model | folderModel = m2 }
|
||||
|
||||
idref =
|
||||
Comp.Dropdown.getSelected m2 |> List.head
|
||||
|
||||
model_ =
|
||||
if isDropdownChangeMsg m then
|
||||
{ newModel | itemFolderId = Maybe.map .id idref }
|
||||
|
||||
else
|
||||
newModel
|
||||
in
|
||||
( model_, NoAction, Cmd.map FolderDropdownMsg c2 )
|
||||
|
||||
|
||||
|
||||
--- View
|
||||
@ -424,7 +524,7 @@ view : String -> UiSettings -> Model -> Html Msg
|
||||
view extraClasses settings model =
|
||||
div
|
||||
[ classList
|
||||
[ ( "ui form", True )
|
||||
[ ( "ui warning form", True )
|
||||
, ( extraClasses, True )
|
||||
, ( "error", isFormError model )
|
||||
, ( "success", isFormSuccess model )
|
||||
@ -547,6 +647,28 @@ view extraClasses settings model =
|
||||
]
|
||||
]
|
||||
]
|
||||
, div [ class "field" ]
|
||||
[ label []
|
||||
[ text "Item Folder"
|
||||
]
|
||||
, Html.map FolderDropdownMsg (Comp.Dropdown.view settings model.folderModel)
|
||||
, span [ class "small-info" ]
|
||||
[ text "Put all items from this mailbox into the selected folder"
|
||||
]
|
||||
, div
|
||||
[ classList
|
||||
[ ( "ui warning message", True )
|
||||
, ( "hidden", isFolderMember model )
|
||||
]
|
||||
]
|
||||
[ Markdown.toHtml [] """
|
||||
You are **not a member** of this folder. Items created from mails in
|
||||
this mailbox will be **hidden** from any search results. Use a folder
|
||||
where you are a member of to make items visible. This message will
|
||||
disappear then.
|
||||
"""
|
||||
]
|
||||
]
|
||||
, div [ class "required field" ]
|
||||
[ label []
|
||||
[ text "Schedule"
|
||||
@ -612,3 +734,14 @@ view extraClasses settings model =
|
||||
[ text "Start Once"
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
isFolderMember : Model -> Bool
|
||||
isFolderMember model =
|
||||
let
|
||||
selected =
|
||||
Comp.Dropdown.getSelected model.folderModel
|
||||
|> List.head
|
||||
|> Maybe.map .id
|
||||
in
|
||||
Util.Folder.isFolderMember model.allFolders selected
|
||||
|
@ -11,6 +11,7 @@ module Comp.SearchMenu exposing
|
||||
import Api
|
||||
import Api.Model.Equipment exposing (Equipment)
|
||||
import Api.Model.EquipmentList exposing (EquipmentList)
|
||||
import Api.Model.FolderList exposing (FolderList)
|
||||
import Api.Model.IdName exposing (IdName)
|
||||
import Api.Model.ItemSearch exposing (ItemSearch)
|
||||
import Api.Model.ReferenceList exposing (ReferenceList)
|
||||
@ -45,6 +46,7 @@ type alias Model =
|
||||
, corrPersonModel : Comp.Dropdown.Model IdName
|
||||
, concPersonModel : Comp.Dropdown.Model IdName
|
||||
, concEquipmentModel : Comp.Dropdown.Model Equipment
|
||||
, folderModel : Comp.Dropdown.Model IdName
|
||||
, inboxCheckbox : Bool
|
||||
, fromDateModel : DatePicker
|
||||
, fromDate : Maybe Int
|
||||
@ -72,6 +74,7 @@ init =
|
||||
\entry ->
|
||||
{ value = Data.Direction.toString entry
|
||||
, text = Data.Direction.toString entry
|
||||
, additional = ""
|
||||
}
|
||||
, options = Data.Direction.all
|
||||
, placeholder = "Choose a direction…"
|
||||
@ -81,28 +84,36 @@ init =
|
||||
Comp.Dropdown.makeModel
|
||||
{ multiple = False
|
||||
, searchable = \n -> n > 5
|
||||
, makeOption = \e -> { value = e.id, text = e.name }
|
||||
, makeOption = \e -> { value = e.id, text = e.name, additional = "" }
|
||||
, labelColor = \_ -> \_ -> ""
|
||||
, placeholder = "Choose an organization"
|
||||
}
|
||||
, corrPersonModel =
|
||||
Comp.Dropdown.makeSingle
|
||||
{ makeOption = \e -> { value = e.id, text = e.name }
|
||||
{ makeOption = \e -> { value = e.id, text = e.name, additional = "" }
|
||||
, placeholder = "Choose a person"
|
||||
}
|
||||
, concPersonModel =
|
||||
Comp.Dropdown.makeSingle
|
||||
{ makeOption = \e -> { value = e.id, text = e.name }
|
||||
{ makeOption = \e -> { value = e.id, text = e.name, additional = "" }
|
||||
, placeholder = "Choose a person"
|
||||
}
|
||||
, concEquipmentModel =
|
||||
Comp.Dropdown.makeModel
|
||||
{ multiple = False
|
||||
, searchable = \n -> n > 5
|
||||
, makeOption = \e -> { value = e.id, text = e.name }
|
||||
, makeOption = \e -> { value = e.id, text = e.name, additional = "" }
|
||||
, labelColor = \_ -> \_ -> ""
|
||||
, placeholder = "Choose an equipment"
|
||||
}
|
||||
, folderModel =
|
||||
Comp.Dropdown.makeModel
|
||||
{ multiple = False
|
||||
, searchable = \n -> n > 5
|
||||
, makeOption = \e -> { value = e.id, text = e.name, additional = "" }
|
||||
, labelColor = \_ -> \_ -> ""
|
||||
, placeholder = "Only items in folder"
|
||||
}
|
||||
, inboxCheckbox = False
|
||||
, fromDateModel = Comp.DatePicker.emptyModel
|
||||
, fromDate = Nothing
|
||||
@ -144,6 +155,8 @@ type Msg
|
||||
| ResetForm
|
||||
| KeyUpMsg (Maybe KeyCode)
|
||||
| ToggleNameHelp
|
||||
| FolderMsg (Comp.Dropdown.Msg IdName)
|
||||
| GetFolderResp (Result Http.Error FolderList)
|
||||
|
||||
|
||||
getDirection : Model -> Maybe Direction
|
||||
@ -184,6 +197,7 @@ getItemSearch model =
|
||||
, corrOrg = Comp.Dropdown.getSelected model.orgModel |> List.map .id |> List.head
|
||||
, concPerson = Comp.Dropdown.getSelected model.concPersonModel |> List.map .id |> List.head
|
||||
, concEquip = Comp.Dropdown.getSelected model.concEquipmentModel |> List.map .id |> List.head
|
||||
, folder = Comp.Dropdown.getSelected model.folderModel |> List.map .id |> List.head
|
||||
, direction = Comp.Dropdown.getSelected model.directionModel |> List.head |> Maybe.map Data.Direction.toString
|
||||
, inbox = model.inboxCheckbox
|
||||
, dateFrom = model.fromDate
|
||||
@ -250,6 +264,7 @@ update flags settings msg model =
|
||||
, Api.getOrgLight flags GetOrgResp
|
||||
, Api.getEquipments flags "" GetEquipResp
|
||||
, Api.getPersonsLight flags GetPersonResp
|
||||
, Api.getFolders flags "" False GetFolderResp
|
||||
, cdp
|
||||
]
|
||||
)
|
||||
@ -513,6 +528,29 @@ update flags settings msg model =
|
||||
ToggleNameHelp ->
|
||||
NextState ( { model | showNameHelp = not model.showNameHelp }, Cmd.none ) False
|
||||
|
||||
GetFolderResp (Ok fs) ->
|
||||
let
|
||||
opts =
|
||||
List.filter .isMember fs.items
|
||||
|> List.map (\e -> IdName e.id e.name)
|
||||
|> Comp.Dropdown.SetOptions
|
||||
in
|
||||
update flags settings (FolderMsg opts) model
|
||||
|
||||
GetFolderResp (Err _) ->
|
||||
noChange ( model, Cmd.none )
|
||||
|
||||
FolderMsg lm ->
|
||||
let
|
||||
( m2, c2 ) =
|
||||
Comp.Dropdown.update lm model.folderModel
|
||||
in
|
||||
NextState
|
||||
( { model | folderModel = m2 }
|
||||
, Cmd.map FolderMsg c2
|
||||
)
|
||||
(isDropdownChangeMsg lm)
|
||||
|
||||
|
||||
|
||||
-- View
|
||||
@ -629,6 +667,11 @@ view flags settings model =
|
||||
[ text "Looks in item name only."
|
||||
]
|
||||
]
|
||||
, formHeader (Icons.folderIcon "") "Folder"
|
||||
, div [ class "field" ]
|
||||
[ label [] [ text "Folder" ]
|
||||
, Html.map FolderMsg (Comp.Dropdown.view settings model.folderModel)
|
||||
]
|
||||
, formHeader (Icons.tagsIcon "") "Tags"
|
||||
, div [ class "field" ]
|
||||
[ label [] [ text "Include (and)" ]
|
||||
|
@ -1,20 +1,29 @@
|
||||
module Comp.SourceForm exposing
|
||||
( Model
|
||||
, Msg(..)
|
||||
, emptyModel
|
||||
, getSource
|
||||
, init
|
||||
, isValid
|
||||
, update
|
||||
, view
|
||||
)
|
||||
|
||||
import Api
|
||||
import Api.Model.FolderItem exposing (FolderItem)
|
||||
import Api.Model.FolderList exposing (FolderList)
|
||||
import Api.Model.IdName exposing (IdName)
|
||||
import Api.Model.Source exposing (Source)
|
||||
import Comp.Dropdown exposing (isDropdownChangeMsg)
|
||||
import Comp.FixedDropdown
|
||||
import Data.Flags exposing (Flags)
|
||||
import Data.Priority exposing (Priority)
|
||||
import Data.UiSettings exposing (UiSettings)
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events exposing (onCheck, onInput)
|
||||
import Http
|
||||
import Markdown
|
||||
import Util.Folder exposing (mkFolderOption)
|
||||
|
||||
|
||||
type alias Model =
|
||||
@ -24,6 +33,9 @@ type alias Model =
|
||||
, priorityModel : Comp.FixedDropdown.Model Priority
|
||||
, priority : Priority
|
||||
, enabled : Bool
|
||||
, folderModel : Comp.Dropdown.Model IdName
|
||||
, allFolders : List FolderItem
|
||||
, folderId : Maybe String
|
||||
}
|
||||
|
||||
|
||||
@ -38,9 +50,23 @@ emptyModel =
|
||||
Data.Priority.all
|
||||
, priority = Data.Priority.Low
|
||||
, enabled = False
|
||||
, folderModel =
|
||||
Comp.Dropdown.makeSingle
|
||||
{ makeOption = \e -> { value = e.id, text = e.name, additional = "" }
|
||||
, placeholder = ""
|
||||
}
|
||||
, allFolders = []
|
||||
, folderId = Nothing
|
||||
}
|
||||
|
||||
|
||||
init : Flags -> ( Model, Cmd Msg )
|
||||
init flags =
|
||||
( emptyModel
|
||||
, Api.getFolders flags "" False GetFolderResp
|
||||
)
|
||||
|
||||
|
||||
isValid : Model -> Bool
|
||||
isValid model =
|
||||
model.abbrev /= ""
|
||||
@ -57,6 +83,7 @@ getSource model =
|
||||
, description = model.description
|
||||
, enabled = model.enabled
|
||||
, priority = Data.Priority.toName model.priority
|
||||
, folder = model.folderId
|
||||
}
|
||||
|
||||
|
||||
@ -66,10 +93,12 @@ type Msg
|
||||
| SetDescr String
|
||||
| ToggleEnabled
|
||||
| PrioDropdownMsg (Comp.FixedDropdown.Msg Priority)
|
||||
| GetFolderResp (Result Http.Error FolderList)
|
||||
| FolderDropdownMsg (Comp.Dropdown.Msg IdName)
|
||||
|
||||
|
||||
update : Flags -> Msg -> Model -> ( Model, Cmd Msg )
|
||||
update _ msg model =
|
||||
update flags msg model =
|
||||
case msg of
|
||||
SetSource t ->
|
||||
let
|
||||
@ -83,19 +112,41 @@ update _ msg model =
|
||||
, description = t.description
|
||||
, priority = t.priority
|
||||
, enabled = t.enabled
|
||||
, folder = t.folder
|
||||
}
|
||||
|
||||
newModel =
|
||||
{ model
|
||||
| source = np
|
||||
, abbrev = t.abbrev
|
||||
, description = t.description
|
||||
, priority =
|
||||
Data.Priority.fromString t.priority
|
||||
|> Maybe.withDefault Data.Priority.Low
|
||||
, enabled = t.enabled
|
||||
, folderId = t.folder
|
||||
}
|
||||
|
||||
mkIdName id =
|
||||
List.filterMap
|
||||
(\f ->
|
||||
if f.id == id then
|
||||
Just (IdName id f.name)
|
||||
|
||||
else
|
||||
Nothing
|
||||
)
|
||||
model.allFolders
|
||||
|
||||
sel =
|
||||
case Maybe.map mkIdName t.folder of
|
||||
Just idref ->
|
||||
idref
|
||||
|
||||
Nothing ->
|
||||
[]
|
||||
in
|
||||
( { model
|
||||
| source = np
|
||||
, abbrev = t.abbrev
|
||||
, description = t.description
|
||||
, priority =
|
||||
Data.Priority.fromString t.priority
|
||||
|> Maybe.withDefault Data.Priority.Low
|
||||
, enabled = t.enabled
|
||||
}
|
||||
, Cmd.none
|
||||
)
|
||||
update flags (FolderDropdownMsg (Comp.Dropdown.SetSelection sel)) newModel
|
||||
|
||||
ToggleEnabled ->
|
||||
( { model | enabled = not model.enabled }, Cmd.none )
|
||||
@ -127,16 +178,60 @@ update _ msg model =
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
GetFolderResp (Ok fs) ->
|
||||
let
|
||||
model_ =
|
||||
{ model
|
||||
| allFolders = fs.items
|
||||
, folderModel =
|
||||
Comp.Dropdown.setMkOption
|
||||
(mkFolderOption flags fs.items)
|
||||
model.folderModel
|
||||
}
|
||||
|
||||
view : Flags -> Model -> Html Msg
|
||||
view flags model =
|
||||
mkIdName fitem =
|
||||
IdName fitem.id fitem.name
|
||||
|
||||
opts =
|
||||
fs.items
|
||||
|> List.map mkIdName
|
||||
|> Comp.Dropdown.SetOptions
|
||||
in
|
||||
update flags (FolderDropdownMsg opts) model_
|
||||
|
||||
GetFolderResp (Err _) ->
|
||||
( model, Cmd.none )
|
||||
|
||||
FolderDropdownMsg m ->
|
||||
let
|
||||
( m2, c2 ) =
|
||||
Comp.Dropdown.update m model.folderModel
|
||||
|
||||
newModel =
|
||||
{ model | folderModel = m2 }
|
||||
|
||||
idref =
|
||||
Comp.Dropdown.getSelected m2 |> List.head
|
||||
|
||||
model_ =
|
||||
if isDropdownChangeMsg m then
|
||||
{ newModel | folderId = Maybe.map .id idref }
|
||||
|
||||
else
|
||||
newModel
|
||||
in
|
||||
( model_, Cmd.map FolderDropdownMsg c2 )
|
||||
|
||||
|
||||
view : Flags -> UiSettings -> Model -> Html Msg
|
||||
view flags settings model =
|
||||
let
|
||||
priorityItem =
|
||||
Comp.FixedDropdown.Item
|
||||
model.priority
|
||||
(Data.Priority.toName model.priority)
|
||||
in
|
||||
div [ class "ui form" ]
|
||||
div [ class "ui warning form" ]
|
||||
[ div
|
||||
[ classList
|
||||
[ ( "field", True )
|
||||
@ -179,6 +274,25 @@ view flags model =
|
||||
model.priorityModel
|
||||
)
|
||||
]
|
||||
, div [ class "field" ]
|
||||
[ label []
|
||||
[ text "Folder"
|
||||
]
|
||||
, Html.map FolderDropdownMsg (Comp.Dropdown.view settings model.folderModel)
|
||||
, div
|
||||
[ classList
|
||||
[ ( "ui warning message", True )
|
||||
, ( "hidden", isFolderMember model )
|
||||
]
|
||||
]
|
||||
[ Markdown.toHtml [] """
|
||||
You are **not a member** of this folder. Items created through this
|
||||
link will be **hidden** from any search results. Use a folder where
|
||||
you are a member of to make items visible. This message will
|
||||
disappear then.
|
||||
"""
|
||||
]
|
||||
]
|
||||
, urlInfoMessage flags model
|
||||
]
|
||||
|
||||
@ -217,3 +331,14 @@ urlInfoMessage flags model =
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
isFolderMember : Model -> Bool
|
||||
isFolderMember model =
|
||||
let
|
||||
selected =
|
||||
Comp.Dropdown.getSelected model.folderModel
|
||||
|> List.head
|
||||
|> Maybe.map .id
|
||||
in
|
||||
Util.Folder.isFolderMember model.allFolders selected
|
||||
|
@ -1,7 +1,7 @@
|
||||
module Comp.SourceManage exposing
|
||||
( Model
|
||||
, Msg(..)
|
||||
, emptyModel
|
||||
, init
|
||||
, update
|
||||
, view
|
||||
)
|
||||
@ -14,6 +14,7 @@ import Comp.SourceForm
|
||||
import Comp.SourceTable
|
||||
import Comp.YesNoDimmer
|
||||
import Data.Flags exposing (Flags)
|
||||
import Data.UiSettings exposing (UiSettings)
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events exposing (onClick, onSubmit)
|
||||
@ -37,15 +38,21 @@ type ViewMode
|
||||
| Form
|
||||
|
||||
|
||||
emptyModel : Model
|
||||
emptyModel =
|
||||
{ tableModel = Comp.SourceTable.emptyModel
|
||||
, formModel = Comp.SourceForm.emptyModel
|
||||
, viewMode = Table
|
||||
, formError = Nothing
|
||||
, loading = False
|
||||
, deleteConfirm = Comp.YesNoDimmer.emptyModel
|
||||
}
|
||||
init : Flags -> ( Model, Cmd Msg )
|
||||
init flags =
|
||||
let
|
||||
( fm, fc ) =
|
||||
Comp.SourceForm.init flags
|
||||
in
|
||||
( { tableModel = Comp.SourceTable.emptyModel
|
||||
, formModel = fm
|
||||
, viewMode = Table
|
||||
, formError = Nothing
|
||||
, loading = False
|
||||
, deleteConfirm = Comp.YesNoDimmer.emptyModel
|
||||
}
|
||||
, Cmd.map FormMsg fc
|
||||
)
|
||||
|
||||
|
||||
type Msg
|
||||
@ -187,13 +194,13 @@ update flags msg model =
|
||||
( { model | deleteConfirm = cm }, cmd )
|
||||
|
||||
|
||||
view : Flags -> Model -> Html Msg
|
||||
view flags model =
|
||||
view : Flags -> UiSettings -> Model -> Html Msg
|
||||
view flags settings model =
|
||||
if model.viewMode == Table then
|
||||
viewTable model
|
||||
|
||||
else
|
||||
div [] (viewForm flags model)
|
||||
div [] (viewForm flags settings model)
|
||||
|
||||
|
||||
viewTable : Model -> Html Msg
|
||||
@ -215,8 +222,8 @@ viewTable model =
|
||||
]
|
||||
|
||||
|
||||
viewForm : Flags -> Model -> List (Html Msg)
|
||||
viewForm flags model =
|
||||
viewForm : Flags -> UiSettings -> Model -> List (Html Msg)
|
||||
viewForm flags settings model =
|
||||
let
|
||||
newSource =
|
||||
model.formModel.source.id == ""
|
||||
@ -236,7 +243,7 @@ viewForm flags model =
|
||||
]
|
||||
, Html.form [ class "ui attached segment", onSubmit Submit ]
|
||||
[ Html.map YesNoMsg (Comp.YesNoDimmer.view model.deleteConfirm)
|
||||
, Html.map FormMsg (Comp.SourceForm.view flags model.formModel)
|
||||
, Html.map FormMsg (Comp.SourceForm.view flags settings model.formModel)
|
||||
, div
|
||||
[ classList
|
||||
[ ( "ui error message", True )
|
||||
|
@ -41,6 +41,7 @@ emptyModel =
|
||||
\s ->
|
||||
{ value = Data.UserState.toString s
|
||||
, text = Data.UserState.toString s
|
||||
, additional = ""
|
||||
}
|
||||
, placeholder = ""
|
||||
, options = Data.UserState.all
|
||||
@ -98,7 +99,12 @@ update _ msg model =
|
||||
let
|
||||
state =
|
||||
Comp.Dropdown.makeSingleList
|
||||
{ makeOption = \s -> { value = Data.UserState.toString s, text = Data.UserState.toString s }
|
||||
{ makeOption =
|
||||
\s ->
|
||||
{ value = Data.UserState.toString s
|
||||
, text = Data.UserState.toString s
|
||||
, additional = ""
|
||||
}
|
||||
, placeholder = ""
|
||||
, options = Data.UserState.all
|
||||
, selected =
|
||||
|
@ -15,6 +15,8 @@ module Data.Icons exposing
|
||||
, editNotesIcon
|
||||
, equipment
|
||||
, equipmentIcon
|
||||
, folder
|
||||
, folderIcon
|
||||
, organization
|
||||
, organizationIcon
|
||||
, person
|
||||
@ -29,6 +31,16 @@ import Html exposing (Html, i)
|
||||
import Html.Attributes exposing (class)
|
||||
|
||||
|
||||
folder : String
|
||||
folder =
|
||||
"folder outline icon"
|
||||
|
||||
|
||||
folderIcon : String -> Html msg
|
||||
folderIcon classes =
|
||||
i [ class (folder ++ " " ++ classes) ] []
|
||||
|
||||
|
||||
concerned : String
|
||||
concerned =
|
||||
"crosshairs icon"
|
||||
|
@ -2,7 +2,7 @@ module Page.CollectiveSettings.Data exposing
|
||||
( Model
|
||||
, Msg(..)
|
||||
, Tab(..)
|
||||
, emptyModel
|
||||
, init
|
||||
)
|
||||
|
||||
import Api.Model.BasicResult exposing (BasicResult)
|
||||
@ -11,6 +11,7 @@ import Api.Model.ItemInsights exposing (ItemInsights)
|
||||
import Comp.CollectiveSettingsForm
|
||||
import Comp.SourceManage
|
||||
import Comp.UserManage
|
||||
import Data.Flags exposing (Flags)
|
||||
import Http
|
||||
|
||||
|
||||
@ -24,15 +25,21 @@ type alias Model =
|
||||
}
|
||||
|
||||
|
||||
emptyModel : Model
|
||||
emptyModel =
|
||||
{ currentTab = Just InsightsTab
|
||||
, sourceModel = Comp.SourceManage.emptyModel
|
||||
, userModel = Comp.UserManage.emptyModel
|
||||
, settingsModel = Comp.CollectiveSettingsForm.init Api.Model.CollectiveSettings.empty
|
||||
, insights = Api.Model.ItemInsights.empty
|
||||
, submitResult = Nothing
|
||||
}
|
||||
init : Flags -> ( Model, Cmd Msg )
|
||||
init flags =
|
||||
let
|
||||
( sm, sc ) =
|
||||
Comp.SourceManage.init flags
|
||||
in
|
||||
( { currentTab = Just InsightsTab
|
||||
, sourceModel = sm
|
||||
, userModel = Comp.UserManage.emptyModel
|
||||
, settingsModel = Comp.CollectiveSettingsForm.init Api.Model.CollectiveSettings.empty
|
||||
, insights = Api.Model.ItemInsights.empty
|
||||
, submitResult = Nothing
|
||||
}
|
||||
, Cmd.map SourceMsg sc
|
||||
)
|
||||
|
||||
|
||||
type Tab
|
||||
|
@ -59,7 +59,7 @@ view flags settings model =
|
||||
[ div [ class "" ]
|
||||
(case model.currentTab of
|
||||
Just SourceTab ->
|
||||
viewSources flags model
|
||||
viewSources flags settings model
|
||||
|
||||
Just UserTab ->
|
||||
viewUsers settings model
|
||||
@ -153,15 +153,15 @@ makeTagStats nc =
|
||||
]
|
||||
|
||||
|
||||
viewSources : Flags -> Model -> List (Html Msg)
|
||||
viewSources flags model =
|
||||
viewSources : Flags -> UiSettings -> Model -> List (Html Msg)
|
||||
viewSources flags settings model =
|
||||
[ h2 [ class "ui header" ]
|
||||
[ i [ class "ui upload icon" ] []
|
||||
, div [ class "content" ]
|
||||
[ text "Sources"
|
||||
]
|
||||
]
|
||||
, Html.map SourceMsg (Comp.SourceManage.view flags model.sourceModel)
|
||||
, Html.map SourceMsg (Comp.SourceManage.view flags settings model.sourceModel)
|
||||
]
|
||||
|
||||
|
||||
|
@ -2,13 +2,15 @@ module Page.ManageData.Data exposing
|
||||
( Model
|
||||
, Msg(..)
|
||||
, Tab(..)
|
||||
, emptyModel
|
||||
, init
|
||||
)
|
||||
|
||||
import Comp.EquipmentManage
|
||||
import Comp.FolderManage
|
||||
import Comp.OrgManage
|
||||
import Comp.PersonManage
|
||||
import Comp.TagManage
|
||||
import Data.Flags exposing (Flags)
|
||||
|
||||
|
||||
type alias Model =
|
||||
@ -17,17 +19,21 @@ type alias Model =
|
||||
, equipManageModel : Comp.EquipmentManage.Model
|
||||
, orgManageModel : Comp.OrgManage.Model
|
||||
, personManageModel : Comp.PersonManage.Model
|
||||
, folderManageModel : Comp.FolderManage.Model
|
||||
}
|
||||
|
||||
|
||||
emptyModel : Model
|
||||
emptyModel =
|
||||
{ currentTab = Nothing
|
||||
, tagManageModel = Comp.TagManage.emptyModel
|
||||
, equipManageModel = Comp.EquipmentManage.emptyModel
|
||||
, orgManageModel = Comp.OrgManage.emptyModel
|
||||
, personManageModel = Comp.PersonManage.emptyModel
|
||||
}
|
||||
init : Flags -> ( Model, Cmd Msg )
|
||||
init _ =
|
||||
( { currentTab = Nothing
|
||||
, tagManageModel = Comp.TagManage.emptyModel
|
||||
, equipManageModel = Comp.EquipmentManage.emptyModel
|
||||
, orgManageModel = Comp.OrgManage.emptyModel
|
||||
, personManageModel = Comp.PersonManage.emptyModel
|
||||
, folderManageModel = Comp.FolderManage.empty
|
||||
}
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
|
||||
type Tab
|
||||
@ -35,6 +41,7 @@ type Tab
|
||||
| EquipTab
|
||||
| OrgTab
|
||||
| PersonTab
|
||||
| FolderTab
|
||||
|
||||
|
||||
type Msg
|
||||
@ -43,3 +50,4 @@ type Msg
|
||||
| EquipManageMsg Comp.EquipmentManage.Msg
|
||||
| OrgManageMsg Comp.OrgManage.Msg
|
||||
| PersonManageMsg Comp.PersonManage.Msg
|
||||
| FolderMsg Comp.FolderManage.Msg
|
||||
|
@ -1,6 +1,7 @@
|
||||
module Page.ManageData.Update exposing (update)
|
||||
|
||||
import Comp.EquipmentManage
|
||||
import Comp.FolderManage
|
||||
import Comp.OrgManage
|
||||
import Comp.PersonManage
|
||||
import Comp.TagManage
|
||||
@ -29,6 +30,13 @@ update flags msg model =
|
||||
PersonTab ->
|
||||
update flags (PersonManageMsg Comp.PersonManage.LoadPersons) m
|
||||
|
||||
FolderTab ->
|
||||
let
|
||||
( sm, sc ) =
|
||||
Comp.FolderManage.init flags
|
||||
in
|
||||
( { m | folderManageModel = sm }, Cmd.map FolderMsg sc )
|
||||
|
||||
TagManageMsg m ->
|
||||
let
|
||||
( m2, c2 ) =
|
||||
@ -56,3 +64,12 @@ update flags msg model =
|
||||
Comp.PersonManage.update flags m model.personManageModel
|
||||
in
|
||||
( { model | personManageModel = m2 }, Cmd.map PersonManageMsg c2 )
|
||||
|
||||
FolderMsg lm ->
|
||||
let
|
||||
( m2, c2 ) =
|
||||
Comp.FolderManage.update flags lm model.folderManageModel
|
||||
in
|
||||
( { model | folderManageModel = m2 }
|
||||
, Cmd.map FolderMsg c2
|
||||
)
|
||||
|
@ -1,9 +1,11 @@
|
||||
module Page.ManageData.View exposing (view)
|
||||
|
||||
import Comp.EquipmentManage
|
||||
import Comp.FolderManage
|
||||
import Comp.OrgManage
|
||||
import Comp.PersonManage
|
||||
import Comp.TagManage
|
||||
import Data.Flags exposing (Flags)
|
||||
import Data.Icons as Icons
|
||||
import Data.UiSettings exposing (UiSettings)
|
||||
import Html exposing (..)
|
||||
@ -13,8 +15,8 @@ import Page.ManageData.Data exposing (..)
|
||||
import Util.Html exposing (classActive)
|
||||
|
||||
|
||||
view : UiSettings -> Model -> Html Msg
|
||||
view settings model =
|
||||
view : Flags -> UiSettings -> Model -> Html Msg
|
||||
view flags settings model =
|
||||
div [ class "managedata-page ui padded grid" ]
|
||||
[ div [ class "sixteen wide mobile four wide tablet four wide computer column" ]
|
||||
[ h4 [ class "ui top attached ablue-comp header" ]
|
||||
@ -50,6 +52,13 @@ view settings model =
|
||||
[ Icons.personIcon ""
|
||||
, text "Person"
|
||||
]
|
||||
, div
|
||||
[ classActive (model.currentTab == Just FolderTab) "link icon item"
|
||||
, onClick (SetTab FolderTab)
|
||||
]
|
||||
[ Icons.folderIcon ""
|
||||
, text "Folder"
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
@ -68,6 +77,9 @@ view settings model =
|
||||
Just PersonTab ->
|
||||
viewPerson settings model
|
||||
|
||||
Just FolderTab ->
|
||||
viewFolder flags settings model
|
||||
|
||||
Nothing ->
|
||||
[]
|
||||
)
|
||||
@ -75,6 +87,22 @@ view settings model =
|
||||
]
|
||||
|
||||
|
||||
viewFolder : Flags -> UiSettings -> Model -> List (Html Msg)
|
||||
viewFolder flags _ model =
|
||||
[ h2
|
||||
[ class "ui header"
|
||||
]
|
||||
[ Icons.folderIcon ""
|
||||
, div
|
||||
[ class "content"
|
||||
]
|
||||
[ text "Folders"
|
||||
]
|
||||
]
|
||||
, Html.map FolderMsg (Comp.FolderManage.view flags model.folderManageModel)
|
||||
]
|
||||
|
||||
|
||||
viewTags : Model -> List (Html Msg)
|
||||
viewTags model =
|
||||
[ h2 [ class "ui header" ]
|
||||
|
53
modules/webapp/src/main/elm/Util/Folder.elm
Normal file
53
modules/webapp/src/main/elm/Util/Folder.elm
Normal file
@ -0,0 +1,53 @@
|
||||
module Util.Folder exposing
|
||||
( isFolderMember
|
||||
, mkFolderOption
|
||||
)
|
||||
|
||||
import Api.Model.FolderItem exposing (FolderItem)
|
||||
import Api.Model.IdName exposing (IdName)
|
||||
import Comp.Dropdown
|
||||
import Data.Flags exposing (Flags)
|
||||
|
||||
|
||||
mkFolderOption : Flags -> List FolderItem -> IdName -> Comp.Dropdown.Option
|
||||
mkFolderOption flags allFolders idref =
|
||||
let
|
||||
folder =
|
||||
List.filter (\e -> e.id == idref.id) allFolders
|
||||
|> List.head
|
||||
|
||||
isMember =
|
||||
folder
|
||||
|> Maybe.map .isMember
|
||||
|> Maybe.withDefault False
|
||||
|
||||
isOwner =
|
||||
Maybe.map .owner folder
|
||||
|> Maybe.map .name
|
||||
|> (==) (Maybe.map .user flags.account)
|
||||
|
||||
adds =
|
||||
if isOwner then
|
||||
"owner"
|
||||
|
||||
else if isMember then
|
||||
"member"
|
||||
|
||||
else
|
||||
""
|
||||
in
|
||||
{ value = idref.id, text = idref.name, additional = adds }
|
||||
|
||||
|
||||
isFolderMember : List FolderItem -> Maybe String -> Bool
|
||||
isFolderMember allFolders selected =
|
||||
let
|
||||
findFolder id =
|
||||
List.filter (\e -> e.id == id) allFolders
|
||||
|> List.head
|
||||
|
||||
folder =
|
||||
Maybe.andThen findFolder selected
|
||||
in
|
||||
Maybe.map .isMember folder
|
||||
|> Maybe.withDefault True
|
@ -10,7 +10,7 @@ makeDropdownModel =
|
||||
Comp.Dropdown.makeModel
|
||||
{ multiple = True
|
||||
, searchable = \n -> n > 5
|
||||
, makeOption = \tag -> { value = tag.id, text = tag.name }
|
||||
, makeOption = \tag -> { value = tag.id, text = tag.name, additional = "" }
|
||||
, labelColor =
|
||||
\tag ->
|
||||
\settings ->
|
||||
|
Loading…
x
Reference in New Issue
Block a user