Skip to content

Commit

Permalink
Preview images for Issue cards in Project Board view (#22112)
Browse files Browse the repository at this point in the history
Original Issue: #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>
  • Loading branch information
4 people authored Feb 11, 2023
1 parent e9288c2 commit fb1a2a1
Show file tree
Hide file tree
Showing 15 changed files with 173 additions and 17 deletions.
2 changes: 2 additions & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions models/migrations/v1_19/v241.go
Original file line number Diff line number Diff line change
@@ -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))
}
21 changes: 21 additions & 0 deletions models/project/board.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand All @@ -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}$")

Expand Down Expand Up @@ -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

Expand Down
34 changes: 29 additions & 5 deletions models/project/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down Expand Up @@ -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:"-"`
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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")
}
Expand Down Expand Up @@ -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
}
Expand Down
1 change: 1 addition & 0 deletions models/project/project_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
15 changes: 15 additions & 0 deletions models/repo/attachment.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions routers/web/org/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
}
Expand Down
24 changes: 22 additions & 2 deletions routers/web/repo/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
Expand All @@ -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
}
Expand All @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
}
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions services/forms/repo_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"`
}

Expand Down
2 changes: 1 addition & 1 deletion templates/projects/new.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
33 changes: 27 additions & 6 deletions templates/repo/projects/new.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
7 changes: 7 additions & 0 deletions templates/repo/projects/view.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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}}">
Expand Down
2 changes: 1 addition & 1 deletion templates/user/project.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Loading

0 comments on commit fb1a2a1

Please sign in to comment.