Amend source form with tags and file-filter

Allow to define tags and a file filter per source.
This commit is contained in:
Eike Kettner 2020-11-12 21:40:53 +01:00
parent 4fd6e02ec0
commit 04ba14f802
18 changed files with 498 additions and 122 deletions

View File

@ -4,44 +4,50 @@ import cats.effect.{Effect, Resource}
import cats.implicits._ import cats.implicits._
import docspell.common.{AccountId, Ident} import docspell.common.{AccountId, Ident}
import docspell.store.UpdateResult
import docspell.store.records.RSource import docspell.store.records.RSource
import docspell.store.records.SourceData
import docspell.store.{AddResult, Store} import docspell.store.{AddResult, Store}
trait OSource[F[_]] { trait OSource[F[_]] {
def findAll(account: AccountId): F[Vector[RSource]] def findAll(account: AccountId): F[Vector[SourceData]]
def add(s: RSource): F[AddResult] def add(s: RSource, tags: List[String]): F[AddResult]
def update(s: RSource): F[AddResult] def update(s: RSource, tags: List[String]): F[AddResult]
def delete(id: Ident, collective: Ident): F[AddResult] def delete(id: Ident, collective: Ident): F[UpdateResult]
} }
object OSource { object OSource {
def apply[F[_]: Effect](store: Store[F]): Resource[F, OSource[F]] = def apply[F[_]: Effect](store: Store[F]): Resource[F, OSource[F]] =
Resource.pure[F, OSource[F]](new OSource[F] { Resource.pure[F, OSource[F]](new OSource[F] {
def findAll(account: AccountId): F[Vector[RSource]] = def findAll(account: AccountId): F[Vector[SourceData]] =
store.transact(RSource.findAll(account.collective, _.abbrev)) store
.transact(SourceData.findAll(account.collective, _.abbrev))
.compile
.to(Vector)
def add(s: RSource): F[AddResult] = { def add(s: RSource, tags: List[String]): F[AddResult] = {
def insert = RSource.insert(s) def insert = SourceData.insert(s, tags)
def exists = RSource.existsByAbbrev(s.cid, s.abbrev) def exists = RSource.existsByAbbrev(s.cid, s.abbrev)
val msg = s"A source with abbrev '${s.abbrev}' already exists" val msg = s"A source with abbrev '${s.abbrev}' already exists"
store.add(insert, exists).map(_.fold(identity, _.withMsg(msg), identity)) store.add(insert, exists).map(_.fold(identity, _.withMsg(msg), identity))
} }
def update(s: RSource): F[AddResult] = { def update(s: RSource, tags: List[String]): F[AddResult] = {
def insert = RSource.updateNoCounter(s) def insert = SourceData.update(s, tags)
def exists = RSource.existsByAbbrev(s.cid, s.abbrev) def exists = RSource.existsByAbbrev(s.cid, s.abbrev)
val msg = s"A source with abbrev '${s.abbrev}' already exists" val msg = s"A source with abbrev '${s.abbrev}' already exists"
store.add(insert, exists).map(_.fold(identity, _.withMsg(msg), identity)) store.add(insert, exists).map(_.fold(identity, _.withMsg(msg), identity))
} }
def delete(id: Ident, collective: Ident): F[AddResult] = def delete(id: Ident, collective: Ident): F[UpdateResult] =
store.transact(RSource.delete(id, collective)).attempt.map(AddResult.fromUpdate) UpdateResult.fromUpdate(store.transact(SourceData.delete(id, collective)))
}) })
} }

View File

@ -4,6 +4,7 @@ import cats.effect.{Effect, Resource}
import cats.implicits._ import cats.implicits._
import docspell.common.{AccountId, Ident} import docspell.common.{AccountId, Ident}
import docspell.store.records.RTagSource
import docspell.store.records.{RTag, RTagItem} import docspell.store.records.{RTag, RTagItem}
import docspell.store.{AddResult, Store} import docspell.store.{AddResult, Store}
@ -49,8 +50,9 @@ object OTag {
val io = for { val io = for {
optTag <- RTag.findByIdAndCollective(id, collective) optTag <- RTag.findByIdAndCollective(id, collective)
n0 <- optTag.traverse(t => RTagItem.deleteTag(t.tagId)) n0 <- optTag.traverse(t => RTagItem.deleteTag(t.tagId))
n1 <- optTag.traverse(t => RTag.delete(t.tagId, collective)) n1 <- optTag.traverse(t => RTagSource.deleteTag(t.tagId))
} yield n0.getOrElse(0) + n1.getOrElse(0) n2 <- optTag.traverse(t => RTag.delete(t.tagId, collective))
} yield (n0 |+| n1 |+| n2).getOrElse(0)
store.transact(io).attempt.map(AddResult.fromUpdate) store.transact(io).attempt.map(AddResult.fromUpdate)
} }

View File

@ -25,6 +25,11 @@ trait OUpload[F[_]] {
itemId: Option[Ident] itemId: Option[Ident]
): F[OUpload.UploadResult] ): F[OUpload.UploadResult]
/** Submit files via a given source identifier. The source is looked
* up to identify the collective the files belong to. Metadata
* defined in the source is used as a fallback to those specified
* here (in UploadData).
*/
def submit( def submit(
data: OUpload.UploadData[F], data: OUpload.UploadData[F],
sourceId: Ident, sourceId: Ident,
@ -153,15 +158,19 @@ object OUpload {
itemId: Option[Ident] itemId: Option[Ident]
): F[OUpload.UploadResult] = ): F[OUpload.UploadResult] =
(for { (for {
src <- OptionT(store.transact(RSource.findEnabled(sourceId))) src <- OptionT(store.transact(SourceData.findEnabled(sourceId)))
updata = data.copy( updata = data.copy(
meta = data.meta.copy( meta = data.meta.copy(
sourceAbbrev = src.abbrev, sourceAbbrev = src.source.abbrev,
folderId = data.meta.folderId.orElse(src.folderId) folderId = data.meta.folderId.orElse(src.source.folderId),
fileFilter =
if (data.meta.fileFilter == Glob.all) src.source.fileFilterOrAll
else data.meta.fileFilter,
tags = (data.meta.tags ++ src.tags.map(_.tagId.id)).distinct
), ),
priority = src.priority priority = src.source.priority
) )
accId = AccountId(src.cid, src.sid) accId = AccountId(src.source.cid, src.source.sid)
result <- OptionT.liftF(submit(updata, accId, notifyJoex, itemId)) result <- OptionT.liftF(submit(updata, accId, notifyJoex, itemId))
} yield result).getOrElse(UploadResult.noSource) } yield result).getOrElse(UploadResult.noSource)

View File

@ -1211,7 +1211,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: "#/components/schemas/Source" $ref: "#/components/schemas/SourceTagIn"
responses: responses:
200: 200:
description: Ok description: Ok
@ -1231,7 +1231,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: "#/components/schemas/Source" $ref: "#/components/schemas/SourceTagIn"
responses: responses:
200: 200:
description: Ok description: Ok
@ -4366,7 +4366,7 @@ components:
items: items:
type: array type: array
items: items:
$ref: "#/components/schemas/Source" $ref: "#/components/schemas/SourceAndTags"
Source: Source:
description: | description: |
Data about a Source. A source defines the endpoint where Data about a Source. A source defines the endpoint where
@ -4400,10 +4400,38 @@ components:
folder: folder:
type: string type: string
format: ident format: ident
fileFilter:
type: string
format: glob
created: created:
description: DateTime description: DateTime
type: integer type: integer
format: date-time format: date-time
SourceTagIn:
description: |
A source and optional tags (ids or names) for updating/adding.
required:
- source
- tags
properties:
source:
$ref: "#/components/schema/Source"
tags:
type: array
items:
type: string
SourceAndTags:
description: |
A source and optional tags.
required:
- source
- tags
properties:
source:
$ref: "#/components/schema/Source"
tags:
$ref: "#/components/schema/TagList"
EquipmentList: EquipmentList:
description: | description: |
A list of equipments. A list of equipments.

View File

@ -524,21 +524,36 @@ trait Conversions {
// sources // sources
def mkSource(s: RSource): Source = def mkSource(s: SourceData): SourceAndTags =
Source( SourceAndTags(
s.sid, Source(
s.abbrev, s.source.sid,
s.description, s.source.abbrev,
s.counter, s.source.description,
s.enabled, s.source.counter,
s.priority, s.source.enabled,
s.folderId, s.source.priority,
s.created s.source.folderId,
s.source.fileFilter,
s.source.created
),
TagList(s.tags.length, s.tags.map(mkTag).toList)
) )
def newSource[F[_]: Sync](s: Source, cid: Ident): F[RSource] = def newSource[F[_]: Sync](s: Source, cid: Ident): F[RSource] =
timeId.map({ case (id, now) => timeId.map({ case (id, now) =>
RSource(id, cid, s.abbrev, s.description, 0, s.enabled, s.priority, now, s.folder) RSource(
id,
cid,
s.abbrev,
s.description,
0,
s.enabled,
s.priority,
now,
s.folder,
s.fileFilter
)
}) })
def changeSource[F[_]: Sync](s: Source, coll: Ident): RSource = def changeSource[F[_]: Sync](s: Source, coll: Ident): RSource =
@ -551,7 +566,8 @@ trait Conversions {
s.enabled, s.enabled,
s.priority, s.priority,
s.created, s.created,
s.folder s.folder,
s.fileFilter
) )
// equipment // equipment

View File

@ -30,17 +30,17 @@ object SourceRoutes {
case req @ POST -> Root => case req @ POST -> Root =>
for { for {
data <- req.as[Source] data <- req.as[SourceTagIn]
src <- newSource(data, user.account.collective) src <- newSource(data.source, user.account.collective)
added <- backend.source.add(src) added <- backend.source.add(src, data.tags)
resp <- Ok(basicResult(added, "Source added.")) resp <- Ok(basicResult(added, "Source added."))
} yield resp } yield resp
case req @ PUT -> Root => case req @ PUT -> Root =>
for { for {
data <- req.as[Source] data <- req.as[SourceTagIn]
src = changeSource(data, user.account.collective) src = changeSource(data.source, user.account.collective)
updated <- backend.source.update(src) updated <- backend.source.update(src, data.tags)
resp <- Ok(basicResult(updated, "Source updated.")) resp <- Ok(basicResult(updated, "Source updated."))
} yield resp } yield resp

View File

@ -0,0 +1,11 @@
ALTER TABLE "source"
ADD COLUMN "file_filter" varchar(254) NULL;
CREATE TABLE "tagsource" (
"id" varchar(254) not null primary key,
"source_id" varchar(254) not null,
"tag_id" varchar(254) not null,
unique ("source_id", "tag_id"),
foreign key ("source_id") references "source"("sid"),
foreign key ("tag_id") references "tag"("tid")
);

View File

@ -0,0 +1,11 @@
ALTER TABLE `source`
ADD COLUMN `file_filter` varchar(254) NULL;
CREATE TABLE `tagsource` (
`id` varchar(254) not null primary key,
`source_id` varchar(254) not null,
`tag_id` varchar(254) not null,
unique (`source_id`, `tag_id`),
foreign key (`source_id`) references `source`(`sid`),
foreign key (`tag_id`) references `tag`(`tid`)
);

View File

@ -0,0 +1,11 @@
ALTER TABLE "source"
ADD COLUMN "file_filter" varchar(254) NULL;
CREATE TABLE "tagsource" (
"id" varchar(254) not null primary key,
"source_id" varchar(254) not null,
"tag_id" varchar(254) not null,
unique ("source_id", "tag_id"),
foreign key ("source_id") references "source"("sid"),
foreign key ("tag_id") references "tag"("tid")
);

View File

@ -91,6 +91,9 @@ trait DoobieMeta extends EmilDoobieMeta {
implicit val metaCalEvent: Meta[CalEvent] = implicit val metaCalEvent: Meta[CalEvent] =
Meta[String].timap(CalEvent.unsafe)(_.asString) Meta[String].timap(CalEvent.unsafe)(_.asString)
implicit val metaGlob: Meta[Glob] =
Meta[String].timap(Glob.apply)(_.asString)
} }
object DoobieMeta extends DoobieMeta { object DoobieMeta extends DoobieMeta {

View File

@ -16,8 +16,13 @@ case class RSource(
enabled: Boolean, enabled: Boolean,
priority: Priority, priority: Priority,
created: Timestamp, created: Timestamp,
folderId: Option[Ident] folderId: Option[Ident],
) {} fileFilter: Option[Glob]
) {
def fileFilterOrAll: Glob =
fileFilter.getOrElse(Glob.all)
}
object RSource { object RSource {
@ -34,9 +39,21 @@ object RSource {
val priority = Column("priority") val priority = Column("priority")
val created = Column("created") val created = Column("created")
val folder = Column("folder_id") val folder = Column("folder_id")
val fileFilter = Column("file_filter")
val all = val all =
List(sid, cid, abbrev, description, counter, enabled, priority, created, folder) List(
sid,
cid,
abbrev,
description,
counter,
enabled,
priority,
created,
folder,
fileFilter
)
} }
import Columns._ import Columns._
@ -45,7 +62,7 @@ object RSource {
val sql = insertRow( val sql = insertRow(
table, table,
all, all,
fr"${v.sid},${v.cid},${v.abbrev},${v.description},${v.counter},${v.enabled},${v.priority},${v.created},${v.folderId}" fr"${v.sid},${v.cid},${v.abbrev},${v.description},${v.counter},${v.enabled},${v.priority},${v.created},${v.folderId},${v.fileFilter}"
) )
sql.update.run sql.update.run
} }
@ -60,7 +77,8 @@ object RSource {
description.setTo(v.description), description.setTo(v.description),
enabled.setTo(v.enabled), enabled.setTo(v.enabled),
priority.setTo(v.priority), priority.setTo(v.priority),
folder.setTo(v.folderId) folder.setTo(v.folderId),
fileFilter.setTo(v.fileFilter)
) )
) )
sql.update.run sql.update.run
@ -83,10 +101,11 @@ object RSource {
sql.query[Int].unique.map(_ > 0) sql.query[Int].unique.map(_ > 0)
} }
def findEnabled(id: Ident): ConnectionIO[Option[RSource]] = { def findEnabled(id: Ident): ConnectionIO[Option[RSource]] =
val sql = selectSimple(all, table, and(sid.is(id), enabled.is(true))) findEnabledSql(id).query[RSource].option
sql.query[RSource].option
} private[records] def findEnabledSql(id: Ident): Fragment =
selectSimple(all, table, and(sid.is(id), enabled.is(true)))
def findCollective(sourceId: Ident): ConnectionIO[Option[Ident]] = def findCollective(sourceId: Ident): ConnectionIO[Option[Ident]] =
selectSimple(List(cid), table, sid.is(sourceId)).query[Ident].option selectSimple(List(cid), table, sid.is(sourceId)).query[Ident].option
@ -94,10 +113,11 @@ object RSource {
def findAll( def findAll(
coll: Ident, coll: Ident,
order: Columns.type => Column order: Columns.type => Column
): ConnectionIO[Vector[RSource]] = { ): ConnectionIO[Vector[RSource]] =
val sql = selectSimple(all, table, cid.is(coll)) ++ orderBy(order(Columns).f) findAllSql(coll, order).query[RSource].to[Vector]
sql.query[RSource].to[Vector]
} private[records] def findAllSql(coll: Ident, order: Columns.type => Column): Fragment =
selectSimple(all, table, cid.is(coll)) ++ orderBy(order(Columns).f)
def delete(sourceId: Ident, coll: Ident): ConnectionIO[Int] = def delete(sourceId: Ident, coll: Ident): ConnectionIO[Int] =
deleteFrom(table, and(sid.is(sourceId), cid.is(coll))).update.run deleteFrom(table, and(sid.is(sourceId), cid.is(coll))).update.run

View File

@ -104,6 +104,18 @@ object RTag {
) ++ orderBy(name.prefix("t").asc)).query[RTag].to[Vector] ) ++ orderBy(name.prefix("t").asc)).query[RTag].to[Vector]
} }
def findBySource(source: Ident): ConnectionIO[Vector[RTag]] = {
val rcol = all.map(_.prefix("t"))
(selectSimple(
rcol,
table ++ fr"t," ++ RTagSource.table ++ fr"s",
and(
RTagSource.Columns.sourceId.prefix("s").is(source),
RTagSource.Columns.tagId.prefix("s").is(tid.prefix("t"))
)
) ++ orderBy(name.prefix("t").asc)).query[RTag].to[Vector]
}
def findAllByNameOrId( def findAllByNameOrId(
nameOrIds: List[String], nameOrIds: List[String],
coll: Ident coll: Ident

View File

@ -0,0 +1,56 @@
package docspell.store.records
import cats.effect.Sync
import cats.implicits._
import docspell.common._
import docspell.store.impl.Implicits._
import docspell.store.impl._
import doobie._
import doobie.implicits._
case class RTagSource(id: Ident, sourceId: Ident, tagId: Ident) {}
object RTagSource {
val table = fr"tagsource"
object Columns {
val id = Column("id")
val sourceId = Column("source_id")
val tagId = Column("tag_id")
val all = List(id, sourceId, tagId)
}
import Columns._
def createNew[F[_]: Sync](source: Ident, tag: Ident): F[RTagSource] =
Ident.randomId[F].map(id => RTagSource(id, source, tag))
def insert(v: RTagSource): ConnectionIO[Int] =
insertRow(table, all, fr"${v.id},${v.sourceId},${v.tagId}").update.run
def deleteSourceTags(source: Ident): ConnectionIO[Int] =
deleteFrom(table, sourceId.is(source)).update.run
def deleteTag(tid: Ident): ConnectionIO[Int] =
deleteFrom(table, tagId.is(tid)).update.run
def findBySource(source: Ident): ConnectionIO[Vector[RTagSource]] =
selectSimple(all, table, sourceId.is(source)).query[RTagSource].to[Vector]
def setAllTags(source: Ident, tags: Seq[Ident]): ConnectionIO[Int] =
if (tags.isEmpty) 0.pure[ConnectionIO]
else
for {
entities <- tags.toList.traverse(tagId =>
Ident.randomId[ConnectionIO].map(id => RTagSource(id, source, tagId))
)
n <- insertRows(
table,
all,
entities.map(v => fr"${v.id},${v.sourceId},${v.tagId}")
).update.run
} yield n
}

View File

@ -0,0 +1,87 @@
package docspell.store.records
import cats.effect.concurrent.Ref
import cats.implicits._
import fs2.Stream
import docspell.common._
import docspell.store.impl.Implicits._
import docspell.store.impl._
import doobie._
import doobie.implicits._
/** Combines a source record (RSource) and a list of associated tags.
*/
case class SourceData(source: RSource, tags: Vector[RTag])
object SourceData {
def fromSource(s: RSource): SourceData =
SourceData(s, Vector.empty)
def findAll(
coll: Ident,
order: RSource.Columns.type => Column
): Stream[ConnectionIO, SourceData] =
findAllWithTags(RSource.findAllSql(coll, order).query[RSource].stream)
private def findAllWithTags(
select: Stream[ConnectionIO, RSource]
): Stream[ConnectionIO, SourceData] = {
def findTag(
cache: Ref[ConnectionIO, Map[Ident, RTag]],
tagSource: RTagSource
): ConnectionIO[Option[RTag]] =
for {
cc <- cache.get
fromCache = cc.get(tagSource.tagId)
orFromDB <-
if (fromCache.isDefined) fromCache.pure[ConnectionIO]
else RTag.findById(tagSource.tagId)
_ <-
if (fromCache.isDefined) ().pure[ConnectionIO]
else
orFromDB match {
case Some(t) => cache.update(tmap => tmap.updated(t.tagId, t))
case None => ().pure[ConnectionIO]
}
} yield orFromDB
for {
resolvedTags <- Stream.eval(Ref.of[ConnectionIO, Map[Ident, RTag]](Map.empty))
source <- select
tagSources <- Stream.eval(RTagSource.findBySource(source.sid))
tags <- Stream.eval(tagSources.traverse(ti => findTag(resolvedTags, ti)))
} yield SourceData(source, tags.flatten)
}
def findEnabled(id: Ident): ConnectionIO[Option[SourceData]] =
findAllWithTags(RSource.findEnabledSql(id).query[RSource].stream).head.compile.last
def insert(data: RSource, tags: List[String]): ConnectionIO[Int] =
for {
n0 <- RSource.insert(data)
tags <- RTag.findAllByNameOrId(tags, data.cid)
n1 <- tags.traverse(tag =>
RTagSource.createNew[ConnectionIO](data.sid, tag.tagId).flatMap(RTagSource.insert)
)
} yield n0 + n1.sum
def update(data: RSource, tags: List[String]): ConnectionIO[Int] =
for {
n0 <- RSource.updateNoCounter(data)
tags <- RTag.findAllByNameOrId(tags, data.cid)
_ <- RTagSource.deleteSourceTags(data.sid)
n1 <- tags.traverse(tag =>
RTagSource.createNew[ConnectionIO](data.sid, tag.tagId).flatMap(RTagSource.insert)
)
} yield n0 + n1.sum
def delete(source: Ident, coll: Ident): ConnectionIO[Int] =
for {
n0 <- RTagSource.deleteSourceTags(source)
n1 <- RSource.delete(source, coll)
} yield n0 + n1
}

View File

@ -175,8 +175,9 @@ import Api.Model.ScanMailboxSettings exposing (ScanMailboxSettings)
import Api.Model.ScanMailboxSettingsList exposing (ScanMailboxSettingsList) import Api.Model.ScanMailboxSettingsList exposing (ScanMailboxSettingsList)
import Api.Model.SentMails exposing (SentMails) import Api.Model.SentMails exposing (SentMails)
import Api.Model.SimpleMail exposing (SimpleMail) import Api.Model.SimpleMail exposing (SimpleMail)
import Api.Model.Source exposing (Source) import Api.Model.SourceAndTags exposing (SourceAndTags)
import Api.Model.SourceList exposing (SourceList) import Api.Model.SourceList exposing (SourceList)
import Api.Model.SourceTagIn exposing (SourceTagIn)
import Api.Model.StringList exposing (StringList) import Api.Model.StringList exposing (StringList)
import Api.Model.Tag exposing (Tag) import Api.Model.Tag exposing (Tag)
import Api.Model.TagCloud exposing (TagCloud) import Api.Model.TagCloud exposing (TagCloud)
@ -1144,17 +1145,22 @@ getSources flags receive =
} }
postSource : Flags -> Source -> (Result Http.Error BasicResult -> msg) -> Cmd msg postSource : Flags -> SourceAndTags -> (Result Http.Error BasicResult -> msg) -> Cmd msg
postSource flags source receive = postSource flags source receive =
let let
st =
{ source = source.source
, tags = List.map .id source.tags.items
}
params = params =
{ url = flags.config.baseUrl ++ "/api/v1/sec/source" { url = flags.config.baseUrl ++ "/api/v1/sec/source"
, account = getAccount flags , account = getAccount flags
, body = Http.jsonBody (Api.Model.Source.encode source) , body = Http.jsonBody (Api.Model.SourceTagIn.encode st)
, expect = Http.expectJson receive Api.Model.BasicResult.decoder , expect = Http.expectJson receive Api.Model.BasicResult.decoder
} }
in in
if source.id == "" then if source.source.id == "" then
Http2.authPost params Http2.authPost params
else else

View File

@ -12,7 +12,9 @@ import Api
import Api.Model.FolderItem exposing (FolderItem) import Api.Model.FolderItem exposing (FolderItem)
import Api.Model.FolderList exposing (FolderList) import Api.Model.FolderList exposing (FolderList)
import Api.Model.IdName exposing (IdName) import Api.Model.IdName exposing (IdName)
import Api.Model.Source exposing (Source) import Api.Model.SourceAndTags exposing (SourceAndTags)
import Api.Model.Tag exposing (Tag)
import Api.Model.TagList exposing (TagList)
import Comp.Dropdown exposing (isDropdownChangeMsg) import Comp.Dropdown exposing (isDropdownChangeMsg)
import Comp.FixedDropdown import Comp.FixedDropdown
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
@ -24,10 +26,13 @@ import Html.Events exposing (onCheck, onInput)
import Http import Http
import Markdown import Markdown
import Util.Folder exposing (mkFolderOption) import Util.Folder exposing (mkFolderOption)
import Util.Maybe
import Util.Tag
import Util.Update
type alias Model = type alias Model =
{ source : Source { source : SourceAndTags
, abbrev : String , abbrev : String
, description : Maybe String , description : Maybe String
, priorityModel : Comp.FixedDropdown.Model Priority , priorityModel : Comp.FixedDropdown.Model Priority
@ -36,12 +41,14 @@ type alias Model =
, folderModel : Comp.Dropdown.Model IdName , folderModel : Comp.Dropdown.Model IdName
, allFolders : List FolderItem , allFolders : List FolderItem
, folderId : Maybe String , folderId : Maybe String
, tagModel : Comp.Dropdown.Model Tag
, fileFilter : Maybe String
} }
emptyModel : Model emptyModel : Model
emptyModel = emptyModel =
{ source = Api.Model.Source.empty { source = Api.Model.SourceAndTags.empty
, abbrev = "" , abbrev = ""
, description = Nothing , description = Nothing
, priorityModel = , priorityModel =
@ -57,13 +64,18 @@ emptyModel =
} }
, allFolders = [] , allFolders = []
, folderId = Nothing , folderId = Nothing
, tagModel = Util.Tag.makeDropdownModel
, fileFilter = Nothing
} }
init : Flags -> ( Model, Cmd Msg ) init : Flags -> ( Model, Cmd Msg )
init flags = init flags =
( emptyModel ( emptyModel
, Api.getFolders flags "" False GetFolderResp , Cmd.batch
[ Api.getFolders flags "" False GetFolderResp
, Api.getTags flags "" GetTagResp
]
) )
@ -72,29 +84,42 @@ isValid model =
model.abbrev /= "" model.abbrev /= ""
getSource : Model -> Source getSource : Model -> SourceAndTags
getSource model = getSource model =
let let
s = st =
model.source model.source
s =
st.source
tags =
Comp.Dropdown.getSelected model.tagModel
n =
{ s
| abbrev = model.abbrev
, description = model.description
, enabled = model.enabled
, priority = Data.Priority.toName model.priority
, folder = model.folderId
, fileFilter = model.fileFilter
}
in in
{ s { st | source = n, tags = TagList (List.length tags) tags }
| abbrev = model.abbrev
, description = model.description
, enabled = model.enabled
, priority = Data.Priority.toName model.priority
, folder = model.folderId
}
type Msg type Msg
= SetAbbrev String = SetAbbrev String
| SetSource Source | SetSource SourceAndTags
| SetDescr String | SetDescr String
| ToggleEnabled | ToggleEnabled
| PrioDropdownMsg (Comp.FixedDropdown.Msg Priority) | PrioDropdownMsg (Comp.FixedDropdown.Msg Priority)
| GetFolderResp (Result Http.Error FolderList) | GetFolderResp (Result Http.Error FolderList)
| FolderDropdownMsg (Comp.Dropdown.Msg IdName) | FolderDropdownMsg (Comp.Dropdown.Msg IdName)
| GetTagResp (Result Http.Error TagList)
| TagDropdownMsg (Comp.Dropdown.Msg Tag)
| SetFileFilter String
@ -106,29 +131,34 @@ update flags msg model =
case msg of case msg of
SetSource t -> SetSource t ->
let let
post = stpost =
model.source model.source
post =
stpost.source
np = np =
{ post { post
| id = t.id | id = t.source.id
, abbrev = t.abbrev , abbrev = t.source.abbrev
, description = t.description , description = t.source.description
, priority = t.priority , priority = t.source.priority
, enabled = t.enabled , enabled = t.source.enabled
, folder = t.folder , folder = t.source.folder
, fileFilter = t.source.fileFilter
} }
newModel = newModel =
{ model { model
| source = np | source = { stpost | source = np }
, abbrev = t.abbrev , abbrev = t.source.abbrev
, description = t.description , description = t.source.description
, priority = , priority =
Data.Priority.fromString t.priority Data.Priority.fromString t.source.priority
|> Maybe.withDefault Data.Priority.Low |> Maybe.withDefault Data.Priority.Low
, enabled = t.enabled , enabled = t.source.enabled
, folderId = t.folder , folderId = t.source.folder
, fileFilter = t.source.fileFilter
} }
mkIdName id = mkIdName id =
@ -143,14 +173,21 @@ update flags msg model =
model.allFolders model.allFolders
sel = sel =
case Maybe.map mkIdName t.folder of case Maybe.map mkIdName t.source.folder of
Just idref -> Just idref ->
idref idref
Nothing -> Nothing ->
[] []
tags =
Comp.Dropdown.SetSelection t.tags.items
in in
update flags (FolderDropdownMsg (Comp.Dropdown.SetSelection sel)) newModel Util.Update.andThen1
[ update flags (FolderDropdownMsg (Comp.Dropdown.SetSelection sel))
, update flags (TagDropdownMsg tags)
]
newModel
ToggleEnabled -> ToggleEnabled ->
( { model | enabled = not model.enabled }, Cmd.none ) ( { model | enabled = not model.enabled }, Cmd.none )
@ -159,14 +196,7 @@ update flags msg model =
( { model | abbrev = n }, Cmd.none ) ( { model | abbrev = n }, Cmd.none )
SetDescr d -> SetDescr d ->
( { model ( { model | description = Util.Maybe.fromString d }
| description =
if d /= "" then
Just d
else
Nothing
}
, Cmd.none , Cmd.none
) )
@ -226,6 +256,31 @@ update flags msg model =
in in
( model_, Cmd.map FolderDropdownMsg c2 ) ( model_, Cmd.map FolderDropdownMsg c2 )
GetTagResp (Ok list) ->
let
opts =
Comp.Dropdown.SetOptions list.items
in
update flags (TagDropdownMsg opts) model
GetTagResp (Err _) ->
( model, Cmd.none )
TagDropdownMsg lm ->
let
( m2, c2 ) =
Comp.Dropdown.update lm model.tagModel
newModel =
{ model | tagModel = m2 }
in
( newModel, Cmd.map TagDropdownMsg c2 )
SetFileFilter d ->
( { model | fileFilter = Util.Maybe.fromString d }
, Cmd.none
)
--- View --- View
@ -260,6 +315,7 @@ view flags settings model =
, textarea , textarea
[ onInput SetDescr [ onInput SetDescr
, model.description |> Maybe.withDefault "" |> value , model.description |> Maybe.withDefault "" |> value
, rows 3
] ]
[] []
] ]
@ -281,12 +337,26 @@ view flags settings model =
(Just priorityItem) (Just priorityItem)
model.priorityModel model.priorityModel
) )
, div [ class "small-info" ]
[ text "The priority used by the scheduler when processing uploaded files."
]
]
, div [ class "ui dividing header" ]
[ text "Metadata"
]
, div [ class "ui message" ]
[ text "Metadata specified here is automatically attached to each item uploaded "
, text "through this source, unless it is overriden in the upload request meta data. "
, text "Tags from the request are added to those defined here."
] ]
, div [ class "field" ] , div [ class "field" ]
[ label [] [ label []
[ text "Folder" [ text "Folder"
] ]
, Html.map FolderDropdownMsg (Comp.Dropdown.view settings model.folderModel) , Html.map FolderDropdownMsg (Comp.Dropdown.view settings model.folderModel)
, div [ class "small-info" ]
[ text "Choose a folder to automatically put items into."
]
, div , div
[ classList [ classList
[ ( "ui warning message", True ) [ ( "ui warning message", True )
@ -301,6 +371,33 @@ disappear then.
""" """
] ]
] ]
, div [ class "field" ]
[ label [] [ text "Tags" ]
, Html.map TagDropdownMsg (Comp.Dropdown.view settings model.tagModel)
, div [ class "small-info" ]
[ text "Choose tags that should be applied to items."
]
]
, div
[ class "field"
]
[ label [] [ text "File Filter" ]
, input
[ type_ "text"
, onInput SetFileFilter
, placeholder "File Filter"
, model.fileFilter
|> Maybe.withDefault ""
|> value
]
[]
, div [ class "small-info" ]
[ text "Specify a file glob to filter files when uploading archives (e.g. for email and zip). For example, to only extract pdf files: "
, code []
[ text "*.pdf"
]
]
]
] ]

View File

@ -8,8 +8,9 @@ module Comp.SourceManage exposing
import Api import Api
import Api.Model.BasicResult exposing (BasicResult) import Api.Model.BasicResult exposing (BasicResult)
import Api.Model.Source exposing (Source) import Api.Model.SourceAndTags exposing (SourceAndTags)
import Api.Model.SourceList exposing (SourceList) import Api.Model.SourceList exposing (SourceList)
import Api.Model.SourceTagIn exposing (SourceTagIn)
import Comp.SourceForm import Comp.SourceForm
import Comp.SourceTable exposing (SelectMode(..)) import Comp.SourceTable exposing (SelectMode(..))
import Comp.YesNoDimmer import Comp.YesNoDimmer
@ -31,7 +32,7 @@ type alias Model =
, formError : Maybe String , formError : Maybe String
, loading : Bool , loading : Bool
, deleteConfirm : Comp.YesNoDimmer.Model , deleteConfirm : Comp.YesNoDimmer.Model
, sources : List Source , sources : List SourceAndTags
} }
@ -145,7 +146,7 @@ update flags msg model =
InitNewSource -> InitNewSource ->
let let
source = source =
Api.Model.Source.empty Api.Model.SourceAndTags.empty
nm = nm =
{ model | viewMode = Edit source, formError = Nothing } { model | viewMode = Edit source, formError = Nothing }
@ -196,7 +197,7 @@ update flags msg model =
cmd = cmd =
if confirmed then if confirmed then
Api.deleteSource flags src.id SubmitResp Api.deleteSource flags src.source.id SubmitResp
else else
Cmd.none Cmd.none
@ -248,22 +249,22 @@ viewTable model =
] ]
viewLinks : Flags -> UiSettings -> Source -> Html Msg viewLinks : Flags -> UiSettings -> SourceAndTags -> Html Msg
viewLinks flags _ source = viewLinks flags _ source =
let let
appUrl = appUrl =
flags.config.baseUrl ++ "/app/upload/" ++ source.id flags.config.baseUrl ++ "/app/upload/" ++ source.source.id
apiUrl = apiUrl =
flags.config.baseUrl ++ "/api/v1/open/upload/item/" ++ source.id flags.config.baseUrl ++ "/api/v1/open/upload/item/" ++ source.source.id
in in
div div
[] []
[ h3 [ class "ui dividing header" ] [ h3 [ class "ui dividing header" ]
[ text "Public Uploads: " [ text "Public Uploads: "
, text source.abbrev , text source.source.abbrev
, div [ class "sub header" ] , div [ class "sub header" ]
[ text source.id [ text source.source.id
] ]
] ]
, p [] , p []
@ -273,7 +274,7 @@ viewLinks flags _ source =
] ]
, p [] , p []
[ text "There have been " [ text "There have been "
, String.fromInt source.counter |> text , String.fromInt source.source.counter |> text
, text " items created through this source." , text " items created through this source."
] ]
, h4 [ class "ui header" ] , h4 [ class "ui header" ]
@ -358,7 +359,7 @@ viewForm : Flags -> UiSettings -> Model -> List (Html Msg)
viewForm flags settings model = viewForm flags settings model =
let let
newSource = newSource =
model.formModel.source.id == "" model.formModel.source.source.id == ""
in in
[ if newSource then [ if newSource then
h3 [ class "ui top attached header" ] h3 [ class "ui top attached header" ]
@ -367,10 +368,10 @@ viewForm flags settings model =
else else
h3 [ class "ui top attached header" ] h3 [ class "ui top attached header" ]
[ text ("Edit: " ++ model.formModel.source.abbrev) [ text ("Edit: " ++ model.formModel.source.source.abbrev)
, div [ class "sub header" ] , div [ class "sub header" ]
[ text "Id: " [ text "Id: "
, text model.formModel.source.id , text model.formModel.source.source.id
] ]
] ]
, Html.form [ class "ui attached segment", onSubmit Submit ] , Html.form [ class "ui attached segment", onSubmit Submit ]

View File

@ -6,7 +6,7 @@ module Comp.SourceTable exposing
, view , view
) )
import Api.Model.Source exposing (Source) import Api.Model.SourceAndTags exposing (SourceAndTags)
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Data.Priority import Data.Priority
import Html exposing (..) import Html exposing (..)
@ -15,8 +15,8 @@ import Html.Events exposing (onClick)
type SelectMode type SelectMode
= Edit Source = Edit SourceAndTags
| Display Source | Display SourceAndTags
| None | None
@ -34,8 +34,8 @@ isEdit m =
type Msg type Msg
= Select Source = Select SourceAndTags
| Show Source | Show SourceAndTags
update : Flags -> Msg -> ( Cmd Msg, SelectMode ) update : Flags -> Msg -> ( Cmd Msg, SelectMode )
@ -48,7 +48,7 @@ update _ msg =
( Cmd.none, Display source ) ( Cmd.none, Display source )
view : List Source -> Html Msg view : List SourceAndTags -> Html Msg
view sources = view sources =
table [ class "ui table" ] table [ class "ui table" ]
[ thead [] [ thead []
@ -66,7 +66,7 @@ view sources =
] ]
renderSourceLine : Source -> Html Msg renderSourceLine : SourceAndTags -> Html Msg
renderSourceLine source = renderSourceLine source =
tr tr
[] []
@ -82,10 +82,10 @@ renderSourceLine source =
, a , a
[ classList [ classList
[ ( "ui basic tiny primary button", True ) [ ( "ui basic tiny primary button", True )
, ( "disabled", not source.enabled ) , ( "disabled", not source.source.enabled )
] ]
, href "#" , href "#"
, disabled (not source.enabled) , disabled (not source.source.enabled)
, onClick (Show source) , onClick (Show source)
] ]
[ i [ class "eye icon" ] [] [ i [ class "eye icon" ] []
@ -93,25 +93,25 @@ renderSourceLine source =
] ]
] ]
, td [ class "collapsing" ] , td [ class "collapsing" ]
[ text source.abbrev [ text source.source.abbrev
] ]
, td [ class "collapsing" ] , td [ class "collapsing" ]
[ if source.enabled then [ if source.source.enabled then
i [ class "check square outline icon" ] [] i [ class "check square outline icon" ] []
else else
i [ class "minus square outline icon" ] [] i [ class "minus square outline icon" ] []
] ]
, td [ class "collapsing" ] , td [ class "collapsing" ]
[ source.counter |> String.fromInt |> text [ source.source.counter |> String.fromInt |> text
] ]
, td [ class "collapsing" ] , td [ class "collapsing" ]
[ Data.Priority.fromString source.priority [ Data.Priority.fromString source.source.priority
|> Maybe.map Data.Priority.toName |> Maybe.map Data.Priority.toName
|> Maybe.withDefault source.priority |> Maybe.withDefault source.source.priority
|> text |> text
] ]
, td [] , td []
[ text source.id [ text source.source.id
] ]
] ]