nexo/services/core/server/public/model/view.go

260 lines
7.4 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"fmt"
"maps"
"net/http"
"strings"
"unicode/utf8"
)
type ViewType string
const (
ViewTypeKanban ViewType = "kanban"
ViewTitleMaxRunes = 256
ViewDescriptionMaxRunes = 1024
MaxViewsPerChannel = 50
BoardsPropertyGroupName = "boards"
BoardsPropertyFieldNameBoard = "board"
BoardsPropertyFieldAssignee = "assignee"
BoardsPropertyFieldStatus = "status"
BoardsStatusOptionTodo = "Todo"
BoardsStatusOptionInProgress = "In Progress"
BoardsStatusOptionComplete = "Complete"
MaxKanbanColumns = 100
)
// KanbanColumn represents a single column in a kanban view.
// Each column maps to one or more option IDs from the grouped property field.
type KanbanColumn struct {
ID string `json:"id"`
Name string `json:"name"`
OptionIDs []string `json:"option_ids"`
}
// KanbanGroupBy defines how a kanban view groups cards into columns.
type KanbanGroupBy struct {
FieldID string `json:"field_id"`
Columns []KanbanColumn `json:"columns"`
}
// KanbanProps is the typed representation of View.Props for kanban views.
type KanbanProps struct {
GroupBy KanbanGroupBy `json:"group_by"`
}
// ToProps converts KanbanProps to a StringInterface map for storage in View.Props.
func (kp *KanbanProps) ToProps() (StringInterface, error) {
raw, err := json.Marshal(kp)
if err != nil {
return nil, fmt.Errorf("kanban props marshal: %w", err)
}
var m StringInterface
if err := json.Unmarshal(raw, &m); err != nil {
return nil, fmt.Errorf("kanban props unmarshal: %w", err)
}
return m, nil
}
// KanbanPropsFromProps parses View.Props into a typed KanbanProps.
func KanbanPropsFromProps(props StringInterface) (*KanbanProps, error) {
raw, err := json.Marshal(props)
if err != nil {
return nil, fmt.Errorf("kanban props marshal: %w", err)
}
var kp KanbanProps
if err := json.Unmarshal(raw, &kp); err != nil {
return nil, fmt.Errorf("kanban props unmarshal: %w", err)
}
return &kp, nil
}
type View struct {
Id string `json:"id"`
ChannelId string `json:"channel_id"`
Type ViewType `json:"type"`
CreatorId string `json:"creator_id"`
Title string `json:"title"`
Description string `json:"description,omitempty"`
SortOrder int `json:"sort_order"`
Props StringInterface `json:"props,omitempty"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
DeleteAt int64 `json:"delete_at"`
}
type ViewPatch struct {
Title *string `json:"title"`
Description *string `json:"description"`
SortOrder *int `json:"sort_order"`
Props *StringInterface `json:"props"`
}
type ViewsWithCount struct {
Views []*View `json:"views"`
TotalCount int64 `json:"total_count"`
}
const ViewQueryDefaultPerPage = 20
const ViewQueryMaxPerPage = 200
type ViewQueryOpts struct {
// Page is the 0-based page number for limit/offset pagination.
Page int
// PerPage specifies the page size. Zero defaults to ViewQueryDefaultPerPage (20).
// Values above ViewQueryMaxPerPage (200) are clamped to ViewQueryMaxPerPage.
PerPage int
}
func (o *View) Auditable() map[string]any {
return map[string]any{
"id": o.Id,
"channel_id": o.ChannelId,
"type": o.Type,
"creator_id": o.CreatorId,
"create_at": o.CreateAt,
"update_at": o.UpdateAt,
"delete_at": o.DeleteAt,
}
}
func (o *View) Clone() *View {
if o == nil {
return nil
}
v := *o
if o.Props != nil {
v.Props = make(StringInterface, len(o.Props))
maps.Copy(v.Props, o.Props)
}
return &v
}
func (o *View) IsValid() *AppError {
if !IsValidId(o.Id) {
return NewAppError("View.IsValid", "model.view.is_valid.id.app_error", nil, "", http.StatusBadRequest)
}
if !IsValidId(o.ChannelId) {
return NewAppError("View.IsValid", "model.view.is_valid.channel_id.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if !IsValidId(o.CreatorId) {
return NewAppError("View.IsValid", "model.view.is_valid.creator_id.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if o.Type != ViewTypeKanban {
return NewAppError("View.IsValid", "model.view.is_valid.type.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if strings.TrimSpace(o.Title) == "" || utf8.RuneCountInString(o.Title) > ViewTitleMaxRunes {
return NewAppError("View.IsValid", "model.view.is_valid.title.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if utf8.RuneCountInString(o.Description) > ViewDescriptionMaxRunes {
return NewAppError("View.IsValid", "model.view.is_valid.description.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if o.CreateAt == 0 {
return NewAppError("View.IsValid", "model.view.is_valid.create_at.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if o.UpdateAt == 0 {
return NewAppError("View.IsValid", "model.view.is_valid.update_at.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if err := validateViewProps(o.Type, o.Props); err != nil {
return err
}
return nil
}
// validateViewProps validates the props map based on the view type.
func validateViewProps(viewType ViewType, props StringInterface) *AppError {
if viewType == ViewTypeKanban {
return validateKanbanProps(props)
}
return nil
}
func validateKanbanProps(props StringInterface) *AppError {
if props == nil {
return NewAppError("View.IsValid", "model.view.is_valid.props.kanban_required.app_error", nil, "", http.StatusBadRequest)
}
kanban, err := KanbanPropsFromProps(props)
if err != nil {
return NewAppError("View.IsValid", "model.view.is_valid.props.kanban_invalid.app_error", nil, err.Error(), http.StatusBadRequest)
}
if !IsValidId(kanban.GroupBy.FieldID) {
return NewAppError("View.IsValid", "model.view.is_valid.props.kanban_field_id.app_error", nil, "", http.StatusBadRequest)
}
if len(kanban.GroupBy.Columns) == 0 {
return NewAppError("View.IsValid", "model.view.is_valid.props.kanban_columns_empty.app_error", nil, "", http.StatusBadRequest)
}
if len(kanban.GroupBy.Columns) > MaxKanbanColumns {
return NewAppError("View.IsValid", "model.view.is_valid.props.kanban_columns_max.app_error", nil, "", http.StatusBadRequest)
}
for i, col := range kanban.GroupBy.Columns {
if !IsValidId(col.ID) {
return NewAppError("View.IsValid", "model.view.is_valid.props.kanban_column_id.app_error", map[string]any{"Index": i}, "", http.StatusBadRequest)
}
if strings.TrimSpace(col.Name) == "" {
return NewAppError("View.IsValid", "model.view.is_valid.props.kanban_column_name.app_error", map[string]any{"Index": i}, "", http.StatusBadRequest)
}
if len(col.OptionIDs) == 0 {
return NewAppError("View.IsValid", "model.view.is_valid.props.kanban_column_options.app_error", map[string]any{"Index": i}, "", http.StatusBadRequest)
}
}
return nil
}
func (o *View) PreSave() {
if o.Id == "" {
o.Id = NewId()
}
if o.CreateAt == 0 {
o.CreateAt = GetMillis()
}
o.UpdateAt = o.CreateAt
o.DeleteAt = 0
}
func (o *View) PreUpdate() {
o.UpdateAt = GetMillis()
}
func (o *View) Patch(patch *ViewPatch) {
if patch == nil {
return
}
if patch.Title != nil {
o.Title = *patch.Title
}
if patch.Description != nil {
o.Description = *patch.Description
}
if patch.SortOrder != nil {
o.SortOrder = *patch.SortOrder
}
if patch.Props != nil {
o.Props = make(StringInterface, len(*patch.Props))
maps.Copy(o.Props, *patch.Props)
}
}