mirror of
https://github.com/mattermost/focalboard.git
synced 2024-12-24 13:43:12 +02:00
Fix notifications for deleted card content blocks (#4508)
* comment only * add getBlockHistoryNewestChildren store method * fixed delete comment notify * fix notification for content block deletion * fix linter errors * address review comments --------- Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
parent
61563daabd
commit
c1c94e9794
@ -95,8 +95,8 @@ func (a *appAPI) GetBlockHistory(blockID string, opts model.QueryBlockHistoryOpt
|
||||
return a.store.GetBlockHistory(blockID, opts)
|
||||
}
|
||||
|
||||
func (a *appAPI) GetSubTree2(boardID, blockID string, opts model.QuerySubtreeOptions) ([]*model.Block, error) {
|
||||
return a.store.GetSubTree2(boardID, blockID, opts)
|
||||
func (a *appAPI) GetBlockHistoryNewestChildren(parentID string, opts model.QueryBlockHistoryChildOptions) ([]*model.Block, bool, error) {
|
||||
return a.store.GetBlockHistoryNewestChildren(parentID, opts)
|
||||
}
|
||||
|
||||
func (a *appAPI) GetBoardAndCardByID(blockID string) (board *model.Board, card *model.Block, err error) {
|
||||
|
@ -199,6 +199,14 @@ type QueryBoardHistoryOptions struct {
|
||||
Descending bool // if true then the records are sorted by insert_at in descending order
|
||||
}
|
||||
|
||||
// QueryBlockHistoryOptions are query options that can be passed to GetBlockHistory.
|
||||
type QueryBlockHistoryChildOptions struct {
|
||||
BeforeUpdateAt int64 // if non-zero then filter for records with update_at less than BeforeUpdateAt
|
||||
AfterUpdateAt int64 // if non-zero then filter for records with update_at greater than AfterUpdateAt
|
||||
Page int // page number to select when paginating
|
||||
PerPage int // number of blocks per page (default=-1, meaning unlimited)
|
||||
}
|
||||
|
||||
func StampModificationMetadata(userID string, blocks []*Block, auditRec *audit.Record) {
|
||||
if userID == SingleUser {
|
||||
userID = ""
|
||||
|
@ -14,13 +14,16 @@ import (
|
||||
type BlockType string
|
||||
|
||||
const (
|
||||
TypeUnknown = "unknown"
|
||||
TypeBoard = "board"
|
||||
TypeCard = "card"
|
||||
TypeView = "view"
|
||||
TypeText = "text"
|
||||
TypeComment = "comment"
|
||||
TypeImage = "image"
|
||||
TypeUnknown = "unknown"
|
||||
TypeBoard = "board"
|
||||
TypeCard = "card"
|
||||
TypeView = "view"
|
||||
TypeText = "text"
|
||||
TypeCheckbox = "checkbox"
|
||||
TypeComment = "comment"
|
||||
TypeImage = "image"
|
||||
TypeAttachment = "attachment"
|
||||
TypeDivider = "divider"
|
||||
)
|
||||
|
||||
func (bt BlockType) String() string {
|
||||
@ -38,10 +41,16 @@ func BlockTypeFromString(s string) (BlockType, error) {
|
||||
return TypeView, nil
|
||||
case "text":
|
||||
return TypeText, nil
|
||||
case "checkbox":
|
||||
return TypeCheckbox, nil
|
||||
case "comment":
|
||||
return TypeComment, nil
|
||||
case "image":
|
||||
return TypeImage, nil
|
||||
case "attachment":
|
||||
return TypeAttachment, nil
|
||||
case "divider":
|
||||
return TypeDivider, nil
|
||||
}
|
||||
return TypeUnknown, ErrInvalidBlockType{s}
|
||||
}
|
||||
@ -55,8 +64,10 @@ func BlockType2IDType(blockType BlockType) utils.IDType {
|
||||
return utils.IDTypeCard
|
||||
case TypeView:
|
||||
return utils.IDTypeView
|
||||
case TypeText, TypeComment:
|
||||
case TypeText, TypeCheckbox, TypeComment, TypeDivider:
|
||||
return utils.IDTypeBlock
|
||||
case TypeImage, TypeAttachment:
|
||||
return utils.IDTypeAttachment
|
||||
}
|
||||
return utils.IDTypeNone
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import (
|
||||
|
||||
type AppAPI interface {
|
||||
GetBlockHistory(blockID string, opts model.QueryBlockHistoryOptions) ([]*model.Block, error)
|
||||
GetSubTree2(boardID, blockID string, opts model.QuerySubtreeOptions) ([]*model.Block, error)
|
||||
GetBlockHistoryNewestChildren(parentID string, opts model.QueryBlockHistoryChildOptions) ([]*model.Block, bool, error)
|
||||
GetBoardAndCardByID(blockID string) (board *model.Board, card *model.Block, err error)
|
||||
|
||||
GetUserByID(userID string) (*model.User, error)
|
||||
|
@ -149,10 +149,10 @@ func (dg *diffGenerator) generateDiffsForCard(card *model.Block, schema model.Pr
|
||||
}
|
||||
|
||||
// fetch all card content blocks that were updated after last notify
|
||||
opts := model.QuerySubtreeOptions{
|
||||
opts := model.QueryBlockHistoryChildOptions{
|
||||
AfterUpdateAt: dg.lastNotifyAt,
|
||||
}
|
||||
blocks, err := dg.store.GetSubTree2(card.BoardID, card.ID, opts)
|
||||
blocks, _, err := dg.store.GetBlockHistoryNewestChildren(card.ID, opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get subtree for card %s: %w", card.ID, err)
|
||||
}
|
||||
|
@ -246,7 +246,7 @@ func appendCommentChanges(fields []*mm_model.SlackAttachmentField, cardDiff *Dif
|
||||
msg = child.NewBlock.Title
|
||||
}
|
||||
|
||||
if child.NewBlock == nil && child.OldBlock != nil {
|
||||
if (child.NewBlock == nil || child.NewBlock.DeleteAt != 0) && child.OldBlock != nil {
|
||||
// deleted comment
|
||||
format = "~~`%s`~~"
|
||||
msg = stripNewlines(child.OldBlock.Title)
|
||||
@ -266,36 +266,73 @@ func appendCommentChanges(fields []*mm_model.SlackAttachmentField, cardDiff *Dif
|
||||
|
||||
func appendContentChanges(fields []*mm_model.SlackAttachmentField, cardDiff *Diff, logger mlog.LoggerIFace) []*mm_model.SlackAttachmentField {
|
||||
for _, child := range cardDiff.Diffs {
|
||||
if child.BlockType != model.TypeComment {
|
||||
var newTitle, oldTitle string
|
||||
if child.OldBlock != nil {
|
||||
oldTitle = child.OldBlock.Title
|
||||
}
|
||||
if child.NewBlock != nil {
|
||||
newTitle = child.NewBlock.Title
|
||||
}
|
||||
var opAdd, opDelete bool
|
||||
var opString string
|
||||
|
||||
// only strip newlines when modifying or deleting
|
||||
if child.OldBlock != nil && child.NewBlock == nil {
|
||||
newTitle = stripNewlines(newTitle)
|
||||
switch {
|
||||
case child.OldBlock == nil && child.NewBlock != nil:
|
||||
opAdd = true
|
||||
opString = "added" // TODO: localize when i18n added to server
|
||||
case child.NewBlock == nil || child.NewBlock.DeleteAt != 0:
|
||||
opDelete = true
|
||||
opString = "deleted"
|
||||
default:
|
||||
opString = "modified"
|
||||
}
|
||||
|
||||
var newTitle, oldTitle string
|
||||
if child.OldBlock != nil {
|
||||
oldTitle = child.OldBlock.Title
|
||||
}
|
||||
if child.NewBlock != nil {
|
||||
newTitle = child.NewBlock.Title
|
||||
}
|
||||
|
||||
switch child.BlockType {
|
||||
case model.TypeDivider, model.TypeComment:
|
||||
// do nothing
|
||||
continue
|
||||
case model.TypeImage:
|
||||
if newTitle == "" {
|
||||
newTitle = "An image was " + opString + "." // TODO: localize when i18n added to server
|
||||
}
|
||||
oldTitle = ""
|
||||
case model.TypeAttachment:
|
||||
if newTitle == "" {
|
||||
newTitle = "A file attachment was " + opString + "." // TODO: localize when i18n added to server
|
||||
}
|
||||
oldTitle = ""
|
||||
default:
|
||||
if !opAdd {
|
||||
if opDelete {
|
||||
newTitle = ""
|
||||
}
|
||||
// only strip newlines when modifying or deleting
|
||||
oldTitle = stripNewlines(oldTitle)
|
||||
newTitle = stripNewlines(newTitle)
|
||||
}
|
||||
|
||||
if newTitle == oldTitle {
|
||||
continue
|
||||
}
|
||||
|
||||
markdown := generateMarkdownDiff(oldTitle, newTitle, logger)
|
||||
if markdown == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
fields = append(fields, &mm_model.SlackAttachmentField{
|
||||
Short: false,
|
||||
Title: "Description",
|
||||
Value: markdown,
|
||||
})
|
||||
}
|
||||
|
||||
logger.Debug("appendContentChanges",
|
||||
mlog.String("type", string(child.BlockType)),
|
||||
mlog.String("opString", opString),
|
||||
mlog.String("oldTitle", oldTitle),
|
||||
mlog.String("newTitle", newTitle),
|
||||
)
|
||||
|
||||
markdown := generateMarkdownDiff(oldTitle, newTitle, logger)
|
||||
if markdown == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
fields = append(fields, &mm_model.SlackAttachmentField{
|
||||
Short: false,
|
||||
Title: "Description",
|
||||
Value: markdown,
|
||||
})
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
@ -200,7 +200,7 @@ func (n *notifier) notifySubscribers(hint *model.NotificationHint) error {
|
||||
}
|
||||
|
||||
opts := DiffConvOpts{
|
||||
Language: "en", // TODO: use correct language with i18n available on server.
|
||||
Language: "en", // TODO: use correct language when i18n is available on server.
|
||||
MakeCardLink: func(block *model.Block, board *model.Board, card *model.Block) string {
|
||||
return fmt.Sprintf("[%s](%s)", block.Title, utils.MakeCardLink(n.serverRoot, board.TeamID, board.ID, card.ID))
|
||||
},
|
||||
|
@ -5,6 +5,8 @@ package notifysubscriptions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
@ -73,6 +75,16 @@ func (b *Backend) Name() string {
|
||||
}
|
||||
|
||||
func (b *Backend) getBlockUpdateFreq(blockType model.BlockType) time.Duration {
|
||||
// check for env variable override
|
||||
sFreq := os.Getenv("MM_BOARDS_NOTIFY_FREQ_SECONDS")
|
||||
if sFreq != "" && sFreq != "0" {
|
||||
if freq, err := strconv.ParseInt(sFreq, 10, 64); err != nil {
|
||||
b.logger.Error("Environment variable MM_BOARDS_NOTIFY_FREQ_SECONDS invalid (ignoring)", mlog.Err(err))
|
||||
} else {
|
||||
return time.Second * time.Duration(freq)
|
||||
}
|
||||
}
|
||||
|
||||
switch blockType {
|
||||
case model.TypeCard:
|
||||
return time.Second * time.Duration(b.notifyFreqCardSeconds)
|
||||
|
@ -457,6 +457,22 @@ func (mr *MockStoreMockRecorder) GetBlockHistoryDescendants(arg0, arg1 interface
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockHistoryDescendants", reflect.TypeOf((*MockStore)(nil).GetBlockHistoryDescendants), arg0, arg1)
|
||||
}
|
||||
|
||||
// GetBlockHistoryNewestChildren mocks base method.
|
||||
func (m *MockStore) GetBlockHistoryNewestChildren(arg0 string, arg1 model.QueryBlockHistoryChildOptions) ([]*model.Block, bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetBlockHistoryNewestChildren", arg0, arg1)
|
||||
ret0, _ := ret[0].([]*model.Block)
|
||||
ret1, _ := ret[1].(bool)
|
||||
ret2, _ := ret[2].(error)
|
||||
return ret0, ret1, ret2
|
||||
}
|
||||
|
||||
// GetBlockHistoryNewestChildren indicates an expected call of GetBlockHistoryNewestChildren.
|
||||
func (mr *MockStoreMockRecorder) GetBlockHistoryNewestChildren(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockHistoryNewestChildren", reflect.TypeOf((*MockStore)(nil).GetBlockHistoryNewestChildren), arg0, arg1)
|
||||
}
|
||||
|
||||
// GetBlocks mocks base method.
|
||||
func (m *MockStore) GetBlocks(arg0 model.QueryBlocksOptions) ([]*model.Block, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
|
||||
@ -42,27 +43,31 @@ func (s *SQLStore) timestampToCharField(name string, as string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SQLStore) blockFields() []string {
|
||||
func (s *SQLStore) blockFields(tableAlias string) []string {
|
||||
if tableAlias != "" && !strings.HasSuffix(tableAlias, ".") {
|
||||
tableAlias += "."
|
||||
}
|
||||
|
||||
return []string{
|
||||
"id",
|
||||
"parent_id",
|
||||
"created_by",
|
||||
"modified_by",
|
||||
s.escapeField("schema"),
|
||||
"type",
|
||||
"title",
|
||||
"COALESCE(fields, '{}')",
|
||||
s.timestampToCharField("insert_at", "insertAt"),
|
||||
"create_at",
|
||||
"update_at",
|
||||
"delete_at",
|
||||
"COALESCE(board_id, '0')",
|
||||
tableAlias + "id",
|
||||
tableAlias + "parent_id",
|
||||
tableAlias + "created_by",
|
||||
tableAlias + "modified_by",
|
||||
tableAlias + s.escapeField("schema"),
|
||||
tableAlias + "type",
|
||||
tableAlias + "title",
|
||||
"COALESCE(" + tableAlias + "fields, '{}')",
|
||||
s.timestampToCharField(tableAlias+"insert_at", "insertAt"),
|
||||
tableAlias + "create_at",
|
||||
tableAlias + "update_at",
|
||||
tableAlias + "delete_at",
|
||||
"COALESCE(" + tableAlias + "board_id, '0')",
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SQLStore) getBlocks(db sq.BaseRunner, opts model.QueryBlocksOptions) ([]*model.Block, error) {
|
||||
query := s.getQueryBuilder(db).
|
||||
Select(s.blockFields()...).
|
||||
Select(s.blockFields("")...).
|
||||
From(s.tablePrefix + "blocks")
|
||||
|
||||
if opts.BoardID != "" {
|
||||
@ -115,7 +120,7 @@ func (s *SQLStore) getBlocksWithParent(db sq.BaseRunner, boardID, parentID strin
|
||||
|
||||
func (s *SQLStore) getBlocksByIDs(db sq.BaseRunner, ids []string) ([]*model.Block, error) {
|
||||
query := s.getQueryBuilder(db).
|
||||
Select(s.blockFields()...).
|
||||
Select(s.blockFields("")...).
|
||||
From(s.tablePrefix + "blocks").
|
||||
Where(sq.Eq{"id": ids})
|
||||
|
||||
@ -150,7 +155,7 @@ func (s *SQLStore) getBlocksWithType(db sq.BaseRunner, boardID, blockType string
|
||||
// getSubTree2 returns blocks within 2 levels of the given blockID.
|
||||
func (s *SQLStore) getSubTree2(db sq.BaseRunner, boardID string, blockID string, opts model.QuerySubtreeOptions) ([]*model.Block, error) {
|
||||
query := s.getQueryBuilder(db).
|
||||
Select(s.blockFields()...).
|
||||
Select(s.blockFields("")...).
|
||||
From(s.tablePrefix + "blocks").
|
||||
Where(sq.Or{sq.Eq{"id": blockID}, sq.Eq{"parent_id": blockID}}).
|
||||
Where(sq.Eq{"board_id": boardID}).
|
||||
@ -550,7 +555,7 @@ func (s *SQLStore) getBoardCount(db sq.BaseRunner) (int64, error) {
|
||||
|
||||
func (s *SQLStore) getBlock(db sq.BaseRunner, blockID string) (*model.Block, error) {
|
||||
query := s.getQueryBuilder(db).
|
||||
Select(s.blockFields()...).
|
||||
Select(s.blockFields("")...).
|
||||
From(s.tablePrefix + "blocks").
|
||||
Where(sq.Eq{"id": blockID})
|
||||
|
||||
@ -580,7 +585,7 @@ func (s *SQLStore) getBlockHistory(db sq.BaseRunner, blockID string, opts model.
|
||||
}
|
||||
|
||||
query := s.getQueryBuilder(db).
|
||||
Select(s.blockFields()...).
|
||||
Select(s.blockFields("")...).
|
||||
From(s.tablePrefix + "blocks_history").
|
||||
Where(sq.Eq{"id": blockID}).
|
||||
OrderBy("insert_at " + order + ", update_at" + order)
|
||||
@ -614,7 +619,7 @@ func (s *SQLStore) getBlockHistoryDescendants(db sq.BaseRunner, boardID string,
|
||||
}
|
||||
|
||||
query := s.getQueryBuilder(db).
|
||||
Select(s.blockFields()...).
|
||||
Select(s.blockFields("")...).
|
||||
From(s.tablePrefix + "blocks_history").
|
||||
Where(sq.Eq{"board_id": boardID}).
|
||||
OrderBy("insert_at " + order + ", update_at" + order)
|
||||
@ -641,6 +646,83 @@ func (s *SQLStore) getBlockHistoryDescendants(db sq.BaseRunner, boardID string,
|
||||
return s.blocksFromRows(rows)
|
||||
}
|
||||
|
||||
// getBlockHistoryNewestChildren returns the newest (latest) version child blocks for the
|
||||
// specified parent from the blocks_history table. This includes any deleted children.
|
||||
func (s *SQLStore) getBlockHistoryNewestChildren(db sq.BaseRunner, parentID string, opts model.QueryBlockHistoryChildOptions) ([]*model.Block, bool, error) {
|
||||
// as we're joining 2 queries, we need to avoid numbered
|
||||
// placeholders until the join is done, so we use the default
|
||||
// question mark placeholder here
|
||||
builder := s.getQueryBuilder(db).PlaceholderFormat(sq.Question)
|
||||
|
||||
sub := builder.
|
||||
Select("bh2.id", "MAX(bh2.insert_at) AS max_insert_at").
|
||||
From(s.tablePrefix + "blocks_history AS bh2").
|
||||
Where(sq.Eq{"bh2.parent_id": parentID}).
|
||||
GroupBy("bh2.id")
|
||||
|
||||
if opts.AfterUpdateAt != 0 {
|
||||
sub = sub.Where(sq.Gt{"bh2.update_at": opts.AfterUpdateAt})
|
||||
}
|
||||
|
||||
if opts.BeforeUpdateAt != 0 {
|
||||
sub = sub.Where(sq.Lt{"bh2.update_at": opts.BeforeUpdateAt})
|
||||
}
|
||||
|
||||
subQuery, subArgs, err := sub.ToSql()
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("getBlockHistoryNewestChildren unable to generate subquery: %w", err)
|
||||
}
|
||||
|
||||
query := s.getQueryBuilder(db).
|
||||
Select(s.blockFields("bh")...).
|
||||
From(s.tablePrefix+"blocks_history AS bh").
|
||||
InnerJoin("("+subQuery+") AS sub ON bh.id=sub.id AND bh.insert_at=sub.max_insert_at", subArgs...)
|
||||
|
||||
if opts.Page != 0 {
|
||||
query = query.Offset(uint64(opts.Page * opts.PerPage))
|
||||
}
|
||||
|
||||
if opts.PerPage > 0 {
|
||||
// limit+1 to detect if more records available
|
||||
query = query.Limit(uint64(opts.PerPage + 1))
|
||||
}
|
||||
|
||||
sql, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("getBlockHistoryNewestChildren unable to generate sql: %w", err)
|
||||
}
|
||||
|
||||
// if we're using postgres or sqlite, we need to replace the
|
||||
// question mark placeholder with the numbered dollar one, now
|
||||
// that the full query is built
|
||||
if s.dbType == model.PostgresDBType || s.dbType == model.SqliteDBType {
|
||||
var rErr error
|
||||
sql, rErr = sq.Dollar.ReplacePlaceholders(sql)
|
||||
if rErr != nil {
|
||||
return nil, false, fmt.Errorf("getBlockHistoryNewestChildren unable to replace sql placeholders: %w", rErr)
|
||||
}
|
||||
}
|
||||
|
||||
rows, err := db.Query(sql, args...)
|
||||
if err != nil {
|
||||
s.logger.Error(`getBlockHistoryNewestChildren ERROR`, mlog.Err(err))
|
||||
return nil, false, err
|
||||
}
|
||||
defer s.CloseRows(rows)
|
||||
|
||||
blocks, err := s.blocksFromRows(rows)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
hasMore := false
|
||||
if opts.PerPage > 0 && len(blocks) > opts.PerPage {
|
||||
blocks = blocks[:opts.PerPage]
|
||||
hasMore = true
|
||||
}
|
||||
return blocks, hasMore, nil
|
||||
}
|
||||
|
||||
// getBoardAndCardByID returns the first parent of type `card` and first parent of type `board` for the block specified by ID.
|
||||
// `board` and/or `card` may return nil without error if the block does not belong to a board or card.
|
||||
func (s *SQLStore) getBoardAndCardByID(db sq.BaseRunner, blockID string) (board *model.Board, card *model.Block, err error) {
|
||||
|
@ -17,41 +17,31 @@ import (
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
func boardFields(prefix string) []string {
|
||||
fields := []string{
|
||||
"id",
|
||||
"team_id",
|
||||
"COALESCE(channel_id, '')",
|
||||
"COALESCE(created_by, '')",
|
||||
"modified_by",
|
||||
"type",
|
||||
"minimum_role",
|
||||
"title",
|
||||
"description",
|
||||
"icon",
|
||||
"show_description",
|
||||
"is_template",
|
||||
"template_version",
|
||||
"COALESCE(properties, '{}')",
|
||||
"COALESCE(card_properties, '[]')",
|
||||
"create_at",
|
||||
"update_at",
|
||||
"delete_at",
|
||||
func boardFields(tableAlias string) []string {
|
||||
if tableAlias != "" && !strings.HasSuffix(tableAlias, ".") {
|
||||
tableAlias += "."
|
||||
}
|
||||
|
||||
if prefix == "" {
|
||||
return fields
|
||||
return []string{
|
||||
tableAlias + "id",
|
||||
tableAlias + "team_id",
|
||||
"COALESCE(" + tableAlias + "channel_id, '')",
|
||||
"COALESCE(" + tableAlias + "created_by, '')",
|
||||
tableAlias + "modified_by",
|
||||
tableAlias + "type",
|
||||
tableAlias + "minimum_role",
|
||||
tableAlias + "title",
|
||||
tableAlias + "description",
|
||||
tableAlias + "icon",
|
||||
tableAlias + "show_description",
|
||||
tableAlias + "is_template",
|
||||
tableAlias + "template_version",
|
||||
"COALESCE(" + tableAlias + "properties, '{}')",
|
||||
"COALESCE(" + tableAlias + "card_properties, '[]')",
|
||||
tableAlias + "create_at",
|
||||
tableAlias + "update_at",
|
||||
tableAlias + "delete_at",
|
||||
}
|
||||
|
||||
prefixedFields := make([]string, len(fields))
|
||||
for i, field := range fields {
|
||||
if strings.HasPrefix(field, "COALESCE(") {
|
||||
prefixedFields[i] = strings.Replace(field, "COALESCE(", "COALESCE("+prefix, 1)
|
||||
} else {
|
||||
prefixedFields[i] = prefix + field
|
||||
}
|
||||
}
|
||||
return prefixedFields
|
||||
}
|
||||
|
||||
func boardHistoryFields() []string {
|
||||
|
@ -328,6 +328,11 @@ func (s *SQLStore) GetBlockHistoryDescendants(boardID string, opts model.QueryBl
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) GetBlockHistoryNewestChildren(parentID string, opts model.QueryBlockHistoryChildOptions) ([]*model.Block, bool, error) {
|
||||
return s.getBlockHistoryNewestChildren(s.db, parentID, opts)
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) GetBlocks(opts model.QueryBlocksOptions) ([]*model.Block, error) {
|
||||
return s.getBlocks(s.db, opts)
|
||||
|
||||
|
@ -38,6 +38,7 @@ type Store interface {
|
||||
PatchBlock(blockID string, blockPatch *model.BlockPatch, userID string) error
|
||||
GetBlockHistory(blockID string, opts model.QueryBlockHistoryOptions) ([]*model.Block, error)
|
||||
GetBlockHistoryDescendants(boardID string, opts model.QueryBlockHistoryOptions) ([]*model.Block, error)
|
||||
GetBlockHistoryNewestChildren(parentID string, opts model.QueryBlockHistoryChildOptions) ([]*model.Block, bool, error)
|
||||
GetBoardHistory(boardID string, opts model.QueryBoardHistoryOptions) ([]*model.Board, error)
|
||||
GetBoardAndCardByID(blockID string) (board *model.Board, card *model.Block, err error)
|
||||
GetBoardAndCard(block *model.Block) (board *model.Board, card *model.Block, err error)
|
||||
|
@ -1,6 +1,8 @@
|
||||
package storetests
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -79,6 +81,11 @@ func StoreTestBlocksStore(t *testing.T, setup func(t *testing.T) (store.Store, f
|
||||
defer tearDown()
|
||||
testUndeleteBlockChildren(t, store)
|
||||
})
|
||||
t.Run("GetBlockHistoryNewestChildren", func(t *testing.T) {
|
||||
store, tearDown := setup(t)
|
||||
defer tearDown()
|
||||
testGetBlockHistoryNewestChildren(t, store)
|
||||
})
|
||||
}
|
||||
|
||||
func testInsertBlock(t *testing.T, store store.Store) {
|
||||
@ -1069,14 +1076,15 @@ func testUndeleteBlockChildren(t *testing.T, store store.Store) {
|
||||
boards := createTestBoards(t, store, testTeamID, testUserID, 2)
|
||||
boardDelete := boards[0]
|
||||
boardKeep := boards[1]
|
||||
userID := testUserID
|
||||
|
||||
// create some blocks to be deleted
|
||||
cardsDelete := createTestCards(t, store, testUserID, boardDelete.ID, 3)
|
||||
cardsDelete := createTestCards(t, store, userID, boardDelete.ID, 3)
|
||||
blocksDelete := createTestBlocksForCard(t, store, cardsDelete[0].ID, 5)
|
||||
require.Len(t, blocksDelete, 5)
|
||||
|
||||
// create some blocks to keep
|
||||
cardsKeep := createTestCards(t, store, testUserID, boardKeep.ID, 3)
|
||||
cardsKeep := createTestCards(t, store, userID, boardKeep.ID, 3)
|
||||
blocksKeep := createTestBlocksForCard(t, store, cardsKeep[0].ID, 4)
|
||||
require.Len(t, blocksKeep, 4)
|
||||
|
||||
@ -1153,3 +1161,94 @@ func testUndeleteBlockChildren(t *testing.T, store store.Store) {
|
||||
assert.Len(t, blocks, len(blocksDelete)+len(cardsDelete))
|
||||
})
|
||||
}
|
||||
|
||||
func testGetBlockHistoryNewestChildren(t *testing.T, store store.Store) {
|
||||
boards := createTestBoards(t, store, testTeamID, testUserID, 2)
|
||||
board := boards[0]
|
||||
|
||||
const cardCount = 10
|
||||
const patchCount = 5
|
||||
|
||||
// create a card and some content blocks
|
||||
cards := createTestCards(t, store, testUserID, board.ID, 1)
|
||||
card := cards[0]
|
||||
content := createTestBlocksForCard(t, store, card.ID, cardCount)
|
||||
|
||||
// patch the content blocks to create some history records
|
||||
for i := 1; i <= patchCount; i++ {
|
||||
for _, block := range content {
|
||||
title := strconv.FormatInt(int64(i), 10)
|
||||
patch := &model.BlockPatch{
|
||||
Title: &title,
|
||||
}
|
||||
err := store.PatchBlock(block.ID, patch, testUserID)
|
||||
require.NoError(t, err, "error patching content blocks")
|
||||
}
|
||||
}
|
||||
|
||||
// delete some of the content blocks
|
||||
err := store.DeleteBlock(content[0].ID, testUserID)
|
||||
require.NoError(t, err, "error deleting content block")
|
||||
err = store.DeleteBlock(content[3].ID, testUserID)
|
||||
require.NoError(t, err, "error deleting content block")
|
||||
err = store.DeleteBlock(content[7].ID, testUserID)
|
||||
require.NoError(t, err, "error deleting content block")
|
||||
|
||||
t.Run("invalid card", func(t *testing.T) {
|
||||
opts := model.QueryBlockHistoryChildOptions{}
|
||||
blocks, hasMore, err := store.GetBlockHistoryNewestChildren(utils.NewID(utils.IDTypeCard), opts)
|
||||
require.NoError(t, err)
|
||||
require.False(t, hasMore)
|
||||
require.Empty(t, blocks)
|
||||
})
|
||||
|
||||
t.Run("valid card with no children", func(t *testing.T) {
|
||||
opts := model.QueryBlockHistoryChildOptions{}
|
||||
emptyCard := createTestCards(t, store, testUserID, board.ID, 1)[0]
|
||||
blocks, hasMore, err := store.GetBlockHistoryNewestChildren(emptyCard.ID, opts)
|
||||
require.NoError(t, err)
|
||||
require.False(t, hasMore)
|
||||
require.Empty(t, blocks)
|
||||
})
|
||||
|
||||
t.Run("valid card with children", func(t *testing.T) {
|
||||
opts := model.QueryBlockHistoryChildOptions{}
|
||||
blocks, hasMore, err := store.GetBlockHistoryNewestChildren(card.ID, opts)
|
||||
require.NoError(t, err)
|
||||
require.False(t, hasMore)
|
||||
require.Len(t, blocks, cardCount)
|
||||
require.ElementsMatch(t, extractIDs(t, blocks), extractIDs(t, content))
|
||||
|
||||
expected := strconv.FormatInt(patchCount, 10)
|
||||
for _, b := range blocks {
|
||||
require.Equal(t, expected, b.Title)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("pagination", func(t *testing.T) {
|
||||
opts := model.QueryBlockHistoryChildOptions{
|
||||
PerPage: 3,
|
||||
}
|
||||
|
||||
collected := make([]*model.Block, 0)
|
||||
reps := 0
|
||||
for {
|
||||
reps++
|
||||
blocks, hasMore, err := store.GetBlockHistoryNewestChildren(card.ID, opts)
|
||||
require.NoError(t, err)
|
||||
collected = append(collected, blocks...)
|
||||
if !hasMore {
|
||||
break
|
||||
}
|
||||
opts.Page++
|
||||
}
|
||||
|
||||
assert.Len(t, collected, cardCount)
|
||||
assert.Equal(t, math.Floor(float64(cardCount/opts.PerPage)+1), float64(reps))
|
||||
|
||||
expected := strconv.FormatInt(patchCount, 10)
|
||||
for _, b := range collected {
|
||||
require.Equal(t, expected, b.Title)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -11,15 +11,16 @@ import (
|
||||
type IDType byte
|
||||
|
||||
const (
|
||||
IDTypeNone IDType = '7'
|
||||
IDTypeTeam IDType = 't'
|
||||
IDTypeBoard IDType = 'b'
|
||||
IDTypeCard IDType = 'c'
|
||||
IDTypeView IDType = 'v'
|
||||
IDTypeSession IDType = 's'
|
||||
IDTypeUser IDType = 'u'
|
||||
IDTypeToken IDType = 'k'
|
||||
IDTypeBlock IDType = 'a'
|
||||
IDTypeNone IDType = '7'
|
||||
IDTypeTeam IDType = 't'
|
||||
IDTypeBoard IDType = 'b'
|
||||
IDTypeCard IDType = 'c'
|
||||
IDTypeView IDType = 'v'
|
||||
IDTypeSession IDType = 's'
|
||||
IDTypeUser IDType = 'u'
|
||||
IDTypeToken IDType = 'k'
|
||||
IDTypeBlock IDType = 'a'
|
||||
IDTypeAttachment IDType = 'i'
|
||||
)
|
||||
|
||||
// NewId is a globally unique identifier. It is a [A-Z0-9] string 27
|
||||
|
Loading…
Reference in New Issue
Block a user