From e06acf43580adc81e0ece46f17888450a4405f54 Mon Sep 17 00:00:00 2001 From: Nathaniel Sabanski Date: Mon, 12 Dec 2022 06:27:56 -0800 Subject: [PATCH 01/22] Preview images for Issue cards in Project Board view --- models/repo/attachment.go | 15 +++++++++++++++ routers/web/repo/projects.go | 11 +++++++++++ templates/repo/projects/view.tmpl | 5 +++++ web_src/less/_base.less | 2 +- web_src/less/features/projects.less | 19 +++++++++++++++++++ 5 files changed, 51 insertions(+), 1 deletion(-) diff --git a/models/repo/attachment.go b/models/repo/attachment.go index 8fbf79a7a053a..6db42cf51e9bf 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, 10) + 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/routers/web/repo/projects.go b/routers/web/repo/projects.go index 75cd290b8f0cb..420b708a8e266 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" @@ -302,6 +303,16 @@ func ViewProject(ctx *context.Context) { return } + 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/templates/repo/projects/view.tmpl b/templates/repo/projects/view.tmpl index 262efd2e0e800..b84edd12089c7 100644 --- a/templates/repo/projects/view.tmpl +++ b/templates/repo/projects/view.tmpl @@ -179,6 +179,11 @@
+
+ {{range (index $.issuesAttachmentMap .ID)}} + {{.Name}} + {{end}} +
diff --git a/web_src/less/_base.less b/web_src/less/_base.less index 022afcea927e7..ef2f97defbc91 100644 --- a/web_src/less/_base.less +++ b/web_src/less/_base.less @@ -980,7 +980,7 @@ a.commit-statuses-trigger { .ui.cards > .card > .content, .ui.card > .content { - border-color: var(--color-secondary); + border: none; } .ui.cards > .card > .extra, diff --git a/web_src/less/features/projects.less b/web_src/less/features/projects.less index 21514688b6fca..b8390247fc4fc 100644 --- a/web_src/less/features/projects.less +++ b/web_src/less/features/projects.less @@ -89,6 +89,25 @@ font-size: 16px !important; } +.board-card .card-attachment-image { + display: inline-block; + white-space: nowrap; + overflow: hidden; + text-align: center; +} + +.board-card .card-attachment-image img { + display: inline-block; + max-height: 50px; + border-radius: var(--border-radius); + margin-right: 2px; +} + +.board-card .card-attachment-image img:only-child { + max-height: 90px; + margin: auto; +} + .card-ghost { border-style: dashed !important; background: none !important; From 0afe78415a7c08cad1b7fab9e7867f8075f2975e Mon Sep 17 00:00:00 2001 From: Nathaniel Sabanski Date: Mon, 12 Dec 2022 06:40:11 -0800 Subject: [PATCH 02/22] Preview images for Issue cards in Project Board view --- templates/repo/projects/view.tmpl | 2 +- web_src/less/features/projects.less | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/repo/projects/view.tmpl b/templates/repo/projects/view.tmpl index b84edd12089c7..2ffe10e05a606 100644 --- a/templates/repo/projects/view.tmpl +++ b/templates/repo/projects/view.tmpl @@ -179,7 +179,7 @@
-
+
{{range (index $.issuesAttachmentMap .ID)}} {{.Name}} {{end}} diff --git a/web_src/less/features/projects.less b/web_src/less/features/projects.less index b8390247fc4fc..3c157691bab96 100644 --- a/web_src/less/features/projects.less +++ b/web_src/less/features/projects.less @@ -89,21 +89,21 @@ font-size: 16px !important; } -.board-card .card-attachment-image { +.board-card .card-attachment-images { display: inline-block; white-space: nowrap; overflow: hidden; text-align: center; } -.board-card .card-attachment-image img { +.board-card .card-attachment-images img { display: inline-block; max-height: 50px; border-radius: var(--border-radius); margin-right: 2px; } -.board-card .card-attachment-image img:only-child { +.board-card .card-attachment-images img:only-child { max-height: 90px; margin: auto; } From a3fcf656a1657bbbab7118c4bdf91fd6570082ac Mon Sep 17 00:00:00 2001 From: Nathaniel Sabanski Date: Wed, 14 Dec 2022 17:21:59 -0800 Subject: [PATCH 03/22] More specific selector for cleaned up card CSS. --- web_src/less/_base.less | 2 +- web_src/less/features/projects.less | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/web_src/less/_base.less b/web_src/less/_base.less index ef2f97defbc91..022afcea927e7 100644 --- a/web_src/less/_base.less +++ b/web_src/less/_base.less @@ -980,7 +980,7 @@ a.commit-statuses-trigger { .ui.cards > .card > .content, .ui.card > .content { - border: none; + border-color: var(--color-secondary); } .ui.cards > .card > .extra, diff --git a/web_src/less/features/projects.less b/web_src/less/features/projects.less index 3c157691bab96..7be222890c4e1 100644 --- a/web_src/less/features/projects.less +++ b/web_src/less/features/projects.less @@ -71,6 +71,10 @@ margin-right: auto !important; } +.board-column .ui.cards > .card > .content { + border: none; +} + .board-card { margin: 4px 2px !important; border-radius: 5px !important; From 3745fc3421b5e4f01f822f546b93961706ee1af7 Mon Sep 17 00:00:00 2001 From: Nathaniel Sabanski Date: Thu, 15 Dec 2022 12:25:52 -0800 Subject: [PATCH 04/22] Added Project option to change Card Type (Image Previews vs Text Only) --- models/project/board.go | 22 ++++++++++++++++++ models/project/project.go | 38 +++++++++++++++++++++++++------ models/project/project_test.go | 1 + options/locale/locale_en-US.ini | 3 +++ routers/web/repo/projects.go | 15 +++++++++--- services/forms/repo_form.go | 2 ++ templates/repo/projects/new.tmpl | 33 ++++++++++++++++++++++----- templates/repo/projects/view.tmpl | 6 +++-- templates/user/project.tmpl | 2 +- 9 files changed, 103 insertions(+), 19 deletions(-) diff --git a/models/project/board.go b/models/project/board.go index d8468f0cb55c6..9b54bbfd136cb 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,15 @@ const ( BoardTypeBugTriage ) +const ( + // CardTypeTextOnly is a project board card type that is text only + CardTypeTextOnly CardType = iota + 1 + + // 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 +97,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 bcf1166408f85..2c8dda3a36b77 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -17,10 +17,16 @@ import ( ) type ( - // ProjectsConfig is used to identify the type of board that is being created - ProjectsConfig struct { - BoardType BoardType - Translation string + // 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 @@ -86,6 +92,7 @@ type Project struct { CreatorID int64 `xorm:"NOT NULL"` IsClosed bool `xorm:"INDEX"` BoardType BoardType + CardType CardType Type Type RenderedContent string `xorm:"-"` @@ -99,15 +106,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 { @@ -175,6 +190,10 @@ func NewProject(p *Project) error { p.BoardType = BoardTypeNone } + if !IsCardTypeValid(p.CardType) { + p.CardType = CardTypeTextOnly + } + if !IsTypeValid(p.Type) { return errors.New("project type is not valid") } @@ -216,9 +235,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 4fde0fc7ce3a4..3d89aa55a27ec 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/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 70f982b8dcc59..3e2afe87806f8 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1219,6 +1219,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/repo/projects.go b/routers/web/repo/projects.go index 420b708a8e266..a7dc233d025ec 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -124,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) } @@ -136,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 } @@ -147,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) @@ -213,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 { @@ -230,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) } @@ -240,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) @@ -262,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 @@ -668,7 +675,8 @@ func MoveIssues(ctx *context.Context) { // CreateProject renders the generic project creation page func CreateProject(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, tplGenericProjectsNew) @@ -699,6 +707,7 @@ func CreateProjectPost(ctx *context.Context, form forms.UserCreateProjectForm) { Description: form.Content, CreatorID: user.ID, BoardType: form.BoardType, + CardType: form.CardType, Type: projectType, }); err != nil { ctx.ServerError("NewProject", err) diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 89a013d9af8d0..093bc8cd284d7 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -510,6 +510,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 @@ -518,6 +519,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/repo/projects/new.tmpl b/templates/repo/projects/new.tmpl index 5edca90886bc6..801b5bb41af88 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 2ffe10e05a606..64813dcbcc226 100644 --- a/templates/repo/projects/view.tmpl +++ b/templates/repo/projects/view.tmpl @@ -180,8 +180,10 @@
- {{range (index $.issuesAttachmentMap .ID)}} - {{.Name}} + {{if eq $.Project.CardType 2}} + {{range (index $.issuesAttachmentMap .ID)}} + {{.Name}} + {{end}} {{end}}
diff --git a/templates/user/project.tmpl b/templates/user/project.tmpl index d38d84d95cfb7..916bbaec617ef 100644 --- a/templates/user/project.tmpl +++ b/templates/user/project.tmpl @@ -48,7 +48,7 @@
{{.locale.Tr "repo.projects.template.desc_helper"}}
From 5f57f9713496e8c05bad726f5f9da166bbb13182 Mon Sep 17 00:00:00 2001 From: Nathaniel Sabanski Date: Thu, 15 Dec 2022 12:34:56 -0800 Subject: [PATCH 05/22] gofmt --- models/project/board.go | 1 - models/project/project.go | 8 ++++---- templates/repo/projects/new.tmpl | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/models/project/board.go b/models/project/board.go index 9b54bbfd136cb..26ff45d3d8b02 100644 --- a/models/project/board.go +++ b/models/project/board.go @@ -45,7 +45,6 @@ const ( CardTypeImagesAndText ) - // BoardColorPattern is a regexp witch can validate BoardColor var BoardColorPattern = regexp.MustCompile("^#[0-9a-fA-F]{6}$") diff --git a/models/project/project.go b/models/project/project.go index 2c8dda3a36b77..9d8241d8c64d1 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -19,14 +19,14 @@ import ( type ( // BoardConfig is used to identify the type of board that is being created BoardConfig struct { - BoardType BoardType - Translation string + BoardType BoardType + Translation string } // CardConfig is used to identify the type of board card that is being used CardConfig struct { - CardType CardType - Translation string + CardType CardType + Translation string } // Type is used to identify the type of project in question and ownership diff --git a/templates/repo/projects/new.tmpl b/templates/repo/projects/new.tmpl index 801b5bb41af88..28efe1eccb363 100644 --- a/templates/repo/projects/new.tmpl +++ b/templates/repo/projects/new.tmpl @@ -53,7 +53,7 @@