Compare commits

...

9 Commits

Author SHA1 Message Date
wxiaoguang
b0f18726a3
Fix milestone title font problem (#22863)
Replace #22853 since it's closed, and actually there are 2 places need
to be fixed.

~~Follow @fsologureng 's suggestion to keep the `<hX>` tags.~~ 

Update: from fsologureng: this doesn't change anything from a11y's point
of view. So I think this PR could be fine to fix the UI looking problems
as a quick patch, then defer the a11y problems to new PRs together.

Before: the font-size is too large.

After: it seems better.

![image](https://user-images.githubusercontent.com/2114189/218266257-fc2d5872-9e96-4c6a-87ea-f27531ac15c0.png)

![image](https://user-images.githubusercontent.com/2114189/218266247-efc09d83-405f-4495-967a-30d9744134ce.png)

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-02-12 11:19:09 +08:00
gempir
e460b6ef4d
Fix PR file tree folders no longer collapsing (#22864)
Collapsing folders currently just throws a console error

```
index.js?v=1.19.0~dev-403-gb6b8feb3d:10 TypeError: this.$set is not a function
    at Proxy.handleClick (index.js?v=1.19.0~dev-403-gb6b8feb3d:58:7159)
    at index.js?v=1.19.0~dev-403-gb6b8feb3d:58:6466
    at index.js?v=1.19.0~dev-403-gb6b8feb3d:10:93922
    at ce (index.js?v=1.19.0~dev-403-gb6b8feb3d:10:1472)
    at Q (index.js?v=1.19.0~dev-403-gb6b8feb3d:10:1567)
    at HTMLDivElement.$e (index.js?v=1.19.0~dev-403-gb6b8feb3d:10:79198)
```

This PR fixes this and allows folders to be collapsed again.

Also:
- better cursor interaction with folders
- added some color to the diff detail stats
- remove green link color from all the file names

Screenshots:

![image](https://user-images.githubusercontent.com/9765622/218269712-2f3dda55-6d70-407f-8d34-2a5d9c8df548.png)

![image](https://user-images.githubusercontent.com/9765622/218269714-6ce8a954-daea-4ed6-9eea-8b2323db4d8f.png)

---------

Co-authored-by: zeripath <art27@cantab.net>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-02-12 10:34:46 +08:00
sillyguodong
51ab495198
escape filename when assemble URL (#22850)
Fixes: #22843 

### Cause:

affdd40296/services/repository/files/content.go (L161)

Previously, we did not escape the **"%"** that might be in "treePath"
when call "url.parse()".


![image](https://user-images.githubusercontent.com/33891828/218066318-5a909e50-2a17-46e6-b32f-684b2aa4b91f.png)

This function will check whether "%" is the beginning of an escape
character. Obviously, the "%" in the example (hello%mother.txt) is not
that. So, the function will return a error.

### Solution:
We can escape "treePath" by call "url.PathEscape()" function firstly.

### Screenshot:

![image](https://user-images.githubusercontent.com/33891828/218069781-1a030f8b-18d0-4804-b0f8-73997849ef43.png)

---------

Signed-off-by: Andrew Thornton <art27@cantab.net>
Co-authored-by: Andrew Thornton <art27@cantab.net>
2023-02-12 09:31:14 +08:00
silverwind
8fa54d0fda
Fix notification and stopwatch empty states (#22845)
Previous solution was relying on fomantic selector `.ui.label.hidden` to
hide the elements in their empty state, but this doesn't work any more
with the removal of the `label` class. Instead, introduce a standalone
CSS rule for the `hidden` class, which is universally usable as a single
class.

We can unfortunately not use the existing `hide` class because without
the `!important`, it does not have enough specificity to win against
fomantic's `.ui.menu:not(.vertical) .item {display: flex}` rule.

Followup and fixes regression from
https://github.com/go-gitea/gitea/pull/22169.

Before:

<img width="98" alt="image"
src="https://user-images.githubusercontent.com/115237/217959380-d3279ff3-526a-4ac4-9a18-3ab7c9ae91dd.png">

After:

<img width="77" alt="image"
src="https://user-images.githubusercontent.com/115237/217959463-44852716-cb25-4110-8481-668842ad4454.png">
2023-02-11 21:45:58 +00:00
zeripath
2152c4e98f
Fix .golangci.yml (#22868)
When we updated the .golangci.yml for 1.20 we should have used a string
as 1.20 is not a valid number.

In doing so we need to restore the nolint markings within the pq driver.

Signed-off-by: Andrew Thornton <art27@cantab.net>
2023-02-11 21:44:53 +00:00
Nathaniel Sabanski
d4a9b35c4b
Fix migration issue. (#22867)
See:
https://github.com/go-gitea/gitea/pull/22112#issuecomment-1426872992
2023-02-11 21:28:41 +00:00
KN4CK3R
9057a008a1
Add /$count endpoints for NuGet v2 (#22855)
Fixes #22838

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-02-11 19:30:44 +08:00
Nathaniel Sabanski
fb1a2a13f0
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>
2023-02-11 16:12:41 +08:00
wxiaoguang
e9288c2477
Fix improper HTMLURL usages in Go code (#22839)
In Go code, HTMLURL should be only used for external systems, like
API/webhook/mail/notification, etc.

If a URL is used by `Redirect` or rendered in a template, it should be a
relative URL (aka `Link()` in Gitea)

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-02-11 14:34:11 +08:00
45 changed files with 326 additions and 86 deletions

View File

@ -28,7 +28,7 @@ linters:
fast: false
run:
go: 1.20
go: "1.20"
timeout: 10m
skip-dirs:
- node_modules

View File

@ -37,7 +37,9 @@ func (d *postgresSchemaDriver) Open(name string) (driver.Conn, error) {
}
schemaValue, _ := driver.String.ConvertValue(setting.Database.Schema)
if execer, ok := conn.(driver.Execer); ok {
// golangci lint is incorrect here - there is no benefit to using driver.ExecerContext here
// and in any case pq does not implement it
if execer, ok := conn.(driver.Execer); ok { //nolint
_, err := execer.Exec(`SELECT set_config(
'search_path',
$1 || ',' || current_setting('search_path'),
@ -61,7 +63,8 @@ func (d *postgresSchemaDriver) Open(name string) (driver.Conn, error) {
// driver.String.ConvertValue will never return err for string
_, err = stmt.Exec([]driver.Value{schemaValue})
// golangci lint is incorrect here - there is no benefit to using stmt.ExecWithContext here
_, err = stmt.Exec([]driver.Value{schemaValue}) //nolint
if err != nil {
_ = conn.Close()
return nil, err

View File

@ -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

View File

@ -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 DEFAULT 0"`
}
return x.Sync(new(Project))
}

View File

@ -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

View File

@ -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
}

View File

@ -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(),

View File

@ -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)

View File

@ -274,7 +274,7 @@ func (repo *Repository) CommitLink(commitID string) (result string) {
if commitID == "" || commitID == "0000000000000000000000000000000000000000" {
result = ""
} else {
result = repo.HTMLURL() + "/commit/" + url.PathEscape(commitID)
result = repo.Link() + "/commit/" + url.PathEscape(commitID)
}
return result
}

View File

@ -743,9 +743,9 @@ func RepoAssignment(ctx *Context) (cancel context.CancelFunc) {
if ctx.FormString("go-get") == "1" {
ctx.Data["GoGetImport"] = ComposeGoGetImport(owner.Name, repo.Name)
prefix := repo.HTMLURL() + "/src/branch/" + util.PathEscapeSegments(ctx.Repo.BranchName)
ctx.Data["GoDocDirectory"] = prefix + "{/dir}"
ctx.Data["GoDocFile"] = prefix + "{/dir}/{file}#L{line}"
fullURLPrefix := repo.HTMLURL() + "/src/branch/" + util.PathEscapeSegments(ctx.Repo.BranchName)
ctx.Data["GoDocDirectory"] = fullURLPrefix + "{/dir}"
ctx.Data["GoDocFile"] = fullURLPrefix + "{/dir}/{file}#L{line}"
}
return cancel
}

View File

@ -4,7 +4,6 @@
package lfs
import (
"fmt"
"net/url"
"os"
"path"
@ -12,6 +11,7 @@ import (
"strings"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
)
// DetermineEndpoint determines an endpoint from the clone url or uses the specified LFS url.
@ -95,7 +95,7 @@ func endpointFromLocalPath(path string) *url.URL {
return nil
}
path = fmt.Sprintf("file://%s%s", slash, filepath.ToSlash(path))
path = "file://" + slash + util.PathEscapeSegments(filepath.ToSlash(path))
u, _ := url.Parse(path)

View File

@ -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

View File

@ -286,9 +286,18 @@ func CommonRoutes(ctx gocontext.Context) *web.Route {
}, reqPackageAccess(perm.AccessModeWrite))
r.Get("/symbols/{filename}/{guid:[0-9a-fA-F]{32}[fF]{8}}/{filename2}", nuget.DownloadSymbolFile)
r.Get("/Packages(Id='{id:[^']+}',Version='{version:[^']+}')", nuget.RegistrationLeafV2)
r.Get("/Packages()", nuget.SearchServiceV2)
r.Get("/FindPackagesById()", nuget.EnumeratePackageVersionsV2)
r.Get("/Search()", nuget.SearchServiceV2)
r.Group("/Packages()", func() {
r.Get("", nuget.SearchServiceV2)
r.Get("/$count", nuget.SearchServiceV2Count)
})
r.Group("/FindPackagesById()", func() {
r.Get("", nuget.EnumeratePackageVersionsV2)
r.Get("/$count", nuget.EnumeratePackageVersionsV2Count)
})
r.Group("/Search()", func() {
r.Get("", nuget.SearchServiceV2)
r.Get("/$count", nuget.SearchServiceV2Count)
})
}, reqPackageAccess(perm.AccessModeRead))
})
r.Group("/npm", func() {

View File

@ -10,6 +10,7 @@ import (
"io"
"net/http"
"regexp"
"strconv"
"strings"
"code.gitea.io/gitea/models/db"
@ -94,8 +95,7 @@ func FeedCapabilityResource(ctx *context.Context) {
var searchTermExtract = regexp.MustCompile(`'([^']+)'`)
// https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Protocol/LegacyFeed/V2FeedQueryBuilder.cs
func SearchServiceV2(ctx *context.Context) {
func getSearchTerm(ctx *context.Context) string {
searchTerm := strings.Trim(ctx.FormTrim("searchTerm"), "'")
if searchTerm == "" {
// $filter contains a query like:
@ -106,7 +106,11 @@ func SearchServiceV2(ctx *context.Context) {
searchTerm = strings.TrimSpace(match[1])
}
}
return searchTerm
}
// https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Protocol/LegacyFeed/V2FeedQueryBuilder.cs
func SearchServiceV2(ctx *context.Context) {
skip, take := ctx.FormInt("skip"), ctx.FormInt("take")
if skip == 0 {
skip = ctx.FormInt("$skip")
@ -118,7 +122,9 @@ func SearchServiceV2(ctx *context.Context) {
pvs, total, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeNuGet,
Name: packages_model.SearchValue{Value: searchTerm},
Name: packages_model.SearchValue{
Value: getSearchTerm(ctx),
},
IsInternal: util.OptionalBoolFalse,
Paginator: db.NewAbsoluteListOptions(
skip,
@ -145,6 +151,24 @@ func SearchServiceV2(ctx *context.Context) {
xmlResponse(ctx, http.StatusOK, resp)
}
// http://docs.oasis-open.org/odata/odata/v4.0/errata03/os/complete/part2-url-conventions/odata-v4.0-errata03-os-part2-url-conventions-complete.html#_Toc453752351
func SearchServiceV2Count(ctx *context.Context) {
count, err := packages_model.CountVersions(ctx, &packages_model.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeNuGet,
Name: packages_model.SearchValue{
Value: getSearchTerm(ctx),
},
IsInternal: util.OptionalBoolFalse,
})
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
ctx.PlainText(http.StatusOK, strconv.FormatInt(count, 10))
}
// https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-for-packages
func SearchServiceV3(ctx *context.Context) {
pvs, count, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
@ -288,6 +312,25 @@ func EnumeratePackageVersionsV2(ctx *context.Context) {
xmlResponse(ctx, http.StatusOK, resp)
}
// http://docs.oasis-open.org/odata/odata/v4.0/errata03/os/complete/part2-url-conventions/odata-v4.0-errata03-os-part2-url-conventions-complete.html#_Toc453752351
func EnumeratePackageVersionsV2Count(ctx *context.Context) {
count, err := packages_model.CountVersions(ctx, &packages_model.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeNuGet,
Name: packages_model.SearchValue{
ExactMatch: true,
Value: strings.Trim(ctx.FormTrim("id"), "'"),
},
IsInternal: util.OptionalBoolFalse,
})
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
ctx.PlainText(http.StatusOK, strconv.FormatInt(count, 10))
}
// https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#enumerate-package-versions
func EnumeratePackageVersionsV3(ctx *context.Context) {
packageName := ctx.Params("id")

View File

@ -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
}

View File

@ -70,7 +70,7 @@ func List(ctx *context.Context) {
}
ctx.Data["workflows"] = workflows
ctx.Data["RepoLink"] = ctx.Repo.Repository.HTMLURL()
ctx.Data["RepoLink"] = ctx.Repo.Repository.Link()
page := ctx.FormInt("page")
if page <= 0 {

View File

@ -100,7 +100,7 @@ func MustAllowUserComment(ctx *context.Context) {
if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin {
ctx.Flash.Error(ctx.Tr("repo.issues.comment_on_locked"))
ctx.Redirect(issue.HTMLURL())
ctx.Redirect(issue.Link())
return
}
}
@ -927,7 +927,7 @@ func NewIssueChooseTemplate(ctx *context.Context) {
if len(issueTemplates) == 0 {
// The "issues/new" and "issues/new/choose" share the same query parameters "project" and "milestone", if no template here, just redirect to the "issues/new" page with these parameters.
ctx.Redirect(fmt.Sprintf("%s/issues/new?%s", ctx.Repo.Repository.HTMLURL(), ctx.Req.URL.RawQuery), http.StatusSeeOther)
ctx.Redirect(fmt.Sprintf("%s/issues/new?%s", ctx.Repo.Repository.Link(), ctx.Req.URL.RawQuery), http.StatusSeeOther)
return
}
@ -950,11 +950,11 @@ func DeleteIssue(ctx *context.Context) {
}
if issue.IsPull {
ctx.Redirect(fmt.Sprintf("%s/pulls", ctx.Repo.Repository.HTMLURL()), http.StatusSeeOther)
ctx.Redirect(fmt.Sprintf("%s/pulls", ctx.Repo.Repository.Link()), http.StatusSeeOther)
return
}
ctx.Redirect(fmt.Sprintf("%s/issues", ctx.Repo.Repository.HTMLURL()), http.StatusSeeOther)
ctx.Redirect(fmt.Sprintf("%s/issues", ctx.Repo.Repository.Link()), http.StatusSeeOther)
}
// ValidateRepoMetas check and returns repository's meta information
@ -1425,7 +1425,7 @@ func ViewIssue(ctx *context.Context) {
return
}
// Add link to the issue of the already running stopwatch
ctx.Data["OtherStopwatchURL"] = otherIssue.HTMLURL()
ctx.Data["OtherStopwatchURL"] = otherIssue.Link()
}
}
ctx.Data["CanUseTimetracker"] = ctx.Repo.CanUseTimetracker(issue, ctx.Doer)
@ -2658,7 +2658,7 @@ func NewComment(ctx *context.Context) {
if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin {
ctx.Flash.Error(ctx.Tr("repo.issues.comment_on_locked"))
ctx.Redirect(issue.HTMLURL())
ctx.Redirect(issue.Link())
return
}
@ -2669,7 +2669,7 @@ func NewComment(ctx *context.Context) {
if ctx.HasError() {
ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
ctx.Redirect(issue.HTMLURL())
ctx.Redirect(issue.Link())
return
}

View File

@ -34,7 +34,7 @@ func AddDependency(ctx *context.Context) {
}
// Redirect
defer ctx.Redirect(issue.HTMLURL())
defer ctx.Redirect(issue.Link())
// Dependency
dep, err := issues_model.GetIssueByID(ctx, depID)
@ -124,5 +124,5 @@ func RemoveDependency(ctx *context.Context) {
}
// Redirect
ctx.Redirect(issue.HTMLURL())
ctx.Redirect(issue.Link())
}

View File

@ -21,13 +21,13 @@ func LockIssue(ctx *context.Context) {
if issue.IsLocked {
ctx.Flash.Error(ctx.Tr("repo.issues.lock_duplicate"))
ctx.Redirect(issue.HTMLURL())
ctx.Redirect(issue.Link())
return
}
if !form.HasValidReason() {
ctx.Flash.Error(ctx.Tr("repo.issues.lock.unknown_reason"))
ctx.Redirect(issue.HTMLURL())
ctx.Redirect(issue.Link())
return
}
@ -40,7 +40,7 @@ func LockIssue(ctx *context.Context) {
return
}
ctx.Redirect(issue.HTMLURL())
ctx.Redirect(issue.Link())
}
// UnlockIssue unlocks a previously locked issue.
@ -52,7 +52,7 @@ func UnlockIssue(ctx *context.Context) {
if !issue.IsLocked {
ctx.Flash.Error(ctx.Tr("repo.issues.unlock_error"))
ctx.Redirect(issue.HTMLURL())
ctx.Redirect(issue.Link())
return
}
@ -64,5 +64,5 @@ func UnlockIssue(ctx *context.Context) {
return
}
ctx.Redirect(issue.HTMLURL())
ctx.Redirect(issue.Link())
}

View File

@ -40,7 +40,7 @@ func IssueStopwatch(c *context.Context) {
c.Flash.Success(c.Tr("repo.issues.tracker_auto_close"))
}
url := issue.HTMLURL()
url := issue.Link()
c.Redirect(url, http.StatusSeeOther)
}
@ -72,7 +72,7 @@ func CancelStopwatch(c *context.Context) {
})
}
url := issue.HTMLURL()
url := issue.Link()
c.Redirect(url, http.StatusSeeOther)
}

View File

@ -26,7 +26,7 @@ func AddTimeManually(c *context.Context) {
c.NotFound("CanUseTimetracker", nil)
return
}
url := issue.HTMLURL()
url := issue.Link()
if c.HasError() {
c.Flash.Error(c.GetErrMsg())
@ -83,5 +83,5 @@ func DeleteTime(c *context.Context) {
}
c.Flash.Success(c.Tr("repo.issues.del_time_history", util.SecToTime(t.Time)))
c.Redirect(issue.HTMLURL())
c.Redirect(issue.Link())
}

View File

@ -52,5 +52,5 @@ func IssueWatch(ctx *context.Context) {
return
}
ctx.Redirect(issue.HTMLURL())
ctx.Redirect(issue.Link())
}

View File

@ -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 {

View File

@ -98,7 +98,7 @@ func CreateCodeComment(ctx *context.Context) {
renderConversation(ctx, comment)
return
}
ctx.Redirect(comment.HTMLURL())
ctx.Redirect(comment.Link())
}
// UpdateResolveConversation add or remove an Conversation resolved mark

View File

@ -295,7 +295,7 @@ func LatestRelease(ctx *context.Context) {
return
}
ctx.Redirect(release.HTMLURL())
ctx.Redirect(release.Link())
}
// NewRelease render creating or edit release page

View File

@ -344,7 +344,7 @@ func acceptOrRejectRepoTransfer(ctx *context.Context, accept bool) error {
ctx.Flash.Success(ctx.Tr("repo.settings.transfer.rejected"))
}
ctx.Redirect(ctx.Repo.Repository.HTMLURL())
ctx.Redirect(ctx.Repo.Repository.Link())
return nil
}

View File

@ -54,7 +54,7 @@ func Search(ctx *context.Context) {
ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable()
}
ctx.Data["SourcePath"] = ctx.Repo.Repository.HTMLURL()
ctx.Data["SourcePath"] = ctx.Repo.Repository.Link()
ctx.Data["SearchResults"] = searchResults
ctx.Data["SearchResultLanguages"] = searchResultLanguages

View File

@ -318,7 +318,7 @@ func renderReadmeFile(ctx *context.Context, readmeFile *namedBlob, readmeTreelin
if fInfo.isLFSFile {
filenameBase64 := base64.RawURLEncoding.EncodeToString([]byte(readmeFile.name))
ctx.Data["RawFileLink"] = fmt.Sprintf("%s.git/info/lfs/objects/%s/%s", ctx.Repo.Repository.HTMLURL(), url.PathEscape(fInfo.lfsMeta.Oid), url.PathEscape(filenameBase64))
ctx.Data["RawFileLink"] = fmt.Sprintf("%s.git/info/lfs/objects/%s/%s", ctx.Repo.Repository.Link(), url.PathEscape(fInfo.lfsMeta.Oid), url.PathEscape(filenameBase64))
}
if !fInfo.isTextFile {
@ -738,7 +738,7 @@ func Home(ctx *context.Context) {
}
ctx.Data["EnableFeed"] = true
ctx.Data["FeedURL"] = ctx.Repo.Repository.HTMLURL()
ctx.Data["FeedURL"] = ctx.Repo.Repository.Link()
}
checkHomeCodeViewable(ctx)

View File

@ -376,7 +376,7 @@ func PackageSettingsPost(ctx *context.Context) {
ctx.Flash.Success(ctx.Tr("packages.settings.delete.success"))
}
ctx.Redirect(ctx.Package.Owner.HTMLURL() + "/-/packages")
ctx.Redirect(ctx.Package.Owner.HomeLink() + "/-/packages")
return
}
}

View File

@ -47,7 +47,7 @@ func Profile(ctx *context.Context) {
}
// advertise feed via meta tag
ctx.Data["FeedURL"] = ctx.ContextUser.HTMLURL()
ctx.Data["FeedURL"] = ctx.ContextUser.HomeLink()
// Show OpenID URIs
openIDs, err := user_model.GetUserOpenIDs(ctx.ContextUser.ID)

View File

@ -59,7 +59,7 @@ func CreateCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er
Creator: creator,
CommitStatus: &git_model.CommitStatus{
SHA: sha,
TargetURL: run.HTMLURL(),
TargetURL: run.Link(),
Description: "",
Context: ctxname,
CreatorID: payload.Pusher.ID,

View File

@ -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"`
}

View File

@ -15,6 +15,7 @@ import (
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
)
// ContentType repo content type
@ -158,7 +159,7 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, treePath, ref
return nil, fmt.Errorf("no commit found for the ref [ref: %s]", ref)
}
selfURL, err := url.Parse(fmt.Sprintf("%s/contents/%s?ref=%s", repo.APIURL(), treePath, origRef))
selfURL, err := url.Parse(repo.APIURL() + "/contents/" + util.PathEscapeSegments(treePath) + "?ref=" + url.QueryEscape(origRef))
if err != nil {
return nil, err
}
@ -217,7 +218,7 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, treePath, ref
}
// Handle links
if entry.IsRegular() || entry.IsLink() {
downloadURL, err := url.Parse(fmt.Sprintf("%s/raw/%s/%s/%s", repo.HTMLURL(), refType, ref, treePath))
downloadURL, err := url.Parse(repo.HTMLURL() + "/raw/" + url.PathEscape(string(refType)) + "/" + util.PathEscapeSegments(ref) + "/" + util.PathEscapeSegments(treePath))
if err != nil {
return nil, err
}
@ -225,7 +226,7 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, treePath, ref
contentsResponse.DownloadURL = &downloadURLString
}
if !entry.IsSubModule() {
htmlURL, err := url.Parse(fmt.Sprintf("%s/src/%s/%s/%s", repo.HTMLURL(), refType, ref, treePath))
htmlURL, err := url.Parse(repo.HTMLURL() + "/src/" + url.PathEscape(string(refType)) + "/" + util.PathEscapeSegments(ref) + "/" + util.PathEscapeSegments(treePath))
if err != nil {
return nil, err
}
@ -233,7 +234,7 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, treePath, ref
contentsResponse.HTMLURL = &htmlURLString
contentsResponse.Links.HTMLURL = &htmlURLString
gitURL, err := url.Parse(fmt.Sprintf("%s/git/blobs/%s", repo.APIURL(), entry.ID.String()))
gitURL, err := url.Parse(repo.APIURL() + "/git/blobs/" + url.PathEscape(entry.ID.String()))
if err != nil {
return nil, err
}

View File

@ -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>

View File

@ -62,9 +62,9 @@
{{range .Milestones}}
<li class="item">
<div class="df ac sb">
<h2 class="df ac m-0 fw">
<h3 class="df ac m-0 fw">
{{svg "octicon-milestone" 16 "mr-3"}}<a class="muted" href="{{$.RepoLink}}/milestone/{{.ID}}">{{.Name}}</a>
</h2>
</h3>
<div class="df ac">
<span class="mr-3">{{.Completeness}}%</span>
<progress value="{{.Completeness}}" max="100"></progress>

View File

@ -34,17 +34,38 @@
</div>
{{if not .PageIsEditProjects}}
<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 := .ProjectTypes}}
{{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">
{{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 := .CardTypes}}
<div class="item" data-id="{{$element.CardType}}" data-value="{{$element.CardType}}">{{$.locale.Tr $element.Translation}}</div>
{{end}}
</div>
</div>
</div>
</div>
<div class="ui container">
<div class="ui divider"></div>

View File

@ -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}}">

View File

@ -81,10 +81,10 @@
{{range .Milestones}}
<li class="item">
<div class="df ac sb">
<h2 class="df ac m-0 fw">
<h3 class="df ac m-0 fw">
<span class="ui large label">{{.Repo.FullName}}</span>
{{svg "octicon-milestone" 16 "mr-3"}}<a class="muted" href="{{.Repo.Link}}/milestone/{{.ID}}">{{.Name}}</a>
</h2>
</h3>
<div class="df ac">
<span class="mr-3">{{.Completeness}}%</span>
<progress value="{{.Completeness}}" max="100"></progress>

View File

@ -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>

View File

@ -12,6 +12,7 @@ import (
"io"
"net/http"
"net/http/httptest"
"strconv"
"testing"
"time"
@ -109,8 +110,6 @@ func TestPackageNuGet(t *testing.T) {
url := fmt.Sprintf("/api/packages/%s/nuget", user.Name)
t.Run("ServiceIndex", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
t.Run("v2", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
@ -374,8 +373,6 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
})
t.Run("SearchService", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
cases := []struct {
Query string
Skip int
@ -391,8 +388,6 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
}
t.Run("v2", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
t.Run("Search()", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
@ -406,6 +401,12 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
assert.Equal(t, c.ExpectedTotal, result.Count, "case %d: unexpected total hits", i)
assert.Len(t, result.Entries, c.ExpectedResults, "case %d: unexpected result count", i)
req = NewRequest(t, "GET", fmt.Sprintf("%s/Search()/$count?searchTerm='%s'&skip=%d&take=%d", url, c.Query, c.Skip, c.Take))
req = AddBasicAuthHeader(req, user.Name)
resp = MakeRequest(t, req, http.StatusOK)
assert.Equal(t, strconv.FormatInt(c.ExpectedTotal, 10), resp.Body.String(), "case %d: unexpected total hits", i)
}
})
@ -413,7 +414,7 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
defer tests.PrintCurrentTest(t)()
for i, c := range cases {
req := NewRequest(t, "GET", fmt.Sprintf("%s/Search()?$filter=substringof('%s',tolower(Id))&$skip=%d&$top=%d", url, c.Query, c.Skip, c.Take))
req := NewRequest(t, "GET", fmt.Sprintf("%s/Packages()?$filter=substringof('%s',tolower(Id))&$skip=%d&$top=%d", url, c.Query, c.Skip, c.Take))
req = AddBasicAuthHeader(req, user.Name)
resp := MakeRequest(t, req, http.StatusOK)
@ -422,6 +423,12 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
assert.Equal(t, c.ExpectedTotal, result.Count, "case %d: unexpected total hits", i)
assert.Len(t, result.Entries, c.ExpectedResults, "case %d: unexpected result count", i)
req = NewRequest(t, "GET", fmt.Sprintf("%s/Packages()/$count?$filter=substringof('%s',tolower(Id))&$skip=%d&$top=%d", url, c.Query, c.Skip, c.Take))
req = AddBasicAuthHeader(req, user.Name)
resp = MakeRequest(t, req, http.StatusOK)
assert.Equal(t, strconv.FormatInt(c.ExpectedTotal, 10), resp.Body.String(), "case %d: unexpected total hits", i)
}
})
})
@ -512,8 +519,6 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
})
t.Run("RegistrationLeaf", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
t.Run("v2", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
@ -549,8 +554,6 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
})
t.Run("PackageService", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
t.Run("v2", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
@ -563,6 +566,12 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
assert.Len(t, result.Entries, 1)
assert.Equal(t, packageVersion, result.Entries[0].Properties.Version)
req = NewRequest(t, "GET", fmt.Sprintf("%s/FindPackagesById()/$count?id='%s'", url, packageName))
req = AddBasicAuthHeader(req, user.Name)
resp = MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "1", resp.Body.String())
})
t.Run("v3", func(t *testing.T) {

View File

@ -10,7 +10,7 @@
/>
<a
v-if="item.isFile"
class="file ellipsis"
class="file ellipsis muted"
:href="item.isFile ? '#diff-' + item.file.NameHash : ''"
>{{ item.name }}</a>
<SvgIcon
@ -63,7 +63,7 @@ export default {
if (itemIsFile) {
return;
}
this.$set(this, 'collapsed', !this.collapsed);
this.collapsed = !this.collapsed;
},
getIconForDiffType(pType) {
const diffTypes = {
@ -83,6 +83,7 @@ export default {
span.svg-icon.status {
float: right;
}
span.svg-icon.file {
color: var(--color-secondary-dark-7);
}
@ -122,6 +123,8 @@ span.svg-icon.octicon-diff-renamed {
div.directory {
display: grid;
grid-template-columns: 18px 20px auto;
user-select: none;
cursor: pointer;
}
div.directory:hover {

View File

@ -1803,6 +1803,7 @@ footer {
}
}
/* TODO: remove in favor of .hidden helper */
.hide {
display: none;

View File

@ -1614,6 +1614,20 @@
margin-right: .25rem;
}
// Because the translations contain the <strong> we need to style with nth-of-type
.diff-detail-stats strong:nth-of-type(1) {
color: var(--color-yellow);
}
.diff-detail-stats strong:nth-of-type(2) {
color: var(--color-green);
}
.diff-detail-stats strong:nth-of-type(3) {
color: var(--color-red);
}
.diff-detail-stats {
@media (max-width: 480px) {
font-size: 0;

View File

@ -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;

View File

@ -22,6 +22,7 @@
/* below class names match Tailwind CSS */
.pointer-events-none { pointer-events: none !important; }
.relative { position: relative !important; }
.hidden { display: none !important; }
.mono {
font-family: var(--fonts-monospace) !important;