mirror of
https://github.com/mattermost/focalboard.git
synced 2024-11-24 08:22:29 +02:00
Card APIs (#3760)
* cards apis wip * create card API * validate cards when creating * create card fixes * patch card wip * wip * unit test for createCard; CardPatch2BlockPatch * unit test for PatchCard * more APIs * unit tests for GetCardByID * register GetCard API * Set FOCALBOARD_UNIT_TESTING for integration tests * integration tests for CreateCard * more integration tests for CreateCard * integtration tests for PatchCard * fix integration tests for PatchCard * integration tests for GetCard * GetCards API wip * fix merge conflict * GetCards API and unit tests * fix linter issues * fix flaky unit test for mySQL * Update server/api/api.go Co-authored-by: Miguel de la Cruz <mgdelacroix@gmail.com> * Update server/api/api.go Co-authored-by: Miguel de la Cruz <mgdelacroix@gmail.com> * address review comments Co-authored-by: Mattermod <mattermod@users.noreply.github.com> Co-authored-by: Miguel de la Cruz <mgdelacroix@gmail.com>
This commit is contained in:
parent
7ebcdf59c2
commit
4652a15bab
@ -4,8 +4,11 @@ go 1.18
|
||||
|
||||
require (
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/mattermost/focalboard/server v0.0.0-20220818150333-feb49eaf197a
|
||||
github.com/mattermost/mattermost-plugin-api v0.0.29-0.20220801143717-73008cfda2fb
|
||||
github.com/stretchr/testify v1.7.2
|
||||
github.com/mattermost/mattermost-server/v6 v6.0.0-20220802151854-f07c31c5d933
|
||||
github.com/stretchr/testify v1.8.0
|
||||
)
|
||||
|
||||
require (
|
||||
@ -69,7 +72,6 @@ require (
|
||||
github.com/gopherjs/gopherjs v1.17.2 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/gorilla/handlers v1.5.1 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/graph-gophers/graphql-go v1.4.0 // indirect
|
||||
github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c // indirect
|
||||
@ -96,7 +98,6 @@ require (
|
||||
github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect
|
||||
github.com/mattermost/ldap v0.0.0-20201202150706-ee0e6284187d // indirect
|
||||
github.com/mattermost/logr/v2 v2.0.15 // indirect
|
||||
github.com/mattermost/mattermost-server/v6 v6.0.0-20220802151854-f07c31c5d933 // indirect
|
||||
github.com/mattermost/morph v0.0.0-20220401091636-39f834798da8 // indirect
|
||||
github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0 // indirect
|
||||
github.com/mattermost/squirrel v0.2.0 // indirect
|
||||
@ -136,7 +137,7 @@ require (
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||
github.com/richardlehane/mscfb v1.0.4 // indirect
|
||||
github.com/richardlehane/msoleps v1.0.3 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/rivo/uniseg v0.3.4 // indirect
|
||||
github.com/rs/cors v1.8.2 // indirect
|
||||
github.com/rs/xid v1.4.0 // indirect
|
||||
github.com/rudderlabs/analytics-go v3.3.2+incompatible // indirect
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -77,6 +77,13 @@ func (a *API) RegisterRoutes(r *mux.Router) {
|
||||
apiv2.Use(a.panicHandler)
|
||||
apiv2.Use(a.requireCSRFToken)
|
||||
|
||||
/* ToDo:
|
||||
apiv3 := r.PathPrefix("/api/v3").Subrouter()
|
||||
apiv3.Use(a.panicHandler)
|
||||
apiv3.Use(a.requireCSRFToken)
|
||||
*/
|
||||
|
||||
// V2 routes (ToDo: migrate these to V3 when ready to ship V3)
|
||||
a.registerUsersRoutes(apiv2)
|
||||
a.registerAuthRoutes(apiv2)
|
||||
a.registerMembersRoutes(apiv2)
|
||||
@ -97,6 +104,9 @@ func (a *API) RegisterRoutes(r *mux.Router) {
|
||||
a.registerBoardsRoutes(apiv2)
|
||||
a.registerBlocksRoutes(apiv2)
|
||||
|
||||
// V3 routes
|
||||
a.registerCardsRoutes(apiv2)
|
||||
|
||||
// System routes are outside the /api/v2 path
|
||||
a.registerSystemRoutes(r)
|
||||
}
|
||||
@ -192,6 +202,11 @@ func (a *API) errorResponse(w http.ResponseWriter, api string, code int, message
|
||||
}
|
||||
|
||||
setResponseHeader(w, "Content-Type", "application/json")
|
||||
|
||||
if sourceError != nil && message != sourceError.Error() {
|
||||
message += "; " + sourceError.Error()
|
||||
}
|
||||
|
||||
data, err := json.Marshal(model.ErrorResponse{Error: message, ErrorCode: code})
|
||||
if err != nil {
|
||||
data = []byte("{}")
|
||||
|
@ -596,7 +596,7 @@ func (a *API) handlePatchBlock(w http.ResponseWriter, r *http.Request) {
|
||||
auditRec.AddMeta("boardID", boardID)
|
||||
auditRec.AddMeta("blockID", blockID)
|
||||
|
||||
err = a.app.PatchBlockAndNotify(blockID, patch, userID, disableNotify)
|
||||
_, err = a.app.PatchBlockAndNotify(blockID, patch, userID, disableNotify)
|
||||
if errors.Is(err, app.ErrPatchUpdatesLimitedCards) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", err)
|
||||
return
|
||||
|
384
server/api/cards.go
Normal file
384
server/api/cards.go
Normal file
@ -0,0 +1,384 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/services/audit"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultPage = "0"
|
||||
defaultPerPage = "100"
|
||||
)
|
||||
|
||||
func (a *API) registerCardsRoutes(r *mux.Router) {
|
||||
// Cards APIs
|
||||
r.HandleFunc("/boards/{boardID}/cards", a.sessionRequired(a.handleCreateCard)).Methods("POST")
|
||||
r.HandleFunc("/boards/{boardID}/cards", a.sessionRequired(a.handleGetCards)).Methods("GET")
|
||||
r.HandleFunc("/cards/{cardID}", a.sessionRequired(a.handlePatchCard)).Methods("PATCH")
|
||||
r.HandleFunc("/cards/{cardID}", a.sessionRequired(a.handleGetCard)).Methods("GET")
|
||||
}
|
||||
|
||||
func (a *API) handleCreateCard(w http.ResponseWriter, r *http.Request) {
|
||||
// swagger:operation POST /boards/{boardID}/cards createCard
|
||||
//
|
||||
// Creates a new card for the specified board.
|
||||
//
|
||||
// ---
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: boardID
|
||||
// in: path
|
||||
// description: Board ID
|
||||
// required: true
|
||||
// type: string
|
||||
// - name: Body
|
||||
// in: body
|
||||
// description: the card to create
|
||||
// required: true
|
||||
// schema:
|
||||
// "$ref": "#/definitions/Card"
|
||||
// - name: disable_notify
|
||||
// in: query
|
||||
// description: Disables notifications (for bulk data inserting)
|
||||
// required: false
|
||||
// type: bool
|
||||
// security:
|
||||
// - BearerAuth: []
|
||||
// responses:
|
||||
// '200':
|
||||
// description: success
|
||||
// schema:
|
||||
// $ref: '#/definitions/Card'
|
||||
// default:
|
||||
// description: internal error
|
||||
// schema:
|
||||
// "$ref": "#/definitions/ErrorResponse"
|
||||
|
||||
userID := getUserID(r)
|
||||
boardID := mux.Vars(r)["boardID"]
|
||||
|
||||
val := r.URL.Query().Get("disable_notify")
|
||||
disableNotify := val == True
|
||||
|
||||
requestBody, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "invalid request body", err)
|
||||
return
|
||||
}
|
||||
|
||||
var newCard *model.Card
|
||||
if err = json.Unmarshal(requestBody, &newCard); err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create card"})
|
||||
return
|
||||
}
|
||||
|
||||
if newCard.BoardID != "" && newCard.BoardID != boardID {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", model.ErrBoardIDMismatch)
|
||||
return
|
||||
}
|
||||
|
||||
newCard.PopulateWithBoardID(boardID)
|
||||
if err = newCard.CheckValid(); err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := a.makeAuditRecord(r, "createCard", audit.Fail)
|
||||
defer a.audit.LogRecord(audit.LevelModify, auditRec)
|
||||
auditRec.AddMeta("boardID", boardID)
|
||||
|
||||
// create card
|
||||
card, err := a.app.CreateCard(newCard, boardID, userID, disableNotify)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
|
||||
a.logger.Debug("CreateCard",
|
||||
mlog.String("boardID", boardID),
|
||||
mlog.String("cardID", card.ID),
|
||||
mlog.String("userID", userID),
|
||||
)
|
||||
|
||||
data, err := json.Marshal(card)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
|
||||
// response
|
||||
jsonBytesResponse(w, http.StatusOK, data)
|
||||
|
||||
auditRec.Success()
|
||||
}
|
||||
|
||||
func (a *API) handleGetCards(w http.ResponseWriter, r *http.Request) {
|
||||
// swagger:operation GET /boards/{boardID}/cards
|
||||
//
|
||||
// Fetches cards for the specified board.
|
||||
//
|
||||
// ---
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: boardID
|
||||
// in: path
|
||||
// description: Board ID
|
||||
// required: true
|
||||
// type: string
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: The page to select (default=0)
|
||||
// required: false
|
||||
// type: integer
|
||||
// - name: per_page
|
||||
// in: query
|
||||
// description: Number of cards to return per page(default=100)
|
||||
// required: false
|
||||
// type: integer
|
||||
// security:
|
||||
// - BearerAuth: []
|
||||
// responses:
|
||||
// '200':
|
||||
// description: success
|
||||
// schema:
|
||||
// type: array
|
||||
// items:
|
||||
// "$ref": "#/definitions/Card"
|
||||
// default:
|
||||
// description: internal error
|
||||
// schema:
|
||||
// "$ref": "#/definitions/ErrorResponse"
|
||||
userID := getUserID(r)
|
||||
boardID := mux.Vars(r)["boardID"]
|
||||
|
||||
query := r.URL.Query()
|
||||
strPage := query.Get("page")
|
||||
strPerPage := query.Get("per_page")
|
||||
|
||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to fetch cards"})
|
||||
return
|
||||
}
|
||||
|
||||
if strPage == "" {
|
||||
strPage = defaultPage
|
||||
}
|
||||
if strPerPage == "" {
|
||||
strPerPage = defaultPerPage
|
||||
}
|
||||
|
||||
page, err := strconv.Atoi(strPage)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "invalid `page` parameter", err)
|
||||
}
|
||||
|
||||
perPage, err := strconv.Atoi(strPerPage)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "invalid `per_page` parameter", err)
|
||||
}
|
||||
|
||||
auditRec := a.makeAuditRecord(r, "getCards", audit.Fail)
|
||||
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
||||
auditRec.AddMeta("boardID", boardID)
|
||||
auditRec.AddMeta("page", page)
|
||||
auditRec.AddMeta("per_page", perPage)
|
||||
|
||||
cards, err := a.app.GetCardsForBoard(boardID, page, perPage)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
|
||||
a.logger.Debug("GetCards",
|
||||
mlog.String("boardID", boardID),
|
||||
mlog.String("userID", userID),
|
||||
mlog.Int("page", page),
|
||||
mlog.Int("per_page", perPage),
|
||||
mlog.Int("count", len(cards)),
|
||||
)
|
||||
|
||||
data, err := json.Marshal(cards)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
|
||||
// response
|
||||
jsonBytesResponse(w, http.StatusOK, data)
|
||||
|
||||
auditRec.Success()
|
||||
}
|
||||
|
||||
func (a *API) handlePatchCard(w http.ResponseWriter, r *http.Request) {
|
||||
// swagger:operation PATCH /cards/{cardID}/cards patchCard
|
||||
//
|
||||
// Patches the specified card.
|
||||
//
|
||||
// ---
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: cardID
|
||||
// in: path
|
||||
// description: Card ID
|
||||
// required: true
|
||||
// type: string
|
||||
// - name: Body
|
||||
// in: body
|
||||
// description: the card patch
|
||||
// required: true
|
||||
// schema:
|
||||
// "$ref": "#/definitions/CardPatch"
|
||||
// - name: disable_notify
|
||||
// in: query
|
||||
// description: Disables notifications (for bulk data patching)
|
||||
// required: false
|
||||
// type: bool
|
||||
// security:
|
||||
// - BearerAuth: []
|
||||
// responses:
|
||||
// '200':
|
||||
// description: success
|
||||
// schema:
|
||||
// $ref: '#/definitions/Card'
|
||||
// default:
|
||||
// description: internal error
|
||||
// schema:
|
||||
// "$ref": "#/definitions/ErrorResponse"
|
||||
|
||||
userID := getUserID(r)
|
||||
cardID := mux.Vars(r)["cardID"]
|
||||
|
||||
val := r.URL.Query().Get("disable_notify")
|
||||
disableNotify := val == True
|
||||
|
||||
requestBody, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
|
||||
card, err := a.app.GetCardByID(cardID)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "could not fetch card "+cardID, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !a.permissions.HasPermissionToBoard(userID, card.BoardID, model.PermissionManageBoardCards) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to patch card"})
|
||||
return
|
||||
}
|
||||
|
||||
var patch *model.CardPatch
|
||||
if err = json.Unmarshal(requestBody, &patch); err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := a.makeAuditRecord(r, "patchCard", audit.Fail)
|
||||
defer a.audit.LogRecord(audit.LevelModify, auditRec)
|
||||
auditRec.AddMeta("boardID", card.BoardID)
|
||||
auditRec.AddMeta("cardID", card.ID)
|
||||
|
||||
// patch card
|
||||
cardPatched, err := a.app.PatchCard(patch, card.ID, userID, disableNotify)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
|
||||
a.logger.Debug("PatchCard",
|
||||
mlog.String("boardID", cardPatched.BoardID),
|
||||
mlog.String("cardID", cardPatched.ID),
|
||||
mlog.String("userID", userID),
|
||||
)
|
||||
|
||||
data, err := json.Marshal(cardPatched)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
|
||||
// response
|
||||
jsonBytesResponse(w, http.StatusOK, data)
|
||||
|
||||
auditRec.Success()
|
||||
}
|
||||
|
||||
func (a *API) handleGetCard(w http.ResponseWriter, r *http.Request) {
|
||||
// swagger:operation GET /cards/{cardID}
|
||||
//
|
||||
// Fetches the specified card.
|
||||
//
|
||||
// ---
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: cardID
|
||||
// in: path
|
||||
// description: Card ID
|
||||
// required: true
|
||||
// type: string
|
||||
// security:
|
||||
// - BearerAuth: []
|
||||
// responses:
|
||||
// '200':
|
||||
// description: success
|
||||
// schema:
|
||||
// $ref: '#/definitions/Card'
|
||||
// default:
|
||||
// description: internal error
|
||||
// schema:
|
||||
// "$ref": "#/definitions/ErrorResponse"
|
||||
|
||||
userID := getUserID(r)
|
||||
cardID := mux.Vars(r)["cardID"]
|
||||
|
||||
card, err := a.app.GetCardByID(cardID)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "could not fetch card "+cardID, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !a.permissions.HasPermissionToBoard(userID, card.BoardID, model.PermissionManageBoardCards) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to fetch card"})
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := a.makeAuditRecord(r, "getCard", audit.Fail)
|
||||
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
||||
auditRec.AddMeta("boardID", card.BoardID)
|
||||
auditRec.AddMeta("cardID", card.ID)
|
||||
|
||||
a.logger.Debug("GetCard",
|
||||
mlog.String("boardID", card.BoardID),
|
||||
mlog.String("cardID", card.ID),
|
||||
mlog.String("userID", userID),
|
||||
)
|
||||
|
||||
data, err := json.Marshal(card)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
|
||||
// response
|
||||
jsonBytesResponse(w, http.StatusOK, data)
|
||||
|
||||
auditRec.Success()
|
||||
}
|
@ -65,40 +65,40 @@ func (a *App) DuplicateBlock(boardID string, blockID string, userID string, asTe
|
||||
return blocks, err
|
||||
}
|
||||
|
||||
func (a *App) PatchBlock(blockID string, blockPatch *model.BlockPatch, modifiedByID string) error {
|
||||
func (a *App) PatchBlock(blockID string, blockPatch *model.BlockPatch, modifiedByID string) (*model.Block, error) {
|
||||
return a.PatchBlockAndNotify(blockID, blockPatch, modifiedByID, false)
|
||||
}
|
||||
|
||||
func (a *App) PatchBlockAndNotify(blockID string, blockPatch *model.BlockPatch, modifiedByID string, disableNotify bool) error {
|
||||
func (a *App) PatchBlockAndNotify(blockID string, blockPatch *model.BlockPatch, modifiedByID string, disableNotify bool) (*model.Block, error) {
|
||||
oldBlock, err := a.store.GetBlock(blockID)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if a.IsCloudLimited() {
|
||||
containsLimitedBlocks, lErr := a.ContainsLimitedBlocks([]model.Block{*oldBlock})
|
||||
if lErr != nil {
|
||||
return lErr
|
||||
return nil, lErr
|
||||
}
|
||||
if containsLimitedBlocks {
|
||||
return ErrPatchUpdatesLimitedCards
|
||||
return nil, ErrPatchUpdatesLimitedCards
|
||||
}
|
||||
}
|
||||
|
||||
board, err := a.store.GetBoard(oldBlock.BoardID)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = a.store.PatchBlock(blockID, blockPatch, modifiedByID)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a.metrics.IncrementBlocksPatched(1)
|
||||
block, err := a.store.GetBlock(blockID)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
a.blockChangeNotifier.Enqueue(func() error {
|
||||
// broadcast on websocket
|
||||
@ -113,7 +113,7 @@ func (a *App) PatchBlockAndNotify(blockID string, blockPatch *model.BlockPatch,
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
return block, nil
|
||||
}
|
||||
|
||||
func (a *App) PatchBlocks(teamID string, blockPatches *model.BlockPatchBatch, modifiedByID string) error {
|
||||
|
96
server/app/cards.go
Normal file
96
server/app/cards.go
Normal file
@ -0,0 +1,96 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
)
|
||||
|
||||
func (a *App) CreateCard(card *model.Card, boardID string, userID string, disableNotify bool) (*model.Card, error) {
|
||||
// Convert the card struct to a block and insert the block.
|
||||
now := utils.GetMillis()
|
||||
|
||||
card.ID = utils.NewID(utils.IDTypeCard)
|
||||
card.BoardID = boardID
|
||||
card.CreatedBy = userID
|
||||
card.ModifiedBy = userID
|
||||
card.CreateAt = now
|
||||
card.UpdateAt = now
|
||||
card.DeleteAt = 0
|
||||
|
||||
block := model.Card2Block(card)
|
||||
|
||||
newBlocks, err := a.InsertBlocksAndNotify([]model.Block{*block}, userID, disableNotify)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create card: %w", err)
|
||||
}
|
||||
|
||||
newCard, err := model.Block2Card(&newBlocks[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newCard, nil
|
||||
}
|
||||
|
||||
func (a *App) GetCardsForBoard(boardID string, page int, perPage int) ([]*model.Card, error) {
|
||||
opts := model.QueryBlocksOptions{
|
||||
BoardID: boardID,
|
||||
BlockType: model.TypeCard,
|
||||
Page: page,
|
||||
PerPage: perPage,
|
||||
}
|
||||
|
||||
blocks, err := a.store.GetBlocks(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cards := make([]*model.Card, 0, len(blocks))
|
||||
for _, blk := range blocks {
|
||||
b := blk
|
||||
if card, err := model.Block2Card(&b); err != nil {
|
||||
return nil, fmt.Errorf("Block2Card fail: %w", err)
|
||||
} else {
|
||||
cards = append(cards, card)
|
||||
}
|
||||
}
|
||||
return cards, nil
|
||||
}
|
||||
|
||||
func (a *App) PatchCard(cardPatch *model.CardPatch, cardID string, userID string, disableNotify bool) (*model.Card, error) {
|
||||
blockPatch, err := model.CardPatch2BlockPatch(cardPatch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newBlock, err := a.PatchBlockAndNotify(cardID, blockPatch, userID, disableNotify)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot patch card %s: %w", cardID, err)
|
||||
}
|
||||
|
||||
newCard, err := model.Block2Card(newBlock)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newCard, nil
|
||||
}
|
||||
|
||||
func (a *App) GetCardByID(cardID string) (*model.Card, error) {
|
||||
cardBlock, err := a.GetBlockByID(cardID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
card, err := model.Block2Card(cardBlock)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return card, nil
|
||||
}
|
266
server/app/cards_test.go
Normal file
266
server/app/cards_test.go
Normal file
@ -0,0 +1,266 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCreateCard(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
board := &model.Board{
|
||||
ID: utils.NewID(utils.IDTypeBoard),
|
||||
}
|
||||
userID := utils.NewID(utils.IDTypeUser)
|
||||
|
||||
props := makeProps(3)
|
||||
|
||||
card := &model.Card{
|
||||
BoardID: board.ID,
|
||||
CreatedBy: userID,
|
||||
ModifiedBy: userID,
|
||||
Title: "test card",
|
||||
ContentOrder: []string{utils.NewID(utils.IDTypeBlock), utils.NewID(utils.IDTypeBlock)},
|
||||
Properties: props,
|
||||
}
|
||||
block := model.Card2Block(card)
|
||||
|
||||
t.Run("success scenario", func(t *testing.T) {
|
||||
th.Store.EXPECT().GetBoard(board.ID).Return(board, nil)
|
||||
th.Store.EXPECT().InsertBlock(gomock.AssignableToTypeOf(reflect.TypeOf(block)), userID).Return(nil)
|
||||
th.Store.EXPECT().GetMembersForBoard(board.ID).Return([]*model.BoardMember{}, nil)
|
||||
|
||||
newCard, err := th.App.CreateCard(card, board.ID, userID, false)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, card.BoardID, newCard.BoardID)
|
||||
require.Equal(t, card.Title, newCard.Title)
|
||||
require.Equal(t, card.ContentOrder, newCard.ContentOrder)
|
||||
require.EqualValues(t, card.Properties, newCard.Properties)
|
||||
})
|
||||
|
||||
t.Run("error scenario", func(t *testing.T) {
|
||||
th.Store.EXPECT().GetBoard(board.ID).Return(board, nil)
|
||||
th.Store.EXPECT().InsertBlock(gomock.AssignableToTypeOf(reflect.TypeOf(block)), userID).Return(blockError{"error"})
|
||||
|
||||
newCard, err := th.App.CreateCard(card, board.ID, userID, false)
|
||||
|
||||
require.Error(t, err, "error")
|
||||
require.Nil(t, newCard)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetCards(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
board := &model.Board{
|
||||
ID: utils.NewID(utils.IDTypeBoard),
|
||||
}
|
||||
|
||||
const cardCount = 25
|
||||
|
||||
// make some cards
|
||||
blocks := make([]model.Block, 0, cardCount)
|
||||
for i := 0; i < cardCount; i++ {
|
||||
card := model.Block{
|
||||
ID: utils.NewID(utils.IDTypeBlock),
|
||||
ParentID: board.ID,
|
||||
Schema: 1,
|
||||
Type: model.TypeCard,
|
||||
Title: fmt.Sprintf("card %d", i),
|
||||
BoardID: board.ID,
|
||||
}
|
||||
blocks = append(blocks, card)
|
||||
}
|
||||
|
||||
t.Run("success scenario", func(t *testing.T) {
|
||||
opts := model.QueryBlocksOptions{
|
||||
BoardID: board.ID,
|
||||
BlockType: model.TypeCard,
|
||||
}
|
||||
|
||||
th.Store.EXPECT().GetBlocks(opts).Return(blocks, nil)
|
||||
|
||||
cards, err := th.App.GetCardsForBoard(board.ID, 0, 0)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, cards, cardCount)
|
||||
})
|
||||
|
||||
t.Run("error scenario", func(t *testing.T) {
|
||||
opts := model.QueryBlocksOptions{
|
||||
BoardID: board.ID,
|
||||
BlockType: model.TypeCard,
|
||||
}
|
||||
|
||||
th.Store.EXPECT().GetBlocks(opts).Return(nil, blockError{"error"})
|
||||
|
||||
cards, err := th.App.GetCardsForBoard(board.ID, 0, 0)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, cards)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPatchCard(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
board := &model.Board{
|
||||
ID: utils.NewID(utils.IDTypeBoard),
|
||||
}
|
||||
userID := utils.NewID(utils.IDTypeUser)
|
||||
|
||||
props := makeProps(3)
|
||||
|
||||
card := &model.Card{
|
||||
BoardID: board.ID,
|
||||
CreatedBy: userID,
|
||||
ModifiedBy: userID,
|
||||
Title: "test card for patch",
|
||||
ContentOrder: []string{utils.NewID(utils.IDTypeBlock), utils.NewID(utils.IDTypeBlock)},
|
||||
Properties: copyProps(props),
|
||||
}
|
||||
|
||||
newTitle := "patched"
|
||||
newIcon := "😀"
|
||||
newContentOrder := reverse(card.ContentOrder)
|
||||
|
||||
cardPatch := &model.CardPatch{
|
||||
Title: &newTitle,
|
||||
ContentOrder: &newContentOrder,
|
||||
Icon: &newIcon,
|
||||
UpdatedProperties: modifyProps(props),
|
||||
}
|
||||
|
||||
t.Run("success scenario", func(t *testing.T) {
|
||||
expectedPatchedCard := cardPatch.Patch(card)
|
||||
expectedPatchedBlock := model.Card2Block(expectedPatchedCard)
|
||||
|
||||
var blockPatch *model.BlockPatch
|
||||
th.Store.EXPECT().GetBoard(board.ID).Return(board, nil)
|
||||
th.Store.EXPECT().PatchBlock(card.ID, gomock.AssignableToTypeOf(reflect.TypeOf(blockPatch)), userID).Return(nil)
|
||||
th.Store.EXPECT().GetMembersForBoard(board.ID).Return([]*model.BoardMember{}, nil)
|
||||
th.Store.EXPECT().GetBlock(card.ID).Return(expectedPatchedBlock, nil).AnyTimes()
|
||||
|
||||
patchedCard, err := th.App.PatchCard(cardPatch, card.ID, userID, false)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, board.ID, patchedCard.BoardID)
|
||||
require.Equal(t, newTitle, patchedCard.Title)
|
||||
require.Equal(t, newIcon, patchedCard.Icon)
|
||||
require.Equal(t, newContentOrder, patchedCard.ContentOrder)
|
||||
require.EqualValues(t, expectedPatchedCard.Properties, patchedCard.Properties)
|
||||
})
|
||||
|
||||
t.Run("error scenario", func(t *testing.T) {
|
||||
var blockPatch *model.BlockPatch
|
||||
th.Store.EXPECT().GetBoard(board.ID).Return(board, nil)
|
||||
th.Store.EXPECT().PatchBlock(card.ID, gomock.AssignableToTypeOf(reflect.TypeOf(blockPatch)), userID).Return(blockError{"error"})
|
||||
|
||||
patchedCard, err := th.App.PatchCard(cardPatch, card.ID, userID, false)
|
||||
|
||||
require.Error(t, err, "error")
|
||||
require.Nil(t, patchedCard)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetCard(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
boardID := utils.NewID(utils.IDTypeBoard)
|
||||
userID := utils.NewID(utils.IDTypeUser)
|
||||
props := makeProps(5)
|
||||
contentOrder := []string{utils.NewID(utils.IDTypeUser), utils.NewID(utils.IDTypeUser)}
|
||||
fields := make(map[string]any)
|
||||
fields["contentOrder"] = contentOrder
|
||||
fields["properties"] = props
|
||||
fields["icon"] = "😀"
|
||||
fields["isTemplate"] = true
|
||||
|
||||
block := &model.Block{
|
||||
ID: utils.NewID(utils.IDTypeBlock),
|
||||
ParentID: boardID,
|
||||
Type: model.TypeCard,
|
||||
Title: "test card",
|
||||
BoardID: boardID,
|
||||
Fields: fields,
|
||||
CreatedBy: userID,
|
||||
ModifiedBy: userID,
|
||||
}
|
||||
|
||||
t.Run("success scenario", func(t *testing.T) {
|
||||
th.Store.EXPECT().GetBlock(block.ID).Return(block, nil)
|
||||
|
||||
card, err := th.App.GetCardByID(block.ID)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, boardID, card.BoardID)
|
||||
require.Equal(t, block.Title, card.Title)
|
||||
require.Equal(t, "😀", card.Icon)
|
||||
require.Equal(t, true, card.IsTemplate)
|
||||
require.Equal(t, contentOrder, card.ContentOrder)
|
||||
require.EqualValues(t, props, card.Properties)
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
bogusID := utils.NewID(utils.IDTypeBlock)
|
||||
th.Store.EXPECT().GetBlock(bogusID).Return(nil, model.NewErrNotFound(bogusID))
|
||||
|
||||
card, err := th.App.GetCardByID(bogusID)
|
||||
|
||||
require.Error(t, err, "error")
|
||||
require.True(t, model.IsErrNotFound(err))
|
||||
require.Nil(t, card)
|
||||
})
|
||||
|
||||
t.Run("error scenario", func(t *testing.T) {
|
||||
th.Store.EXPECT().GetBlock(block.ID).Return(nil, blockError{"error"})
|
||||
|
||||
card, err := th.App.GetCardByID(block.ID)
|
||||
|
||||
require.Error(t, err, "error")
|
||||
require.Nil(t, card)
|
||||
})
|
||||
}
|
||||
|
||||
// reverse is a helper function to copy and reverse a slice of strings.
|
||||
func reverse(src []string) []string {
|
||||
out := make([]string, 0, len(src))
|
||||
for i := len(src) - 1; i >= 0; i-- {
|
||||
out = append(out, src[i])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func makeProps(count int) map[string]any {
|
||||
props := make(map[string]any)
|
||||
for i := 0; i < count; i++ {
|
||||
props[utils.NewID(utils.IDTypeBlock)] = utils.NewID(utils.IDTypeBlock)
|
||||
}
|
||||
return props
|
||||
}
|
||||
|
||||
func copyProps(m map[string]any) map[string]any {
|
||||
out := make(map[string]any)
|
||||
for k, v := range m {
|
||||
out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func modifyProps(m map[string]any) map[string]any {
|
||||
out := make(map[string]any)
|
||||
for k := range m {
|
||||
out[k] = utils.NewID(utils.IDTypeBlock)
|
||||
}
|
||||
return out
|
||||
}
|
@ -196,6 +196,14 @@ func (c *Client) GetBoardsAndBlocksRoute() string {
|
||||
return "/boards-and-blocks"
|
||||
}
|
||||
|
||||
func (c *Client) GetCardsRoute() string {
|
||||
return "/cards"
|
||||
}
|
||||
|
||||
func (c *Client) GetCardRoute(cardID string) string {
|
||||
return fmt.Sprintf("%s/%s", c.GetCardsRoute(), cardID)
|
||||
}
|
||||
|
||||
func (c *Client) GetTeam(teamID string) (*model.Team, *Response) {
|
||||
r, err := c.DoAPIGet(c.GetTeamRoute(teamID), "")
|
||||
if err != nil {
|
||||
@ -341,7 +349,80 @@ func (c *Client) DeleteBlock(boardID, blockID string, disableNotify bool) (bool,
|
||||
return true, BuildResponse(r)
|
||||
}
|
||||
|
||||
//
|
||||
// Cards
|
||||
//
|
||||
|
||||
func (c *Client) CreateCard(boardID string, card *model.Card, disableNotify bool) (*model.Card, *Response) {
|
||||
var queryParams string
|
||||
if disableNotify {
|
||||
queryParams = "?" + disableNotifyQueryParam
|
||||
}
|
||||
r, err := c.DoAPIPost(c.GetBoardRoute(boardID)+"/cards"+queryParams, toJSON(card))
|
||||
if err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
var cardNew *model.Card
|
||||
if err := json.NewDecoder(r.Body).Decode(&cardNew); err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
}
|
||||
|
||||
return cardNew, BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) GetCards(boardID string, page int, perPage int) ([]*model.Card, *Response) {
|
||||
url := fmt.Sprintf("%s/cards?page=%d&per_page=%d", c.GetBoardRoute(boardID), page, perPage)
|
||||
r, err := c.DoAPIGet(url, "")
|
||||
if err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
}
|
||||
|
||||
var cards []*model.Card
|
||||
if err := json.NewDecoder(r.Body).Decode(&cards); err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
}
|
||||
|
||||
return cards, BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) PatchCard(cardID string, cardPatch *model.CardPatch, disableNotify bool) (*model.Card, *Response) {
|
||||
var queryParams string
|
||||
if disableNotify {
|
||||
queryParams = "?" + disableNotifyQueryParam
|
||||
}
|
||||
r, err := c.DoAPIPatch(c.GetCardRoute(cardID)+queryParams, toJSON(cardPatch))
|
||||
if err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
}
|
||||
|
||||
var cardNew *model.Card
|
||||
if err := json.NewDecoder(r.Body).Decode(&cardNew); err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
}
|
||||
|
||||
return cardNew, BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) GetCard(cardID string) (*model.Card, *Response) {
|
||||
r, err := c.DoAPIGet(c.GetCardRoute(cardID), "")
|
||||
if err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
}
|
||||
|
||||
var card *model.Card
|
||||
if err := json.NewDecoder(r.Body).Decode(&card); err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
}
|
||||
|
||||
return card, BuildResponse(r)
|
||||
}
|
||||
|
||||
//
|
||||
// Boards and blocks.
|
||||
//
|
||||
|
||||
func (c *Client) CreateBoardsAndBlocks(bab *model.BoardsAndBlocks) (*model.BoardsAndBlocks, *Response) {
|
||||
r, err := c.DoAPIPost(c.GetBoardsAndBlocksRoute(), toJSON(bab))
|
||||
if err != nil {
|
||||
|
@ -17,6 +17,7 @@ require (
|
||||
github.com/oklog/run v1.1.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.12.1
|
||||
github.com/rivo/uniseg v0.2.0
|
||||
github.com/rudderlabs/analytics-go v3.3.2+incompatible
|
||||
github.com/sergi/go-diff v1.2.0
|
||||
github.com/spf13/viper v1.10.1
|
||||
|
@ -1092,6 +1092,8 @@ github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
|
289
server/integrationtests/cards_test.go
Normal file
289
server/integrationtests/cards_test.go
Normal file
@ -0,0 +1,289 @@
|
||||
package integrationtests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCreateCard(t *testing.T) {
|
||||
t.Run("a non authenticated user should be rejected", func(t *testing.T) {
|
||||
th := SetupTestHelper(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
board := th.CreateBoard(testTeamID, model.BoardTypeOpen)
|
||||
th.Logout(th.Client)
|
||||
|
||||
card := &model.Card{
|
||||
Title: "basic card",
|
||||
}
|
||||
cardNew, resp := th.Client.CreateCard(board.ID, card, false)
|
||||
th.CheckUnauthorized(resp)
|
||||
require.Nil(t, cardNew)
|
||||
})
|
||||
|
||||
t.Run("good", func(t *testing.T) {
|
||||
th := SetupTestHelper(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
board := th.CreateBoard(testTeamID, model.BoardTypeOpen)
|
||||
contentOrder := []string{utils.NewID(utils.IDTypeBlock), utils.NewID(utils.IDTypeBlock), utils.NewID(utils.IDTypeBlock)}
|
||||
|
||||
card := &model.Card{
|
||||
Title: "test card 1",
|
||||
Icon: "😱",
|
||||
ContentOrder: contentOrder,
|
||||
}
|
||||
|
||||
cardNew, resp := th.Client.CreateCard(board.ID, card, false)
|
||||
require.NoError(t, resp.Error)
|
||||
th.CheckOK(resp)
|
||||
require.NotNil(t, cardNew)
|
||||
|
||||
require.Equal(t, board.ID, cardNew.BoardID)
|
||||
require.Equal(t, "test card 1", cardNew.Title)
|
||||
require.Equal(t, "😱", cardNew.Icon)
|
||||
require.Equal(t, contentOrder, cardNew.ContentOrder)
|
||||
})
|
||||
|
||||
t.Run("invalid card", func(t *testing.T) {
|
||||
th := SetupTestHelper(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
board := th.CreateBoard(testTeamID, model.BoardTypeOpen)
|
||||
|
||||
card := &model.Card{
|
||||
Title: "too many emoji's",
|
||||
Icon: "😱😱😱😱",
|
||||
}
|
||||
|
||||
cardNew, resp := th.Client.CreateCard(board.ID, card, false)
|
||||
require.Error(t, resp.Error)
|
||||
require.Nil(t, cardNew)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetCards(t *testing.T) {
|
||||
th := SetupTestHelper(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
board := th.CreateBoard(testTeamID, model.BoardTypeOpen)
|
||||
userID := th.GetUser1().ID
|
||||
|
||||
const cardCount = 25
|
||||
|
||||
// make some cards with content
|
||||
for i := 0; i < cardCount; i++ {
|
||||
card := &model.Card{
|
||||
BoardID: board.ID,
|
||||
CreatedBy: userID,
|
||||
ModifiedBy: userID,
|
||||
Title: fmt.Sprintf("%d", i),
|
||||
}
|
||||
cardNew, resp := th.Client.CreateCard(board.ID, card, true)
|
||||
th.CheckOK(resp)
|
||||
|
||||
blocks := make([]model.Block, 0, 3)
|
||||
for j := 0; j < 3; j++ {
|
||||
now := model.GetMillis()
|
||||
block := model.Block{
|
||||
ID: utils.NewID(utils.IDTypeBlock),
|
||||
ParentID: cardNew.ID,
|
||||
CreatedBy: userID,
|
||||
ModifiedBy: userID,
|
||||
CreateAt: now,
|
||||
UpdateAt: now,
|
||||
Schema: 1,
|
||||
Type: model.TypeText,
|
||||
Title: fmt.Sprintf("text %d for card %d", j, i),
|
||||
BoardID: board.ID,
|
||||
}
|
||||
blocks = append(blocks, block)
|
||||
}
|
||||
_, resp = th.Client.InsertBlocks(board.ID, blocks, true)
|
||||
th.CheckOK(resp)
|
||||
}
|
||||
|
||||
t.Run("fetch all cards", func(t *testing.T) {
|
||||
cards, resp := th.Client.GetCards(board.ID, 0, -1)
|
||||
th.CheckOK(resp)
|
||||
assert.Len(t, cards, cardCount)
|
||||
})
|
||||
|
||||
t.Run("fetch with pagination", func(t *testing.T) {
|
||||
cardNums := make(map[int]struct{})
|
||||
|
||||
// return first 10
|
||||
cards, resp := th.Client.GetCards(board.ID, 0, 10)
|
||||
th.CheckOK(resp)
|
||||
assert.Len(t, cards, 10)
|
||||
for _, card := range cards {
|
||||
cardNum, err := strconv.Atoi(card.Title)
|
||||
require.NoError(t, err)
|
||||
cardNums[cardNum] = struct{}{}
|
||||
}
|
||||
|
||||
// return second 10
|
||||
cards, resp = th.Client.GetCards(board.ID, 1, 10)
|
||||
th.CheckOK(resp)
|
||||
assert.Len(t, cards, 10)
|
||||
for _, card := range cards {
|
||||
cardNum, err := strconv.Atoi(card.Title)
|
||||
require.NoError(t, err)
|
||||
cardNums[cardNum] = struct{}{}
|
||||
}
|
||||
|
||||
// return remaining 5
|
||||
cards, resp = th.Client.GetCards(board.ID, 2, 10)
|
||||
th.CheckOK(resp)
|
||||
assert.Len(t, cards, 5)
|
||||
for _, card := range cards {
|
||||
cardNum, err := strconv.Atoi(card.Title)
|
||||
require.NoError(t, err)
|
||||
cardNums[cardNum] = struct{}{}
|
||||
}
|
||||
|
||||
// make sure all card numbers were returned
|
||||
assert.Len(t, cardNums, cardCount)
|
||||
for i := 0; i < cardCount; i++ {
|
||||
_, ok := cardNums[i]
|
||||
assert.True(t, ok)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a non authenticated user should be rejected", func(t *testing.T) {
|
||||
th.Logout(th.Client)
|
||||
|
||||
cards, resp := th.Client.GetCards(board.ID, 0, 10)
|
||||
th.CheckUnauthorized(resp)
|
||||
require.Nil(t, cards)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPatchCard(t *testing.T) {
|
||||
t.Run("a non authenticated user should be rejected", func(t *testing.T) {
|
||||
th := SetupTestHelper(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
_, cards := th.CreateBoardAndCards(testTeamID, model.BoardTypeOpen, 1)
|
||||
card := cards[0]
|
||||
|
||||
th.Logout(th.Client)
|
||||
|
||||
newTitle := "another title"
|
||||
patch := &model.CardPatch{
|
||||
Title: &newTitle,
|
||||
}
|
||||
|
||||
patchedCard, resp := th.Client.PatchCard(card.ID, patch, false)
|
||||
th.CheckUnauthorized(resp)
|
||||
require.Nil(t, patchedCard)
|
||||
})
|
||||
|
||||
t.Run("good", func(t *testing.T) {
|
||||
th := SetupTestHelper(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
board, cards := th.CreateBoardAndCards(testTeamID, model.BoardTypeOpen, 1)
|
||||
card := cards[0]
|
||||
|
||||
// Patch the card
|
||||
newTitle := "another title"
|
||||
newIcon := "🐿"
|
||||
newContentOrder := reverse(card.ContentOrder)
|
||||
updatedProps := modifyCardProps(card.Properties)
|
||||
patch := &model.CardPatch{
|
||||
Title: &newTitle,
|
||||
Icon: &newIcon,
|
||||
ContentOrder: &newContentOrder,
|
||||
UpdatedProperties: updatedProps,
|
||||
}
|
||||
|
||||
patchedCard, resp := th.Client.PatchCard(card.ID, patch, false)
|
||||
|
||||
th.CheckOK(resp)
|
||||
require.NotNil(t, patchedCard)
|
||||
require.Equal(t, board.ID, patchedCard.BoardID)
|
||||
require.Equal(t, newTitle, patchedCard.Title)
|
||||
require.Equal(t, newIcon, patchedCard.Icon)
|
||||
require.NotEqual(t, card.ContentOrder, patchedCard.ContentOrder)
|
||||
require.ElementsMatch(t, card.ContentOrder, patchedCard.ContentOrder)
|
||||
require.EqualValues(t, updatedProps, patchedCard.Properties)
|
||||
})
|
||||
|
||||
t.Run("invalid card patch", func(t *testing.T) {
|
||||
th := SetupTestHelper(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
_, cards := th.CreateBoardAndCards(testTeamID, model.BoardTypeOpen, 1)
|
||||
card := cards[0]
|
||||
|
||||
// Bad patch (too many emoji)
|
||||
newIcon := "🐿🐿🐿"
|
||||
patch := &model.CardPatch{
|
||||
Icon: &newIcon,
|
||||
}
|
||||
|
||||
cardNew, resp := th.Client.PatchCard(card.ID, patch, false)
|
||||
require.Error(t, resp.Error)
|
||||
require.Nil(t, cardNew)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetCard(t *testing.T) {
|
||||
t.Run("a non authenticated user should be rejected", func(t *testing.T) {
|
||||
th := SetupTestHelper(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
_, cards := th.CreateBoardAndCards(testTeamID, model.BoardTypeOpen, 1)
|
||||
card := cards[0]
|
||||
|
||||
th.Logout(th.Client)
|
||||
|
||||
cardFetched, resp := th.Client.GetCard(card.ID)
|
||||
th.CheckUnauthorized(resp)
|
||||
require.Nil(t, cardFetched)
|
||||
})
|
||||
|
||||
t.Run("good", func(t *testing.T) {
|
||||
th := SetupTestHelper(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
board, cards := th.CreateBoardAndCards(testTeamID, model.BoardTypeOpen, 1)
|
||||
card := cards[0]
|
||||
|
||||
cardFetched, resp := th.Client.GetCard(card.ID)
|
||||
|
||||
th.CheckOK(resp)
|
||||
require.NotNil(t, cardFetched)
|
||||
require.Equal(t, board.ID, cardFetched.BoardID)
|
||||
require.Equal(t, card.Title, cardFetched.Title)
|
||||
require.Equal(t, card.Icon, cardFetched.Icon)
|
||||
require.Equal(t, card.ContentOrder, cardFetched.ContentOrder)
|
||||
require.EqualValues(t, card.Properties, cardFetched.Properties)
|
||||
})
|
||||
}
|
||||
|
||||
//
|
||||
// Helpers.
|
||||
//
|
||||
func reverse(src []string) []string {
|
||||
out := make([]string, 0, len(src))
|
||||
for i := len(src) - 1; i >= 0; i-- {
|
||||
out = append(out, src[i])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func modifyCardProps(m map[string]any) map[string]any {
|
||||
out := make(map[string]any)
|
||||
for k := range m {
|
||||
out[k] = utils.NewID(utils.IDTypeBlock)
|
||||
}
|
||||
return out
|
||||
}
|
@ -2,6 +2,7 @@ package integrationtests
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
@ -16,6 +17,7 @@ import (
|
||||
"github.com/mattermost/focalboard/server/services/permissions/mmpermissions"
|
||||
"github.com/mattermost/focalboard/server/services/store"
|
||||
"github.com/mattermost/focalboard/server/services/store/sqlstore"
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
|
||||
mmModel "github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
@ -451,6 +453,31 @@ func (th *TestHelper) CreateBoard(teamID string, boardType model.BoardType) *mod
|
||||
return board
|
||||
}
|
||||
|
||||
func (th *TestHelper) CreateBoardAndCards(teamdID string, boardType model.BoardType, numCards int) (*model.Board, []*model.Card) {
|
||||
board := th.CreateBoard(teamdID, boardType)
|
||||
cards := make([]*model.Card, 0, numCards)
|
||||
for i := 0; i < numCards; i++ {
|
||||
card := &model.Card{
|
||||
Title: fmt.Sprintf("test card %d", i+1),
|
||||
ContentOrder: []string{utils.NewID(utils.IDTypeBlock), utils.NewID(utils.IDTypeBlock), utils.NewID(utils.IDTypeBlock)},
|
||||
Icon: "😱",
|
||||
Properties: th.MakeCardProps(5),
|
||||
}
|
||||
newCard, resp := th.Client.CreateCard(board.ID, card, true)
|
||||
th.CheckOK(resp)
|
||||
cards = append(cards, newCard)
|
||||
}
|
||||
return board, cards
|
||||
}
|
||||
|
||||
func (th *TestHelper) MakeCardProps(count int) map[string]any {
|
||||
props := make(map[string]any)
|
||||
for i := 0; i < count; i++ {
|
||||
props[utils.NewID(utils.IDTypeBlock)] = utils.NewID(utils.IDTypeBlock)
|
||||
}
|
||||
return props
|
||||
}
|
||||
|
||||
func (th *TestHelper) GetUser1() *model.User {
|
||||
return th.Me(th.Client)
|
||||
}
|
||||
|
323
server/model/card.go
Normal file
323
server/model/card.go
Normal file
@ -0,0 +1,323 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
"github.com/rivo/uniseg"
|
||||
)
|
||||
|
||||
var ErrBoardIDMismatch = errors.New("Board IDs do not match")
|
||||
|
||||
type ErrInvalidCard struct {
|
||||
msg string
|
||||
}
|
||||
|
||||
func NewErrInvalidCard(msg string) ErrInvalidCard {
|
||||
return ErrInvalidCard{
|
||||
msg: msg,
|
||||
}
|
||||
}
|
||||
|
||||
func (e ErrInvalidCard) Error() string {
|
||||
return fmt.Sprintf("invalid card, %s", e.msg)
|
||||
}
|
||||
|
||||
var ErrNotCardBlock = errors.New("not a card block")
|
||||
|
||||
type ErrInvalidFieldType struct {
|
||||
field string
|
||||
}
|
||||
|
||||
func (e ErrInvalidFieldType) Error() string {
|
||||
return fmt.Sprintf("invalid type for field '%s'", e.field)
|
||||
}
|
||||
|
||||
// Card represents a group of content blocks and properties.
|
||||
// swagger:model
|
||||
type Card struct {
|
||||
// The id for this card
|
||||
// required: false
|
||||
ID string `json:"id"`
|
||||
|
||||
// The id for board this card belongs to.
|
||||
// required: false
|
||||
BoardID string `json:"boardId"`
|
||||
|
||||
// The id for user who created this card
|
||||
// required: false
|
||||
CreatedBy string `json:"createdBy"`
|
||||
|
||||
// The id for user who last modified this card
|
||||
// required: false
|
||||
ModifiedBy string `json:"modifiedBy"`
|
||||
|
||||
// The display title
|
||||
// required: false
|
||||
Title string `json:"title"`
|
||||
|
||||
// An array of content block ids specifying the ordering of content for this card.
|
||||
// required: false
|
||||
ContentOrder []string `json:"contentOrder"`
|
||||
|
||||
// The icon of the card
|
||||
// required: false
|
||||
Icon string `json:"icon"`
|
||||
|
||||
// True if this card belongs to a template
|
||||
// required: false
|
||||
IsTemplate bool `json:"isTemplate"`
|
||||
|
||||
// A map of property ids to property values (option ids, strings, array of option ids)
|
||||
// required: false
|
||||
Properties map[string]any `json:"properties"`
|
||||
|
||||
// The creation time in milliseconds since the current epoch
|
||||
// required: false
|
||||
CreateAt int64 `json:"createAt"`
|
||||
|
||||
// The last modified time in milliseconds since the current epoch
|
||||
// required: false
|
||||
UpdateAt int64 `json:"updateAt"`
|
||||
|
||||
// The deleted time in milliseconds since the current epoch. Set to indicate this card is deleted
|
||||
// required: false
|
||||
DeleteAt int64 `json:"deleteAt"`
|
||||
}
|
||||
|
||||
// Populate populates a Card with default values.
|
||||
func (c *Card) Populate() {
|
||||
if c.ID == "" {
|
||||
c.ID = utils.NewID(utils.IDTypeCard)
|
||||
}
|
||||
if c.ContentOrder == nil {
|
||||
c.ContentOrder = make([]string, 0)
|
||||
}
|
||||
if c.Properties == nil {
|
||||
c.Properties = make(map[string]any)
|
||||
}
|
||||
now := utils.GetMillis()
|
||||
if c.CreateAt == 0 {
|
||||
c.CreateAt = now
|
||||
}
|
||||
if c.UpdateAt == 0 {
|
||||
c.UpdateAt = now
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Card) PopulateWithBoardID(boardID string) {
|
||||
c.BoardID = boardID
|
||||
c.Populate()
|
||||
}
|
||||
|
||||
// CheckValid returns an error if the Card has invalid field values.
|
||||
func (c *Card) CheckValid() error {
|
||||
if c.ID == "" {
|
||||
return ErrInvalidCard{"ID is missing"}
|
||||
}
|
||||
if c.BoardID == "" {
|
||||
return ErrInvalidCard{"BoardID is missing"}
|
||||
}
|
||||
if c.ContentOrder == nil {
|
||||
return ErrInvalidCard{"ContentOrder is missing"}
|
||||
}
|
||||
if uniseg.GraphemeClusterCount(c.Icon) > 1 {
|
||||
return ErrInvalidCard{"Icon can have only one grapheme"}
|
||||
}
|
||||
if c.Properties == nil {
|
||||
return ErrInvalidCard{"Properties"}
|
||||
}
|
||||
if c.CreateAt == 0 {
|
||||
return ErrInvalidCard{"CreateAt"}
|
||||
}
|
||||
if c.UpdateAt == 0 {
|
||||
return ErrInvalidCard{"UpdateAt"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CardPatch is a patch for modifying cards
|
||||
// swagger:model
|
||||
type CardPatch struct {
|
||||
// The display title
|
||||
// required: false
|
||||
Title *string `json:"title"`
|
||||
|
||||
// An array of content block ids specifying the ordering of content for this card.
|
||||
// required: false
|
||||
ContentOrder *[]string `json:"contentOrder"`
|
||||
|
||||
// The icon of the card
|
||||
// required: false
|
||||
Icon *string `json:"icon"`
|
||||
|
||||
// A map of property ids to property option ids to be updated
|
||||
// required: false
|
||||
UpdatedProperties map[string]any `json:"updatedProperties"`
|
||||
}
|
||||
|
||||
// Patch returns an updated version of the card.
|
||||
func (p *CardPatch) Patch(card *Card) *Card {
|
||||
if p.Title != nil {
|
||||
card.Title = *p.Title
|
||||
}
|
||||
|
||||
if p.ContentOrder != nil {
|
||||
card.ContentOrder = *p.ContentOrder
|
||||
}
|
||||
|
||||
if p.Icon != nil {
|
||||
card.Icon = *p.Icon
|
||||
}
|
||||
|
||||
if card.Properties == nil {
|
||||
card.Properties = make(map[string]any)
|
||||
}
|
||||
|
||||
// if there are properties marked for update, we replace the
|
||||
// existing ones or add them
|
||||
for propID, propVal := range p.UpdatedProperties {
|
||||
card.Properties[propID] = propVal
|
||||
}
|
||||
|
||||
return card
|
||||
}
|
||||
|
||||
// CheckValid returns an error if the CardPatch has invalid field values.
|
||||
func (p *CardPatch) CheckValid() error {
|
||||
if p.Icon != nil && uniseg.GraphemeClusterCount(*p.Icon) > 1 {
|
||||
return ErrInvalidCard{"Icon can have only one grapheme"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Card2Block converts a card to block using a shallow copy. Not needed once cards are first class entities.
|
||||
func Card2Block(card *Card) *Block {
|
||||
fields := make(map[string]interface{})
|
||||
|
||||
fields["contentOrder"] = card.ContentOrder
|
||||
fields["icon"] = card.Icon
|
||||
fields["isTemplate"] = card.IsTemplate
|
||||
fields["properties"] = card.Properties
|
||||
|
||||
return &Block{
|
||||
ID: card.ID,
|
||||
ParentID: card.BoardID,
|
||||
CreatedBy: card.CreatedBy,
|
||||
ModifiedBy: card.ModifiedBy,
|
||||
Schema: 1,
|
||||
Type: TypeCard,
|
||||
Title: card.Title,
|
||||
Fields: fields,
|
||||
CreateAt: card.CreateAt,
|
||||
UpdateAt: card.UpdateAt,
|
||||
DeleteAt: card.DeleteAt,
|
||||
BoardID: card.BoardID,
|
||||
}
|
||||
}
|
||||
|
||||
// Block2Card converts a block to a card. Not needed once cards are first class entities.
|
||||
func Block2Card(block *Block) (*Card, error) {
|
||||
if block.Type != TypeCard {
|
||||
return nil, fmt.Errorf("cannot convert block to card: %w", ErrNotCardBlock)
|
||||
}
|
||||
|
||||
contentOrder := make([]string, 0)
|
||||
icon := ""
|
||||
isTemplate := false
|
||||
properties := make(map[string]any)
|
||||
|
||||
if co, ok := block.Fields["contentOrder"]; ok {
|
||||
switch arr := co.(type) {
|
||||
case []any:
|
||||
for _, str := range arr {
|
||||
if id, ok := str.(string); ok {
|
||||
contentOrder = append(contentOrder, id)
|
||||
} else {
|
||||
return nil, ErrInvalidFieldType{"contentOrder item"}
|
||||
}
|
||||
}
|
||||
case []string:
|
||||
contentOrder = append(contentOrder, arr...)
|
||||
default:
|
||||
return nil, ErrInvalidFieldType{"contentOrder"}
|
||||
}
|
||||
}
|
||||
|
||||
if iconAny, ok := block.Fields["icon"]; ok {
|
||||
if id, ok := iconAny.(string); ok {
|
||||
icon = id
|
||||
} else {
|
||||
return nil, ErrInvalidFieldType{"icon"}
|
||||
}
|
||||
}
|
||||
|
||||
if isTemplateAny, ok := block.Fields["isTemplate"]; ok {
|
||||
if b, ok := isTemplateAny.(bool); ok {
|
||||
isTemplate = b
|
||||
} else {
|
||||
return nil, ErrInvalidFieldType{"isTemplate"}
|
||||
}
|
||||
}
|
||||
|
||||
if props, ok := block.Fields["properties"]; ok {
|
||||
if propMap, ok := props.(map[string]any); ok {
|
||||
for k, v := range propMap {
|
||||
properties[k] = v
|
||||
}
|
||||
} else {
|
||||
return nil, ErrInvalidFieldType{"properties"}
|
||||
}
|
||||
}
|
||||
|
||||
card := &Card{
|
||||
ID: block.ID,
|
||||
BoardID: block.BoardID,
|
||||
CreatedBy: block.CreatedBy,
|
||||
ModifiedBy: block.ModifiedBy,
|
||||
Title: block.Title,
|
||||
ContentOrder: contentOrder,
|
||||
Icon: icon,
|
||||
IsTemplate: isTemplate,
|
||||
Properties: properties,
|
||||
CreateAt: block.CreateAt,
|
||||
UpdateAt: block.UpdateAt,
|
||||
DeleteAt: block.DeleteAt,
|
||||
}
|
||||
card.Populate()
|
||||
return card, nil
|
||||
}
|
||||
|
||||
// CardPatch2BlockPatch converts a CardPatch to a BlockPatch. Not needed once cards are first class entities.
|
||||
func CardPatch2BlockPatch(cardPatch *CardPatch) (*BlockPatch, error) {
|
||||
if err := cardPatch.CheckValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blockPatch := &BlockPatch{
|
||||
Title: cardPatch.Title,
|
||||
}
|
||||
|
||||
updatedFields := make(map[string]any, 0)
|
||||
|
||||
if cardPatch.ContentOrder != nil {
|
||||
updatedFields["contentOrder"] = cardPatch.ContentOrder
|
||||
}
|
||||
if cardPatch.Icon != nil {
|
||||
updatedFields["icon"] = cardPatch.Icon
|
||||
}
|
||||
|
||||
properties := make(map[string]any)
|
||||
for k, v := range cardPatch.UpdatedProperties {
|
||||
properties[k] = v
|
||||
}
|
||||
|
||||
if len(properties) != 0 {
|
||||
updatedFields["properties"] = cardPatch.UpdatedProperties
|
||||
}
|
||||
|
||||
blockPatch.UpdatedFields = updatedFields
|
||||
|
||||
return blockPatch, nil
|
||||
}
|
84
server/model/card_test.go
Normal file
84
server/model/card_test.go
Normal file
@ -0,0 +1,84 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBlock2Card(t *testing.T) {
|
||||
blockID := utils.NewID(utils.IDTypeCard)
|
||||
boardID := utils.NewID(utils.IDTypeBoard)
|
||||
userID := utils.NewID(utils.IDTypeUser)
|
||||
now := utils.GetMillis()
|
||||
|
||||
var fields map[string]any
|
||||
err := json.Unmarshal([]byte(sampleBlockFieldsJSON), &fields)
|
||||
require.NoError(t, err)
|
||||
|
||||
block := &Block{
|
||||
ID: blockID,
|
||||
ParentID: boardID,
|
||||
CreatedBy: userID,
|
||||
ModifiedBy: userID,
|
||||
Schema: 1,
|
||||
Type: TypeCard,
|
||||
Title: "My card title",
|
||||
Fields: fields,
|
||||
CreateAt: now,
|
||||
UpdateAt: now,
|
||||
DeleteAt: 0,
|
||||
BoardID: boardID,
|
||||
}
|
||||
|
||||
t.Run("Good block", func(t *testing.T) {
|
||||
card, err := Block2Card(block)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, block.ID, card.ID)
|
||||
assert.Equal(t, []string{"acdxa8r8aht85pyoeuj1ed7tu8w", "73urm1huoupd4idzkdq5yaeuyay", "ay6sogs9owtd9xbyn49qt3395ko"}, card.ContentOrder)
|
||||
assert.EqualValues(t, fields["icon"], card.Icon)
|
||||
assert.EqualValues(t, fields["isTemplate"], card.IsTemplate)
|
||||
assert.EqualValues(t, fields["properties"], card.Properties)
|
||||
})
|
||||
|
||||
t.Run("Not a card", func(t *testing.T) {
|
||||
blockNotCard := &Block{}
|
||||
|
||||
card, err := Block2Card(blockNotCard)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, card)
|
||||
})
|
||||
}
|
||||
|
||||
const sampleBlockFieldsJSON = `
|
||||
{
|
||||
"contentOrder":[
|
||||
"acdxa8r8aht85pyoeuj1ed7tu8w",
|
||||
"73urm1huoupd4idzkdq5yaeuyay",
|
||||
"ay6sogs9owtd9xbyn49qt3395ko"
|
||||
],
|
||||
"icon":"🎨",
|
||||
"isTemplate":false,
|
||||
"properties":{
|
||||
"aa7swu9zz3ofdkcna3h867cum4y":"212-444-1234",
|
||||
"af6fcbb8-ca56-4b73-83eb-37437b9a667d":"77c539af-309c-4db1-8329-d20ef7e9eacd",
|
||||
"aiwt9ibi8jjrf9hzi1xzk8no8mo":"foo",
|
||||
"aj65h4s6ghr6wgh3bnhqbzzmiaa":"77",
|
||||
"ajy6xbebzopojaenbnmfpgtdwso":"{\"from\":1660046400000}",
|
||||
"amc8wnk1xqj54rymkoqffhtw7ie":"zhqsoeqs1pg9i8gk81k9ryy83h",
|
||||
"aooz77t119y7xtfmoyeiy4up75c":"someone@example.com",
|
||||
"auskzaoaccsn55icuwarf4o3tfe":"https://www.google.com",
|
||||
"aydsk41h6cs1z7nmghaw16jqcia":[
|
||||
"aw565znut6zphbxqhbwyawiuggy",
|
||||
"aefd3pxciomrkur4rc6smg1usoc",
|
||||
"a6c96kwrqaskbtochq9wunmzweh",
|
||||
"atyexeuq993fwwb84bxoqixxqqr"
|
||||
],
|
||||
"d6b1249b-bc18-45fc-889e-bec48fce80ef":"9a090e33-b110-4268-8909-132c5002c90e",
|
||||
"d9725d14-d5a8-48e5-8de1-6f8c004a9680":"3245a32d-f688-463b-87f4-8e7142c1b397"
|
||||
}
|
||||
}`
|
@ -81,7 +81,7 @@ func (s *SQLStore) getBlocks(db sq.BaseRunner, opts model.QueryBlocksOptions) ([
|
||||
}
|
||||
|
||||
if opts.Page != 0 {
|
||||
query = query.Offset(uint64(opts.Page))
|
||||
query = query.Offset(uint64(opts.Page * opts.PerPage))
|
||||
}
|
||||
|
||||
if opts.PerPage > 0 {
|
||||
|
Loading…
Reference in New Issue
Block a user