Instead of client only, make bookmarks a server aware feature

Makes it much more useful
This commit is contained in:
eikek 2022-01-09 23:50:34 +01:00
parent 063ae56488
commit 9415f72ec0
19 changed files with 618 additions and 345 deletions

View File

@ -49,6 +49,7 @@ trait BackendApp[F[_]] {
def pubSub: PubSubT[F]
def events: EventExchange[F]
def notification: ONotification[F]
def bookmarks: OQueryBookmarks[F]
}
object BackendApp {
@ -89,6 +90,7 @@ object BackendApp {
OShare(store, itemSearchImpl, simpleSearchImpl, javaEmil)
)
notifyImpl <- ONotification(store, notificationMod)
bookmarksImpl <- OQueryBookmarks(store)
} yield new BackendApp[F] {
val pubSub = pubSubT
val login = loginImpl
@ -115,5 +117,6 @@ object BackendApp {
val share = shareImpl
val events = notificationMod
val notification = notifyImpl
val bookmarks = bookmarksImpl
}
}

View File

@ -0,0 +1,83 @@
package docspell.backend.ops
import docspell.common._
import docspell.query.ItemQuery
import cats.effect._
import docspell.store.Store
import docspell.store.records.RQueryBookmark
import cats.implicits._
import docspell.store.UpdateResult
import docspell.store.AddResult
trait OQueryBookmarks[F[_]] {
def getAll(account: AccountId): F[Vector[OQueryBookmarks.Bookmark]]
def create(account: AccountId, bookmark: OQueryBookmarks.NewBookmark): F[AddResult]
def update(
account: AccountId,
id: Ident,
bookmark: OQueryBookmarks.NewBookmark
): F[UpdateResult]
def delete(account: AccountId, bookmark: Ident): F[Unit]
}
object OQueryBookmarks {
final case class NewBookmark(
name: String,
label: Option[String],
query: ItemQuery,
personal: Boolean
)
final case class Bookmark(
id: Ident,
name: String,
label: Option[String],
query: ItemQuery,
personal: Boolean,
created: Timestamp
)
def apply[F[_]: Sync](store: Store[F]): Resource[F, OQueryBookmarks[F]] =
Resource.pure(new OQueryBookmarks[F] {
def getAll(account: AccountId): F[Vector[Bookmark]] =
store
.transact(RQueryBookmark.allForUser(account))
.map(
_.map(r => Bookmark(r.id, r.name, r.label, r.query, r.isPersonal, r.created))
)
def create(account: AccountId, b: NewBookmark): F[AddResult] =
store
.transact(for {
r <- RQueryBookmark.createNew(account, b.name, b.label, b.query, b.personal)
n <- RQueryBookmark.insert(r)
} yield n)
.attempt
.map(AddResult.fromUpdate)
def update(account: AccountId, id: Ident, b: NewBookmark): F[UpdateResult] =
UpdateResult.fromUpdate(
store.transact(
RQueryBookmark.update(
RQueryBookmark(
id,
b.name,
b.label,
None, // userId and some other values are not used
account.collective,
b.query,
Timestamp.Epoch
)
)
)
)
def delete(account: AccountId, bookmark: Ident): F[Unit] =
store.transact(RQueryBookmark.deleteById(account.collective, bookmark)).as(())
})
}

View File

@ -1880,6 +1880,98 @@ paths:
application/json:
schema: {}
/sec/querybookmark:
get:
operationId: "sec-querybookmark-get-all"
tags: [Query Bookmarks]
summary: Return all query bookmarks
description: |
Returns all query bookmarks of the current user.
Bookmarks can be "global", where they belong to the whole
collective or personal, so they are only for the user. This
returns both.
security:
- authTokenHeader: []
responses:
422:
description: BadRequest
200:
description: Ok
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/BookmarkedQuery"
post:
operationId: "sec-querybookmark-post"
tags: [Query Bookmarks]
summary: Create a new query bookmark
description: |
Creates a new query bookmark.
A bookmark must have a unique name (within both collective and
personal scope). If a name already exists, a failure is
returned - use PUT instead for changing existing bookmarks.
security:
- authTokenHeader: []
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/BookmarkedQuery"
responses:
422:
description: BadRequest
200:
description: Ok
content:
application/json:
schema:
$ref: "#/components/schemas/BasicResult"
put:
operationId: "sec-querybookmark-put"
tags: [Query Bookmarks]
summary: Change a query bookmark
description: |
Changes an existing query bookmark.
A bookmark must have a unique name within the collective
(considering collective and personal scope). The bookmark is
identified by its id, which must exist.
security:
- authTokenHeader: []
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/BookmarkedQuery"
responses:
422:
description: BadRequest
200:
description: Ok
content:
application/json:
schema:
$ref: "#/components/schemas/BasicResult"
/sec/querybookmark/{bookmarkId}:
delete:
operationId: "sec-querybookmark-delete"
tags: [Query Bookmark]
summary: Delete a bookmark.
description: |
Deletes a bookmarks by its id.
responses:
422:
description: BadRequest
200:
description: Ok
content:
application/json:
schema:
$ref: "#/components/schemas/BasicResult"
/sec/clientSettings/{clientId}:
parameters:
@ -5314,6 +5406,32 @@ paths:
components:
schemas:
BookmarkedQuery:
description: |
A query bookmark.
required:
- id
- name
- query
- personal
- created
properties:
id:
type: string
format: ident
name:
type: string
label:
type: string
query:
type: string
format: itemquery
personal:
type: boolean
created:
type: integer
format: date-time
StringValue:
description: |
A generic string value

View File

@ -172,7 +172,8 @@ object RestServer {
"folder" -> FolderRoutes(restApp.backend, token),
"customfield" -> CustomFieldRoutes(restApp.backend, token),
"clientSettings" -> ClientSettingsRoutes(restApp.backend, token),
"notification" -> NotificationRoutes(cfg, restApp.backend, token)
"notification" -> NotificationRoutes(cfg, restApp.backend, token),
"querybookmark" -> BookmarkRoutes(restApp.backend, token)
)
def openRoutes[F[_]: Async](

View File

@ -0,0 +1,58 @@
package docspell.restserver.routes
import docspell.backend.auth.AuthToken
import org.http4s.HttpRoutes
import org.http4s.circe.CirceEntityDecoder._
import org.http4s.circe.CirceEntityEncoder._
import org.http4s.dsl.Http4sDsl
import cats.effect.Async
import docspell.backend.ops.OQueryBookmarks
import docspell.restapi.model.BookmarkedQuery
import docspell.backend.BackendApp
import cats.implicits._
import docspell.restserver.conv.Conversions
import docspell.common.Ident
object BookmarkRoutes {
def apply[F[_]: Async](backend: BackendApp[F], token: AuthToken): HttpRoutes[F] = {
val dsl = new Http4sDsl[F] {}
import dsl._
HttpRoutes.of {
case GET -> Root =>
for {
all <- backend.bookmarks.getAll(token.account)
resp <- Ok(all.map(convert.toApi))
} yield resp
case req @ POST -> Root =>
for {
data <- req.as[BookmarkedQuery]
res <- backend.bookmarks.create(token.account, convert.toModel(data))
resp <- Ok(Conversions.basicResult(res, "Bookmark created"))
} yield resp
case req @ PUT -> Root =>
for {
data <- req.as[BookmarkedQuery]
res <- backend.bookmarks.update(token.account, data.id, convert.toModel(data))
resp <- Ok(Conversions.basicResult(res, "Bookmark updated"))
} yield resp
case DELETE -> Root / Ident(id) =>
for {
res <- backend.bookmarks.delete(token.account, id).attempt
resp <- Ok(Conversions.basicResult(res, "Bookmark deleted"))
} yield resp
}
}
object convert {
def toApi(b: OQueryBookmarks.Bookmark): BookmarkedQuery =
BookmarkedQuery(b.id, b.name, b.label, b.query, b.personal, b.created)
def toModel(b: BookmarkedQuery): OQueryBookmarks.NewBookmark =
OQueryBookmarks.NewBookmark(b.name, b.label, b.query, b.personal)
}
}

View File

@ -0,0 +1,12 @@
CREATE TABLE "query_bookmark" (
"id" varchar(254) not null primary key,
"name" varchar(254) not null,
"label" varchar(254),
"user_id" varchar(254),
"cid" varchar(254) not null,
"query" varchar(2000) not null,
"created" timestamp,
foreign key ("user_id") references "user_"("uid") on delete cascade,
foreign key ("cid") references "collective"("cid") on delete cascade,
unique("cid", "user_id", "name")
)

View File

@ -0,0 +1,12 @@
CREATE TABLE `query_bookmark` (
`id` varchar(254) not null primary key,
`name` varchar(254) not null,
`label` varchar(254),
`user_id` varchar(254),
`cid` varchar(254) not null,
`query` varchar(2000) not null,
`created` timestamp,
foreign key (`user_id`) references `user_`(`uid`) on delete cascade,
foreign key (`cid`) references `collective`(`cid`) on delete cascade,
unique(`cid`, `user_id`, `name`)
)

View File

@ -0,0 +1,12 @@
CREATE TABLE "query_bookmark" (
"id" varchar(254) not null primary key,
"name" varchar(254) not null,
"label" varchar(254),
"user_id" varchar(254),
"cid" varchar(254) not null,
"query" varchar(2000) not null,
"created" timestamp,
foreign key ("user_id") references "user_"("uid") on delete cascade,
foreign key ("cid") references "collective"("cid") on delete cascade,
unique("cid", "user_id", "name")
)

View File

@ -0,0 +1,108 @@
package docspell.store.records
import docspell.common._
import docspell.query.ItemQuery
import docspell.store.qb.DSL._
import docspell.store.qb._
import doobie._
import doobie.implicits._
import cats.data.NonEmptyList
import cats.syntax.option._
final case class RQueryBookmark(
id: Ident,
name: String,
label: Option[String],
userId: Option[Ident],
cid: Ident,
query: ItemQuery,
created: Timestamp
) {
def isPersonal: Boolean =
userId.isDefined
def asGlobal: RQueryBookmark =
copy(userId = None)
def asPersonal(userId: Ident): RQueryBookmark =
copy(userId = userId.some)
}
object RQueryBookmark {
final case class Table(alias: Option[String]) extends TableDef {
val tableName = "query_bookmark";
val id = Column[Ident]("id", this)
val name = Column[String]("name", this)
val label = Column[String]("label", this)
val userId = Column[Ident]("user_id", this)
val cid = Column[Ident]("cid", this)
val query = Column[ItemQuery]("query", this)
val created = Column[Timestamp]("created", this)
val all: NonEmptyList[Column[_]] =
NonEmptyList.of(id, name, label, userId, cid, query, created)
}
val T: Table = Table(None)
def as(alias: String): Table = Table(Some(alias))
def createNew(
account: AccountId,
name: String,
label: Option[String],
query: ItemQuery,
personal: Boolean
): ConnectionIO[RQueryBookmark] =
for {
userId <- RUser.getIdByAccount(account)
curTime <- Timestamp.current[ConnectionIO]
id <- Ident.randomId[ConnectionIO]
} yield RQueryBookmark(
id,
name,
label,
if (personal) userId.some else None,
account.collective,
query,
curTime
)
def insert(r: RQueryBookmark): ConnectionIO[Int] =
DML.insert(
T,
T.all,
sql"${r.id},${r.name},${r.label},${r.userId},${r.cid},${r.query},${r.created}"
)
def update(r: RQueryBookmark): ConnectionIO[Int] =
DML.update(
T,
T.id === r.id,
DML.set(
T.name.setTo(r.name),
T.label.setTo(r.label),
T.query.setTo(r.query)
)
)
def deleteById(cid: Ident, id: Ident): ConnectionIO[Int] =
DML.delete(T, T.id === id && T.cid === cid)
def allForUser(account: AccountId): ConnectionIO[Vector[RQueryBookmark]] = {
val user = RUser.as("u")
val bm = RQueryBookmark.as("bm")
val users = Select(
user.uid.s,
from(user),
user.cid === account.collective && user.login === account.user
)
Select(
select(bm.all),
from(bm),
bm.cid === account.collective && (bm.userId.isNull || bm.userId.in(users))
).build.query[RQueryBookmark].to[Vector]
}
}

View File

@ -14,6 +14,8 @@ import docspell.store.qb._
import doobie._
import doobie.implicits._
import cats.data.OptionT
import cats.effect.Sync
case class RUser(
uid: Ident,
@ -150,6 +152,13 @@ object RUser {
.query[Ident]
.option
def getIdByAccount(account: AccountId): ConnectionIO[Ident] =
OptionT(findIdByAccount(account)).getOrElseF(
Sync[ConnectionIO].raiseError(
new Exception(s"No user found for: ${account.asString}")
)
)
def updateLogin(accountId: AccountId): ConnectionIO[Int] = {
val t = Table(None)
def stmt(now: Timestamp) =

View File

@ -124,7 +124,6 @@ module Api exposing
, restoreAllItems
, restoreItem
, sampleEvent
, saveBookmarks
, saveClientSettings
, searchShare
, searchShareStats
@ -188,6 +187,7 @@ module Api exposing
import Api.Model.AttachmentMeta exposing (AttachmentMeta)
import Api.Model.AuthResult exposing (AuthResult)
import Api.Model.BasicResult exposing (BasicResult)
import Api.Model.BookmarkedQuery exposing (BookmarkedQuery)
import Api.Model.CalEventCheck exposing (CalEventCheck)
import Api.Model.CalEventCheckResult exposing (CalEventCheckResult)
import Api.Model.Collective exposing (Collective)
@ -266,7 +266,7 @@ import Api.Model.User exposing (User)
import Api.Model.UserList exposing (UserList)
import Api.Model.UserPass exposing (UserPass)
import Api.Model.VersionInfo exposing (VersionInfo)
import Data.BookmarkedQuery exposing (AllBookmarks, BookmarkedQuery, BookmarkedQueryDef, Bookmarks)
import Data.Bookmarks exposing (AllBookmarks, Bookmarks)
import Data.ContactType exposing (ContactType)
import Data.CustomFieldOrder exposing (CustomFieldOrder)
import Data.EquipmentOrder exposing (EquipmentOrder)
@ -2295,46 +2295,29 @@ saveClientSettings flags settings receive =
--- Query Bookmarks
type alias BookmarkLocation =
Data.BookmarkedQuery.Location
bookmarkUri : Flags -> String
bookmarkUri flags =
flags.config.baseUrl ++ "/api/v1/sec/querybookmark"
bookmarkLocationUri : Flags -> BookmarkLocation -> String
bookmarkLocationUri flags loc =
case loc of
Data.BookmarkedQuery.User ->
flags.config.baseUrl ++ "/api/v1/sec/clientSettings/user/webClientBookmarks"
Data.BookmarkedQuery.Collective ->
flags.config.baseUrl ++ "/api/v1/sec/clientSettings/collective/webClientBookmarks"
getBookmarksTask : Flags -> BookmarkLocation -> Task.Task Http.Error Bookmarks
getBookmarksTask flags loc =
getBookmarksTask : Flags -> Task.Task Http.Error Bookmarks
getBookmarksTask flags =
Http2.authTask
{ method = "GET"
, url = bookmarkLocationUri flags loc
, url = bookmarkUri flags
, account = getAccount flags
, body = Http.emptyBody
, resolver = Http2.jsonResolver Data.BookmarkedQuery.bookmarksDecoder
, resolver = Http2.jsonResolver Data.Bookmarks.bookmarksDecoder
, headers = []
, timeout = Nothing
}
getBookmarksFor : Flags -> BookmarkLocation -> (Result Http.Error Bookmarks -> msg) -> Cmd msg
getBookmarksFor flags loc receive =
Task.attempt receive (getBookmarksTask flags loc)
getBookmarks : Flags -> (Result Http.Error AllBookmarks -> msg) -> Cmd msg
getBookmarks flags receive =
let
coll =
getBookmarksTask flags Data.BookmarkedQuery.Collective
user =
getBookmarksTask flags Data.BookmarkedQuery.User
bms =
getBookmarksTask flags
shares =
getSharesTask flags "" False
@ -2342,86 +2325,57 @@ getBookmarks flags receive =
activeShare s =
s.enabled && s.name /= Nothing
combine bc bu bs =
AllBookmarks bc bu (List.filter activeShare bs.items)
combine bm bs =
AllBookmarks (Data.Bookmarks.sort bm) (List.filter activeShare bs.items)
in
Task.map3 combine coll user shares
Task.map2 combine bms shares
|> Task.attempt receive
saveBookmarksTask : Flags -> BookmarkLocation -> Bookmarks -> Task.Task Http.Error BasicResult
saveBookmarksTask flags loc bookmarks =
Http2.authTask
{ method = "PUT"
, url = bookmarkLocationUri flags loc
addBookmark : Flags -> BookmarkedQuery -> (Result Http.Error BasicResult -> msg) -> Cmd msg
addBookmark flags model receive =
Http2.authPost
{ url = bookmarkUri flags
, account = getAccount flags
, body = Http.jsonBody (Data.BookmarkedQuery.bookmarksEncode bookmarks)
, resolver = Http2.jsonResolver Api.Model.BasicResult.decoder
, headers = []
, timeout = Nothing
, body = Http.jsonBody (Api.Model.BookmarkedQuery.encode model)
, expect = Http.expectJson receive Api.Model.BasicResult.decoder
}
saveBookmarks : Flags -> Bookmarks -> BookmarkLocation -> (Result Http.Error BasicResult -> msg) -> Cmd msg
saveBookmarks flags bookmarks loc receive =
Task.attempt receive (saveBookmarksTask flags loc bookmarks)
updateBookmark : Flags -> BookmarkedQuery -> (Result Http.Error BasicResult -> msg) -> Cmd msg
updateBookmark flags model receive =
Http2.authPut
{ url = bookmarkUri flags
, account = getAccount flags
, body = Http.jsonBody (Api.Model.BookmarkedQuery.encode model)
, expect = Http.expectJson receive Api.Model.BasicResult.decoder
}
addBookmark : Flags -> BookmarkedQueryDef -> (Result Http.Error BasicResult -> msg) -> Cmd msg
addBookmark flags model receive =
bookmarkNameExistsTask : Flags -> String -> Task.Task Http.Error Bool
bookmarkNameExistsTask flags name =
let
load =
getBookmarksTask flags model.location
add current =
Data.BookmarkedQuery.add model.query current
|> saveBookmarksTask flags model.location
in
Task.andThen add load |> Task.attempt receive
updateBookmark : Flags -> String -> BookmarkedQueryDef -> (Result Http.Error BasicResult -> msg) -> Cmd msg
updateBookmark flags oldName model receive =
let
load =
getBookmarksTask flags model.location
add current =
Data.BookmarkedQuery.remove oldName current
|> Data.BookmarkedQuery.add model.query
|> saveBookmarksTask flags model.location
in
Task.andThen add load |> Task.attempt receive
bookmarkNameExistsTask : Flags -> BookmarkLocation -> String -> Task.Task Http.Error Bool
bookmarkNameExistsTask flags loc name =
let
load =
getBookmarksTask flags loc
getBookmarksTask flags
exists current =
Data.BookmarkedQuery.exists name current
Data.Bookmarks.exists name current
in
Task.map exists load
bookmarkNameExists : Flags -> BookmarkLocation -> String -> (Result Http.Error Bool -> msg) -> Cmd msg
bookmarkNameExists flags loc name receive =
bookmarkNameExistsTask flags loc name |> Task.attempt receive
bookmarkNameExists : Flags -> String -> (Result Http.Error Bool -> msg) -> Cmd msg
bookmarkNameExists flags name receive =
bookmarkNameExistsTask flags name |> Task.attempt receive
deleteBookmark : Flags -> BookmarkLocation -> String -> (Result Http.Error BasicResult -> msg) -> Cmd msg
deleteBookmark flags loc name receive =
let
load =
getBookmarksTask flags loc
remove current =
Data.BookmarkedQuery.remove name current
|> saveBookmarksTask flags loc
in
Task.andThen remove load |> Task.attempt receive
deleteBookmark : Flags -> String -> (Result Http.Error BasicResult -> msg) -> Cmd msg
deleteBookmark flags id receive =
Http2.authDelete
{ url = bookmarkUri flags ++ "/" ++ id
, account = getAccount flags
, expect = Http.expectJson receive Api.Model.BasicResult.decoder
}

View File

@ -11,8 +11,9 @@ module Comp.BookmarkChooser exposing
, view
)
import Api.Model.BookmarkedQuery exposing (BookmarkedQuery)
import Api.Model.ShareDetail exposing (ShareDetail)
import Data.BookmarkedQuery exposing (AllBookmarks, BookmarkedQuery)
import Data.Bookmarks exposing (AllBookmarks)
import Data.Icons as Icons
import Html exposing (Html, a, div, i, label, span, text)
import Html.Attributes exposing (class, classList, href)
@ -34,19 +35,18 @@ init all =
isEmpty : Model -> Bool
isEmpty model =
model.all == Data.BookmarkedQuery.allBookmarksEmpty
model.all == Data.Bookmarks.empty
type alias Selection =
{ user : Set String
, collective : Set String
{ bookmarks : Set String
, shares : Set String
}
emptySelection : Selection
emptySelection =
{ user = Set.empty, collective = Set.empty, shares = Set.empty }
{ bookmarks = Set.empty, shares = Set.empty }
isEmptySelection : Selection -> Bool
@ -55,8 +55,7 @@ isEmptySelection sel =
type Kind
= User
| Collective
= Bookmark
| Share
@ -68,14 +67,13 @@ getQueries : Model -> Selection -> List BookmarkedQuery
getQueries model sel =
let
member set bm =
Set.member bm.name set
Set.member bm.id set
filterBookmarks f bms =
Data.BookmarkedQuery.filter f bms |> Data.BookmarkedQuery.map identity
List.filter f bms |> List.map identity
in
List.concat
[ filterBookmarks (member sel.user) model.all.user
, filterBookmarks (member sel.collective) model.all.collective
[ filterBookmarks (member sel.bookmarks) model.all.bookmarks
, List.map shareToBookmark model.all.shares
|> List.filter (member sel.shares)
]
@ -96,16 +94,13 @@ update msg model current =
Set.insert name set
in
case msg of
Toggle kind name ->
Toggle kind id ->
case kind of
User ->
( model, { current | user = toggle name current.user } )
Collective ->
( model, { current | collective = toggle name current.collective } )
Bookmark ->
( model, { current | bookmarks = toggle id current.bookmarks } )
Share ->
( model, { current | shares = toggle name current.shares } )
( model, { current | shares = toggle id current.shares } )
@ -114,9 +109,13 @@ update msg model current =
view : Texts -> Model -> Selection -> Html Msg
view texts model selection =
let
( user, coll ) =
List.partition .personal model.all.bookmarks
in
div [ class "flex flex-col" ]
[ userBookmarks texts model selection
, collBookmarks texts model selection
[ userBookmarks texts user selection
, collBookmarks texts coll selection
, shares texts model selection
]
@ -130,27 +129,27 @@ titleDiv label =
]
userBookmarks : Texts -> Model -> Selection -> Html Msg
userBookmarks : Texts -> List BookmarkedQuery -> Selection -> Html Msg
userBookmarks texts model sel =
div
[ class "mb-2"
, classList [ ( "hidden", Data.BookmarkedQuery.emptyBookmarks == model.all.user ) ]
, classList [ ( "hidden", model == [] ) ]
]
[ titleDiv texts.userLabel
, div [ class "flex flex-col space-y-2 md:space-y-1" ]
(Data.BookmarkedQuery.map (mkItem "fa fa-bookmark" sel User) model.all.user)
(List.map (mkItem "fa fa-bookmark" sel Bookmark) model)
]
collBookmarks : Texts -> Model -> Selection -> Html Msg
collBookmarks : Texts -> List BookmarkedQuery -> Selection -> Html Msg
collBookmarks texts model sel =
div
[ class "mb-2"
, classList [ ( "hidden", Data.BookmarkedQuery.emptyBookmarks == model.all.collective ) ]
, classList [ ( "hidden", [] == model ) ]
]
[ titleDiv texts.collectiveLabel
, div [ class "flex flex-col space-y-2 md:space-y-1" ]
(Data.BookmarkedQuery.map (mkItem "fa fa-bookmark font-light" sel Collective) model.all.collective)
(List.map (mkItem "fa fa-bookmark font-light" sel Bookmark) model)
]
@ -175,9 +174,9 @@ mkItem icon sel kind bm =
a
[ class "flex flex-row items-center rounded px-1 py-1 hover:bg-blue-100 dark:hover:bg-slate-600"
, href "#"
, onClick (Toggle kind bm.name)
, onClick (Toggle kind bm.id)
]
[ if isSelected sel kind bm.name then
[ if isSelected sel kind bm.id then
i [ class "fa fa-check" ] []
else
@ -187,14 +186,11 @@ mkItem icon sel kind bm =
isSelected : Selection -> Kind -> String -> Bool
isSelected sel kind name =
Set.member name <|
isSelected sel kind id =
Set.member id <|
case kind of
User ->
sel.user
Collective ->
sel.collective
Bookmark ->
sel.bookmarks
Share ->
sel.shares
@ -202,4 +198,4 @@ isSelected sel kind name =
shareToBookmark : ShareDetail -> BookmarkedQuery
shareToBookmark share =
BookmarkedQuery (Maybe.withDefault "-" share.name) share.query
BookmarkedQuery share.id (Maybe.withDefault "-" share.name) share.name share.query False 0

View File

@ -14,7 +14,7 @@ import Comp.BookmarkQueryForm
import Comp.BookmarkTable
import Comp.ItemDetail.Model exposing (Msg(..))
import Comp.MenuBar as MB
import Data.BookmarkedQuery exposing (AllBookmarks)
import Data.Bookmarks exposing (AllBookmarks)
import Data.Flags exposing (Flags)
import Data.UiSettings exposing (UiSettings)
import Html exposing (..)
@ -43,16 +43,10 @@ type DeleteConfirm
| DeleteConfirmOn
type alias FormData =
{ model : Comp.BookmarkQueryForm.Model
, oldName : Maybe String
}
type alias Model =
{ viewMode : ViewMode
, bookmarks : AllBookmarks
, formData : FormData
, formModel : Comp.BookmarkQueryForm.Model
, loading : Bool
, formError : FormError
, deleteConfirm : DeleteConfirm
@ -66,11 +60,8 @@ init flags =
Comp.BookmarkQueryForm.init
in
( { viewMode = Table
, bookmarks = Data.BookmarkedQuery.allBookmarksEmpty
, formData =
{ model = fm
, oldName = Nothing
}
, bookmarks = Data.Bookmarks.empty
, formModel = fm
, loading = False
, formError = FormErrorNone
, deleteConfirm = DeleteConfirmOff
@ -84,14 +75,14 @@ init flags =
type Msg
= LoadBookmarks
| TableMsg Data.BookmarkedQuery.Location Comp.BookmarkTable.Msg
| TableMsg Comp.BookmarkTable.Msg
| FormMsg Comp.BookmarkQueryForm.Msg
| InitNewBookmark
| SetViewMode ViewMode
| Submit
| RequestDelete
| CancelDelete
| DeleteBookmarkNow Data.BookmarkedQuery.Location String
| DeleteBookmarkNow String
| LoadBookmarksResp (Result Http.Error AllBookmarks)
| AddBookmarkResp (Result Http.Error BasicResult)
| UpdateBookmarkResp (Result Http.Error BasicResult)
@ -119,8 +110,7 @@ update flags msg model =
{ model
| viewMode = Form
, formError = FormErrorNone
, formData =
{ model = bm, oldName = Nothing }
, formModel = bm
}
in
( nm, Cmd.map FormMsg bc, Sub.none )
@ -138,14 +128,14 @@ update flags msg model =
FormMsg lm ->
let
( fm, fc, fs ) =
Comp.BookmarkQueryForm.update flags lm model.formData.model
Comp.BookmarkQueryForm.update flags lm model.formModel
in
( { model | formData = { model = fm, oldName = model.formData.oldName }, formError = FormErrorNone }
( { model | formModel = fm, formError = FormErrorNone }
, Cmd.map FormMsg fc
, Sub.map FormMsg fs
)
TableMsg loc lm ->
TableMsg lm ->
let
action =
Comp.BookmarkTable.update lm
@ -154,15 +144,12 @@ update flags msg model =
Comp.BookmarkTable.Edit bookmark ->
let
( bm, bc ) =
Comp.BookmarkQueryForm.initWith
{ query = bookmark
, location = loc
}
Comp.BookmarkQueryForm.initWith bookmark
in
( { model
| viewMode = Form
, formError = FormErrorNone
, formData = { model = bm, oldName = Just bookmark.name }
, formModel = bm
}
, Cmd.map FormMsg bc
, Sub.none
@ -174,9 +161,9 @@ update flags msg model =
CancelDelete ->
( { model | deleteConfirm = DeleteConfirmOff }, Cmd.none, Sub.none )
DeleteBookmarkNow loc name ->
DeleteBookmarkNow id ->
( { model | deleteConfirm = DeleteConfirmOff, loading = True }
, Api.deleteBookmark flags loc name DeleteBookmarkResp
, Api.deleteBookmark flags id DeleteBookmarkResp
, Sub.none
)
@ -196,14 +183,13 @@ update flags msg model =
( { model | loading = False, formError = FormErrorHttp err }, Cmd.none, Sub.none )
Submit ->
case Comp.BookmarkQueryForm.get model.formData.model of
case Comp.BookmarkQueryForm.get model.formModel of
Just data ->
case model.formData.oldName of
Just prevName ->
( { model | loading = True }, Api.updateBookmark flags prevName data AddBookmarkResp, Sub.none )
if data.id /= "" then
( { model | loading = True }, Api.updateBookmark flags data AddBookmarkResp, Sub.none )
Nothing ->
( { model | loading = True }, Api.addBookmark flags data AddBookmarkResp, Sub.none )
else
( { model | loading = True }, Api.addBookmark flags data AddBookmarkResp, Sub.none )
Nothing ->
( { model | formError = FormErrorInvalid }, Cmd.none, Sub.none )
@ -254,6 +240,10 @@ view texts settings flags model =
viewTable : Texts -> Model -> Html Msg
viewTable texts model =
let
( user, coll ) =
List.partition .personal model.bookmarks.bookmarks
in
div [ class "flex flex-col" ]
[ MB.view
{ start =
@ -268,17 +258,23 @@ viewTable texts model =
]
, rootClasses = "mb-4"
}
, div [ class "flex flex-col" ]
, div
[ class "flex flex-col"
, classList [ ( "hidden", user == [] ) ]
]
[ h3 [ class S.header3 ]
[ text texts.userBookmarks ]
, Html.map (TableMsg Data.BookmarkedQuery.User)
(Comp.BookmarkTable.view texts.bookmarkTable model.bookmarks.user)
, Html.map TableMsg
(Comp.BookmarkTable.view texts.bookmarkTable user)
]
, div
[ class "flex flex-col mt-3"
, classList [ ( "hidden", coll == [] ) ]
]
, div [ class "flex flex-col mt-3" ]
[ h3 [ class S.header3 ]
[ text texts.collectiveBookmarks ]
, Html.map (TableMsg Data.BookmarkedQuery.Collective)
(Comp.BookmarkTable.view texts.bookmarkTable model.bookmarks.collective)
, Html.map TableMsg
(Comp.BookmarkTable.view texts.bookmarkTable coll)
]
, B.loadingDimmer
{ label = ""
@ -291,10 +287,10 @@ viewForm : Texts -> UiSettings -> Flags -> Model -> Html Msg
viewForm texts _ _ model =
let
newBookmark =
model.formData.oldName == Nothing
model.formModel.bookmark.id == ""
isValid =
Comp.BookmarkQueryForm.get model.formData.model /= Nothing
Comp.BookmarkQueryForm.get model.formModel /= Nothing
in
div []
[ Html.form []
@ -305,7 +301,7 @@ viewForm texts _ _ model =
else
h1 [ class S.header2 ]
[ text (Maybe.withDefault "" model.formData.model.name)
[ text (Maybe.withDefault "" model.formModel.name)
]
, MB.view
{ start =
@ -360,7 +356,7 @@ viewForm texts _ _ model =
text m
]
, div []
[ Html.map FormMsg (Comp.BookmarkQueryForm.view texts.bookmarkForm model.formData.model)
[ Html.map FormMsg (Comp.BookmarkQueryForm.view texts.bookmarkForm model.formModel)
]
, B.loadingDimmer
{ active = model.loading
@ -378,11 +374,7 @@ viewForm texts _ _ model =
{ label = texts.basics.yes
, icon = "fa fa-check"
, disabled = False
, handler =
onClick
(DeleteBookmarkNow model.formData.model.location
(Maybe.withDefault "" model.formData.model.name)
)
, handler = onClick (DeleteBookmarkNow model.formModel.bookmark.id)
, attrs = [ href "#" ]
}
, B.secondaryButton

View File

@ -8,9 +8,9 @@
module Comp.BookmarkQueryForm exposing (Model, Msg, get, init, initQuery, initWith, update, view)
import Api
import Api.Model.BookmarkedQuery exposing (BookmarkedQuery)
import Comp.Basic as B
import Comp.PowerSearchInput
import Data.BookmarkedQuery exposing (BookmarkedQueryDef, Location(..))
import Data.Flags exposing (Flags)
import Html exposing (..)
import Html.Attributes exposing (..)
@ -24,10 +24,11 @@ import Util.Maybe
type alias Model =
{ name : Maybe String
{ bookmark : BookmarkedQuery
, name : Maybe String
, nameExists : Bool
, queryModel : Comp.PowerSearchInput.Model
, location : Location
, isPersonal : Bool
, nameExistsThrottle : Throttle Msg
}
@ -40,10 +41,11 @@ initQuery q =
(Comp.PowerSearchInput.setSearchString q)
Comp.PowerSearchInput.init
in
( { name = Nothing
( { bookmark = Api.Model.BookmarkedQuery.empty
, name = Nothing
, nameExists = False
, queryModel = res.model
, location = User
, isPersonal = True
, nameExistsThrottle = Throttle.create 1
}
, Cmd.batch
@ -57,15 +59,16 @@ init =
initQuery ""
initWith : BookmarkedQueryDef -> ( Model, Cmd Msg )
initWith : BookmarkedQuery -> ( Model, Cmd Msg )
initWith bm =
let
( m, c ) =
initQuery bm.query.query
initQuery bm.query
in
( { m
| name = Just bm.query.name
, location = bm.location
| name = Just bm.name
, isPersonal = bm.personal
, bookmark = bm
}
, c
)
@ -78,19 +81,21 @@ isValid model =
/= Nothing
get : Model -> Maybe BookmarkedQueryDef
get : Model -> Maybe BookmarkedQuery
get model =
let
qStr =
Maybe.withDefault "" model.queryModel.input
bm =
model.bookmark
in
if isValid model then
Just
{ query =
{ query = qStr
{ bm
| query = qStr
, name = Maybe.withDefault "" model.name
}
, location = model.location
, personal = model.isPersonal
}
else
@ -100,7 +105,7 @@ get model =
type Msg
= SetName String
| QueryMsg Comp.PowerSearchInput.Msg
| SetLocation Location
| SetPersonal Bool
| NameExistsResp (Result Http.Error Bool)
| UpdateThrottle
@ -109,12 +114,12 @@ update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Sub Msg )
update flags msg model =
let
nameCheck1 name =
Api.bookmarkNameExists flags model.location name NameExistsResp
Api.bookmarkNameExists flags name NameExistsResp
nameCheck2 loc =
nameCheck2 =
case model.name of
Just n ->
Api.bookmarkNameExists flags loc n NameExistsResp
Api.bookmarkNameExists flags n NameExistsResp
Nothing ->
Cmd.none
@ -135,12 +140,12 @@ update flags msg model =
, throttleSub
)
SetLocation loc ->
SetPersonal flag ->
let
( newThrottle, cmd ) =
Throttle.try (nameCheck2 loc) model.nameExistsThrottle
Throttle.try nameCheck2 model.nameExistsThrottle
in
( { model | location = loc, nameExistsThrottle = newThrottle }, cmd, throttleSub )
( { model | isPersonal = flag, nameExistsThrottle = newThrottle }, cmd, throttleSub )
QueryMsg lm ->
let
@ -224,8 +229,8 @@ view texts model =
[ label [ class "inline-flex items-center" ]
[ input
[ type_ "radio"
, checked (model.location == User)
, onCheck (\_ -> SetLocation User)
, checked model.isPersonal
, onCheck (\_ -> SetPersonal True)
, class S.radioInput
]
[]
@ -235,9 +240,9 @@ view texts model =
, label [ class "inline-flex items-center" ]
[ input
[ type_ "radio"
, checked (model.location == Collective)
, checked (not model.isPersonal)
, class S.radioInput
, onCheck (\_ -> SetLocation Collective)
, onCheck (\_ -> SetPersonal False)
]
[]
, span [ class "ml-2" ] [ text texts.collectiveLocation ]

View File

@ -2,12 +2,12 @@ module Comp.BookmarkQueryManage exposing (..)
import Api
import Api.Model.BasicResult exposing (BasicResult)
import Api.Model.BookmarkedQuery exposing (BookmarkedQuery)
import Comp.Basic as B
import Comp.BookmarkQueryForm
import Data.BookmarkedQuery exposing (BookmarkedQueryDef)
import Data.Flags exposing (Flags)
import Html exposing (Html, div, text)
import Html.Attributes exposing (class, classList, href)
import Html.Attributes exposing (class, href)
import Html.Events exposing (onClick)
import Http
import Messages.Comp.BookmarkQueryManage exposing (Texts)
@ -55,7 +55,7 @@ type Msg
type FormResult
= Submitted BookmarkedQueryDef
= Submitted BookmarkedQuery
| Cancelled
| Done
| None
@ -117,7 +117,7 @@ update flags msg model =
{ empty | model = { model | loading = False, formState = FormStateError err } }
save : Flags -> BookmarkedQueryDef -> Cmd Msg
save : Flags -> BookmarkedQuery -> Cmd Msg
save flags model =
Api.addBookmark flags model SaveResp

View File

@ -12,8 +12,8 @@ module Comp.BookmarkTable exposing
, view
)
import Api.Model.BookmarkedQuery exposing (BookmarkedQuery)
import Comp.Basic as B
import Data.BookmarkedQuery exposing (BookmarkedQuery, Bookmarks)
import Html exposing (..)
import Html.Attributes exposing (..)
import Messages.Comp.BookmarkTable exposing (Texts)
@ -39,7 +39,7 @@ update msg =
--- View
view : Texts -> Bookmarks -> Html Msg
view : Texts -> List BookmarkedQuery -> Html Msg
view texts bms =
table [ class S.tableMain ]
[ thead []
@ -51,7 +51,7 @@ view texts bms =
]
]
, tbody []
(Data.BookmarkedQuery.map (renderBookmarkLine texts) bms)
(List.map (renderBookmarkLine texts) bms)
]

View File

@ -43,7 +43,7 @@ import Comp.LinkTarget exposing (LinkTarget)
import Comp.MenuBar as MB
import Comp.Tabs
import Comp.TagSelect
import Data.BookmarkedQuery exposing (AllBookmarks)
import Data.Bookmarks exposing (AllBookmarks)
import Data.CustomFieldChange exposing (CustomFieldValueCollect)
import Data.Direction exposing (Direction)
import Data.DropdownStyle as DS
@ -146,7 +146,7 @@ init flags =
, customFieldModel = Comp.CustomFieldMultiInput.initWith []
, customValues = Data.CustomFieldChange.emptyCollect
, sourceModel = Nothing
, allBookmarks = Comp.BookmarkChooser.init Data.BookmarkedQuery.allBookmarksEmpty
, allBookmarks = Comp.BookmarkChooser.init Data.Bookmarks.empty
, selectedBookmarks = Comp.BookmarkChooser.emptySelection
, openTabs = Set.fromList [ "Tags", "Inbox" ]
, searchMode = Data.SearchMode.Normal

View File

@ -1,138 +0,0 @@
module Data.BookmarkedQuery exposing
( AllBookmarks
, BookmarkedQuery
, BookmarkedQueryDef
, Bookmarks
, Location(..)
, add
, allBookmarksEmpty
, bookmarksDecoder
, bookmarksEncode
, emptyBookmarks
, exists
, filter
, map
, remove
)
import Api.Model.ShareDetail exposing (ShareDetail)
import Json.Decode as D
import Json.Encode as E
type Location
= User
| Collective
type alias BookmarkedQuery =
{ name : String
, query : String
}
bookmarkedQueryDecoder : D.Decoder BookmarkedQuery
bookmarkedQueryDecoder =
D.map2 BookmarkedQuery
(D.field "name" D.string)
(D.field "query" D.string)
bookmarkedQueryEncode : BookmarkedQuery -> E.Value
bookmarkedQueryEncode bq =
E.object
[ ( "name", E.string bq.name )
, ( "query", E.string bq.query )
]
type alias BookmarkedQueryDef =
{ query : BookmarkedQuery
, location : Location
}
type Bookmarks
= Bookmarks (List BookmarkedQuery)
map : (BookmarkedQuery -> a) -> Bookmarks -> List a
map f bms =
case bms of
Bookmarks items ->
List.map f items
filter : (BookmarkedQuery -> Bool) -> Bookmarks -> Bookmarks
filter f bms =
case bms of
Bookmarks items ->
Bookmarks <| List.filter f items
emptyBookmarks : Bookmarks
emptyBookmarks =
Bookmarks []
type alias AllBookmarks =
{ collective : Bookmarks
, user : Bookmarks
, shares : List ShareDetail
}
allBookmarksEmpty : AllBookmarks
allBookmarksEmpty =
AllBookmarks emptyBookmarks emptyBookmarks []
{-| Checks wether a bookmark of this name already exists.
-}
exists : String -> Bookmarks -> Bool
exists name bookmarks =
case bookmarks of
Bookmarks list ->
List.any (\b -> b.name == name) list
remove : String -> Bookmarks -> Bookmarks
remove name bookmarks =
case bookmarks of
Bookmarks list ->
Bookmarks <| List.filter (\b -> b.name /= name) list
sortByName : Bookmarks -> Bookmarks
sortByName bm =
case bm of
Bookmarks all ->
Bookmarks <| List.sortBy .name all
add : BookmarkedQuery -> Bookmarks -> Bookmarks
add query bookmarks =
case remove query.name bookmarks of
Bookmarks all ->
sortByName (Bookmarks (query :: all))
bookmarksDecoder : D.Decoder Bookmarks
bookmarksDecoder =
D.maybe
(D.field "bookmarks"
(D.list bookmarkedQueryDecoder
|> D.map Bookmarks
|> D.map sortByName
)
)
|> D.map (Maybe.withDefault emptyBookmarks)
bookmarksEncode : Bookmarks -> E.Value
bookmarksEncode bookmarks =
case bookmarks of
Bookmarks all ->
E.object
[ ( "bookmarks", E.list bookmarkedQueryEncode all )
]

View File

@ -0,0 +1,48 @@
module Data.Bookmarks exposing
( AllBookmarks
, Bookmarks
, bookmarksDecoder
, empty
, exists
, sort
)
import Api.Model.BookmarkedQuery exposing (BookmarkedQuery)
import Api.Model.ShareDetail exposing (ShareDetail)
import Json.Decode as D
type alias AllBookmarks =
{ bookmarks : List BookmarkedQuery
, shares : List ShareDetail
}
empty : AllBookmarks
empty =
AllBookmarks [] []
type alias Bookmarks =
List BookmarkedQuery
{-| Checks wether a bookmark of this name already exists.
-}
exists : String -> Bookmarks -> Bool
exists name bookmarks =
List.any (\b -> b.name == name) bookmarks
sort : Bookmarks -> Bookmarks
sort bms =
let
labelName b =
Maybe.withDefault b.name b.label
in
List.sortBy labelName bms
bookmarksDecoder : D.Decoder Bookmarks
bookmarksDecoder =
D.list Api.Model.BookmarkedQuery.decoder