0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-05-21 00:24:57 +02:00

Merge remote-tracking branch 'origin/main' into playwright-on-unsupported

This commit is contained in:
TheFox0x7 2026-04-20 22:01:44 +02:00
commit 2991c5057a
No known key found for this signature in database
80 changed files with 319 additions and 642 deletions

View File

@ -519,7 +519,8 @@ test-mssql-migration: migrations.mssql.test migrations.individual.mssql.test
.PHONY: playwright
playwright: deps-frontend
@pnpm exec playwright install --with-deps chromium firefox webkit $(PLAYWRIGHT_FLAGS)
@# on GitHub Actions VMs, playwright's system deps are pre-installed
@pnpm exec playwright install $(if $(GITHUB_ACTIONS),,--with-deps) chromium firefox $(PLAYWRIGHT_FLAGS)
.PHONY: test-e2e
test-e2e: $(EXECUTABLE_E2E)

View File

@ -7,7 +7,6 @@ import (
"context"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/translation"
@ -25,12 +24,6 @@ func (runs RunList) GetUserIDs() []int64 {
})
}
func (runs RunList) GetRepoIDs() []int64 {
return container.FilterSlice(runs, func(run *ActionRun) (int64, bool) {
return run.RepoID, true
})
}
func (runs RunList) LoadTriggerUser(ctx context.Context) error {
userIDs := runs.GetUserIDs()
users := make(map[int64]*user_model.User, len(userIDs))
@ -50,18 +43,6 @@ func (runs RunList) LoadTriggerUser(ctx context.Context) error {
return nil
}
func (runs RunList) LoadRepos(ctx context.Context) error {
repoIDs := runs.GetRepoIDs()
repos, err := repo_model.GetRepositoriesMapByIDs(ctx, repoIDs)
if err != nil {
return err
}
for _, run := range runs {
run.Repo = repos[run.RepoID]
}
return nil
}
type FindRunOptions struct {
db.ListOptions
RepoID int64

View File

@ -4,62 +4,13 @@
package actions
import (
"context"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container"
"xorm.io/builder"
)
type ScheduleList []*ActionSchedule
// GetUserIDs returns a slice of user's id
func (schedules ScheduleList) GetUserIDs() []int64 {
return container.FilterSlice(schedules, func(schedule *ActionSchedule) (int64, bool) {
return schedule.TriggerUserID, true
})
}
func (schedules ScheduleList) GetRepoIDs() []int64 {
return container.FilterSlice(schedules, func(schedule *ActionSchedule) (int64, bool) {
return schedule.RepoID, true
})
}
func (schedules ScheduleList) LoadTriggerUser(ctx context.Context) error {
userIDs := schedules.GetUserIDs()
users := make(map[int64]*user_model.User, len(userIDs))
if err := db.GetEngine(ctx).In("id", userIDs).Find(&users); err != nil {
return err
}
for _, schedule := range schedules {
if schedule.TriggerUserID == user_model.ActionsUserID {
schedule.TriggerUser = user_model.NewActionsUser()
} else {
schedule.TriggerUser = users[schedule.TriggerUserID]
if schedule.TriggerUser == nil {
schedule.TriggerUser = user_model.NewGhostUser()
}
}
}
return nil
}
func (schedules ScheduleList) LoadRepos(ctx context.Context) error {
repoIDs := schedules.GetRepoIDs()
repos, err := repo_model.GetRepositoriesMapByIDs(ctx, repoIDs)
if err != nil {
return err
}
for _, schedule := range schedules {
schedule.Repo = repos[schedule.RepoID]
}
return nil
}
type FindScheduleOptions struct {
db.ListOptions
RepoID int64

View File

@ -282,9 +282,3 @@ func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, err
return actions, count, nil
}
func CountUserFeeds(ctx context.Context, userID int64) (int64, error) {
return db.GetEngine(ctx).Where("user_id = ?", userID).
And("is_deleted = ?", false).
Count(&Action{})
}

View File

@ -192,28 +192,6 @@ func (err ErrGPGKeyIDAlreadyUsed) Unwrap() error {
return util.ErrAlreadyExist
}
// ErrGPGKeyAccessDenied represents a "GPGKeyAccessDenied" kind of Error.
type ErrGPGKeyAccessDenied struct {
UserID int64
KeyID int64
}
// IsErrGPGKeyAccessDenied checks if an error is a ErrGPGKeyAccessDenied.
func IsErrGPGKeyAccessDenied(err error) bool {
_, ok := err.(ErrGPGKeyAccessDenied)
return ok
}
// Error pretty-prints an error of type ErrGPGKeyAccessDenied.
func (err ErrGPGKeyAccessDenied) Error() string {
return fmt.Sprintf("user does not have access to the key [user_id: %d, key_id: %d]",
err.UserID, err.KeyID)
}
func (err ErrGPGKeyAccessDenied) Unwrap() error {
return util.ErrPermissionDenied
}
// ErrKeyAccessDenied represents a "KeyAccessDenied" kind of error.
type ErrKeyAccessDenied struct {
UserID int64

View File

@ -105,14 +105,6 @@ func addDeployKey(ctx context.Context, keyID, repoID int64, name, fingerprint st
return key, db.Insert(ctx, key)
}
// HasDeployKey returns true if public key is a deploy key of given repository.
func HasDeployKey(ctx context.Context, keyID, repoID int64) bool {
has, _ := db.GetEngine(ctx).
Where("key_id = ? AND repo_id = ?", keyID, repoID).
Get(new(DeployKey))
return has
}
// AddDeployKey add new deploy key to database and authorized_keys file.
func AddDeployKey(ctx context.Context, repoID int64, name, content string, readOnly bool) (*DeployKey, error) {
fingerprint, err := CalcFingerprint(content)

View File

@ -200,13 +200,3 @@ func DeleteCredential(ctx context.Context, id, userID int64) (bool, error) {
had, err := db.GetEngine(ctx).ID(id).Where("user_id = ?", userID).Delete(&WebAuthnCredential{})
return had > 0, err
}
// WebAuthnCredentials implements the webauthn.User interface
func WebAuthnCredentials(ctx context.Context, userID int64) ([]webauthn.Credential, error) {
dbCreds, err := GetWebAuthnCredentialsByUID(ctx, userID)
if err != nil {
return nil, err
}
return dbCreds.ToCredentials(), nil
}

View File

@ -7,7 +7,6 @@ import (
"context"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/optional"
@ -60,24 +59,6 @@ func (branches BranchList) LoadPusher(ctx context.Context) error {
return nil
}
func (branches BranchList) LoadRepo(ctx context.Context) error {
ids := container.FilterSlice(branches, func(branch *Branch) (int64, bool) {
return branch.RepoID, branch.RepoID > 0 && branch.Repo == nil
})
reposMap := make(map[int64]*repo_model.Repository, len(ids))
if err := db.GetEngine(ctx).In("id", ids).Find(&reposMap); err != nil {
return err
}
for _, branch := range branches {
if branch.RepoID <= 0 || branch.Repo != nil {
continue
}
branch.Repo = reposMap[branch.RepoID]
}
return nil
}
type FindBranchOptions struct {
db.ListOptions
RepoID int64

View File

@ -89,12 +89,6 @@ type ErrUnknownDependencyType struct {
Type DependencyType
}
// IsErrUnknownDependencyType checks if an error is ErrUnknownDependencyType
func IsErrUnknownDependencyType(err error) bool {
_, ok := err.(ErrUnknownDependencyType)
return ok
}
func (err ErrUnknownDependencyType) Error() string {
return fmt.Sprintf("unknown dependency type [type: %d]", err.Type)
}

View File

@ -48,21 +48,6 @@ func (err ErrIssueNotExist) Unwrap() error {
return util.ErrNotExist
}
// ErrNewIssueInsert is used when the INSERT statement in newIssue fails
type ErrNewIssueInsert struct {
OriginalError error
}
// IsErrNewIssueInsert checks if an error is a ErrNewIssueInsert.
func IsErrNewIssueInsert(err error) bool {
_, ok := err.(ErrNewIssueInsert)
return ok
}
func (err ErrNewIssueInsert) Error() string {
return err.OriginalError.Error()
}
var ErrIssueAlreadyChanged = util.NewInvalidArgumentErrorf("the issue is already changed")
// Issue represents an issue or pull request of repository.

View File

@ -165,27 +165,6 @@ func MovePin(ctx context.Context, issue *Issue, newPosition int) error {
})
}
func GetPinnedIssueIDs(ctx context.Context, repoID int64, isPull bool) ([]int64, error) {
var issuePins []IssuePin
if err := db.GetEngine(ctx).
Table("issue_pin").
Where("repo_id = ?", repoID).
And("is_pull = ?", isPull).
Find(&issuePins); err != nil {
return nil, err
}
sort.Slice(issuePins, func(i, j int) bool {
return issuePins[i].PinOrder < issuePins[j].PinOrder
})
var ids []int64
for _, pin := range issuePins {
ids = append(ids, pin.IssueID)
}
return ids, nil
}
func GetIssuePinsByRepoID(ctx context.Context, repoID int64, isPull bool) ([]*IssuePin, error) {
var pins []*IssuePin
if err := db.GetEngine(ctx).Where("repo_id = ? AND is_pull = ?", repoID, isPull).Find(&pins); err != nil {

View File

@ -93,12 +93,6 @@ type ErrIssueIsOpen struct {
Index int64
}
// IsErrIssueIsOpen checks if an error is a ErrIssueIsOpen.
func IsErrIssueIsOpen(err error) bool {
_, ok := err.(ErrIssueIsOpen)
return ok
}
func (err ErrIssueIsOpen) Error() string {
return fmt.Sprintf("%s [id: %d, repo_id: %d, index: %d] is already open", util.Iif(err.IsPull, "Pull Request", "Issue"), err.ID, err.RepoID, err.Index)
}
@ -441,7 +435,7 @@ func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *Issue, la
LabelIDs: labelIDs,
Attachments: uuids,
}); err != nil {
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) {
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
return err
}
return fmt.Errorf("newIssue: %w", err)

View File

@ -475,7 +475,7 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *Iss
LabelIDs: labelIDs,
Attachments: uuids,
}); err != nil {
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) {
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
return err
}
return fmt.Errorf("newIssue: %w", err)

View File

@ -28,19 +28,3 @@ func (t *TeamUnit) Unit() unit.Unit {
func getUnitsByTeamID(ctx context.Context, teamID int64) (units []*TeamUnit, err error) {
return units, db.GetEngine(ctx).Where("team_id = ?", teamID).Find(&units)
}
// UpdateTeamUnits updates a teams's units
func UpdateTeamUnits(ctx context.Context, team *Team, units []TeamUnit) (err error) {
return db.WithTx(ctx, func(ctx context.Context) error {
if _, err = db.GetEngine(ctx).Where("team_id = ?", team.ID).Delete(new(TeamUnit)); err != nil {
return err
}
if len(units) > 0 {
if err = db.Insert(ctx, units); err != nil {
return err
}
}
return nil
})
}

View File

@ -36,14 +36,6 @@ type SearchMembersOptions struct {
TeamID int64
}
func (opts SearchMembersOptions) ToConds() builder.Cond {
cond := builder.NewCond()
if opts.TeamID > 0 {
cond = cond.And(builder.Eq{"": opts.TeamID})
}
return cond
}
// GetTeamMembers returns all members in given team of organization.
func GetTeamMembers(ctx context.Context, opts *SearchMembersOptions) ([]*user_model.User, error) {
var members []*user_model.User

View File

@ -337,20 +337,6 @@ func SetDefaultColumn(ctx context.Context, projectID, columnID int64) error {
})
}
// UpdateColumnSorting update project column sorting
func UpdateColumnSorting(ctx context.Context, cl ColumnList) error {
return db.WithTx(ctx, func(ctx context.Context) error {
for i := range cl {
if _, err := db.GetEngine(ctx).ID(cl[i].ID).Cols(
"sorting",
).Update(cl[i]); err != nil {
return err
}
}
return nil
})
}
func GetColumnsByIDs(ctx context.Context, projectID int64, columnsIDs []int64) (ColumnList, error) {
columns := make([]*Column, 0, 5)
if len(columnsIDs) == 0 {

View File

@ -44,12 +44,6 @@ type ErrTopicNotExist struct {
Name string
}
// IsErrTopicNotExist checks if an error is an ErrTopicNotExist.
func IsErrTopicNotExist(err error) bool {
_, ok := err.(ErrTopicNotExist)
return ok
}
// Error implements error interface
func (err ErrTopicNotExist) Error() string {
return fmt.Sprintf("topic is not exist [name: %s]", err.Name)

View File

@ -13,7 +13,6 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
)
// NoticeType describes the notice type
@ -60,18 +59,6 @@ func CreateRepositoryNotice(desc string, args ...any) error {
return CreateNotice(graceful.GetManager().ShutdownContext(), NoticeRepository, desc, args...)
}
// RemoveAllWithNotice removes all directories in given path and
// creates a system notice when error occurs.
func RemoveAllWithNotice(ctx context.Context, title, path string) {
if err := util.RemoveAll(path); err != nil {
desc := fmt.Sprintf("%s [%s]: %v", title, path, err)
log.Warn(title+" [%s]: %v", path, err)
if err = CreateNotice(graceful.GetManager().ShutdownContext(), NoticeRepository, desc); err != nil {
log.Error("CreateRepositoryNotice: %v", err)
}
}
}
// RemoveStorageWithNotice removes a file from the storage and
// creates a system notice when error occurs.
func RemoveStorageWithNotice(ctx context.Context, bucket storage.ObjectStorage, title, path string) {

View File

@ -212,12 +212,6 @@ func RemoveUserBadges(ctx context.Context, u *User, badges []*Badge) error {
})
}
// RemoveAllUserBadges removes all badges from a user.
func RemoveAllUserBadges(ctx context.Context, u *User) error {
_, err := db.GetEngine(ctx).Where("user_id=?", u.ID).Delete(&UserBadge{})
return err
}
// SearchBadgeOptions represents the options when finding badges
type SearchBadgeOptions struct {
db.ListOptions
@ -258,16 +252,3 @@ func (opts *SearchBadgeOptions) ToOrders() string {
func SearchBadges(ctx context.Context, opts *SearchBadgeOptions) ([]*Badge, int64, error) {
return db.FindAndCount[Badge](ctx, opts)
}
// GetBadgeByID returns a specific badge by ID
func GetBadgeByID(ctx context.Context, id int64) (*Badge, error) {
badge := new(Badge)
has, err := db.GetEngine(ctx).ID(id).Get(badge)
if err != nil {
return nil, err
}
if !has {
return nil, util.NewNotExistErrorf("badge does not exist [id: %d]", id)
}
return badge, nil
}

View File

@ -147,11 +147,6 @@ func InsertEmailAddress(ctx context.Context, email *EmailAddress) (*EmailAddress
return email, nil
}
func UpdateEmailAddress(ctx context.Context, email *EmailAddress) error {
_, err := db.GetEngine(ctx).ID(email.ID).AllCols().Update(email)
return err
}
// ValidateEmail check if email is a valid & allowed address
func ValidateEmail(email string) error {
if err := validateEmailBasic(email); err != nil {

View File

@ -71,27 +71,6 @@ func (err ErrUserProhibitLogin) Unwrap() error {
return util.ErrPermissionDenied
}
// ErrUserInactive represents a "ErrUserInactive" kind of error.
type ErrUserInactive struct {
UID int64
Name string
}
// IsErrUserInactive checks if an error is a ErrUserInactive
func IsErrUserInactive(err error) bool {
_, ok := err.(ErrUserInactive)
return ok
}
func (err ErrUserInactive) Error() string {
return fmt.Sprintf("user is inactive [uid: %d, name: %s]", err.UID, err.Name)
}
// Unwrap unwraps this error as a ErrPermission error
func (err ErrUserInactive) Unwrap() error {
return util.ErrPermissionDenied
}
// ErrUserIsNotLocal represents a "ErrUserIsNotLocal" kind of error.
type ErrUserIsNotLocal struct {
UID int64

View File

@ -21,12 +21,6 @@ type ErrExternalLoginUserAlreadyExist struct {
LoginSourceID int64
}
// IsErrExternalLoginUserAlreadyExist checks if an error is a ExternalLoginUserAlreadyExist.
func IsErrExternalLoginUserAlreadyExist(err error) bool {
_, ok := err.(ErrExternalLoginUserAlreadyExist)
return ok
}
func (err ErrExternalLoginUserAlreadyExist) Error() string {
return fmt.Sprintf("external login user already exists [externalID: %s, userID: %d, loginSourceID: %d]", err.ExternalID, err.UserID, err.LoginSourceID)
}
@ -41,12 +35,6 @@ type ErrExternalLoginUserNotExist struct {
LoginSourceID int64
}
// IsErrExternalLoginUserNotExist checks if an error is a ExternalLoginUserNotExist.
func IsErrExternalLoginUserNotExist(err error) bool {
_, ok := err.(ErrExternalLoginUserNotExist)
return ok
}
func (err ErrExternalLoginUserNotExist) Error() string {
return fmt.Sprintf("external login user link does not exists [userID: %d, loginSourceID: %d]", err.UserID, err.LoginSourceID)
}

View File

@ -50,12 +50,6 @@ func (err ErrUserSettingIsNotExist) Unwrap() error {
return util.ErrNotExist
}
// IsErrUserSettingIsNotExist return true if err is ErrSettingIsNotExist
func IsErrUserSettingIsNotExist(err error) bool {
_, ok := err.(ErrUserSettingIsNotExist)
return ok
}
// genSettingCacheKey returns the cache key for some configuration
func genSettingCacheKey(userID int64, key string) string {
return fmt.Sprintf("user_%d.setting.%s", userID, key)

View File

@ -83,12 +83,6 @@ func Parse(content []byte, options ...ParseOption) ([]*SingleWorkflow, error) {
return ret, nil
}
func WithJobResults(results map[string]string) ParseOption {
return func(c *parseContext) {
c.jobResults = results
}
}
func WithGitContext(context *model.GithubContext) ParseOption {
return func(c *parseContext) {
c.gitContext = context

View File

@ -7,7 +7,6 @@ import (
"bytes"
"fmt"
"io"
"strings"
"time"
"code.gitea.io/gitea/modules/json"
@ -110,9 +109,3 @@ func (e *Event) WriteTo(w io.Writer) (int64, error) {
return sum, err
}
func (e *Event) String() string {
buf := new(strings.Builder)
_, _ = e.WriteTo(buf)
return buf.String()
}

View File

@ -44,23 +44,6 @@ func CommitsCount(ctx context.Context, repo Repository, opts CommitsCountOptions
return strconv.ParseInt(strings.TrimSpace(stdout), 10, 64)
}
// CommitsCountBetween return numbers of commits between two commits
func CommitsCountBetween(ctx context.Context, repo Repository, start, end string) (int64, error) {
count, err := CommitsCount(ctx, repo, CommitsCountOptions{
Revision: []string{start + ".." + end},
})
if err != nil && strings.Contains(err.Error(), "no merge base") {
// future versions of git >= 2.28 are likely to return an error if before and last have become unrelated.
// previously it would return the results of git rev-list before last so let's try that...
return CommitsCount(ctx, repo, CommitsCountOptions{
Revision: []string{start, end},
})
}
return count, err
}
// FileCommitsCount return the number of files at a revision
func FileCommitsCount(ctx context.Context, repo Repository, revision, file string) (int64, error) {
return CommitsCount(ctx, repo,

View File

@ -5,21 +5,11 @@ package gitrepo
import (
"context"
"strings"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/globallock"
)
func GitConfigGet(ctx context.Context, repo Repository, key string) (string, error) {
result, _, err := RunCmdString(ctx, repo, gitcmd.NewCommand("config", "--get").
AddDynamicArguments(key))
if err != nil {
return "", err
}
return strings.TrimSpace(result), nil
}
func getRepoConfigLockKey(repoStoragePath string) string {
return "repo-config:" + repoStoragePath
}

View File

@ -78,11 +78,6 @@ func (hl *HostMatchList) AppendBuiltin(builtin string) {
hl.builtins = append(hl.builtins, builtin)
}
// AppendPattern appends more pattern to match
func (hl *HostMatchList) AppendPattern(pattern string) {
hl.patterns = append(hl.patterns, pattern)
}
// IsEmpty checks if the checklist is empty
func (hl *HostMatchList) IsEmpty() bool {
return hl == nil || (len(hl.builtins) == 0 && len(hl.patterns) == 0 && len(hl.ipNets) == 0)

View File

@ -74,10 +74,6 @@ func (r *GlodmarkRender) Convert(source []byte, writer io.Writer, opts ...parser
return r.goldmarkMarkdown.Convert(source, writer, opts...)
}
func (r *GlodmarkRender) Renderer() renderer.Renderer {
return r.goldmarkMarkdown.Renderer()
}
func (r *GlodmarkRender) highlightingRenderer(w util.BufWriter, c highlighting.CodeBlockContext, entering bool) {
if entering {
languageBytes, _ := c.Language()

View File

@ -21,25 +21,6 @@ import (
// ErrURLNotSupported represents url is not supported
var ErrURLNotSupported = errors.New("url method not supported")
// ErrInvalidConfiguration is called when there is invalid configuration for a storage
type ErrInvalidConfiguration struct {
cfg any
err error
}
func (err ErrInvalidConfiguration) Error() string {
if err.err != nil {
return fmt.Sprintf("Invalid Configuration Argument: %v: Error: %v", err.cfg, err.err)
}
return fmt.Sprintf("Invalid Configuration Argument: %v", err.cfg)
}
// IsErrInvalidConfiguration checks if an error is an ErrInvalidConfiguration
func IsErrInvalidConfiguration(err error) bool {
_, ok := err.(ErrInvalidConfiguration)
return ok
}
type Type = setting.StorageType
// NewStorageFunc is a function that creates a storage

View File

@ -47,7 +47,7 @@ func ParseJSONError(buf []byte) (ret struct {
}
func ParseJSONRedirect(buf []byte) (ret struct {
Redirect string `json:"redirect"`
Redirect *string `json:"redirect"`
},
) {
_ = json.Unmarshal(buf, &ret)

View File

@ -51,9 +51,3 @@ func Locale(resp http.ResponseWriter, req *http.Request) translation.Locale {
func SetLocaleCookie(resp http.ResponseWriter, lang string, maxAge int) {
SetSiteCookie(resp, "lang", lang, maxAge)
}
// DeleteLocaleCookie convenience function to delete the locale cookie consistently
// Setting the lang cookie will trigger the middleware to reset the language to previous state.
func DeleteLocaleCookie(resp http.ResponseWriter) {
SetSiteCookie(resp, "lang", "", -1)
}

View File

@ -34,7 +34,6 @@
"@replit/codemirror-lang-svelte": "6.0.0",
"@replit/codemirror-vscode-keymap": "6.0.2",
"@resvg/resvg-wasm": "2.6.2",
"@silverwind/vue3-calendar-heatmap": "2.1.1",
"@vitejs/plugin-vue": "6.0.6",
"ansi_up": "6.0.6",
"asciinema-player": "3.15.1",

View File

@ -36,11 +36,5 @@ export default defineConfig({
...devices['Desktop Firefox'],
},
},
{
name: 'webkit',
use: {
...devices['Desktop Safari'],
},
},
],
});

15
pnpm-lock.yaml generated
View File

@ -112,9 +112,6 @@ importers:
'@resvg/resvg-wasm':
specifier: 2.6.2
version: 2.6.2
'@silverwind/vue3-calendar-heatmap':
specifier: 2.1.1
version: 2.1.1(tippy.js@6.3.7)(vue@3.5.32(typescript@6.0.2))
'@vitejs/plugin-vue':
specifier: 6.0.6
version: 6.0.6(vite@8.0.8(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1))(vue@3.5.32(typescript@6.0.2))
@ -1249,13 +1246,6 @@ packages:
'@scarf/scarf@1.4.0':
resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==}
'@silverwind/vue3-calendar-heatmap@2.1.1':
resolution: {integrity: sha512-RQtLOpkysm0LR3PbUoc+aDcYxzy7xboygb1SQEwrUm2/XB2nmt0BEra2ADXpu4kwFxtk0+IyNwzFvbBai/wvTg==}
engines: {node: '>=16'}
peerDependencies:
tippy.js: ^6.3.7
vue: ^3.2.29
'@simonwep/pickr@1.9.0':
resolution: {integrity: sha512-oEYvv15PyfZzjoAzvXYt3UyNGwzsrpFxLaZKzkOSd0WYBVwLd19iJerePDONxC1iF6+DpcswPdLIM2KzCJuYFg==}
@ -5052,11 +5042,6 @@ snapshots:
'@scarf/scarf@1.4.0': {}
'@silverwind/vue3-calendar-heatmap@2.1.1(tippy.js@6.3.7)(vue@3.5.32(typescript@6.0.2))':
dependencies:
tippy.js: 6.3.7
vue: 3.5.32(typescript@6.0.2)
'@simonwep/pickr@1.9.0':
dependencies:
core-js: 3.32.2

View File

@ -281,11 +281,7 @@ func DeleteGPGKey(ctx *context.APIContext) {
}
if err := asymkey_model.DeleteGPGKey(ctx, ctx.Doer, ctx.PathParamInt64("id")); err != nil {
if asymkey_model.IsErrGPGKeyAccessDenied(err) {
ctx.APIError(http.StatusForbidden, "You do not have access to this key")
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorInternal(err)
return
}
@ -295,8 +291,6 @@ func DeleteGPGKey(ctx *context.APIContext) {
// HandleAddGPGKeyError handle add GPGKey error
func HandleAddGPGKeyError(ctx *context.APIContext, err error, token string) {
switch {
case asymkey_model.IsErrGPGKeyAccessDenied(err):
ctx.APIError(http.StatusUnprocessableEntity, "You do not have access to this GPG key")
case asymkey_model.IsErrGPGKeyIDAlreadyUsed(err):
ctx.APIError(http.StatusUnprocessableEntity, "A key with the same id already exists")
case asymkey_model.IsErrGPGKeyParsing(err):

View File

@ -314,15 +314,6 @@ func SignInPost(ctx *context.Context) {
log.Warn("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err)
ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
ctx.HTML(http.StatusOK, "user/auth/prohibit_login")
} else if user_model.IsErrUserInactive(err) {
if setting.Service.RegisterEmailConfirm {
ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
ctx.HTML(http.StatusOK, TplActivate)
} else {
log.Warn("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err)
ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
ctx.HTML(http.StatusOK, "user/auth/prohibit_login")
}
} else {
ctx.ServerError("UserSignIn", err)
}

View File

@ -105,16 +105,6 @@ func handleSignInError(ctx *context.Context, userName string, ptrForm any, tmpl
log.Info("Failed authentication attempt for %s from %s: %v", userName, ctx.RemoteAddr(), err)
ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
ctx.HTML(http.StatusOK, "user/auth/prohibit_login")
} else if user_model.IsErrUserInactive(err) {
ctx.Data["user_exists"] = true
if setting.Service.RegisterEmailConfirm {
ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
ctx.HTML(http.StatusOK, TplActivate)
} else {
log.Info("Failed authentication attempt for %s from %s: %v", userName, ctx.RemoteAddr(), err)
ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
ctx.HTML(http.StatusOK, "user/auth/prohibit_login")
}
} else {
ctx.ServerError(invoker, err)
}

View File

@ -109,9 +109,3 @@ func HomeSitemap(ctx *context.Context) {
log.Error("Failed writing sitemap: %v", err)
}
}
// NotFound render 404 page
func NotFound(ctx *context.Context) {
ctx.Data["Title"] = "Page Not Found"
ctx.NotFound(nil)
}

View File

@ -35,14 +35,6 @@ const (
tplProjectsView templates.TplName = "org/projects/view"
)
// MustEnableProjects check if projects are enabled in settings
func MustEnableProjects(ctx *context.Context) {
if unit.TypeProjects.UnitGlobalDisabled() {
ctx.NotFound(nil)
return
}
}
// Projects renders the home page of projects
func Projects(ctx *context.Context) {
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {

View File

@ -725,16 +725,16 @@ func handleSettingsPostConvert(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RepoSettingForm)
repo := ctx.Repo.Repository
if !ctx.Repo.IsOwner() {
ctx.HTTPError(http.StatusNotFound)
ctx.JSONErrorNotFound()
return
}
if repo.Name != form.RepoName {
ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
ctx.JSONError(ctx.Tr("form.enterred_invalid_repo_name"))
return
}
if !repo.IsMirror {
ctx.HTTPError(http.StatusNotFound)
ctx.JSONErrorNotFound()
return
}
repo.IsMirror = false
@ -748,14 +748,14 @@ func handleSettingsPostConvert(ctx *context.Context) {
}
log.Trace("Repository converted from mirror to regular: %s", repo.FullName())
ctx.Flash.Success(ctx.Tr("repo.settings.convert_succeed"))
ctx.Redirect(repo.Link())
ctx.JSONRedirect(repo.Link())
}
func handleSettingsPostConvertFork(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RepoSettingForm)
repo := ctx.Repo.Repository
if !ctx.Repo.IsOwner() {
ctx.HTTPError(http.StatusNotFound)
ctx.JSONErrorNotFound()
return
}
if err := repo.LoadOwner(ctx); err != nil {
@ -763,12 +763,12 @@ func handleSettingsPostConvertFork(ctx *context.Context) {
return
}
if repo.Name != form.RepoName {
ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
ctx.JSONError(ctx.Tr("form.enterred_invalid_repo_name"))
return
}
if !repo.IsFork {
ctx.HTTPError(http.StatusNotFound)
ctx.JSONErrorNotFound()
return
}
@ -776,7 +776,7 @@ func handleSettingsPostConvertFork(ctx *context.Context) {
maxCreationLimit := ctx.Repo.Owner.MaxCreationLimit()
msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
ctx.Flash.Error(msg)
ctx.Redirect(repo.Link() + "/settings")
ctx.JSONRedirect(repo.Link() + "/settings")
return
}
@ -788,25 +788,25 @@ func handleSettingsPostConvertFork(ctx *context.Context) {
log.Trace("Repository converted from fork to regular: %s", repo.FullName())
ctx.Flash.Success(ctx.Tr("repo.settings.convert_fork_succeed"))
ctx.Redirect(repo.Link())
ctx.JSONRedirect(repo.Link())
}
func handleSettingsPostTransfer(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RepoSettingForm)
repo := ctx.Repo.Repository
if !ctx.Repo.IsOwner() {
ctx.HTTPError(http.StatusNotFound)
ctx.JSONErrorNotFound()
return
}
if repo.Name != form.RepoName {
ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
ctx.JSONError(ctx.Tr("form.enterred_invalid_repo_name"))
return
}
newOwner, err := user_model.GetUserByName(ctx, ctx.FormString("new_owner_name"))
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil)
ctx.JSONError(ctx.Tr("form.enterred_invalid_owner_name"))
return
}
ctx.ServerError("IsUserExist", err)
@ -816,7 +816,7 @@ func handleSettingsPostTransfer(ctx *context.Context) {
if newOwner.Type == user_model.UserTypeOrganization {
if !ctx.Doer.IsAdmin && newOwner.Visibility == structs.VisibleTypePrivate && !organization.OrgFromUser(newOwner).HasMemberWithUserID(ctx, ctx.Doer.ID) {
// The user shouldn't know about this organization
ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil)
ctx.JSONError(ctx.Tr("form.enterred_invalid_owner_name"))
return
}
}
@ -830,14 +830,14 @@ func handleSettingsPostTransfer(ctx *context.Context) {
oldFullname := repo.FullName()
if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, repo, nil); err != nil {
if repo_model.IsErrRepoAlreadyExist(err) {
ctx.RenderWithErrDeprecated(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil)
ctx.JSONError(ctx.Tr("repo.settings.new_owner_has_same_repo"))
} else if repo_model.IsErrRepoTransferInProgress(err) {
ctx.RenderWithErrDeprecated(ctx.Tr("repo.settings.transfer_in_progress"), tplSettingsOptions, nil)
ctx.JSONError(ctx.Tr("repo.settings.transfer_in_progress"))
} else if repo_service.IsRepositoryLimitReached(err) {
limit := err.(repo_service.LimitReachedError).Limit
ctx.RenderWithErrDeprecated(ctx.TrN(limit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", limit), tplSettingsOptions, nil)
ctx.JSONError(ctx.TrN(limit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", limit))
} else if errors.Is(err, user_model.ErrBlockedUser) {
ctx.RenderWithErrDeprecated(ctx.Tr("repo.settings.transfer.blocked_user"), tplSettingsOptions, nil)
ctx.JSONError(ctx.Tr("repo.settings.transfer.blocked_user"))
} else {
ctx.ServerError("TransferOwnership", err)
}
@ -852,7 +852,7 @@ func handleSettingsPostTransfer(ctx *context.Context) {
log.Trace("Repository transferred: %s -> %s", oldFullname, ctx.Repo.Repository.FullName())
ctx.Flash.Success(ctx.Tr("repo.settings.transfer_succeed"))
}
ctx.Redirect(repo.Link() + "/settings")
ctx.JSONRedirect(repo.Link() + "/settings")
}
func handleSettingsPostCancelTransfer(ctx *context.Context) {
@ -887,11 +887,11 @@ func handleSettingsPostDelete(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RepoSettingForm)
repo := ctx.Repo.Repository
if !ctx.Repo.IsOwner() {
ctx.HTTPError(http.StatusNotFound)
ctx.JSONErrorNotFound()
return
}
if repo.Name != form.RepoName {
ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
ctx.JSONError(ctx.Tr("form.enterred_invalid_repo_name"))
return
}
@ -907,18 +907,18 @@ func handleSettingsPostDelete(ctx *context.Context) {
log.Trace("Repository deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name)
ctx.Flash.Success(ctx.Tr("repo.settings.deletion_success"))
ctx.Redirect(ctx.Repo.Owner.DashboardLink())
ctx.JSONRedirect(ctx.Repo.Owner.DashboardLink())
}
func handleSettingsPostDeleteWiki(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RepoSettingForm)
repo := ctx.Repo.Repository
if !ctx.Repo.IsOwner() {
ctx.HTTPError(http.StatusNotFound)
ctx.JSONErrorNotFound()
return
}
if repo.Name != form.RepoName {
ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
ctx.JSONError(ctx.Tr("form.enterred_invalid_repo_name"))
return
}
@ -929,7 +929,7 @@ func handleSettingsPostDeleteWiki(ctx *context.Context) {
log.Trace("Repository wiki deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name)
ctx.Flash.Success(ctx.Tr("repo.settings.wiki_deletion_success"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings")
ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings")
}
func handleSettingsPostArchive(ctx *context.Context) {

View File

@ -362,10 +362,6 @@ func RunnerUpdatePost(ctx *context.Context) {
ctx.JSONRedirect("")
}
func RedirectToDefaultSetting(ctx *context.Context) {
ctx.Redirect(ctx.Repo.RepoLink + "/settings/actions/runners")
}
func findActionsRunner(ctx *context.Context, rCtx *runnersCtx) *actions_model.ActionRunner {
runnerID := ctx.PathParamInt64("runnerid")
opts := &actions_model.FindRunnerOptions{

View File

@ -30,27 +30,6 @@ func UserAssignmentWeb() func(ctx *Context) {
}
}
// UserIDAssignmentAPI returns a middleware to handle context-user assignment for api routes
func UserIDAssignmentAPI() func(ctx *APIContext) {
return func(ctx *APIContext) {
userID := ctx.PathParamInt64("user-id")
if ctx.IsSigned && ctx.Doer.ID == userID {
ctx.ContextUser = ctx.Doer
} else {
var err error
ctx.ContextUser, err = user_model.GetUserByID(ctx, userID)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusNotFound, err)
} else {
ctx.APIErrorInternal(err)
}
}
}
}
}
// UserAssignmentAPI returns a middleware to handle context-user assignment for api routes
func UserAssignmentAPI() func(ctx *APIContext) {
return func(ctx *APIContext) {

View File

@ -1,18 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package convert
import (
secret_model "code.gitea.io/gitea/models/secret"
api "code.gitea.io/gitea/modules/structs"
)
// ToSecret converts Secret to API format
func ToSecret(secret *secret_model.Secret) *api.Secret {
result := &api.Secret{
Name: secret.Name,
}
return result
}

View File

@ -740,14 +740,3 @@ func (f *AddTimeManuallyForm) Validate(req *http.Request, errs binding.Errors) b
type SaveTopicForm struct {
Topics []string `binding:"topics;Required;"`
}
// DeadlineForm hold the validation rules for deadlines
type DeadlineForm struct {
DateString string `form:"date" binding:"Required;Size(10)"`
}
// Validate validates the fields
func (f *DeadlineForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}

View File

@ -32,11 +32,6 @@ const (
ContentTypeSubmodule ContentType = "submodule" // submodule content type (submodule)
)
// String gets the string of ContentType
func (ct *ContentType) String() string {
return string(*ct)
}
type GetContentsOrListOptions struct {
TreePath string
IncludeSingleFileContent bool // include the file's content when the tree path is a file

View File

@ -26,12 +26,6 @@ type ErrSHANotFound struct {
SHA string
}
// IsErrSHANotFound checks if an error is a ErrSHANotFound.
func IsErrSHANotFound(err error) bool {
_, ok := err.(ErrSHANotFound)
return ok
}
func (err ErrSHANotFound) Error() string {
return fmt.Sprintf("sha not found [%s]", err.SHA)
}

View File

@ -8,13 +8,19 @@ description: |
icon: public/assets/img/logo.png
confinement: strict
base: core22
base: core24
adopt-info: gitea
architectures:
- build-on: armhf
- build-on: amd64
- build-on: arm64
platforms:
armhf:
build-on: [armhf]
build-for: [armhf]
amd64:
build-on: [amd64]
build-for: [amd64]
arm64:
build-on: [arm64]
build-for: [arm64]
environment:
GITEA_CUSTOM: "$SNAP_COMMON"

View File

@ -885,7 +885,7 @@
<div class="ui warning message">
{{ctx.Locale.Tr "repo.settings.convert_notices_1"}}
</div>
<form class="ui form" action="{{.Link}}" method="post">
<form class="ui form form-fetch-action" action="{{.Link}}" method="post">
<input type="hidden" name="action" value="convert">
{{template "repo/settings/repo_name_confirm_fields" (dict "RepoName" .Repository.Name)}}
{{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.settings.convert_confirm"))}}
@ -902,7 +902,7 @@
<div class="ui warning message">
{{ctx.Locale.Tr "repo.settings.convert_fork_notices_1"}}
</div>
<form class="ui form" action="{{.Link}}" method="post">
<form class="ui form form-fetch-action" action="{{.Link}}" method="post">
<input type="hidden" name="action" value="convert_fork">
{{template "repo/settings/repo_name_confirm_fields" (dict "RepoName" .Repository.Name)}}
{{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.settings.convert_fork_confirm"))}}
@ -921,7 +921,7 @@
{{ctx.Locale.Tr "repo.settings.transfer_notices_3"}} <br>
{{ctx.Locale.Tr "repo.settings.transfer_notices_4"}}
</div>
<form class="ui form" action="{{.Link}}" method="post">
<form class="ui form form-fetch-action" action="{{.Link}}" method="post">
<input type="hidden" name="action" value="transfer">
{{template "repo/settings/repo_name_confirm_fields" (dict "RepoName" .Repository.Name)}}
<div class="required field">
@ -946,7 +946,7 @@
{{ctx.Locale.Tr "repo.settings.delete_notices_fork_1"}}
{{end}}
</div>
<form class="ui form" action="{{.Link}}" method="post">
<form class="ui form form-fetch-action" action="{{.Link}}" method="post">
<input type="hidden" name="action" value="delete">
{{template "repo/settings/repo_name_confirm_fields" (dict "RepoName" .Repository.Name)}}
{{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.settings.confirm_delete"))}}
@ -1012,7 +1012,7 @@
{{ctx.Locale.Tr "repo.settings.delete_notices_1"}}<br>
{{ctx.Locale.Tr "repo.settings.wiki_delete_notices_1" .Repository.Name}}
</div>
<form class="ui form" action="{{.Link}}" method="post">
<form class="ui form form-fetch-action" action="{{.Link}}" method="post">
<input type="hidden" name="action" value="delete-wiki">
{{template "repo/settings/repo_name_confirm_fields" (dict "RepoName" .Repository.Name)}}
{{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.settings.confirm_wiki_delete"))}}

View File

@ -0,0 +1,9 @@
import {test, expect} from '@playwright/test';
import {login} from './utils.ts';
test('heatmap tooltip shows on hover', async ({page}) => {
await login(page);
await page.goto('/');
await page.locator('.heatmap-day').first().hover();
await expect(page.locator('.tippy-box[data-state="visible"]')).toBeVisible();
});

View File

@ -372,7 +372,8 @@ func testForkToEditFile(t *testing.T, session *TestSession, user, owner, repo, b
"action": "convert_fork",
},
)
session.MakeRequest(t, req, http.StatusSeeOther)
resp = session.MakeRequest(t, req, http.StatusOK)
assert.NotNil(t, test.ParseJSONRedirect(resp.Body.Bytes()).Redirect)
})
// Fork repository again, and check the existence of the forked repo with unique name

View File

@ -189,7 +189,8 @@ func testDeleteRepository(t *testing.T, session *TestSession, ownerName, repoNam
req := NewRequestWithValues(t, "POST", relURL+"?action=delete", map[string]string{
"repo_name": repoName,
})
session.MakeRequest(t, req, http.StatusSeeOther)
resp := session.MakeRequest(t, req, http.StatusOK)
assert.NotNil(t, test.ParseJSONRedirect(resp.Body.Bytes()).Redirect)
}
func TestPullBranchDelete(t *testing.T) {

View File

@ -39,7 +39,7 @@ func TestRepositoryVisibilityChange(t *testing.T) {
"confirm_repo_name": "user2/repo1",
})
resp = session.MakeRequest(t, req, http.StatusOK)
assert.NotEmpty(t, test.ParseJSONRedirect(resp.Body.Bytes()).Redirect)
assert.NotNil(t, test.ParseJSONRedirect(resp.Body.Bytes()).Redirect)
repo1 = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
assert.True(t, repo1.IsPrivate)
@ -51,7 +51,7 @@ func TestRepositoryVisibilityChange(t *testing.T) {
"private": "false",
})
resp := session.MakeRequest(t, req, http.StatusOK)
assert.NotEmpty(t, test.ParseJSONRedirect(resp.Body.Bytes()).Redirect)
assert.NotNil(t, test.ParseJSONRedirect(resp.Body.Bytes()).Redirect)
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
assert.False(t, repo2.IsPrivate)

View File

@ -35,8 +35,6 @@
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"strictPropertyInitialization": false,
"useUnknownInCatchVariables": false,
"stripInternal": true,
"verbatimModuleSyntax": true,
"types": [

View File

@ -86,7 +86,7 @@ function iifeBuildOpts({sourceFileName, write}: {sourceFileName: string, write?:
}
// Build iife.js as a blocking IIFE bundle. In dev mode, serves it from memory
// and rebuilds on file changes. In prod mode, writes to disk during closeBundle.
// and rebuilds on file changes. In prod mode, writes to disk and updates "manifest.json".
function iifePlugin(sourceFileName: string): Plugin {
let iifeCode = '', iifeMap = '';
const iifeModules = new Set<string>();
@ -143,7 +143,7 @@ function iifePlugin(sourceFileName: string): Plugin {
}
});
},
async closeBundle() {
async writeBundle() {
for (const file of globSync(`js/${sourceBaseName}.*.js*`, {cwd: outDir})) unlinkSync(join(outDir, file));
const result = await build(iifeBuildOpts({sourceFileName}));
@ -152,15 +152,9 @@ function iifePlugin(sourceFileName: string): Plugin {
if (!entry) throw new Error('IIFE build produced no output');
const manifestPath = join(outDir, '.vite', 'manifest.json');
try {
const manifestData = JSON.parse(readFileSync(manifestPath, 'utf8'));
manifestData[`web_src/js/${sourceFileName}`] = {file: entry.fileName, name: sourceBaseName, isEntry: true};
writeFileSync(manifestPath, JSON.stringify(manifestData, null, 2));
} catch {
// FIXME: if it throws error here, the real Vite compilation error will be hidden, and makes the debug very difficult
// Need to find a correct way to handle errors.
console.error(`Failed to update manifest for ${sourceFileName}`);
}
const manifestData = JSON.parse(readFileSync(manifestPath, 'utf8'));
manifestData[`web_src/js/${sourceFileName}`] = {file: entry.fileName, name: sourceBaseName, isEntry: true};
writeFileSync(manifestPath, JSON.stringify(manifestData, null, 2));
},
};
}

View File

@ -32,26 +32,29 @@
fill: currentcolor !important;
}
/* root legend */
#user-heatmap .vch__container > .vch__legend {
#user-heatmap .heatmap-footer {
display: flex;
font-size: 11px;
justify-content: space-between;
}
/* for the "Less" and "More" legend */
#user-heatmap .vch__legend .vch__legend {
/* "Less [colors] More" scale */
#user-heatmap .heatmap-legend {
display: flex;
align-items: center;
justify-content: right;
}
#user-heatmap .vch__legend .vch__legend div:first-child,
#user-heatmap .vch__legend .vch__legend div:last-child {
#user-heatmap .heatmap-legend-svg {
margin-right: -12px;
}
#user-heatmap .heatmap-legend > div:first-child,
#user-heatmap .heatmap-legend > div:last-child {
display: inline-block;
padding: 0 5px;
}
#user-heatmap .vch__day__square:hover {
#user-heatmap .heatmap-day:hover {
outline: 1.5px solid var(--color-text);
}

View File

@ -1,21 +1,23 @@
<script lang="ts" setup>
// TODO: Switch to upstream after https://github.com/razorness/vue3-calendar-heatmap/pull/34 is merged
import {CalendarHeatmap} from '@silverwind/vue3-calendar-heatmap';
import {onMounted, shallowRef} from 'vue';
import type {Value as HeatmapValue, Locale as HeatmapLocale} from '@silverwind/vue3-calendar-heatmap';
import {computed, onBeforeUnmount, onMounted} from 'vue';
import tippy, {createSingleton} from 'tippy.js';
import type {CreateSingletonInstance, Instance} from 'tippy.js';
defineProps<{
type HeatmapValue = {date: Date; count: number};
type HeatmapCell = {date: Date; colorIndex: number; ariaLabel: string; tooltip: string};
type MonthLabel = {monthIdx: number; weekIdx: number};
const props = defineProps<{
values: HeatmapValue[];
locale: {
textTotalContributions: string;
heatMapLocale: Partial<HeatmapLocale>;
heatMapLocale: {months: string[]; days: string[]; on: string; more: string; less: string};
noDataText: string;
tooltipUnit: string;
};
}>();
const colorRange = [
'var(--color-secondary-alpha-60)',
'var(--color-secondary-alpha-60)',
'var(--color-primary-light-4)',
'var(--color-primary-light-2)',
@ -24,21 +26,112 @@ const colorRange = [
'var(--color-primary-dark-4)',
];
const endDate = shallowRef(new Date());
const squareSize = 10;
const squareBorder = 2;
const cellSize = squareSize + squareBorder;
const daysInWeek = 7;
const trailingDays = 365;
const gridLeft = Math.ceil(squareSize * 2.5);
const gridTop = squareSize + squareSize / 2;
onMounted(() => {
// work around issue with first legend color being rendered twice and legend cut off
const legend = document.querySelector<HTMLElement>('.vch__external-legend-wrapper')!;
legend.setAttribute('viewBox', '12 0 80 10');
legend.style.marginRight = '-12px';
const now = new Date();
function dateKey(d: Date): string {
return `${d.getFullYear()}${String(d.getMonth()).padStart(2, '0')}${String(d.getDate()).padStart(2, '0')}`;
}
function shiftDate(d: Date, days: number): Date {
const out = new Date(d);
out.setDate(out.getDate() + days);
return out;
}
const grid = computed(() => {
const start = shiftDate(now, -trailingDays);
const padStart = start.getDay();
const padEnd = daysInWeek - 1 - now.getDay();
const weekCount = (trailingDays + 1 + padStart + padEnd) / daysInWeek;
const maxCount = props.values.length ? Math.max(...props.values.map((v) => v.count)) : 0;
const max = maxCount > 0 ? Math.ceil(maxCount / 5 * 4) : 1;
const activities = new Map<string, {count: number; colorIndex: number}>();
for (const {date, count} of props.values) {
const colorIndex = count >= max ? 4 : Math.max(1, Math.ceil((count / max) * 3));
activities.set(dateKey(date), {count, colorIndex});
}
const {months, on} = props.locale.heatMapLocale;
const {noDataText, tooltipUnit} = props.locale;
const cursorStart = shiftDate(start, -padStart);
const cursor = new Date(cursorStart.getFullYear(), cursorStart.getMonth(), cursorStart.getDate());
const calendar: HeatmapCell[][] = [];
for (let w = 0; w < weekCount; w++) {
const week: HeatmapCell[] = [];
for (let d = 0; d < daysInWeek; d++) {
const hit = activities.get(dateKey(cursor));
const dateStr = `${months[cursor.getMonth()]} ${cursor.getDate()}, ${cursor.getFullYear()}`;
const head = hit ? `${hit.count} ${tooltipUnit}` : noDataText;
week.push({
date: new Date(cursor),
colorIndex: hit ? hit.colorIndex : 0,
ariaLabel: `${head} ${on} ${dateStr}`,
tooltip: `<b>${head}</b> ${on} ${dateStr}`,
});
cursor.setDate(cursor.getDate() + 1);
}
calendar.push(week);
}
const monthLabels: MonthLabel[] = [];
for (let w = 1; w < calendar.length; w++) {
const prev = calendar[w - 1][0].date;
const curr = calendar[w][0].date;
if (prev.getMonth() !== curr.getMonth()) {
monthLabels.push({monthIdx: curr.getMonth(), weekIdx: w});
}
}
const width = gridLeft + (cellSize * weekCount) + squareBorder;
const height = gridTop + (cellSize * daysInWeek);
return {calendar, monthLabels, width, height};
});
function handleDayClick(e: Event & {date: Date}) {
// Reset filter if same date is clicked
const legendViewBox = `${cellSize} 0 ${squareSize * (colorRange.length + 2)} ${squareSize}`;
const cellInstances = new Map<Element, Instance>();
let singleton: CreateSingletonInstance | null = null;
onMounted(() => {
singleton = createSingleton([], {
overrides: [],
moveTransition: 'transform 0.1s ease-out',
allowHTML: true,
theme: 'tooltip',
role: 'tooltip',
placement: 'top',
});
});
onBeforeUnmount(() => {
singleton?.destroy();
for (const instance of cellInstances.values()) instance.destroy();
cellInstances.clear();
});
function lazyInitTooltip(e: MouseEvent) {
const el = e.target as Element;
if (!singleton || cellInstances.has(el) || !el.classList.contains('heatmap-day')) return;
cellInstances.set(el, tippy(el, {content: el.getAttribute('data-tooltip')!}));
singleton.setInstances([...cellInstances.values()]);
}
function handleDayClick(date: Date) {
const params = new URLSearchParams(document.location.search);
const queryDate = params.get('date');
// Timezone has to be stripped because toISOString() converts to UTC
const clickedDate = new Date(e.date.getTime() - (e.date.getTimezoneOffset() * 60000)).toISOString().substring(0, 10);
const clickedDate = new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().substring(0, 10);
if (queryDate && queryDate === clickedDate) {
params.delete('date');
@ -53,16 +146,63 @@ function handleDayClick(e: Event & {date: Date}) {
}
</script>
<template>
<calendar-heatmap
:locale="locale.heatMapLocale"
:no-data-text="locale.noDataText"
:tooltip-unit="locale.tooltipUnit"
:end-date="endDate"
:values="values"
:range-color="colorRange"
@day-click="handleDayClick($event)"
:tippy-props="{theme: 'tooltip'}"
>
<template #vch__legend-left>{{ locale.textTotalContributions }}</template>
</calendar-heatmap>
<div>
<svg class="heatmap-svg" :viewBox="`0 0 ${grid.width} ${grid.height}`">
<g class="heatmap-month-labels" :transform="`translate(${gridLeft}, 0)`">
<text
v-for="m in grid.monthLabels"
:key="m.weekIdx"
class="heatmap-month-label"
:x="cellSize * m.weekIdx"
:y="cellSize - squareBorder"
>
{{ locale.heatMapLocale.months[m.monthIdx] }}
</text>
</g>
<g class="heatmap-day-labels" :transform="`translate(0, ${gridTop})`">
<text class="heatmap-day-label" :x="0" :y="20">{{ locale.heatMapLocale.days[1] }}</text>
<text class="heatmap-day-label" :x="0" :y="44">{{ locale.heatMapLocale.days[3] }}</text>
<text class="heatmap-day-label" :x="0" :y="69">{{ locale.heatMapLocale.days[5] }}</text>
</g>
<g class="heatmap-grid" :transform="`translate(${gridLeft}, ${gridTop})`" @mouseover="lazyInitTooltip">
<g
v-for="(week, w) in grid.calendar"
:key="w"
class="heatmap-week"
:transform="`translate(${w * cellSize}, 0)`"
>
<template v-for="(day, d) in week" :key="d">
<rect
v-if="day.date < now"
class="heatmap-day"
:transform="`translate(0, ${d * cellSize})`"
:width="squareSize"
:height="squareSize"
:style="{fill: colorRange[day.colorIndex]}"
:aria-label="day.ariaLabel"
:data-tooltip="day.tooltip"
@click="handleDayClick(day.date)"
/>
</template>
</g>
</g>
</svg>
<div class="heatmap-footer">
<div>{{ locale.textTotalContributions }}</div>
<div class="heatmap-legend">
<div>{{ locale.heatMapLocale.less }}</div>
<svg class="heatmap-legend-svg" :viewBox="legendViewBox" :height="squareSize">
<rect
v-for="(color, i) in colorRange"
:key="i"
:width="squareSize"
:height="squareSize"
:x="(i + 1) * cellSize"
:style="{fill: color}"
/>
</svg>
<div>{{ locale.heatMapLocale.more }}</div>
</div>
</div>
</div>
</template>

View File

@ -21,6 +21,7 @@ import {
type DayDataObject,
} from '../utils/time.ts';
import {chartJsColors} from '../utils/color.ts';
import {errorMessage} from '../modules/errors.ts';
import {sleep} from '../utils.ts';
import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
import {onMounted, shallowRef} from 'vue';
@ -78,7 +79,7 @@ async function fetchGraphData() {
errorText.value = response.statusText;
}
} catch (err) {
errorText.value = err.message;
errorText.value = errorMessage(err);
} finally {
isLoading.value = false;
}

View File

@ -24,6 +24,7 @@ import {
fillEmptyStartDaysWithZeroes,
} from '../utils/time.ts';
import {chartJsColors} from '../utils/color.ts';
import {errorMessage} from '../modules/errors.ts';
import {sleep} from '../utils.ts';
import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
import {fomanticQuery} from '../modules/fomantic/base.ts';
@ -166,7 +167,7 @@ export default defineComponent({
this.errorText = response.statusText;
}
} catch (err) {
this.errorText = err.message;
this.errorText = errorMessage(err);
} finally {
this.isLoading = false;
}

View File

@ -20,6 +20,7 @@ import {
type DayDataObject,
} from '../utils/time.ts';
import {chartJsColors} from '../utils/color.ts';
import {errorMessage} from '../modules/errors.ts';
import {sleep} from '../utils.ts';
import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
import {onMounted, ref, shallowRef} from 'vue';
@ -74,7 +75,7 @@ async function fetchGraphData() {
errorText.value = response.statusText;
}
} catch (err) {
errorText.value = err.message;
errorText.value = errorMessage(err);
} finally {
isLoading.value = false;
}

View File

@ -2,6 +2,7 @@ import {showTemporaryTooltip} from '../../modules/tippy.ts';
import {POST} from '../../modules/fetch.ts';
import {registerGlobalInitFunc} from '../../modules/observer.ts';
import {queryElems} from '../../utils/dom.ts';
import {errorMessage} from '../../modules/errors.ts';
import {submitFormFetchAction} from '../common-fetch-action.ts';
const {appSubUrl} = window.config;
@ -26,7 +27,7 @@ function initSystemConfigAutoCheckbox(el: HTMLInputElement) {
const json: Record<string, any> = await resp.json();
if (json.errorMessage) throw new Error(json.errorMessage);
} catch (ex) {
showTemporaryTooltip(el, ex.toString());
showTemporaryTooltip(el, errorMessage(ex));
el.checked = !el.checked;
}
});

View File

@ -1,4 +1,5 @@
import {getCurrentLocale} from '../utils.ts';
import {errorMessage} from '../modules/errors.ts';
import {fomanticQuery} from '../modules/fomantic/base.ts';
import {localUserSettings} from '../modules/user-settings.ts';
@ -46,7 +47,7 @@ export async function initCitationFileCopyContent() {
try {
await initInputCitationValue(citationCopyApa, citationCopyBibtex);
} catch (e) {
console.error(`initCitationFileCopyContent error: ${e}`, e);
console.error(`initCitationFileCopyContent error: ${errorMessage(e)}`, e);
return;
}
updateUi();

View File

@ -1,6 +1,7 @@
import {GET, request} from '../modules/fetch.ts';
import {hideToastsAll, showErrorToast} from '../modules/toast.ts';
import {addDelegatedEventListener, createElementFromHTML} from '../utils/dom.ts';
import {errorMessage} from '../modules/errors.ts';
import {confirmModal, createConfirmModal} from './comp/ConfirmModal.ts';
import {ignoreAreYouSure} from '../vendor/jquery.are-you-sure.ts';
import {registerGlobalSelectorFunc} from '../modules/observer.ts';
@ -90,7 +91,7 @@ async function handleFetchActionSuccess(el: HTMLElement, opt: FetchActionOpts, r
async function handleFetchActionError(resp: Response) {
const isRespJson = resp.headers.get('content-type')?.includes('application/json');
const respText = await resp.text();
const respJson = isRespJson ? JSON.parse(await resp.text()) : null;
const respJson = isRespJson ? JSON.parse(respText) : null;
if (respJson?.errorMessage) {
// the code was quite messy, sometimes the backend uses "err", sometimes it uses "error", and even "user_error"
// but at the moment, as a new approach, we only use "errorMessage" here, backend can use JSONError() to respond.
@ -134,10 +135,10 @@ async function performActionRequest(el: HTMLElement, opt: FetchActionOpts) {
return;
}
await handleFetchActionError(resp);
} catch (e) {
if (e.name !== 'AbortError') {
console.error(`Fetch action request error:`, e);
showErrorToast(`Error: ${e.message ?? e}`);
} catch (err) {
if ((err as Error).name !== 'AbortError') {
console.error(`Fetch action request error:`, err);
showErrorToast(`Error: ${errorMessage(err)}`);
}
} finally {
toggleLoadingIndicator(el, opt, false);

View File

@ -71,26 +71,26 @@ export class ComboMarkdownEditor {
options: ComboMarkdownEditorOptions;
tabEditor: HTMLElement;
tabPreviewer: HTMLElement;
tabEditor?: HTMLElement;
tabPreviewer?: HTMLElement;
supportEasyMDE: boolean;
supportEasyMDE!: boolean;
easyMDE: any;
easyMDEToolbarActions: any;
easyMDEToolbarDefault: any;
textarea: ComboMarkdownEditorTextarea;
textareaMarkdownToolbar: HTMLElement;
textarea!: ComboMarkdownEditorTextarea;
textareaMarkdownToolbar!: HTMLElement;
textareaAutosize: any;
buttonMonospace: HTMLButtonElement;
buttonMonospace!: HTMLButtonElement;
dropzone: HTMLElement | null;
dropzone: HTMLElement | null = null;
attachedDropzoneInst: any;
previewMode: string;
previewUrl: string;
previewContext: string;
previewMode!: string;
previewUrl!: string;
previewContext!: string;
constructor(container: ComboMarkdownEditorContainer, options:ComboMarkdownEditorOptions = {}) {
if (container._giteaComboMarkdownEditor) throw new Error('ComboMarkdownEditor already initialized');
@ -291,7 +291,7 @@ export class ComboMarkdownEditor {
}
switchTabToEditor() {
this.tabEditor.click();
this.tabEditor!.click(); // when this function is called, the tab must exist
}
prepareEasyMDEToolbarActions() {

View File

@ -5,6 +5,7 @@ import {showTemporaryTooltip} from '../modules/tippy.ts';
import {GET, POST} from '../modules/fetch.ts';
import {showErrorToast} from '../modules/toast.ts';
import {createElementFromHTML, createElementFromAttrs} from '../utils/dom.ts';
import {errorMessage} from '../modules/errors.ts';
import {isImageFile, isVideoFile} from '../utils.ts';
import type Dropzone from '@deltablot/dropzone';
@ -149,7 +150,7 @@ export async function initDropzone(dropzoneEl: HTMLElement) {
} catch (error) {
// TODO: if listing the existing attachments failed, it should stop from operating the content or attachments,
// otherwise the attachments might be lost.
showErrorToast(`Failed to load attachments: ${error}`);
showErrorToast(`Failed to load attachments: ${errorMessage(error)}`);
console.error(error);
}
});

View File

@ -2,6 +2,7 @@ import type {InplaceRenderPlugin} from '../render/plugin.ts';
import {newInplacePluginPdfViewer} from '../render/plugins/inplace-pdf-viewer.ts';
import {registerGlobalInitFunc} from '../modules/observer.ts';
import {createElementFromHTML} from '../utils/dom.ts';
import {errorMessage} from '../modules/errors.ts';
import {html} from '../utils/html.ts';
import {basename} from '../utils.ts';
@ -30,7 +31,7 @@ async function renderRawFileToContainer(container: HTMLElement, rawFileLink: str
rendered = true;
}
} catch (e) {
errorMsg = `${e}`;
errorMsg = errorMessage(e);
} finally {
container.classList.remove('is-loading');
}

View File

@ -94,8 +94,8 @@ function createContext(imageAfter: HTMLImageElement, imageBefore: HTMLImageEleme
}
class ImageDiff {
containerEl: HTMLElement;
diffContainerWidth: number;
containerEl!: HTMLElement;
diffContainerWidth!: number;
async init(containerEl: HTMLElement) {
this.containerEl = containerEl;

View File

@ -1,4 +1,5 @@
import {queryElems} from '../utils/dom.ts';
import {errorMessage} from '../modules/errors.ts';
import {POST} from '../modules/fetch.ts';
import {showErrorToast} from '../modules/toast.ts';
import {sleep} from '../utils.ts';
@ -26,7 +27,7 @@ async function onDownloadArchive(e: Event) {
window.location.href = el.href; // the archive is ready, start real downloading
} catch (e) {
console.error(e);
showErrorToast(`Failed to download the archive: ${e}`, {duration: 2500});
showErrorToast(`Failed to download the archive: ${errorMessage(e)}`, {duration: 2500});
} finally {
targetLoading.classList.remove('is-loading', 'loading-icon-2px');
}

View File

@ -6,6 +6,7 @@ import {initViewedCheckboxListenerFor, initExpandAndCollapseFilesButton} from '.
import {initImageDiff} from './imagediff.ts';
import {showErrorToast} from '../modules/toast.ts';
import {queryElemSiblings, hideElem, showElem, animateOnce, addDelegatedEventListener, createElementFromHTML, queryElems} from '../utils/dom.ts';
import {errorMessage} from '../modules/errors.ts';
import {POST, GET} from '../modules/fetch.ts';
import {createTippy} from '../modules/tippy.ts';
import {invertFileFolding} from './file-fold.ts';
@ -85,7 +86,7 @@ function initRepoDiffConversationForm() {
}
} catch (error) {
console.error('Error:', error);
showErrorToast(`Submit form failed: ${error}`);
showErrorToast(`Submit form failed: ${errorMessage(error)}`);
} finally {
form?.classList.remove('is-loading');
}

View File

@ -3,6 +3,7 @@ import {getComboMarkdownEditor, initComboMarkdownEditor, ComboMarkdownEditor} fr
import {POST} from '../modules/fetch.ts';
import {showErrorToast} from '../modules/toast.ts';
import {hideElem, querySingleVisibleElem, showElem} from '../utils/dom.ts';
import {errorMessage} from '../modules/errors.ts';
import {triggerUploadStateChanged} from './comp/EditorUpload.ts';
import {convertHtmlToMarkdown} from '../markup/html2markdown.ts';
import {applyAreYouSure, reinitializeAreYouSure} from '../vendor/jquery.are-you-sure.ts';
@ -73,7 +74,7 @@ async function tryOnEditContent(e: Event) {
}
comboMarkdownEditor.dropzoneSubmitReload();
} catch (error) {
showErrorToast(`Failed to save the content: ${error}`);
showErrorToast(`Failed to save the content: ${errorMessage(error)}`);
console.error(error);
} finally {
renderContent.classList.remove('is-loading');

View File

@ -2,6 +2,7 @@ import {updateIssuesMeta} from './repo-common.ts';
import {toggleElem, queryElems, isElemVisible} from '../utils/dom.ts';
import {html, htmlRaw} from '../utils/html.ts';
import {confirmModal} from './comp/ConfirmModal.ts';
import {errorMessage} from '../modules/errors.ts';
import {showErrorToast} from '../modules/toast.ts';
import {createSortable} from '../modules/sortable.ts';
import {DELETE, POST} from '../modules/fetch.ts';
@ -87,7 +88,9 @@ function initRepoIssueListCheckboxes() {
await updateIssuesMeta(url, action, issueIDs, elementId);
window.location.reload();
} catch (err) {
showErrorToast(err.responseJSON?.error ?? err.message);
// FIXME: this logic (including updateIssuesMeta) is not right, should refactor to our JSONError framework
const e = err as {responseJSON?: {error: string}};
showErrorToast(e.responseJSON?.error ?? errorMessage(err));
}
},
));

View File

@ -2,6 +2,7 @@ import {fomanticQuery} from '../modules/fomantic/base.ts';
import {GET, POST} from '../modules/fetch.ts';
import {showErrorToast} from '../modules/toast.ts';
import {addDelegatedEventListener, queryElemChildren, queryElems, toggleElem} from '../utils/dom.ts';
import {errorMessage} from '../modules/errors.ts';
import {parseDom} from '../utils.ts';
export function syncIssueMainContentTimelineItems(oldMainContent: Element, newMainContent: Element) {
@ -42,7 +43,7 @@ export class IssueSidebarComboList {
elDropdown: HTMLElement;
elList: HTMLElement | null;
elComboValue: HTMLInputElement;
initialValues: string[];
initialValues: string[] = [];
container: HTMLElement;
elIssueMainContent: HTMLElement;
@ -129,7 +130,7 @@ export class IssueSidebarComboList {
await this.reloadPagePartially();
} catch (e) {
console.error('Failed to update to backend', e);
showErrorToast(`Failed to update to backend: ${e}`);
showErrorToast(`Failed to update to backend: ${errorMessage(e)}`);
} finally {
this.elIssueSidebar.classList.remove('is-loading');
}

View File

@ -1,3 +1,4 @@
import {errorMessage} from '../modules/errors.ts';
import {htmlEscape} from '../utils/html.ts';
import {createTippy} from '../modules/tippy.ts';
import {
@ -425,7 +426,7 @@ export function initRepoIssueTitleEdit() {
window.location.reload();
} catch (error) {
console.error(error);
showErrorToast(error.message);
showErrorToast(errorMessage(error));
}
});
}

View File

@ -2,6 +2,7 @@ import {createSortable} from '../modules/sortable.ts';
import {POST} from '../modules/fetch.ts';
import {showErrorToast} from '../modules/toast.ts';
import {queryElemChildren} from '../utils/dom.ts';
import {errorMessage} from '../modules/errors.ts';
export function initRepoSettingsBranchesDrag() {
const protectedBranchesList = document.querySelector<HTMLElement>('#protected-branches-list');
@ -23,8 +24,7 @@ export function initRepoSettingsBranchesDrag() {
},
});
} catch (err) {
const errorMessage = String(err);
showErrorToast(`Failed to update branch protection rule priority:, error: ${errorMessage}`);
showErrorToast(`Failed to update branch protection rule priority: ${errorMessage(err)}`);
}
})();
},

View File

@ -1,5 +1,6 @@
import {encodeURLEncodedBase64, decodeURLEncodedBase64} from '../utils.ts';
import {hideElem, showElem} from '../utils/dom.ts';
import {errorMessage} from '../modules/errors.ts';
import {GET, POST} from '../modules/fetch.ts';
const {appSubUrl} = window.config;
@ -80,7 +81,7 @@ async function loginPasskey() {
window.location.href = reply?.redirect ?? `${appSubUrl}/`;
} catch (err) {
webAuthnError('general', err.message);
webAuthnError('general', errorMessage(err));
}
}
@ -104,7 +105,7 @@ async function login2FA() {
await verifyAssertion(credential);
} catch (err) {
if (!options.publicKey.extensions?.appid) {
webAuthnError('general', err.message);
webAuthnError('general', errorMessage(err));
return;
}
delete options.publicKey.extensions.appid;
@ -114,7 +115,7 @@ async function login2FA() {
});
await verifyAssertion(credential);
} catch (err) {
webAuthnError('general', err.message);
webAuthnError('general', errorMessage(err));
}
}
}
@ -262,6 +263,6 @@ async function webAuthnRegisterRequest() {
});
await webauthnRegistered(credential);
} catch (err) {
webAuthnError('unknown', err);
webAuthnError('unknown', errorMessage(err));
}
}

View File

@ -1,8 +1,10 @@
export function displayError(el: Element, err: Error): void {
import {errorMessage} from '../modules/errors.ts';
export function displayError(el: Element, err: unknown): void {
el.classList.remove('is-loading');
const errorNode = document.createElement('pre');
errorNode.setAttribute('class', 'ui message error markup-block-error');
errorNode.textContent = err.message || String(err);
errorNode.textContent = errorMessage(err);
el.before(errorNode);
el.setAttribute('data-render-done', 'true');
}

View File

@ -1,4 +1,5 @@
import {generateElemId} from '../utils/dom.ts';
import {errorMessage} from '../modules/errors.ts';
import {isDarkTheme} from '../utils.ts';
import {GET} from '../modules/fetch.ts';
@ -11,7 +12,7 @@ function safeRenderIframeLink(link: any): string | null {
}
return url.href;
} catch (e) {
console.error(`Failed to parse link: ${link}, error: ${e}`);
console.error(`Failed to parse link: ${link}, error: ${errorMessage(e)}`);
return null;
}
}

View File

@ -2,6 +2,10 @@
import {html} from '../utils/html.ts';
import type {Intent} from '../types.ts';
export function errorMessage(err: unknown): string {
return (err as Error)?.message || String(err);
}
export function showGlobalErrorMessage(msg: string, msgType: Intent = 'error') {
const msgContainer = document.querySelector('.page-content') ?? document.body;
if (!msgContainer) {

View File

@ -2,6 +2,7 @@ import emojis from '../../../assets/emoji.json' with {type: 'json'};
import {GET} from '../modules/fetch.ts';
import {showErrorToast} from '../modules/toast.ts';
import {parseIssuePageInfo} from '../utils.ts';
import {errorMessage} from '../modules/errors.ts';
import type {Issue, Mention} from '../types.ts';
const maxMatches = 6;
@ -47,7 +48,7 @@ export function fetchMentions(mentionsUrl: string): Promise<Mention[]> {
if (!res.ok) throw new Error(res.statusText);
return await res.json() as Mention[];
} catch (e) {
showErrorToast(`Failed to load mentions: ${e}`);
showErrorToast(`Failed to load mentions: ${errorMessage(e)}`);
return [];
}
})();

View File

@ -3,13 +3,13 @@ import {addDelegatedEventListener, generateElemId, isDocumentFragmentOrElementNo
import octiconKebabHorizontal from '../../../public/assets/img/svg/octicon-kebab-horizontal.svg';
window.customElements.define('overflow-menu', class extends HTMLElement {
popup: HTMLDivElement;
overflowItems: Array<HTMLElement>;
button: HTMLButtonElement | null;
menuItemsEl: HTMLElement;
resizeObserver: ResizeObserver;
mutationObserver: MutationObserver;
lastWidth: number;
popup!: HTMLDivElement;
overflowItems: Array<HTMLElement> = [];
button: HTMLButtonElement | null = null;
menuItemsEl!: HTMLElement;
resizeObserver!: ResizeObserver;
mutationObserver!: MutationObserver;
lastWidth!: number;
updateButtonActivationState() {
if (!this.button || !this.popup) return;
@ -100,7 +100,7 @@ window.customElements.define('overflow-menu', class extends HTMLElement {
const itemOverFlowMenuButton = this.querySelector<HTMLButtonElement>('.overflow-menu-button');
// move items in popup back into the menu items for subsequent measurement
for (const item of this.overflowItems || []) {
for (const item of this.overflowItems) {
if (!itemFlexSpace || item.getAttribute('data-after-flex-space')) {
this.menuItemsEl.append(item);
} else {