Rename space -> folder

This commit is contained in:
Eike Kettner 2020-07-11 11:38:57 +02:00
parent 0365c1980a
commit 2ab0b5e222
22 changed files with 755 additions and 754 deletions
modules
backend/src/main/scala/docspell/backend
restapi/src/main/resources
restserver/src/main/scala/docspell/restserver
store/src/main
webapp/src/main/elm

@ -35,7 +35,7 @@ trait BackendApp[F[_]] {
def mail: OMail[F]
def joex: OJoex[F]
def userTask: OUserTask[F]
def space: OSpace[F]
def folder: OFolder[F]
}
object BackendApp {
@ -68,7 +68,7 @@ object BackendApp {
JavaMailEmil(blocker, Settings.defaultSettings.copy(debug = cfg.mailDebug))
mailImpl <- OMail(store, javaEmil)
userTaskImpl <- OUserTask(utStore, queue, joexImpl)
spaceImpl <- OSpace(store)
folderImpl <- OFolder(store)
} yield new BackendApp[F] {
val login: Login[F] = loginImpl
val signup: OSignup[F] = signupImpl
@ -86,7 +86,7 @@ object BackendApp {
val mail = mailImpl
val joex = joexImpl
val userTask = userTaskImpl
val space = spaceImpl
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.{AddResult, Store}
import docspell.store.records.{RFolder, RUser}
import docspell.store.queries.QFolder
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))
})
}

@ -1,110 +0,0 @@
package docspell.backend.ops
import cats.effect._
import docspell.common._
import docspell.store.{AddResult, Store}
import docspell.store.records.{RSpace, RUser}
import docspell.store.queries.QSpace
trait OSpace[F[_]] {
def findAll(
account: AccountId,
ownerLogin: Option[Ident],
nameQuery: Option[String]
): F[Vector[OSpace.SpaceItem]]
def findById(id: Ident, account: AccountId): F[Option[OSpace.SpaceDetail]]
/** Adds a new space. If `login` is non-empty, the `space.user`
* property is ignored and the user-id is determined by the given
* login name.
*/
def add(space: RSpace, login: Option[Ident]): F[AddResult]
def changeName(
space: Ident,
account: AccountId,
name: String
): F[OSpace.SpaceChangeResult]
def addMember(
space: Ident,
account: AccountId,
member: Ident
): F[OSpace.SpaceChangeResult]
def removeMember(
space: Ident,
account: AccountId,
member: Ident
): F[OSpace.SpaceChangeResult]
def delete(id: Ident, account: AccountId): F[OSpace.SpaceChangeResult]
}
object OSpace {
type SpaceChangeResult = QSpace.SpaceChangeResult
val SpaceChangeResult = QSpace.SpaceChangeResult
type SpaceItem = QSpace.SpaceItem
val SpaceItem = QSpace.SpaceItem
type SpaceDetail = QSpace.SpaceDetail
val SpaceDetail = QSpace.SpaceDetail
def apply[F[_]: Effect](store: Store[F]): Resource[F, OSpace[F]] =
Resource.pure[F, OSpace[F]](new OSpace[F] {
def findAll(
account: AccountId,
ownerLogin: Option[Ident],
nameQuery: Option[String]
): F[Vector[SpaceItem]] =
store.transact(QSpace.findAll(account, None, ownerLogin, nameQuery))
def findById(id: Ident, account: AccountId): F[Option[SpaceDetail]] =
store.transact(QSpace.findById(id, account))
def add(space: RSpace, login: Option[Ident]): F[AddResult] = {
val insert = login match {
case Some(n) =>
for {
user <- RUser.findByAccount(AccountId(space.collectiveId, n))
s = user.map(u => space.copy(owner = u.uid)).getOrElse(space)
n <- RSpace.insert(s)
} yield n
case None =>
RSpace.insert(space)
}
val exists = RSpace.existsByName(space.collectiveId, space.name)
store.add(insert, exists)
}
def changeName(
space: Ident,
account: AccountId,
name: String
): F[SpaceChangeResult] =
store.transact(QSpace.changeName(space, account, name))
def addMember(
space: Ident,
account: AccountId,
member: Ident
): F[SpaceChangeResult] =
store.transact(QSpace.addMember(space, account, member))
def removeMember(
space: Ident,
account: AccountId,
member: Ident
): F[SpaceChangeResult] =
store.transact(QSpace.removeMember(space, account, member))
def delete(id: Ident, account: AccountId): F[SpaceChangeResult] =
store.transact(QSpace.delete(id, account))
})
}

@ -795,14 +795,14 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/BasicResult"
/sec/space:
/sec/folder:
get:
tags: [ Space ]
summary: Get a list of spaces.
tags: [ Folder ]
summary: Get a list of folders.
description: |
Return a list of spaces for the current collective.
Return a list of folders for the current collective.
All spaces are returned, including those not owned by the
All folders are returned, including those not owned by the
current user.
It is possible to restrict the results by a substring match of
@ -818,12 +818,12 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/SpaceList"
$ref: "#/components/schemas/FolderList"
post:
tags: [ Space ]
summary: Create a new space
tags: [ Folder ]
summary: Create a new folder
description: |
Create a new space owned by the current user. If a space with
Create a new folder owned by the current user. If a folder with
the same name already exists, an error is thrown.
security:
- authTokenHeader: []
@ -831,7 +831,7 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/NewSpace"
$ref: "#/components/schemas/NewFolder"
responses:
200:
description: Ok
@ -839,12 +839,12 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/IdResult"
/sec/space/{id}:
/sec/folder/{id}:
get:
tags: [ Space ]
summary: Get space details.
tags: [ Folder ]
summary: Get folder details.
description: |
Return details about a space.
Return details about a folder.
security:
- authTokenHeader: []
parameters:
@ -855,12 +855,12 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/SpaceDetail"
$ref: "#/components/schemas/FolderDetail"
put:
tags: [ Space ]
summary: Change the name of a space
tags: [ Folder ]
summary: Change the name of a folder
description: |
Changes the name of a space. The new name must not exists.
Changes the name of a folder. The new name must not exists.
security:
- authTokenHeader: []
parameters:
@ -869,7 +869,7 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/NewSpace"
$ref: "#/components/schemas/NewFolder"
responses:
200:
description: Ok
@ -878,10 +878,10 @@ paths:
schema:
$ref: "#/components/schemas/BasicResult"
delete:
tags: [ Space ]
summary: Delete a space by its id.
tags: [ Folder ]
summary: Delete a folder by its id.
description: |
Deletes a space.
Deletes a folder.
security:
- authTokenHeader: []
parameters:
@ -893,12 +893,12 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/BasicResult"
/sec/space/{id}/member/{userId}:
/sec/folder/{id}/member/{userId}:
put:
tags: [ Space ]
summary: Add a member to this space
tags: [ Folder ]
summary: Add a member to this folder
description: |
Adds a member to this space (identified by `id`).
Adds a member to this folder (identified by `id`).
security:
- authTokenHeader: []
parameters:
@ -912,10 +912,10 @@ paths:
schema:
$ref: "#/components/schemas/BasicResult"
delete:
tags: [ Space ]
summary: Removes a member from this space.
tags: [ Folder ]
summary: Removes a member from this folder.
description: |
Removes a member from this space.
Removes a member from this folder.
security:
- authTokenHeader: []
parameters:
@ -984,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:
@ -2492,19 +2492,19 @@ paths:
components:
schemas:
SpaceList:
FolderList:
description: |
A list of spaces with their member counts.
A list of folders with their member counts.
required:
- items
properties:
items:
type: array
items:
$ref: "#/components/schemas/SpaceItem"
SpaceItem:
$ref: "#/components/schemas/FolderItem"
FolderItem:
description: |
An item in a space list.
An item in a folder list.
required:
- id
- name
@ -2528,17 +2528,17 @@ components:
memberCount:
type: integer
format: int32
NewSpace:
NewFolder:
description: |
Data required to create a new space.
Data required to create a new folder.
required:
- name
properties:
name:
type: string
SpaceDetail:
FolderDetail:
description: |
Details about a space.
Details about a folder.
required:
- id
- name
@ -2567,9 +2567,9 @@ components:
type: array
items:
$ref: "#/components/schemas/IdName"
SpaceMember:
FolderMember:
description: |
Information to add or remove a space member.
Information to add or remove a folder member.
required:
- userId
properties:
@ -4001,7 +4001,7 @@ components:
owning:
name: full
in: query
description: Whether to get owning spaces
description: Whether to get owning folders
required: false
schema:
type: boolean

@ -82,7 +82,7 @@ object RestServer {
"usertask/scanmailbox" -> ScanMailboxRoutes(restApp.backend, token),
"calevent/check" -> CalEventCheckRoutes(),
"fts" -> FullTextIndexRoutes.secured(cfg, restApp.backend, token),
"space" -> SpaceRoutes(restApp.backend, token)
"folder" -> FolderRoutes(restApp.backend, token)
)
def openRoutes[F[_]: Effect](cfg: Config, restApp: RestApp[F]): HttpRoutes[F] =

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

@ -1,112 +0,0 @@
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.OSpace
import docspell.common._
import docspell.restapi.model._
import docspell.restserver.conv.Conversions
import docspell.restserver.http4s._
import docspell.store.records.RSpace
import org.http4s.HttpRoutes
import org.http4s.circe.CirceEntityDecoder._
import org.http4s.circe.CirceEntityEncoder._
import org.http4s.dsl.Http4sDsl
object SpaceRoutes {
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.space.findAll(user.account, login, q.map(_.q))
resp <- Ok(SpaceList(all.map(mkSpace).toList))
} yield resp
case req @ POST -> Root =>
for {
data <- req.as[NewSpace]
nspace <- newSpace(data, user.account)
res <- backend.space.add(nspace, Some(user.account.user))
resp <- Ok(Conversions.idResult(res, nspace.id, "Space successfully created."))
} yield resp
case GET -> Root / Ident(id) =>
(for {
space <- OptionT(backend.space.findById(id, user.account))
resp <- OptionT.liftF(Ok(mkSpaceDetail(space)))
} yield resp).getOrElseF(NotFound())
case req @ PUT -> Root / Ident(id) =>
for {
data <- req.as[NewSpace]
res <- backend.space.changeName(id, user.account, data.name)
resp <- Ok(mkSpaceChangeResult(res))
} yield resp
case DELETE -> Root / Ident(id) =>
for {
res <- backend.space.delete(id, user.account)
resp <- Ok(mkSpaceChangeResult(res))
} yield resp
case PUT -> Root / Ident(id) / "member" / Ident(userId) =>
for {
res <- backend.space.addMember(id, user.account, userId)
resp <- Ok(mkSpaceChangeResult(res))
} yield resp
case DELETE -> Root / Ident(id) / "member" / Ident(userId) =>
for {
res <- backend.space.removeMember(id, user.account, userId)
resp <- Ok(mkSpaceChangeResult(res))
} yield resp
}
}
private def newSpace[F[_]: Sync](ns: NewSpace, account: AccountId): F[RSpace] =
RSpace.newSpace(ns.name, account)
private def mkSpace(item: OSpace.SpaceItem): SpaceItem =
SpaceItem(
item.id,
item.name,
Conversions.mkIdName(item.owner),
item.created,
item.member,
item.memberCount
)
private def mkSpaceDetail(item: OSpace.SpaceDetail): SpaceDetail =
SpaceDetail(
item.id,
item.name,
Conversions.mkIdName(item.owner),
item.created,
item.member,
item.memberCount,
item.members.map(Conversions.mkIdName)
)
private def mkSpaceChangeResult(r: OSpace.SpaceChangeResult): BasicResult =
r match {
case OSpace.SpaceChangeResult.Success =>
BasicResult(true, "Successfully changed space.")
case OSpace.SpaceChangeResult.NotFound =>
BasicResult(false, "Space or user not found.")
case OSpace.SpaceChangeResult.Forbidden =>
BasicResult(false, "Not allowed to edit space.")
case OSpace.SpaceChangeResult.Exists =>
BasicResult(false, "The member already exists.")
}
}

@ -1,4 +1,4 @@
CREATE TABLE "space" (
CREATE TABLE "folder" (
"id" varchar(254) not null primary key,
"name" varchar(254) not null,
"cid" varchar(254) not null,
@ -9,15 +9,15 @@ CREATE TABLE "space" (
foreign key ("owner") references "user_"("uid")
);
CREATE TABLE "space_member" (
CREATE TABLE "folder_member" (
"id" varchar(254) not null primary key,
"space_id" varchar(254) not null,
"folder_id" varchar(254) not null,
"user_id" varchar(254) not null,
"created" timestamp not null,
unique ("space_id", "user_id"),
foreign key ("space_id") references "space"("id"),
unique ("folder_id", "user_id"),
foreign key ("folder_id") references "folder"("id"),
foreign key ("user_id") references "user_"("uid")
);
ALTER TABLE "item"
ADD COLUMN "space_id" varchar(254) NULL;
ADD COLUMN "folder_id" varchar(254) NULL;

@ -0,0 +1,249 @@
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)
_ <- 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]
}
private def findUserId(account: AccountId): ConnectionIO[Option[Ident]] =
RUser.findByAccount(account).map(_.map(_.uid))
}

@ -1,249 +0,0 @@
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 QSpace {
final case class SpaceItem(
id: Ident,
name: String,
owner: IdRef,
created: Timestamp,
member: Boolean,
memberCount: Int
) {
def withMembers(members: List[IdRef]): SpaceDetail =
SpaceDetail(id, name, owner, created, member, memberCount, members)
}
final case class SpaceDetail(
id: Ident,
name: String,
owner: IdRef,
created: Timestamp,
member: Boolean,
memberCount: Int,
members: List[IdRef]
)
sealed trait SpaceChangeResult
object SpaceChangeResult {
case object Success extends SpaceChangeResult
def success: SpaceChangeResult = Success
case object NotFound extends SpaceChangeResult
def notFound: SpaceChangeResult = NotFound
case object Forbidden extends SpaceChangeResult
def forbidden: SpaceChangeResult = Forbidden
case object Exists extends SpaceChangeResult
def exists: SpaceChangeResult = Exists
}
def delete(id: Ident, account: AccountId): ConnectionIO[SpaceChangeResult] = {
def tryDelete =
for {
_ <- RItem.removeSpace(id)
_ <- RSpaceMember.deleteAll(id)
_ <- RSpace.delete(id)
} yield SpaceChangeResult.success
(for {
uid <- OptionT(findUserId(account))
space <- OptionT(RSpace.findById(id))
res <- OptionT.liftF(
if (space.owner == uid) tryDelete
else SpaceChangeResult.forbidden.pure[ConnectionIO]
)
} yield res).getOrElse(SpaceChangeResult.notFound)
}
def changeName(
space: Ident,
account: AccountId,
name: String
): ConnectionIO[SpaceChangeResult] = {
def tryUpdate(ns: RSpace): ConnectionIO[SpaceChangeResult] =
for {
n <- RSpace.update(ns)
res =
if (n == 0) SpaceChangeResult.notFound
else SpaceChangeResult.Success
} yield res
(for {
uid <- OptionT(findUserId(account))
space <- OptionT(RSpace.findById(space))
res <- OptionT.liftF(
if (space.owner == uid) tryUpdate(space.copy(name = name))
else SpaceChangeResult.forbidden.pure[ConnectionIO]
)
} yield res).getOrElse(SpaceChangeResult.notFound)
}
def removeMember(
space: Ident,
account: AccountId,
member: Ident
): ConnectionIO[SpaceChangeResult] = {
def tryRemove: ConnectionIO[SpaceChangeResult] =
for {
n <- RSpaceMember.delete(member, space)
res =
if (n == 0) SpaceChangeResult.notFound
else SpaceChangeResult.Success
} yield res
(for {
uid <- OptionT(findUserId(account))
space <- OptionT(RSpace.findById(space))
res <- OptionT.liftF(
if (space.owner == uid) tryRemove
else SpaceChangeResult.forbidden.pure[ConnectionIO]
)
} yield res).getOrElse(SpaceChangeResult.notFound)
}
def addMember(
space: Ident,
account: AccountId,
member: Ident
): ConnectionIO[SpaceChangeResult] = {
def tryAdd: ConnectionIO[SpaceChangeResult] =
for {
spm <- RSpaceMember.findByUserId(member, space)
mem <- RSpaceMember.newMember[ConnectionIO](space, member)
res <-
if (spm.isDefined) SpaceChangeResult.exists.pure[ConnectionIO]
else RSpaceMember.insert(mem).map(_ => SpaceChangeResult.Success)
} yield res
(for {
uid <- OptionT(findUserId(account))
space <- OptionT(RSpace.findById(space))
res <- OptionT.liftF(
if (space.owner == uid) tryAdd
else SpaceChangeResult.forbidden.pure[ConnectionIO]
)
} yield res).getOrElse(SpaceChangeResult.notFound)
}
def findById(id: Ident, account: AccountId): ConnectionIO[Option[SpaceDetail]] = {
val mUserId = RSpaceMember.Columns.user.prefix("m")
val mSpaceId = RSpaceMember.Columns.space.prefix("m")
val uId = RUser.Columns.uid.prefix("u")
val uLogin = RUser.Columns.login.prefix("u")
val sColl = RSpace.Columns.collective.prefix("s")
val sId = RSpace.Columns.id.prefix("s")
val from = RSpaceMember.table ++ fr"m INNER JOIN" ++
RUser.table ++ fr"u ON" ++ mUserId.is(uId) ++ fr"INNER JOIN" ++
RSpace.table ++ fr"s ON" ++ mSpaceId.is(sId)
val memberQ = selectSimple(
Seq(uId, uLogin),
from,
and(mSpaceId.is(id), sColl.is(account.collective))
).query[IdRef].to[Vector]
(for {
space <- OptionT(findAll(account, Some(id), None, None).map(_.headOption))
memb <- OptionT.liftF(memberQ)
} yield space.withMembers(memb.toList)).value
}
def findAll(
account: AccountId,
idQ: Option[Ident],
ownerLogin: Option[Ident],
nameQ: Option[String]
): ConnectionIO[Vector[SpaceItem]] = {
// with memberlogin as
// (select m.space_id,u.login
// from space_member m
// inner join user_ u on u.uid = m.user_id
// inner join space s on s.id = m.space_id
// where s.cid = 'eike'
// union all
// select s.id,u.login
// from space 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 space_id = s.id and login = 'eike') as member
// ,(select count(*) - 1 from memberlogin where space_id = s.id) as member_count
// from space 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 = RSpace.Columns.id.prefix("s")
val sOwner = RSpace.Columns.owner.prefix("s")
val sName = RSpace.Columns.name.prefix("s")
val sColl = RSpace.Columns.collective.prefix("s")
val mUser = RSpaceMember.Columns.user.prefix("m")
val mSpace = RSpaceMember.Columns.space.prefix("m")
//CTE
val cte: Fragment = {
val from1 = RSpaceMember.table ++ fr"m INNER JOIN" ++
RUser.table ++ fr"u ON" ++ uId.is(mUser) ++ fr"INNER JOIN" ++
RSpace.table ++ fr"s ON" ++ sId.is(mSpace)
val from2 = RSpace.table ++ fr"s INNER JOIN" ++
RUser.table ++ fr"u ON" ++ uId.is(sOwner)
withCTE(
"memberlogin" ->
(selectSimple(Seq(mSpace, 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" ++ mSpace.prefix("").is(sId) ++
fr"AND" ++ uLogin.prefix("").is(account.user)
val memberCount =
fr"SELECT COUNT(*) - 1 FROM memberlogin WHERE" ++ mSpace.prefix("").is(sId)
//Query
val cols = Seq(
sId.f,
sName.f,
sOwner.f,
uLogin.f,
RSpace.Columns.created.prefix("s").f,
fr"(" ++ isMember ++ fr") as mem",
fr"(" ++ memberCount ++ fr") as cnt"
)
val from = RSpace.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[SpaceItem]
.to[Vector]
}
private def findUserId(account: AccountId): ConnectionIO[Option[Ident]] =
RUser.findByAccount(account).map(_.map(_.uid))
}

@ -9,7 +9,7 @@ import docspell.store.impl.Implicits._
import doobie._
import doobie.implicits._
case class RSpace(
case class RFolder(
id: Ident,
name: String,
collectiveId: Ident,
@ -17,15 +17,15 @@ case class RSpace(
created: Timestamp
)
object RSpace {
object RFolder {
def newSpace[F[_]: Sync](name: String, account: AccountId): F[RSpace] =
def newFolder[F[_]: Sync](name: String, account: AccountId): F[RFolder] =
for {
nId <- Ident.randomId[F]
now <- Timestamp.current[F]
} yield RSpace(nId, name, account.collective, account.user, now)
} yield RFolder(nId, name, account.collective, account.user, now)
val table = fr"space"
val table = fr"folder"
object Columns {
@ -40,7 +40,7 @@ object RSpace {
import Columns._
def insert(value: RSpace): ConnectionIO[Int] = {
def insert(value: RFolder): ConnectionIO[Int] = {
val sql = insertRow(
table,
all,
@ -49,37 +49,37 @@ object RSpace {
sql.update.run
}
def update(v: RSpace): ConnectionIO[Int] =
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, spaceName: String): ConnectionIO[Boolean] =
selectCount(id, table, and(collective.is(coll), name.is(spaceName)))
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(spaceId: Ident): ConnectionIO[Option[RSpace]] = {
val sql = selectSimple(all, table, id.is(spaceId))
sql.query[RSpace].option
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[RSpace]] = {
): 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[RSpace].to[Vector]
sql.query[RFolder].to[Vector]
}
def delete(spaceId: Ident): ConnectionIO[Int] =
deleteFrom(table, id.is(spaceId)).update.run
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
}

@ -28,7 +28,7 @@ case class RItem(
created: Timestamp,
updated: Timestamp,
notes: Option[String],
spaceId: Option[Ident]
folderId: Option[Ident]
) {}
object RItem {
@ -82,7 +82,7 @@ object RItem {
val created = Column("created")
val updated = Column("updated")
val notes = Column("notes")
val space = Column("space_id")
val folder = Column("folder_id")
val all = List(
id,
cid,
@ -100,7 +100,7 @@ object RItem {
created,
updated,
notes,
space
folder
)
}
import Columns._
@ -111,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},${v.spaceId}"
fr"${v.created},${v.updated},${v.notes},${v.folderId}"
).update.run
def getCollective(itemId: Ident): ConnectionIO[Option[Ident]] =
@ -300,8 +300,8 @@ 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 removeSpace(spaceId: Ident): ConnectionIO[Int] = {
def removeFolder(folderId: Ident): ConnectionIO[Int] = {
val empty: Option[Ident] = None
updateRow(table, space.is(spaceId), space.setTo(empty)).update.run
updateRow(table, folder.is(folderId), folder.setTo(empty)).update.run
}
}

@ -1,61 +0,0 @@
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 RSpaceMember(
id: Ident,
spaceId: Ident,
userId: Ident,
created: Timestamp
)
object RSpaceMember {
def newMember[F[_]: Sync](space: Ident, user: Ident): F[RSpaceMember] =
for {
nId <- Ident.randomId[F]
now <- Timestamp.current[F]
} yield RSpaceMember(nId, space, user, now)
val table = fr"space_member"
object Columns {
val id = Column("id")
val space = Column("space_id")
val user = Column("user_id")
val created = Column("created")
val all = List(id, space, user, created)
}
import Columns._
def insert(value: RSpaceMember): ConnectionIO[Int] = {
val sql = insertRow(
table,
all,
fr"${value.id},${value.spaceId},${value.userId},${value.created}"
)
sql.update.run
}
def findByUserId(userId: Ident, spaceId: Ident): ConnectionIO[Option[RSpaceMember]] =
selectSimple(all, table, and(space.is(spaceId), user.is(userId)))
.query[RSpaceMember]
.option
def delete(userId: Ident, spaceId: Ident): ConnectionIO[Int] =
deleteFrom(table, and(space.is(spaceId), user.is(userId))).update.run
def deleteAll(spaceId: Ident): ConnectionIO[Int] =
deleteFrom(table, space.is(spaceId)).update.run
}

@ -6,16 +6,17 @@ module Api exposing
, addMember
, addTag
, cancelJob
, changeFolderName
, changePassword
, changeSpaceName
, checkCalEvent
, createImapSettings
, createMailSettings
, createNewSpace
, createNewFolder
, createNotifyDueItems
, createScanMailbox
, deleteAttachment
, deleteEquip
, deleteFolder
, deleteImapSettings
, deleteItem
, deleteMailSettings
@ -24,7 +25,6 @@ module Api exposing
, deletePerson
, deleteScanMailbox
, deleteSource
, deleteSpace
, deleteTag
, deleteUser
, getAttachmentMeta
@ -32,6 +32,8 @@ module Api exposing
, getCollectiveSettings
, getContacts
, getEquipments
, getFolderDetail
, getFolders
, getImapSettings
, getInsights
, getItemProposals
@ -46,8 +48,6 @@ module Api exposing
, getScanMailbox
, getSentMails
, getSources
, getSpaceDetail
, getSpaces
, getTags
, getUsers
, itemDetail
@ -108,6 +108,8 @@ 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)
@ -122,7 +124,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.NewSpace exposing (NewSpace)
import Api.Model.NewFolder exposing (NewFolder)
import Api.Model.NotificationSettings exposing (NotificationSettings)
import Api.Model.NotificationSettingsList exposing (NotificationSettingsList)
import Api.Model.OptionalDate exposing (OptionalDate)
@ -141,8 +143,6 @@ import Api.Model.SentMails exposing (SentMails)
import Api.Model.SimpleMail exposing (SimpleMail)
import Api.Model.Source exposing (Source)
import Api.Model.SourceList exposing (SourceList)
import Api.Model.SpaceDetail exposing (SpaceDetail)
import Api.Model.SpaceList exposing (SpaceList)
import Api.Model.Tag exposing (Tag)
import Api.Model.TagList exposing (TagList)
import Api.Model.User exposing (User)
@ -161,13 +161,13 @@ import Util.Http as Http2
--- Spaces
--- Folders
deleteSpace : Flags -> String -> (Result Http.Error BasicResult -> msg) -> Cmd msg
deleteSpace flags id receive =
deleteFolder : Flags -> String -> (Result Http.Error BasicResult -> msg) -> Cmd msg
deleteFolder flags id receive =
Http2.authDelete
{ url = flags.config.baseUrl ++ "/api/v1/sec/space/" ++ id
{ url = flags.config.baseUrl ++ "/api/v1/sec/folder/" ++ id
, account = getAccount flags
, expect = Http.expectJson receive Api.Model.BasicResult.decoder
}
@ -176,7 +176,7 @@ deleteSpace flags id receive =
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/space/" ++ id ++ "/member/" ++ user
{ url = flags.config.baseUrl ++ "/api/v1/sec/folder/" ++ id ++ "/member/" ++ user
, account = getAccount flags
, expect = Http.expectJson receive Api.Model.BasicResult.decoder
}
@ -185,48 +185,48 @@ removeMember flags id user receive =
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/space/" ++ id ++ "/member/" ++ user
{ 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
}
changeSpaceName : Flags -> String -> NewSpace -> (Result Http.Error BasicResult -> msg) -> Cmd msg
changeSpaceName flags id ns receive =
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/space/" ++ id
{ url = flags.config.baseUrl ++ "/api/v1/sec/folder/" ++ id
, account = getAccount flags
, body = Http.jsonBody (Api.Model.NewSpace.encode ns)
, body = Http.jsonBody (Api.Model.NewFolder.encode ns)
, expect = Http.expectJson receive Api.Model.BasicResult.decoder
}
createNewSpace : Flags -> NewSpace -> (Result Http.Error IdResult -> msg) -> Cmd msg
createNewSpace flags ns receive =
createNewFolder : Flags -> NewFolder -> (Result Http.Error IdResult -> msg) -> Cmd msg
createNewFolder flags ns receive =
Http2.authPost
{ url = flags.config.baseUrl ++ "/api/v1/sec/space"
{ url = flags.config.baseUrl ++ "/api/v1/sec/folder"
, account = getAccount flags
, body = Http.jsonBody (Api.Model.NewSpace.encode ns)
, body = Http.jsonBody (Api.Model.NewFolder.encode ns)
, expect = Http.expectJson receive Api.Model.IdResult.decoder
}
getSpaceDetail : Flags -> String -> (Result Http.Error SpaceDetail -> msg) -> Cmd msg
getSpaceDetail flags id receive =
getFolderDetail : Flags -> String -> (Result Http.Error FolderDetail -> msg) -> Cmd msg
getFolderDetail flags id receive =
Http2.authGet
{ url = flags.config.baseUrl ++ "/api/v1/sec/space/" ++ id
{ url = flags.config.baseUrl ++ "/api/v1/sec/folder/" ++ id
, account = getAccount flags
, expect = Http.expectJson receive Api.Model.SpaceDetail.decoder
, expect = Http.expectJson receive Api.Model.FolderDetail.decoder
}
getSpaces : Flags -> String -> Bool -> (Result Http.Error SpaceList -> msg) -> Cmd msg
getSpaces flags query owningOnly receive =
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/space?q="
++ "/api/v1/sec/folder?q="
++ Url.percentEncode query
++ (if owningOnly then
"&owning=true"
@ -235,7 +235,7 @@ getSpaces flags query owningOnly receive =
""
)
, account = getAccount flags
, expect = Http.expectJson receive Api.Model.SpaceList.decoder
, expect = Http.expectJson receive Api.Model.FolderList.decoder
}

@ -1,4 +1,4 @@
module Comp.SpaceDetail exposing
module Comp.FolderDetail exposing
( Model
, Msg
, init
@ -9,10 +9,10 @@ module Comp.SpaceDetail exposing
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.NewSpace exposing (NewSpace)
import Api.Model.SpaceDetail exposing (SpaceDetail)
import Api.Model.NewFolder exposing (NewFolder)
import Api.Model.User exposing (User)
import Api.Model.UserList exposing (UserList)
import Comp.FixedDropdown
@ -28,7 +28,7 @@ import Util.Maybe
type alias Model =
{ result : Maybe BasicResult
, space : SpaceDetail
, folder : FolderDetail
, name : Maybe String
, members : List IdName
, users : List User
@ -43,10 +43,10 @@ type Msg
= SetName String
| MemberDropdownMsg (Comp.FixedDropdown.Msg IdName)
| SaveName
| NewSpaceResp (Result Http.Error IdResult)
| ChangeSpaceResp (Result Http.Error BasicResult)
| NewFolderResp (Result Http.Error IdResult)
| ChangeFolderResp (Result Http.Error BasicResult)
| ChangeNameResp (Result Http.Error BasicResult)
| SpaceDetailResp (Result Http.Error SpaceDetail)
| FolderDetailResp (Result Http.Error FolderDetail)
| AddMember
| RemoveMember IdName
| RequestDelete
@ -55,16 +55,16 @@ type Msg
| GoBack
init : List User -> SpaceDetail -> Model
init users space =
init : List User -> FolderDetail -> Model
init users folder =
{ result = Nothing
, space = space
, name = Util.Maybe.fromString space.name
, members = space.members
, folder = folder
, name = Util.Maybe.fromString folder.name
, members = folder.members
, users = users
, memberDropdown =
Comp.FixedDropdown.initMap .name
(makeOptions users space)
(makeOptions users folder)
, selectedMember = Nothing
, loading = False
, deleteDimmer = Comp.YesNoDimmer.emptyModel
@ -73,17 +73,17 @@ init users space =
initEmpty : List User -> Model
initEmpty users =
init users Api.Model.SpaceDetail.empty
init users Api.Model.FolderDetail.empty
makeOptions : List User -> SpaceDetail -> List IdName
makeOptions users space =
makeOptions : List User -> FolderDetail -> List IdName
makeOptions users folder =
let
toIdName u =
IdName u.id u.login
notMember idn =
List.member idn (space.owner :: space.members) |> not
List.member idn (folder.owner :: folder.members) |> not
in
List.map toIdName users
|> List.filter notMember
@ -129,13 +129,13 @@ update flags msg model =
Just name ->
let
cmd =
if model.space.id == "" then
Api.createNewSpace flags (NewSpace name) NewSpaceResp
if model.folder.id == "" then
Api.createNewFolder flags (NewFolder name) NewFolderResp
else
Api.changeSpaceName flags
model.space.id
(NewSpace name)
Api.changeFolderName flags
model.folder.id
(NewFolder name)
ChangeNameResp
in
( { model
@ -149,9 +149,9 @@ update flags msg model =
Nothing ->
( model, Cmd.none, False )
NewSpaceResp (Ok ir) ->
NewFolderResp (Ok ir) ->
if ir.success then
( model, Api.getSpaceDetail flags ir.id SpaceDetailResp, False )
( model, Api.getFolderDetail flags ir.id FolderDetailResp, False )
else
( { model
@ -162,7 +162,7 @@ update flags msg model =
, False
)
NewSpaceResp (Err err) ->
NewFolderResp (Err err) ->
( { model
| loading = False
, result = Just (BasicResult False (Util.Http.errorToString err))
@ -171,10 +171,10 @@ update flags msg model =
, False
)
ChangeSpaceResp (Ok r) ->
ChangeFolderResp (Ok r) ->
if r.success then
( model
, Api.getSpaceDetail flags model.space.id SpaceDetailResp
, Api.getFolderDetail flags model.folder.id FolderDetailResp
, False
)
@ -184,7 +184,7 @@ update flags msg model =
, False
)
ChangeSpaceResp (Err err) ->
ChangeFolderResp (Err err) ->
( { model
| loading = False
, result = Just (BasicResult False (Util.Http.errorToString err))
@ -209,10 +209,10 @@ update flags msg model =
, False
)
SpaceDetailResp (Ok sd) ->
FolderDetailResp (Ok sd) ->
( init model.users sd, Cmd.none, False )
SpaceDetailResp (Err err) ->
FolderDetailResp (Err err) ->
( { model
| loading = False
, result = Just (BasicResult False (Util.Http.errorToString err))
@ -225,7 +225,7 @@ update flags msg model =
case model.selectedMember of
Just mem ->
( { model | loading = True }
, Api.addMember flags model.space.id mem.id ChangeSpaceResp
, Api.addMember flags model.folder.id mem.id ChangeFolderResp
, False
)
@ -234,7 +234,7 @@ update flags msg model =
RemoveMember idname ->
( { model | loading = True }
, Api.removeMember flags model.space.id idname.id ChangeSpaceResp
, Api.removeMember flags model.folder.id idname.id ChangeFolderResp
, False
)
@ -252,7 +252,7 @@ update flags msg model =
cmd =
if flag then
Api.deleteSpace flags model.space.id DeleteResp
Api.deleteFolder flags model.folder.id DeleteResp
else
Cmd.none
@ -278,23 +278,23 @@ view flags model =
let
isOwner =
Maybe.map .user flags.account
|> Maybe.map ((==) model.space.owner.name)
|> Maybe.map ((==) model.folder.owner.name)
|> Maybe.withDefault False
in
div []
([ Html.map DeleteMsg (Comp.YesNoDimmer.view model.deleteDimmer)
, if model.space.id == "" then
, if model.folder.id == "" then
div []
[ text "Create a new space. You are automatically set as owner of this new space."
[ text "Create a new folder. You are automatically set as owner of this new folder."
]
else
div []
[ text "Modify this space by changing the name or add/remove members."
[ text "Modify this folder by changing the name or add/remove members."
]
, if model.space.id /= "" && not isOwner then
, if model.folder.id /= "" && not isOwner then
div [ class "ui info message" ]
[ text "You are not the owner of this space and therefore are not allowed to edit it."
[ text "You are not the owner of this folder and therefore are not allowed to edit it."
]
else
@ -315,7 +315,7 @@ view flags model =
[ text "Owner"
]
, div [ class "" ]
[ text model.space.owner.name
[ text model.folder.owner.name
]
, div [ class "ui header" ]
[ text "Name"
@ -361,7 +361,7 @@ viewButtons _ =
viewMembers : Model -> List (Html Msg)
viewMembers model =
if model.space.id == "" then
if model.folder.id == "" then
[]
else

@ -1,4 +1,4 @@
module Comp.SpaceManage exposing
module Comp.FolderManage exposing
( Model
, Msg
, empty
@ -8,13 +8,13 @@ module Comp.SpaceManage exposing
)
import Api
import Api.Model.SpaceDetail exposing (SpaceDetail)
import Api.Model.SpaceItem exposing (SpaceItem)
import Api.Model.SpaceList exposing (SpaceList)
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.SpaceDetail
import Comp.SpaceTable
import Comp.FolderDetail
import Comp.FolderTable
import Data.Flags exposing (Flags)
import Html exposing (..)
import Html.Attributes exposing (..)
@ -23,9 +23,9 @@ import Http
type alias Model =
{ tableModel : Comp.SpaceTable.Model
, detailModel : Maybe Comp.SpaceDetail.Model
, spaces : List SpaceItem
{ tableModel : Comp.FolderTable.Model
, detailModel : Maybe Comp.FolderDetail.Model
, folders : List FolderItem
, users : List User
, query : String
, owningOnly : Bool
@ -34,21 +34,21 @@ type alias Model =
type Msg
= TableMsg Comp.SpaceTable.Msg
| DetailMsg Comp.SpaceDetail.Msg
= TableMsg Comp.FolderTable.Msg
| DetailMsg Comp.FolderDetail.Msg
| UserListResp (Result Http.Error UserList)
| SpaceListResp (Result Http.Error SpaceList)
| SpaceDetailResp (Result Http.Error SpaceDetail)
| FolderListResp (Result Http.Error FolderList)
| FolderDetailResp (Result Http.Error FolderDetail)
| SetQuery String
| InitNewSpace
| InitNewFolder
| ToggleOwningOnly
empty : Model
empty =
{ tableModel = Comp.SpaceTable.init
{ tableModel = Comp.FolderTable.init
, detailModel = Nothing
, spaces = []
, folders = []
, users = []
, query = ""
, owningOnly = True
@ -61,7 +61,7 @@ init flags =
( empty
, Cmd.batch
[ Api.getUsers flags UserListResp
, Api.getSpaces flags empty.query empty.owningOnly SpaceListResp
, Api.getFolders flags empty.query empty.owningOnly FolderListResp
]
)
@ -76,14 +76,14 @@ update flags msg model =
TableMsg lm ->
let
( tm, action ) =
Comp.SpaceTable.update lm model.tableModel
Comp.FolderTable.update lm model.tableModel
cmd =
case action of
Comp.SpaceTable.EditAction item ->
Api.getSpaceDetail flags item.id SpaceDetailResp
Comp.FolderTable.EditAction item ->
Api.getFolderDetail flags item.id FolderDetailResp
Comp.SpaceTable.NoAction ->
Comp.FolderTable.NoAction ->
Cmd.none
in
( { model | tableModel = tm }, cmd )
@ -93,11 +93,11 @@ update flags msg model =
Just detail ->
let
( dm, dc, back ) =
Comp.SpaceDetail.update flags lm detail
Comp.FolderDetail.update flags lm detail
cmd =
if back then
Api.getSpaces flags model.query model.owningOnly SpaceListResp
Api.getFolders flags model.query model.owningOnly FolderListResp
else
Cmd.none
@ -121,7 +121,7 @@ update flags msg model =
SetQuery str ->
( { model | query = str }
, Api.getSpaces flags str model.owningOnly SpaceListResp
, Api.getFolders flags str model.owningOnly FolderListResp
)
ToggleOwningOnly ->
@ -130,7 +130,7 @@ update flags msg model =
not model.owningOnly
in
( { model | owningOnly = newOwning }
, Api.getSpaces flags model.query newOwning SpaceListResp
, Api.getFolders flags model.query newOwning FolderListResp
)
UserListResp (Ok ul) ->
@ -139,24 +139,24 @@ update flags msg model =
UserListResp (Err err) ->
( model, Cmd.none )
SpaceListResp (Ok sl) ->
( { model | spaces = sl.items }, Cmd.none )
FolderListResp (Ok sl) ->
( { model | folders = sl.items }, Cmd.none )
SpaceListResp (Err err) ->
FolderListResp (Err err) ->
( model, Cmd.none )
SpaceDetailResp (Ok sd) ->
( { model | detailModel = Comp.SpaceDetail.init model.users sd |> Just }
FolderDetailResp (Ok sd) ->
( { model | detailModel = Comp.FolderDetail.init model.users sd |> Just }
, Cmd.none
)
SpaceDetailResp (Err err) ->
FolderDetailResp (Err err) ->
( model, Cmd.none )
InitNewSpace ->
InitNewFolder ->
let
sd =
Comp.SpaceDetail.initEmpty model.users
Comp.FolderDetail.initEmpty model.users
in
( { model | detailModel = Just sd }
, Cmd.none
@ -177,10 +177,10 @@ view flags model =
viewTable model
viewDetail : Flags -> Comp.SpaceDetail.Model -> Html Msg
viewDetail : Flags -> Comp.FolderDetail.Model -> Html Msg
viewDetail flags detailModel =
div []
[ Html.map DetailMsg (Comp.SpaceDetail.view flags detailModel)
[ Html.map DetailMsg (Comp.FolderDetail.view flags detailModel)
]
@ -209,7 +209,7 @@ viewTable model =
, checked model.owningOnly
]
[]
, label [] [ text "Show owning spaces only" ]
, label [] [ text "Show owning folders only" ]
]
]
, div [ class "right menu" ]
@ -217,15 +217,15 @@ viewTable model =
[ a
[ class "ui primary button"
, href "#"
, onClick InitNewSpace
, onClick InitNewFolder
]
[ i [ class "plus icon" ] []
, text "New Space"
, text "New Folder"
]
]
]
]
, Html.map TableMsg (Comp.SpaceTable.view model.tableModel model.spaces)
, Html.map TableMsg (Comp.FolderTable.view model.tableModel model.folders)
, div
[ classList
[ ( "ui dimmer", True )

@ -1,4 +1,4 @@
module Comp.SpaceTable exposing
module Comp.FolderTable exposing
( Action(..)
, Model
, Msg
@ -7,7 +7,7 @@ module Comp.SpaceTable exposing
, view
)
import Api.Model.SpaceItem exposing (SpaceItem)
import Api.Model.FolderItem exposing (FolderItem)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
@ -20,12 +20,12 @@ type alias Model =
type Msg
= EditItem SpaceItem
= EditItem FolderItem
type Action
= NoAction
| EditAction SpaceItem
| EditAction FolderItem
init : Model
@ -40,7 +40,7 @@ update msg model =
( model, EditAction item )
view : Model -> List SpaceItem -> Html Msg
view : Model -> List FolderItem -> Html Msg
view _ items =
div []
[ table [ class "ui very basic center aligned table" ]
@ -58,7 +58,7 @@ view _ items =
]
viewItem : SpaceItem -> Html Msg
viewItem : FolderItem -> Html Msg
viewItem item =
tr []
[ td [ class "collapsing" ]

@ -15,12 +15,12 @@ module Data.Icons exposing
, editNotesIcon
, equipment
, equipmentIcon
, folder
, folderIcon
, organization
, organizationIcon
, person
, personIcon
, space
, spaceIcon
, tag
, tagIcon
, tags
@ -31,14 +31,14 @@ import Html exposing (Html, i)
import Html.Attributes exposing (class)
space : String
space =
folder : String
folder =
"folder outline icon"
spaceIcon : String -> Html msg
spaceIcon classes =
i [ class (space ++ " " ++ classes) ] []
folderIcon : String -> Html msg
folderIcon classes =
i [ class (folder ++ " " ++ classes) ] []
concerned : String

@ -6,9 +6,9 @@ module Page.ManageData.Data exposing
)
import Comp.EquipmentManage
import Comp.FolderManage
import Comp.OrgManage
import Comp.PersonManage
import Comp.SpaceManage
import Comp.TagManage
import Data.Flags exposing (Flags)
@ -19,7 +19,7 @@ type alias Model =
, equipManageModel : Comp.EquipmentManage.Model
, orgManageModel : Comp.OrgManage.Model
, personManageModel : Comp.PersonManage.Model
, spaceManageModel : Comp.SpaceManage.Model
, folderManageModel : Comp.FolderManage.Model
}
@ -30,7 +30,7 @@ init _ =
, equipManageModel = Comp.EquipmentManage.emptyModel
, orgManageModel = Comp.OrgManage.emptyModel
, personManageModel = Comp.PersonManage.emptyModel
, spaceManageModel = Comp.SpaceManage.empty
, folderManageModel = Comp.FolderManage.empty
}
, Cmd.none
)
@ -41,7 +41,7 @@ type Tab
| EquipTab
| OrgTab
| PersonTab
| SpaceTab
| FolderTab
type Msg
@ -50,4 +50,4 @@ type Msg
| EquipManageMsg Comp.EquipmentManage.Msg
| OrgManageMsg Comp.OrgManage.Msg
| PersonManageMsg Comp.PersonManage.Msg
| SpaceMsg Comp.SpaceManage.Msg
| FolderMsg Comp.FolderManage.Msg

@ -1,9 +1,9 @@
module Page.ManageData.Update exposing (update)
import Comp.EquipmentManage
import Comp.FolderManage
import Comp.OrgManage
import Comp.PersonManage
import Comp.SpaceManage
import Comp.TagManage
import Data.Flags exposing (Flags)
import Page.ManageData.Data exposing (..)
@ -30,12 +30,12 @@ update flags msg model =
PersonTab ->
update flags (PersonManageMsg Comp.PersonManage.LoadPersons) m
SpaceTab ->
FolderTab ->
let
( sm, sc ) =
Comp.SpaceManage.init flags
Comp.FolderManage.init flags
in
( { m | spaceManageModel = sm }, Cmd.map SpaceMsg sc )
( { m | folderManageModel = sm }, Cmd.map FolderMsg sc )
TagManageMsg m ->
let
@ -65,11 +65,11 @@ update flags msg model =
in
( { model | personManageModel = m2 }, Cmd.map PersonManageMsg c2 )
SpaceMsg lm ->
FolderMsg lm ->
let
( m2, c2 ) =
Comp.SpaceManage.update flags lm model.spaceManageModel
Comp.FolderManage.update flags lm model.folderManageModel
in
( { model | spaceManageModel = m2 }
, Cmd.map SpaceMsg c2
( { model | folderManageModel = m2 }
, Cmd.map FolderMsg c2
)

@ -1,9 +1,9 @@
module Page.ManageData.View exposing (view)
import Comp.EquipmentManage
import Comp.FolderManage
import Comp.OrgManage
import Comp.PersonManage
import Comp.SpaceManage
import Comp.TagManage
import Data.Flags exposing (Flags)
import Data.Icons as Icons
@ -53,11 +53,11 @@ view flags settings model =
, text "Person"
]
, div
[ classActive (model.currentTab == Just SpaceTab) "link icon item"
, onClick (SetTab SpaceTab)
[ classActive (model.currentTab == Just FolderTab) "link icon item"
, onClick (SetTab FolderTab)
]
[ Icons.spaceIcon ""
, text "Space"
[ Icons.folderIcon ""
, text "Folder"
]
]
]
@ -77,8 +77,8 @@ view flags settings model =
Just PersonTab ->
viewPerson settings model
Just SpaceTab ->
viewSpace flags settings model
Just FolderTab ->
viewFolder flags settings model
Nothing ->
[]
@ -87,19 +87,19 @@ view flags settings model =
]
viewSpace : Flags -> UiSettings -> Model -> List (Html Msg)
viewSpace flags _ model =
viewFolder : Flags -> UiSettings -> Model -> List (Html Msg)
viewFolder flags _ model =
[ h2
[ class "ui header"
]
[ Icons.spaceIcon ""
[ Icons.folderIcon ""
, div
[ class "content"
]
[ text "Spaces"
[ text "Folders"
]
]
, Html.map SpaceMsg (Comp.SpaceManage.view flags model.spaceManageModel)
, Html.map FolderMsg (Comp.FolderManage.view flags model.folderManageModel)
]