diff options
author | Nathaniel Sabanski <sabanski.n@gmail.com> | 2023-02-11 00:12:41 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-02-11 16:12:41 +0800 |
commit | fb1a2a13f05665764a6f91ed1a421b9e93c4e8c9 (patch) | |
tree | 7b1501f82d484811bd5630f3fc9233f132ee0df4 | |
parent | e9288c24773157411edec17c9bbcc8c1567e91ee (diff) | |
download | gitea-fb1a2a13f05665764a6f91ed1a421b9e93c4e8c9.tar.gz gitea-fb1a2a13f05665764a6f91ed1a421b9e93c4e8c9.zip |
Preview images for Issue cards in Project Board view (#22112)
Original Issue: https://github.com/go-gitea/gitea/issues/22102
This addition would be a big benefit for design and art teams using the
issue tracking.
The preview will be the latest "image type" attachments on an issue-
simple, and allows for automatic updates of the cover image as issue
progress is made!
This would make Gitea competitive with Trello... wouldn't it be amazing
to say goodbye to Atlassian products? Ha.
First image is the most recent, the SQL will fetch up to 5 latest images
(URL string).
All images supported by browsers plus upcoming formats: *.avif *.bmp
*.gif *.jpg *.jpeg *.jxl *.png *.svg *.webp
The CSS will try to center-align images until it cannot, then it will
left align with overflow hidden. Single images get to be slightly
larger!
Tested so far on: Chrome, Firefox, Android Chrome, Android Firefox.
Current revision with light and dark themes:
![image](https://user-images.githubusercontent.com/24665/207066878-58e6bf73-0c93-4caa-8d40-38f4432b3578.png)
![image](https://user-images.githubusercontent.com/24665/207066555-293f65c3-e706-4888-8516-de8ec632d638.png)
---------
Co-authored-by: Jason Song <i@wolfogre.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: delvh <dev.lh@web.de>
-rw-r--r-- | models/migrations/migrations.go | 2 | ||||
-rw-r--r-- | models/migrations/v1_19/v241.go | 17 | ||||
-rw-r--r-- | models/project/board.go | 21 | ||||
-rw-r--r-- | models/project/project.go | 34 | ||||
-rw-r--r-- | models/project/project_test.go | 1 | ||||
-rw-r--r-- | models/repo/attachment.go | 15 | ||||
-rw-r--r-- | options/locale/locale_en-US.ini | 3 | ||||
-rw-r--r-- | routers/web/org/projects.go | 4 | ||||
-rw-r--r-- | routers/web/repo/projects.go | 24 | ||||
-rw-r--r-- | services/forms/repo_form.go | 2 | ||||
-rw-r--r-- | templates/projects/new.tmpl | 2 | ||||
-rw-r--r-- | templates/repo/projects/new.tmpl | 33 | ||||
-rw-r--r-- | templates/repo/projects/view.tmpl | 7 | ||||
-rw-r--r-- | templates/user/project.tmpl | 2 | ||||
-rw-r--r-- | web_src/less/features/projects.less | 23 |
15 files changed, 173 insertions, 17 deletions
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 15600f057c..79e8573881 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 0000000000..332be580fa --- /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 d8468f0cb5..dc4e2e6882 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 9074fd0c15..931ef44675 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 c2d9005c43..6caa244f54 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 8fbf79a7a0..cb05386d93 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 f784b10c8d..5d0fd044f4 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 1ce44d4866..6449d12de1 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 3becf799c5..967b81c608 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 436d79df68..db336e25e3 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 04192c64c2..19bf503692 100644 --- a/templates/projects/new.tmpl +++ b/templates/projects/new.tmpl @@ -36,7 +36,7 @@ <input type="hidden" name="board_type" value="{{.type}}"> <div class="default text">{{.locale.Tr "repo.projects.template.desc_helper"}}</div> <div class="menu"> - {{range $element := .ProjectTypes}} + {{range $element := .BoardTypes}} <div class="item" data-id="{{$element.BoardType}}" data-value="{{$element.BoardType}}">{{$.locale.Tr $element.Translation}}</div> {{end}} </div> diff --git a/templates/repo/projects/new.tmpl b/templates/repo/projects/new.tmpl index 79f9380dce..c90fa4369c 100644 --- a/templates/repo/projects/new.tmpl +++ b/templates/repo/projects/new.tmpl @@ -34,17 +34,38 @@ </div> {{if not .PageIsEditProjects}} - <label>{{.locale.Tr "repo.projects.template.desc"}}</label> + <div class="field"> + <label>{{.locale.Tr "repo.projects.template.desc"}}</label> + <div class="ui selection dropdown"> + <input type="hidden" name="board_type" value="{{.type}}"> + <div class="default text">{{.locale.Tr "repo.projects.template.desc_helper"}}</div> + <div class="menu"> + {{range $element := .BoardTypes}} + <div class="item" data-id="{{$element.BoardType}}" data-value="{{$element.BoardType}}">{{$.locale.Tr $element.Translation}}</div> + {{end}} + </div> + </div> + </div> + {{end}} + + <div class="field"> + <label>{{.locale.Tr "repo.projects.card_type.desc"}}</label> <div class="ui selection dropdown"> - <input type="hidden" name="board_type" value="{{.type}}"> - <div class="default text">{{.locale.Tr "repo.projects.template.desc_helper"}}</div> + {{svg "octicon-triangle-down" 14 "dropdown icon"}} + {{range $element := .CardTypes}} + {{if or (eq $.card_type $element.CardType) (and (not $.card_type) (eq $element.CardType 2))}} + <input type="hidden" name="card_type" value="{{$element.CardType}}"> + <div class="default text">{{$.locale.Tr $element.Translation}}</div> + {{end}} + {{end}} <div class="menu"> - {{range $element := .ProjectTypes}} - <div class="item" data-id="{{$element.BoardType}}" data-value="{{$element.BoardType}}">{{$.locale.Tr $element.Translation}}</div> + {{range $element := .CardTypes}} + <div class="item" data-id="{{$element.CardType}}" data-value="{{$element.CardType}}">{{$.locale.Tr $element.Translation}}</div> {{end}} </div> </div> - {{end}} + </div> + </div> <div class="ui container"> <div class="ui divider"></div> diff --git a/templates/repo/projects/view.tmpl b/templates/repo/projects/view.tmpl index 63d2727b63..711b488180 100644 --- a/templates/repo/projects/view.tmpl +++ b/templates/repo/projects/view.tmpl @@ -179,6 +179,13 @@ <!-- start issue card --> <div class="card board-card" data-issue="{{.ID}}"> + {{if eq $.Project.CardType 1}}{{/* Images and Text*/}} + <div class="card-attachment-images"> + {{range (index $.issuesAttachmentMap .ID)}} + <img src="{{.DownloadURL}}" alt="{{.Name}}" /> + {{end}} + </div> + {{end}} <div class="content p-0"> <div class="header"> <span class="dif ac vm {{if .IsClosed}}red{{else}}green{{end}}"> diff --git a/templates/user/project.tmpl b/templates/user/project.tmpl index 59eff13aa7..7016c4d8b7 100644 --- a/templates/user/project.tmpl +++ b/templates/user/project.tmpl @@ -48,7 +48,7 @@ <input type="hidden" name="board_type" value="{{.type}}"> <div class="default text">{{.locale.Tr "repo.projects.template.desc_helper"}}</div> <div class="menu"> - {{range $element := .ProjectTypes}} + {{range $element := .BoardTypes}} <div class="item" data-id="{{$element.BoardType}}" data-value="{{$element.BoardType}}">{{$.locale.Tr $element.Translation}}</div> {{end}} </div> diff --git a/web_src/less/features/projects.less b/web_src/less/features/projects.less index b0f674060a..cbdb1a3c9f 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; |