Skip to content

Commit fb1a2a1

Browse files
gnatwolfogrelunnydelvh
authored
Preview images for Issue cards in Project Board view (#22112)
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 <[email protected]> Co-authored-by: Lunny Xiao <[email protected]> Co-authored-by: delvh <[email protected]>
1 parent e9288c2 commit fb1a2a1

File tree

15 files changed

+173
-17
lines changed

15 files changed

+173
-17
lines changed

models/migrations/migrations.go

+2
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,8 @@ var migrations = []Migration{
455455
NewMigration("Add scope for access_token", v1_19.AddScopeForAccessTokens),
456456
// v240 -> v241
457457
NewMigration("Add actions tables", v1_19.AddActionsTables),
458+
// v241 -> v242
459+
NewMigration("Add card_type column to project table", v1_19.AddCardTypeToProjectTable),
458460
}
459461

460462
// GetCurrentDBVersion returns the current db version

models/migrations/v1_19/v241.go

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright 2022 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package v1_19 //nolint
5+
6+
import (
7+
"xorm.io/xorm"
8+
)
9+
10+
// AddCardTypeToProjectTable: add CardType column, setting existing rows to CardTypeTextOnly
11+
func AddCardTypeToProjectTable(x *xorm.Engine) error {
12+
type Project struct {
13+
CardType int `xorm:"NOT NULL"`
14+
}
15+
16+
return x.Sync(new(Project))
17+
}

models/project/board.go

+21
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ type (
1919
// BoardType is used to represent a project board type
2020
BoardType uint8
2121

22+
// CardType is used to represent a project board card type
23+
CardType uint8
24+
2225
// BoardList is a list of all project boards in a repository
2326
BoardList []*Board
2427
)
@@ -34,6 +37,14 @@ const (
3437
BoardTypeBugTriage
3538
)
3639

40+
const (
41+
// CardTypeTextOnly is a project board card type that is text only
42+
CardTypeTextOnly CardType = iota
43+
44+
// CardTypeImagesAndText is a project board card type that has images and text
45+
CardTypeImagesAndText
46+
)
47+
3748
// BoardColorPattern is a regexp witch can validate BoardColor
3849
var BoardColorPattern = regexp.MustCompile("^#[0-9a-fA-F]{6}$")
3950

@@ -85,6 +96,16 @@ func IsBoardTypeValid(p BoardType) bool {
8596
}
8697
}
8798

99+
// IsCardTypeValid checks if the project board card type is valid
100+
func IsCardTypeValid(p CardType) bool {
101+
switch p {
102+
case CardTypeTextOnly, CardTypeImagesAndText:
103+
return true
104+
default:
105+
return false
106+
}
107+
}
108+
88109
func createBoardsForProjectsType(ctx context.Context, project *Project) error {
89110
var items []string
90111

models/project/project.go

+29-5
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,18 @@ import (
1919
)
2020

2121
type (
22-
// ProjectsConfig is used to identify the type of board that is being created
23-
ProjectsConfig struct {
22+
// BoardConfig is used to identify the type of board that is being created
23+
BoardConfig struct {
2424
BoardType BoardType
2525
Translation string
2626
}
2727

28+
// CardConfig is used to identify the type of board card that is being used
29+
CardConfig struct {
30+
CardType CardType
31+
Translation string
32+
}
33+
2834
// Type is used to identify the type of project in question and ownership
2935
Type uint8
3036
)
@@ -91,6 +97,7 @@ type Project struct {
9197
CreatorID int64 `xorm:"NOT NULL"`
9298
IsClosed bool `xorm:"INDEX"`
9399
BoardType BoardType
100+
CardType CardType
94101
Type Type
95102

96103
RenderedContent string `xorm:"-"`
@@ -145,15 +152,23 @@ func init() {
145152
db.RegisterModel(new(Project))
146153
}
147154

148-
// GetProjectsConfig retrieves the types of configurations projects could have
149-
func GetProjectsConfig() []ProjectsConfig {
150-
return []ProjectsConfig{
155+
// GetBoardConfig retrieves the types of configurations project boards could have
156+
func GetBoardConfig() []BoardConfig {
157+
return []BoardConfig{
151158
{BoardTypeNone, "repo.projects.type.none"},
152159
{BoardTypeBasicKanban, "repo.projects.type.basic_kanban"},
153160
{BoardTypeBugTriage, "repo.projects.type.bug_triage"},
154161
}
155162
}
156163

164+
// GetCardConfig retrieves the types of configurations project board cards could have
165+
func GetCardConfig() []CardConfig {
166+
return []CardConfig{
167+
{CardTypeTextOnly, "repo.projects.card_type.text_only"},
168+
{CardTypeImagesAndText, "repo.projects.card_type.images_and_text"},
169+
}
170+
}
171+
157172
// IsTypeValid checks if a project type is valid
158173
func IsTypeValid(p Type) bool {
159174
switch p {
@@ -237,6 +252,10 @@ func NewProject(p *Project) error {
237252
p.BoardType = BoardTypeNone
238253
}
239254

255+
if !IsCardTypeValid(p.CardType) {
256+
p.CardType = CardTypeTextOnly
257+
}
258+
240259
if !IsTypeValid(p.Type) {
241260
return util.NewInvalidArgumentErrorf("project type is not valid")
242261
}
@@ -280,9 +299,14 @@ func GetProjectByID(ctx context.Context, id int64) (*Project, error) {
280299

281300
// UpdateProject updates project properties
282301
func UpdateProject(ctx context.Context, p *Project) error {
302+
if !IsCardTypeValid(p.CardType) {
303+
p.CardType = CardTypeTextOnly
304+
}
305+
283306
_, err := db.GetEngine(ctx).ID(p.ID).Cols(
284307
"title",
285308
"description",
309+
"card_type",
286310
).Update(p)
287311
return err
288312
}

models/project/project_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ func TestProject(t *testing.T) {
5353
project := &Project{
5454
Type: TypeRepository,
5555
BoardType: BoardTypeBasicKanban,
56+
CardType: CardTypeTextOnly,
5657
Title: "New Project",
5758
RepoID: 1,
5859
CreatedUnix: timeutil.TimeStampNow(),

models/repo/attachment.go

+15
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,21 @@ func GetAttachmentsByIssueID(ctx context.Context, issueID int64) ([]*Attachment,
132132
return attachments, db.GetEngine(ctx).Where("issue_id = ? AND comment_id = 0", issueID).Find(&attachments)
133133
}
134134

135+
// GetAttachmentsByIssueIDImagesLatest returns the latest image attachments of an issue.
136+
func GetAttachmentsByIssueIDImagesLatest(ctx context.Context, issueID int64) ([]*Attachment, error) {
137+
attachments := make([]*Attachment, 0, 5)
138+
return attachments, db.GetEngine(ctx).Where(`issue_id = ? AND (name like '%.apng'
139+
OR name like '%.avif'
140+
OR name like '%.bmp'
141+
OR name like '%.gif'
142+
OR name like '%.jpg'
143+
OR name like '%.jpeg'
144+
OR name like '%.jxl'
145+
OR name like '%.png'
146+
OR name like '%.svg'
147+
OR name like '%.webp')`, issueID).Desc("comment_id").Limit(5).Find(&attachments)
148+
}
149+
135150
// GetAttachmentsByCommentID returns all attachments if comment by given ID.
136151
func GetAttachmentsByCommentID(ctx context.Context, commentID int64) ([]*Attachment, error) {
137152
attachments := make([]*Attachment, 0, 10)

options/locale/locale_en-US.ini

+3
Original file line numberDiff line numberDiff line change
@@ -1231,6 +1231,9 @@ projects.board.color = "Color"
12311231
projects.open = Open
12321232
projects.close = Close
12331233
projects.board.assigned_to = Assigned to
1234+
projects.card_type.desc = "Card Previews"
1235+
projects.card_type.images_and_text = "Images and Text"
1236+
projects.card_type.text_only = "Text Only"
12341237
12351238
issues.desc = Organize bug reports, tasks and milestones.
12361239
issues.filter_assignees = Filter Assignee

routers/web/org/projects.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ func canWriteUnit(ctx *context.Context) bool {
121121
// NewProject render creating a project page
122122
func NewProject(ctx *context.Context) {
123123
ctx.Data["Title"] = ctx.Tr("repo.projects.new")
124-
ctx.Data["ProjectTypes"] = project_model.GetProjectsConfig()
124+
ctx.Data["BoardTypes"] = project_model.GetBoardConfig()
125125
ctx.Data["CanWriteProjects"] = canWriteUnit(ctx)
126126
ctx.Data["HomeLink"] = ctx.ContextUser.HomeLink()
127127
shared_user.RenderUserHeader(ctx)
@@ -137,7 +137,7 @@ func NewProjectPost(ctx *context.Context) {
137137
if ctx.HasError() {
138138
ctx.Data["CanWriteProjects"] = canWriteUnit(ctx)
139139
ctx.Data["PageIsViewProjects"] = true
140-
ctx.Data["ProjectTypes"] = project_model.GetProjectsConfig()
140+
ctx.Data["BoardTypes"] = project_model.GetBoardConfig()
141141
ctx.HTML(http.StatusOK, tplProjectsNew)
142142
return
143143
}

routers/web/repo/projects.go

+22-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
issues_model "code.gitea.io/gitea/models/issues"
1414
"code.gitea.io/gitea/models/perm"
1515
project_model "code.gitea.io/gitea/models/project"
16+
attachment_model "code.gitea.io/gitea/models/repo"
1617
"code.gitea.io/gitea/models/unit"
1718
"code.gitea.io/gitea/modules/base"
1819
"code.gitea.io/gitea/modules/context"
@@ -123,7 +124,8 @@ func Projects(ctx *context.Context) {
123124
// NewProject render creating a project page
124125
func NewProject(ctx *context.Context) {
125126
ctx.Data["Title"] = ctx.Tr("repo.projects.new")
126-
ctx.Data["ProjectTypes"] = project_model.GetProjectsConfig()
127+
ctx.Data["BoardTypes"] = project_model.GetBoardConfig()
128+
ctx.Data["CardTypes"] = project_model.GetCardConfig()
127129
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
128130
ctx.HTML(http.StatusOK, tplProjectsNew)
129131
}
@@ -135,7 +137,8 @@ func NewProjectPost(ctx *context.Context) {
135137

136138
if ctx.HasError() {
137139
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
138-
ctx.Data["ProjectTypes"] = project_model.GetProjectsConfig()
140+
ctx.Data["BoardTypes"] = project_model.GetBoardConfig()
141+
ctx.Data["CardTypes"] = project_model.GetCardConfig()
139142
ctx.HTML(http.StatusOK, tplProjectsNew)
140143
return
141144
}
@@ -146,6 +149,7 @@ func NewProjectPost(ctx *context.Context) {
146149
Description: form.Content,
147150
CreatorID: ctx.Doer.ID,
148151
BoardType: form.BoardType,
152+
CardType: form.CardType,
149153
Type: project_model.TypeRepository,
150154
}); err != nil {
151155
ctx.ServerError("NewProject", err)
@@ -212,6 +216,7 @@ func EditProject(ctx *context.Context) {
212216
ctx.Data["Title"] = ctx.Tr("repo.projects.edit")
213217
ctx.Data["PageIsEditProjects"] = true
214218
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
219+
ctx.Data["CardTypes"] = project_model.GetCardConfig()
215220

216221
p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
217222
if err != nil {
@@ -229,6 +234,7 @@ func EditProject(ctx *context.Context) {
229234

230235
ctx.Data["title"] = p.Title
231236
ctx.Data["content"] = p.Description
237+
ctx.Data["card_type"] = p.CardType
232238

233239
ctx.HTML(http.StatusOK, tplProjectsNew)
234240
}
@@ -239,6 +245,7 @@ func EditProjectPost(ctx *context.Context) {
239245
ctx.Data["Title"] = ctx.Tr("repo.projects.edit")
240246
ctx.Data["PageIsEditProjects"] = true
241247
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
248+
ctx.Data["CardTypes"] = project_model.GetCardConfig()
242249

243250
if ctx.HasError() {
244251
ctx.HTML(http.StatusOK, tplProjectsNew)
@@ -261,6 +268,7 @@ func EditProjectPost(ctx *context.Context) {
261268

262269
p.Title = form.Title
263270
p.Description = form.Content
271+
p.CardType = form.CardType
264272
if err = project_model.UpdateProject(ctx, p); err != nil {
265273
ctx.ServerError("UpdateProjects", err)
266274
return
@@ -302,6 +310,18 @@ func ViewProject(ctx *context.Context) {
302310
return
303311
}
304312

313+
if project.CardType != project_model.CardTypeTextOnly {
314+
issuesAttachmentMap := make(map[int64][]*attachment_model.Attachment)
315+
for _, issuesList := range issuesMap {
316+
for _, issue := range issuesList {
317+
if issueAttachment, err := attachment_model.GetAttachmentsByIssueIDImagesLatest(ctx, issue.ID); err == nil {
318+
issuesAttachmentMap[issue.ID] = issueAttachment
319+
}
320+
}
321+
}
322+
ctx.Data["issuesAttachmentMap"] = issuesAttachmentMap
323+
}
324+
305325
linkedPrsMap := make(map[int64][]*issues_model.Issue)
306326
for _, issuesList := range issuesMap {
307327
for _, issue := range issuesList {

services/forms/repo_form.go

+2
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,7 @@ type CreateProjectForm struct {
512512
Title string `binding:"Required;MaxSize(100)"`
513513
Content string
514514
BoardType project_model.BoardType
515+
CardType project_model.CardType
515516
}
516517

517518
// UserCreateProjectForm is a from for creating an individual or organization
@@ -520,6 +521,7 @@ type UserCreateProjectForm struct {
520521
Title string `binding:"Required;MaxSize(100)"`
521522
Content string
522523
BoardType project_model.BoardType
524+
CardType project_model.CardType
523525
UID int64 `binding:"Required"`
524526
}
525527

templates/projects/new.tmpl

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
<input type="hidden" name="board_type" value="{{.type}}">
3737
<div class="default text">{{.locale.Tr "repo.projects.template.desc_helper"}}</div>
3838
<div class="menu">
39-
{{range $element := .ProjectTypes}}
39+
{{range $element := .BoardTypes}}
4040
<div class="item" data-id="{{$element.BoardType}}" data-value="{{$element.BoardType}}">{{$.locale.Tr $element.Translation}}</div>
4141
{{end}}
4242
</div>

templates/repo/projects/new.tmpl

+27-6
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,38 @@
3434
</div>
3535

3636
{{if not .PageIsEditProjects}}
37-
<label>{{.locale.Tr "repo.projects.template.desc"}}</label>
37+
<div class="field">
38+
<label>{{.locale.Tr "repo.projects.template.desc"}}</label>
39+
<div class="ui selection dropdown">
40+
<input type="hidden" name="board_type" value="{{.type}}">
41+
<div class="default text">{{.locale.Tr "repo.projects.template.desc_helper"}}</div>
42+
<div class="menu">
43+
{{range $element := .BoardTypes}}
44+
<div class="item" data-id="{{$element.BoardType}}" data-value="{{$element.BoardType}}">{{$.locale.Tr $element.Translation}}</div>
45+
{{end}}
46+
</div>
47+
</div>
48+
</div>
49+
{{end}}
50+
51+
<div class="field">
52+
<label>{{.locale.Tr "repo.projects.card_type.desc"}}</label>
3853
<div class="ui selection dropdown">
39-
<input type="hidden" name="board_type" value="{{.type}}">
40-
<div class="default text">{{.locale.Tr "repo.projects.template.desc_helper"}}</div>
54+
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
55+
{{range $element := .CardTypes}}
56+
{{if or (eq $.card_type $element.CardType) (and (not $.card_type) (eq $element.CardType 2))}}
57+
<input type="hidden" name="card_type" value="{{$element.CardType}}">
58+
<div class="default text">{{$.locale.Tr $element.Translation}}</div>
59+
{{end}}
60+
{{end}}
4161
<div class="menu">
42-
{{range $element := .ProjectTypes}}
43-
<div class="item" data-id="{{$element.BoardType}}" data-value="{{$element.BoardType}}">{{$.locale.Tr $element.Translation}}</div>
62+
{{range $element := .CardTypes}}
63+
<div class="item" data-id="{{$element.CardType}}" data-value="{{$element.CardType}}">{{$.locale.Tr $element.Translation}}</div>
4464
{{end}}
4565
</div>
4666
</div>
47-
{{end}}
67+
</div>
68+
4869
</div>
4970
<div class="ui container">
5071
<div class="ui divider"></div>

templates/repo/projects/view.tmpl

+7
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,13 @@
179179

180180
<!-- start issue card -->
181181
<div class="card board-card" data-issue="{{.ID}}">
182+
{{if eq $.Project.CardType 1}}{{/* Images and Text*/}}
183+
<div class="card-attachment-images">
184+
{{range (index $.issuesAttachmentMap .ID)}}
185+
<img src="{{.DownloadURL}}" alt="{{.Name}}" />
186+
{{end}}
187+
</div>
188+
{{end}}
182189
<div class="content p-0">
183190
<div class="header">
184191
<span class="dif ac vm {{if .IsClosed}}red{{else}}green{{end}}">

templates/user/project.tmpl

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
<input type="hidden" name="board_type" value="{{.type}}">
4949
<div class="default text">{{.locale.Tr "repo.projects.template.desc_helper"}}</div>
5050
<div class="menu">
51-
{{range $element := .ProjectTypes}}
51+
{{range $element := .BoardTypes}}
5252
<div class="item" data-id="{{$element.BoardType}}" data-value="{{$element.BoardType}}">{{$.locale.Tr $element.Translation}}</div>
5353
{{end}}
5454
</div>

0 commit comments

Comments
 (0)