focalboard/server/model/card.go

324 lines
7.5 KiB
Go
Raw Normal View History

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
}