diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 15600f057..79e857388 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -455,6 +455,8 @@ var migrations = []Migration{ NewMigration("Add scope for access_token", v1_19.AddScopeForAccessTokens), // v240 -> v241 NewMigration("Add actions tables", v1_19.AddActionsTables), + // v241 -> v242 + NewMigration("Add card_type column to project table", v1_19.AddCardTypeToProjectTable), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v1_19/v241.go b/models/migrations/v1_19/v241.go new file mode 100644 index 000000000..332be580f --- /dev/null +++ b/models/migrations/v1_19/v241.go @@ -0,0 +1,17 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_19 //nolint + +import ( + "xorm.io/xorm" +) + +// AddCardTypeToProjectTable: add CardType column, setting existing rows to CardTypeTextOnly +func AddCardTypeToProjectTable(x *xorm.Engine) error { + type Project struct { + CardType int `xorm:"NOT NULL"` + } + + return x.Sync(new(Project)) +} diff --git a/models/project/board.go b/models/project/board.go index d8468f0cb..dc4e2e688 100644 --- a/models/project/board.go +++ b/models/project/board.go @@ -19,6 +19,9 @@ type ( // BoardType is used to represent a project board type BoardType uint8 + // CardType is used to represent a project board card type + CardType uint8 + // BoardList is a list of all project boards in a repository BoardList []*Board ) @@ -34,6 +37,14 @@ const ( BoardTypeBugTriage ) +const ( + // CardTypeTextOnly is a project board card type that is text only + CardTypeTextOnly CardType = iota + + // CardTypeImagesAndText is a project board card type that has images and text + CardTypeImagesAndText +) + // BoardColorPattern is a regexp witch can validate BoardColor var BoardColorPattern = regexp.MustCompile("^#[0-9a-fA-F]{6}$") @@ -85,6 +96,16 @@ func IsBoardTypeValid(p BoardType) bool { } } +// IsCardTypeValid checks if the project board card type is valid +func IsCardTypeValid(p CardType) bool { + switch p { + case CardTypeTextOnly, CardTypeImagesAndText: + return true + default: + return false + } +} + func createBoardsForProjectsType(ctx context.Context, project *Project) error { var items []string diff --git a/models/project/project.go b/models/project/project.go index 9074fd0c1..931ef4467 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -19,12 +19,18 @@ import ( ) type ( - // ProjectsConfig is used to identify the type of board that is being created - ProjectsConfig struct { + // BoardConfig is used to identify the type of board that is being created + BoardConfig struct { BoardType BoardType Translation string } + // CardConfig is used to identify the type of board card that is being used + CardConfig struct { + CardType CardType + Translation string + } + // Type is used to identify the type of project in question and ownership Type uint8 ) @@ -91,6 +97,7 @@ type Project struct { CreatorID int64 `xorm:"NOT NULL"` IsClosed bool `xorm:"INDEX"` BoardType BoardType + CardType CardType Type Type RenderedContent string `xorm:"-"` @@ -145,15 +152,23 @@ func init() { db.RegisterModel(new(Project)) } -// GetProjectsConfig retrieves the types of configurations projects could have -func GetProjectsConfig() []ProjectsConfig { - return []ProjectsConfig{ +// GetBoardConfig retrieves the types of configurations project boards could have +func GetBoardConfig() []BoardConfig { + return []BoardConfig{ {BoardTypeNone, "repo.projects.type.none"}, {BoardTypeBasicKanban, "repo.projects.type.basic_kanban"}, {BoardTypeBugTriage, "repo.projects.type.bug_triage"}, } } +// GetCardConfig retrieves the types of configurations project board cards could have +func GetCardConfig() []CardConfig { + return []CardConfig{ + {CardTypeTextOnly, "repo.projects.card_type.text_only"}, + {CardTypeImagesAndText, "repo.projects.card_type.images_and_text"}, + } +} + // IsTypeValid checks if a project type is valid func IsTypeValid(p Type) bool { switch p { @@ -237,6 +252,10 @@ func NewProject(p *Project) error { p.BoardType = BoardTypeNone } + if !IsCardTypeValid(p.CardType) { + p.CardType = CardTypeTextOnly + } + if !IsTypeValid(p.Type) { return util.NewInvalidArgumentErrorf("project type is not valid") } @@ -280,9 +299,14 @@ func GetProjectByID(ctx context.Context, id int64) (*Project, error) { // UpdateProject updates project properties func UpdateProject(ctx context.Context, p *Project) error { + if !IsCardTypeValid(p.CardType) { + p.CardType = CardTypeTextOnly + } + _, err := db.GetEngine(ctx).ID(p.ID).Cols( "title", "description", + "card_type", ).Update(p) return err } diff --git a/models/project/project_test.go b/models/project/project_test.go index c2d9005c4..6caa244f5 100644 --- a/models/project/project_test.go +++ b/models/project/project_test.go @@ -53,6 +53,7 @@ func TestProject(t *testing.T) { project := &Project{ Type: TypeRepository, BoardType: BoardTypeBasicKanban, + CardType: CardTypeTextOnly, Title: "New Project", RepoID: 1, CreatedUnix: timeutil.TimeStampNow(), diff --git a/models/repo/attachment.go b/models/repo/attachment.go index 8fbf79a7a..cb05386d9 100644 --- a/models/repo/attachment.go +++ b/models/repo/attachment.go @@ -132,6 +132,21 @@ func GetAttachmentsByIssueID(ctx context.Context, issueID int64) ([]*Attachment, return attachments, db.GetEngine(ctx).Where("issue_id = ? AND comment_id = 0", issueID).Find(&attachments) } +// GetAttachmentsByIssueIDImagesLatest returns the latest image attachments of an issue. +func GetAttachmentsByIssueIDImagesLatest(ctx context.Context, issueID int64) ([]*Attachment, error) { + attachments := make([]*Attachment, 0, 5) + return attachments, db.GetEngine(ctx).Where(`issue_id = ? AND (name like '%.apng' + OR name like '%.avif' + OR name like '%.bmp' + OR name like '%.gif' + OR name like '%.jpg' + OR name like '%.jpeg' + OR name like '%.jxl' + OR name like '%.png' + OR name like '%.svg' + OR name like '%.webp')`, issueID).Desc("comment_id").Limit(5).Find(&attachments) +} + // GetAttachmentsByCommentID returns all attachments if comment by given ID. func GetAttachmentsByCommentID(ctx context.Context, commentID int64) ([]*Attachment, error) { attachments := make([]*Attachment, 0, 10) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index f784b10c8..5d0fd044f 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1231,6 +1231,9 @@ projects.board.color = "Color" projects.open = Open projects.close = Close projects.board.assigned_to = Assigned to +projects.card_type.desc = "Card Previews" +projects.card_type.images_and_text = "Images and Text" +projects.card_type.text_only = "Text Only" issues.desc = Organize bug reports, tasks and milestones. issues.filter_assignees = Filter Assignee diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index 1ce44d486..6449d12de 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -121,7 +121,7 @@ func canWriteUnit(ctx *context.Context) bool { // NewProject render creating a project page func NewProject(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.projects.new") - ctx.Data["ProjectTypes"] = project_model.GetProjectsConfig() + ctx.Data["BoardTypes"] = project_model.GetBoardConfig() ctx.Data["CanWriteProjects"] = canWriteUnit(ctx) ctx.Data["HomeLink"] = ctx.ContextUser.HomeLink() shared_user.RenderUserHeader(ctx) @@ -137,7 +137,7 @@ func NewProjectPost(ctx *context.Context) { if ctx.HasError() { ctx.Data["CanWriteProjects"] = canWriteUnit(ctx) ctx.Data["PageIsViewProjects"] = true - ctx.Data["ProjectTypes"] = project_model.GetProjectsConfig() + ctx.Data["BoardTypes"] = project_model.GetBoardConfig() ctx.HTML(http.StatusOK, tplProjectsNew) return } diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 3becf799c..967b81c60 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -13,6 +13,7 @@ import ( issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/perm" project_model "code.gitea.io/gitea/models/project" + attachment_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" @@ -123,7 +124,8 @@ func Projects(ctx *context.Context) { // NewProject render creating a project page func NewProject(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.projects.new") - ctx.Data["ProjectTypes"] = project_model.GetProjectsConfig() + ctx.Data["BoardTypes"] = project_model.GetBoardConfig() + ctx.Data["CardTypes"] = project_model.GetCardConfig() ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) ctx.HTML(http.StatusOK, tplProjectsNew) } @@ -135,7 +137,8 @@ func NewProjectPost(ctx *context.Context) { if ctx.HasError() { ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) - ctx.Data["ProjectTypes"] = project_model.GetProjectsConfig() + ctx.Data["BoardTypes"] = project_model.GetBoardConfig() + ctx.Data["CardTypes"] = project_model.GetCardConfig() ctx.HTML(http.StatusOK, tplProjectsNew) return } @@ -146,6 +149,7 @@ func NewProjectPost(ctx *context.Context) { Description: form.Content, CreatorID: ctx.Doer.ID, BoardType: form.BoardType, + CardType: form.CardType, Type: project_model.TypeRepository, }); err != nil { ctx.ServerError("NewProject", err) @@ -212,6 +216,7 @@ func EditProject(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.projects.edit") ctx.Data["PageIsEditProjects"] = true ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) + ctx.Data["CardTypes"] = project_model.GetCardConfig() p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) if err != nil { @@ -229,6 +234,7 @@ func EditProject(ctx *context.Context) { ctx.Data["title"] = p.Title ctx.Data["content"] = p.Description + ctx.Data["card_type"] = p.CardType ctx.HTML(http.StatusOK, tplProjectsNew) } @@ -239,6 +245,7 @@ func EditProjectPost(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.projects.edit") ctx.Data["PageIsEditProjects"] = true ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) + ctx.Data["CardTypes"] = project_model.GetCardConfig() if ctx.HasError() { ctx.HTML(http.StatusOK, tplProjectsNew) @@ -261,6 +268,7 @@ func EditProjectPost(ctx *context.Context) { p.Title = form.Title p.Description = form.Content + p.CardType = form.CardType if err = project_model.UpdateProject(ctx, p); err != nil { ctx.ServerError("UpdateProjects", err) return @@ -302,6 +310,18 @@ func ViewProject(ctx *context.Context) { return } + if project.CardType != project_model.CardTypeTextOnly { + issuesAttachmentMap := make(map[int64][]*attachment_model.Attachment) + for _, issuesList := range issuesMap { + for _, issue := range issuesList { + if issueAttachment, err := attachment_model.GetAttachmentsByIssueIDImagesLatest(ctx, issue.ID); err == nil { + issuesAttachmentMap[issue.ID] = issueAttachment + } + } + } + ctx.Data["issuesAttachmentMap"] = issuesAttachmentMap + } + linkedPrsMap := make(map[int64][]*issues_model.Issue) for _, issuesList := range issuesMap { for _, issue := range issuesList { diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 436d79df6..db336e25e 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -512,6 +512,7 @@ type CreateProjectForm struct { Title string `binding:"Required;MaxSize(100)"` Content string BoardType project_model.BoardType + CardType project_model.CardType } // UserCreateProjectForm is a from for creating an individual or organization @@ -520,6 +521,7 @@ type UserCreateProjectForm struct { Title string `binding:"Required;MaxSize(100)"` Content string BoardType project_model.BoardType + CardType project_model.CardType UID int64 `binding:"Required"` } diff --git a/templates/projects/new.tmpl b/templates/projects/new.tmpl index 04192c64c..19bf50369 100644 --- a/templates/projects/new.tmpl +++ b/templates/projects/new.tmpl @@ -36,7 +36,7 @@
{{.locale.Tr "repo.projects.template.desc_helper"}}
diff --git a/templates/repo/projects/new.tmpl b/templates/repo/projects/new.tmpl index 79f9380dc..c90fa4369 100644 --- a/templates/repo/projects/new.tmpl +++ b/templates/repo/projects/new.tmpl @@ -34,17 +34,38 @@ {{if not .PageIsEditProjects}} - +
+ + +
+ {{end}} + +
+ - {{end}} +
+
diff --git a/templates/repo/projects/view.tmpl b/templates/repo/projects/view.tmpl index 63d2727b6..711b48818 100644 --- a/templates/repo/projects/view.tmpl +++ b/templates/repo/projects/view.tmpl @@ -179,6 +179,13 @@
+ {{if eq $.Project.CardType 1}}{{/* Images and Text*/}} +
+ {{range (index $.issuesAttachmentMap .ID)}} + {{.Name}} + {{end}} +
+ {{end}}
diff --git a/templates/user/project.tmpl b/templates/user/project.tmpl index 59eff13aa..7016c4d8b 100644 --- a/templates/user/project.tmpl +++ b/templates/user/project.tmpl @@ -48,7 +48,7 @@
{{.locale.Tr "repo.projects.template.desc_helper"}}
diff --git a/web_src/less/features/projects.less b/web_src/less/features/projects.less index b0f674060..cbdb1a3c9 100644 --- a/web_src/less/features/projects.less +++ b/web_src/less/features/projects.less @@ -72,6 +72,10 @@ margin-right: auto !important; } +.board-column .ui.cards > .card > .content { + border: none; +} + .board-card { margin: 4px 2px !important; border-radius: 5px !important; @@ -90,6 +94,25 @@ font-size: 16px !important; } +.board-card .card-attachment-images { + display: inline-block; + white-space: nowrap; + overflow: hidden; + text-align: center; +} + +.board-card .card-attachment-images img { + display: inline-block; + max-height: 50px; + border-radius: var(--border-radius); + margin-right: 2px; +} + +.board-card .card-attachment-images img:only-child { + max-height: 90px; + margin: auto; +} + .card-ghost { border-style: dashed !important; background: none !important;