aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--modules/git/attribute/checker.go7
-rw-r--r--modules/git/attribute/checker_test.go12
-rw-r--r--options/locale/locale_en-US.ini11
-rw-r--r--routers/web/shared/secrets/secrets.go4
-rw-r--r--services/repository/branch.go5
-rw-r--r--services/repository/files/upload.go7
-rw-r--r--templates/admin/auth/edit.tmpl2
-rw-r--r--templates/admin/user/edit.tmpl6
-rw-r--r--templates/org/settings/options.tmpl2
-rw-r--r--templates/repo/branch/list.tmpl6
-rw-r--r--templates/repo/settings/collaboration.tmpl2
-rw-r--r--templates/repo/settings/options.tmpl2
-rw-r--r--templates/shared/secrets/add_list.tmpl24
-rw-r--r--templates/user/settings/profile.tmpl2
-rw-r--r--tests/integration/change_default_branch_test.go97
-rw-r--r--web_src/js/features/common-button.test.ts14
-rw-r--r--web_src/js/features/common-button.ts34
-rw-r--r--web_src/js/features/common-issue-list.ts6
-rw-r--r--web_src/js/features/repo-issue-list.ts6
-rw-r--r--web_src/js/utils/dom.test.ts6
-rw-r--r--web_src/js/utils/dom.ts49
21 files changed, 236 insertions, 68 deletions
diff --git a/modules/git/attribute/checker.go b/modules/git/attribute/checker.go
index c17006a154..167b31416e 100644
--- a/modules/git/attribute/checker.go
+++ b/modules/git/attribute/checker.go
@@ -39,7 +39,12 @@ func checkAttrCommand(gitRepo *git.Repository, treeish string, filenames, attrib
)
cancel = deleteTemporaryFile
}
- } // else: no treeish, assume it is a not a bare repo, read from working directory
+ } else {
+ // Read from existing index, in cases where the repo is bare and has an index,
+ // or the work tree contains unstaged changes that shouldn't affect the attribute check.
+ // It is caller's responsibility to add changed ".gitattributes" into the index if they want to respect the new changes.
+ cmd.AddArguments("--cached")
+ }
cmd.AddDynamicArguments(attributes...)
if len(filenames) > 0 {
diff --git a/modules/git/attribute/checker_test.go b/modules/git/attribute/checker_test.go
index 97db43460b..67fbda8918 100644
--- a/modules/git/attribute/checker_test.go
+++ b/modules/git/attribute/checker_test.go
@@ -57,8 +57,18 @@ func Test_Checker(t *testing.T) {
assert.Equal(t, expectedAttrs(), attrs["i-am-a-python.p"])
})
+ t.Run("Run git check-attr in bare repository using index", func(t *testing.T) {
+ attrs, err := CheckAttributes(t.Context(), gitRepo, "", CheckAttributeOpts{
+ Filenames: []string{"i-am-a-python.p"},
+ Attributes: LinguistAttributes,
+ })
+ assert.NoError(t, err)
+ assert.Len(t, attrs, 1)
+ assert.Equal(t, expectedAttrs(), attrs["i-am-a-python.p"])
+ })
+
if !git.DefaultFeatures().SupportCheckAttrOnBare {
- t.Skip("git version 2.40 is required to support run check-attr on bare repo")
+ t.Skip("git version 2.40 is required to support run check-attr on bare repo without using index")
return
}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 9091b6bc4b..af3b948a88 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -3722,13 +3722,18 @@ owner.settings.chef.keypair.description = A key pair is necessary to authenticat
secrets = Secrets
description = Secrets will be passed to certain actions and cannot be read otherwise.
none = There are no secrets yet.
-creation = Add Secret
+
+; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation
creation.description = Description
creation.name_placeholder = case-insensitive, alphanumeric characters or underscores only, cannot start with GITEA_ or GITHUB_
creation.value_placeholder = Input any content. Whitespace at the start and end will be omitted.
creation.description_placeholder = Enter short description (optional).
-creation.success = The secret "%s" has been added.
-creation.failed = Failed to add secret.
+
+save_success = The secret "%s" has been saved.
+save_failed = Failed to save secret.
+
+add_secret = Add secret
+edit_secret = Edit secret
deletion = Remove secret
deletion.description = Removing a secret is permanent and cannot be undone. Continue?
deletion.success = The secret has been removed.
diff --git a/routers/web/shared/secrets/secrets.go b/routers/web/shared/secrets/secrets.go
index c8b80ebb26..29f4e9520d 100644
--- a/routers/web/shared/secrets/secrets.go
+++ b/routers/web/shared/secrets/secrets.go
@@ -32,11 +32,11 @@ func PerformSecretsPost(ctx *context.Context, ownerID, repoID int64, redirectURL
s, _, err := secret_service.CreateOrUpdateSecret(ctx, ownerID, repoID, form.Name, util.ReserveLineBreakForTextarea(form.Data), form.Description)
if err != nil {
log.Error("CreateOrUpdateSecret failed: %v", err)
- ctx.JSONError(ctx.Tr("secrets.creation.failed"))
+ ctx.JSONError(ctx.Tr("secrets.save_failed"))
return
}
- ctx.Flash.Success(ctx.Tr("secrets.creation.success", s.Name))
+ ctx.Flash.Success(ctx.Tr("secrets.save_success", s.Name))
ctx.JSONRedirect(redirectURL)
}
diff --git a/services/repository/branch.go b/services/repository/branch.go
index 94c47ffdc4..dd00ca7dcd 100644
--- a/services/repository/branch.go
+++ b/services/repository/branch.go
@@ -663,6 +663,11 @@ func SetRepoDefaultBranch(ctx context.Context, repo *repo_model.Repository, newB
}
}
+ // clear divergence cache
+ if err := DelRepoDivergenceFromCache(ctx, repo.ID); err != nil {
+ log.Error("DelRepoDivergenceFromCache: %v", err)
+ }
+
notify_service.ChangeDefaultBranch(ctx, repo)
return nil
diff --git a/services/repository/files/upload.go b/services/repository/files/upload.go
index f348cb68ab..68a071cd28 100644
--- a/services/repository/files/upload.go
+++ b/services/repository/files/upload.go
@@ -107,6 +107,7 @@ func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
}
var attributesMap map[string]*attribute.Attributes
+ // when uploading to an empty repo, the old branch doesn't exist, but some "global gitattributes" or "info/attributes" may exist
if setting.LFS.StartServer {
attributesMap, err = attribute.CheckAttributes(ctx, t.gitRepo, "" /* use temp repo's working dir */, attribute.CheckAttributeOpts{
Attributes: []string{attribute.Filter},
@@ -118,6 +119,12 @@ func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
}
// Copy uploaded files into repository.
+ // TODO: there is a small problem: when uploading LFS files with ".gitattributes", the "check-attr" runs before this loop,
+ // so LFS files are not able to be added as LFS objects. Ideally we need to do in 3 steps in the future:
+ // 1. Add ".gitattributes" to git index
+ // 2. Run "check-attr" (the previous attribute.CheckAttributes call)
+ // 3. Add files to git index (this loop)
+ // This problem is trivial so maybe no need to spend too much time on it at the moment.
for i := range infos {
if err := copyUploadedLFSFileIntoRepository(ctx, &infos[i], attributesMap, t, opts.TreePath); err != nil {
return err
diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl
index 15683307ed..91b84e13b6 100644
--- a/templates/admin/auth/edit.tmpl
+++ b/templates/admin/auth/edit.tmpl
@@ -15,7 +15,7 @@
</div>
<div class="required inline field {{if .Err_Name}}error{{end}}">
<label for="auth_name">{{ctx.Locale.Tr "admin.auths.auth_name"}}</label>
- <input id="auth_name" name="name" value="{{.Source.Name}}" autofocus required>
+ <input id="auth_name" name="name" value="{{.Source.Name}}" required>
</div>
<div class="inline field">
<div class="ui checkbox">
diff --git a/templates/admin/user/edit.tmpl b/templates/admin/user/edit.tmpl
index c04d332660..879b5cb550 100644
--- a/templates/admin/user/edit.tmpl
+++ b/templates/admin/user/edit.tmpl
@@ -9,7 +9,7 @@
{{.CsrfTokenHtml}}
<div class="field {{if .Err_UserName}}error{{end}}">
<label for="user_name">{{ctx.Locale.Tr "username"}}</label>
- <input id="user_name" name="user_name" value="{{.User.Name}}" autofocus {{if not .User.IsLocal}}disabled{{end}} maxlength="40">
+ <input id="user_name" name="user_name" value="{{.User.Name}}" {{if not .User.IsLocal}}disabled{{end}} maxlength="40">
</div>
<!-- Types and name -->
<div class="inline required field {{if .Err_LoginType}}error{{end}}">
@@ -55,7 +55,7 @@
<div class="required non-local field {{if .Err_LoginName}}error{{end}} {{if eq .User.LoginSource 0}}tw-hidden{{end}}">
<label for="login_name">{{ctx.Locale.Tr "admin.users.auth_login_name"}}</label>
- <input id="login_name" name="login_name" value="{{.User.LoginName}}" autofocus>
+ <input id="login_name" name="login_name" value="{{.User.LoginName}}">
</div>
<div class="field {{if .Err_FullName}}error{{end}}">
<label for="full_name">{{ctx.Locale.Tr "settings.full_name"}}</label>
@@ -63,7 +63,7 @@
</div>
<div class="required field {{if .Err_Email}}error{{end}}">
<label for="email">{{ctx.Locale.Tr "email"}}</label>
- <input id="email" name="email" type="email" value="{{.User.Email}}" autofocus required>
+ <input id="email" name="email" type="email" value="{{.User.Email}}" required>
</div>
<div class="local field {{if .Err_Password}}error{{end}} {{if not (or (.User.IsLocal) (.User.IsOAuth2))}}tw-hidden{{end}}">
<label for="password">{{ctx.Locale.Tr "password"}}</label>
diff --git a/templates/org/settings/options.tmpl b/templates/org/settings/options.tmpl
index 76315f3eac..f4583bbe36 100644
--- a/templates/org/settings/options.tmpl
+++ b/templates/org/settings/options.tmpl
@@ -12,7 +12,7 @@
<br>{{ctx.Locale.Tr "org.settings.change_orgname_prompt"}}<br>{{ctx.Locale.Tr "org.settings.change_orgname_redirect_prompt"}}
</span>
</label>
- <input id="org_name" name="name" value="{{.Org.Name}}" data-org-name="{{.Org.Name}}" autofocus required maxlength="40">
+ <input id="org_name" name="name" value="{{.Org.Name}}" data-org-name="{{.Org.Name}}" required maxlength="40">
</div>
<div class="field {{if .Err_FullName}}error{{end}}">
<label for="full_name">{{ctx.Locale.Tr "org.org_full_name_holder"}}</label>
diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl
index 19797229bf..aa38413ad7 100644
--- a/templates/repo/branch/list.tmpl
+++ b/templates/repo/branch/list.tmpl
@@ -20,7 +20,7 @@
<tr>
<td>
<div class="flex-text-block">
- <a class="gt-ellipsis" href="{{.RepoLink}}/src/branch/{{PathEscapeSegments .DefaultBranchBranch.DBBranch.Name}}">{{.DefaultBranchBranch.DBBranch.Name}}</a>
+ <a class="gt-ellipsis branch-name" href="{{.RepoLink}}/src/branch/{{PathEscapeSegments .DefaultBranchBranch.DBBranch.Name}}">{{.DefaultBranchBranch.DBBranch.Name}}</a>
{{if .DefaultBranchBranch.IsProtected}}
<span data-tooltip-content="{{ctx.Locale.Tr "repo.settings.protected_branch"}}">{{svg "octicon-shield-lock"}}</span>
{{end}}
@@ -90,13 +90,13 @@
<td class="eight wide">
{{if .DBBranch.IsDeleted}}
<div class="flex-text-block">
- <span class="gt-ellipsis">{{.DBBranch.Name}}</span>
+ <span class="gt-ellipsis branch-name">{{.DBBranch.Name}}</span>
<button class="btn interact-fg tw-px-1" data-clipboard-text="{{.DBBranch.Name}}" data-tooltip-content="{{ctx.Locale.Tr "copy_branch"}}">{{svg "octicon-copy" 14}}</button>
</div>
<p class="info">{{ctx.Locale.Tr "repo.branch.deleted_by" .DBBranch.DeletedBy.Name}} {{DateUtils.TimeSince .DBBranch.DeletedUnix}}</p>
{{else}}
<div class="flex-text-block">
- <a class="gt-ellipsis" href="{{$.RepoLink}}/src/branch/{{PathEscapeSegments .DBBranch.Name}}">{{.DBBranch.Name}}</a>
+ <a class="gt-ellipsis branch-name" href="{{$.RepoLink}}/src/branch/{{PathEscapeSegments .DBBranch.Name}}">{{.DBBranch.Name}}</a>
{{if .IsProtected}}
<span data-tooltip-content="{{ctx.Locale.Tr "repo.settings.protected_branch"}}">{{svg "octicon-shield-lock"}}</span>
{{end}}
diff --git a/templates/repo/settings/collaboration.tmpl b/templates/repo/settings/collaboration.tmpl
index 4461398258..7064b4c7ba 100644
--- a/templates/repo/settings/collaboration.tmpl
+++ b/templates/repo/settings/collaboration.tmpl
@@ -90,7 +90,7 @@
<form class="ui form" id="repo-collab-team-form" action="{{.Link}}/team" method="post">
{{.CsrfTokenHtml}}
<div id="search-team-box" class="ui search input tw-align-middle" data-org-name="{{.OrgName}}">
- <input class="prompt" name="team" placeholder="{{ctx.Locale.Tr "search.team_kind"}}" autocomplete="off" autofocus required>
+ <input class="prompt" name="team" placeholder="{{ctx.Locale.Tr "search.team_kind"}}" autocomplete="off" required>
</div>
<button class="ui primary button">{{ctx.Locale.Tr "repo.settings.add_team"}}</button>
</form>
diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl
index 4d61604612..fc42056e0a 100644
--- a/templates/repo/settings/options.tmpl
+++ b/templates/repo/settings/options.tmpl
@@ -10,7 +10,7 @@
<input type="hidden" name="action" value="update">
<div class="required field {{if .Err_RepoName}}error{{end}}">
<label>{{ctx.Locale.Tr "repo.repo_name"}}</label>
- <input name="repo_name" value="{{.Repository.Name}}" data-repo-name="{{.Repository.Name}}" autofocus required>
+ <input name="repo_name" value="{{.Repository.Name}}" data-repo-name="{{.Repository.Name}}" required>
</div>
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.repo_size"}}</label>
diff --git a/templates/shared/secrets/add_list.tmpl b/templates/shared/secrets/add_list.tmpl
index 977f308b71..a4ef2e5384 100644
--- a/templates/shared/secrets/add_list.tmpl
+++ b/templates/shared/secrets/add_list.tmpl
@@ -4,9 +4,13 @@
<button class="ui primary tiny button show-modal"
data-modal="#add-secret-modal"
data-modal-form.action="{{.Link}}"
- data-modal-header="{{ctx.Locale.Tr "secrets.creation"}}"
+ data-modal-header="{{ctx.Locale.Tr "secrets.add_secret"}}"
+ data-modal-secret-name.value=""
+ data-modal-secret-name.read-only="false"
+ data-modal-secret-data=""
+ data-modal-secret-description=""
>
- {{ctx.Locale.Tr "secrets.creation"}}
+ {{ctx.Locale.Tr "secrets.add_secret"}}
</button>
</div>
</h4>
@@ -33,6 +37,18 @@
<span class="color-text-light-2">
{{ctx.Locale.Tr "settings.added_on" (DateUtils.AbsoluteShort .CreatedUnix)}}
</span>
+ <button class="ui btn interact-bg show-modal tw-p-2"
+ data-modal="#add-secret-modal"
+ data-modal-form.action="{{$.Link}}"
+ data-modal-header="{{ctx.Locale.Tr "secrets.edit_secret"}}"
+ data-tooltip-content="{{ctx.Locale.Tr "secrets.edit_secret"}}"
+ data-modal-secret-name.value="{{.Name}}"
+ data-modal-secret-name.read-only="true"
+ data-modal-secret-data=""
+ data-modal-secret-description="{{if .Description}}{{.Description}}{{end}}"
+ >
+ {{svg "octicon-pencil"}}
+ </button>
<button class="ui btn interact-bg link-action tw-p-2"
data-url="{{$.Link}}/delete?id={{.ID}}"
data-modal-confirm="{{ctx.Locale.Tr "secrets.deletion.description"}}"
@@ -51,9 +67,7 @@
{{/* Add secret dialog */}}
<div class="ui small modal" id="add-secret-modal">
- <div class="header">
- <span id="actions-modal-header"></span>
- </div>
+ <div class="header"></div>
<form class="ui form form-fetch-action" method="post">
<div class="content">
{{.CsrfTokenHtml}}
diff --git a/templates/user/settings/profile.tmpl b/templates/user/settings/profile.tmpl
index 03c3c18f28..d8e5e27b89 100644
--- a/templates/user/settings/profile.tmpl
+++ b/templates/user/settings/profile.tmpl
@@ -12,7 +12,7 @@
<span class="text red tw-hidden" id="name-change-prompt"> {{ctx.Locale.Tr "settings.change_username_prompt"}}</span>
<span class="text red tw-hidden" id="name-change-redirect-prompt"> {{ctx.Locale.Tr "settings.change_username_redirect_prompt"}}</span>
</label>
- <input id="username" name="name" value="{{.SignedUser.Name}}" data-name="{{.SignedUser.Name}}" autofocus required {{if or (not .SignedUser.IsLocal) ($.UserDisabledFeatures.Contains "change_username") .IsReverseProxy}}disabled{{end}} maxlength="40">
+ <input id="username" name="name" value="{{.SignedUser.Name}}" data-name="{{.SignedUser.Name}}" required {{if or (not .SignedUser.IsLocal) ($.UserDisabledFeatures.Contains "change_username") .IsReverseProxy}}disabled{{end}} maxlength="40">
{{if or (not .SignedUser.IsLocal) ($.UserDisabledFeatures.Contains "change_username") .IsReverseProxy}}
<p class="help text blue">{{ctx.Locale.Tr "settings.password_username_disabled"}}</p>
{{end}}
diff --git a/tests/integration/change_default_branch_test.go b/tests/integration/change_default_branch_test.go
index 729eb1e4ce..9b61cff9fd 100644
--- a/tests/integration/change_default_branch_test.go
+++ b/tests/integration/change_default_branch_test.go
@@ -6,12 +6,16 @@ package integration
import (
"fmt"
"net/http"
+ "strconv"
"testing"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
)
func TestChangeDefaultBranch(t *testing.T) {
@@ -38,3 +42,96 @@ func TestChangeDefaultBranch(t *testing.T) {
})
session.MakeRequest(t, req, http.StatusNotFound)
}
+
+func checkDivergence(t *testing.T, session *TestSession, branchesURL, expectedDefaultBranch string, expectedBranchToDivergence map[string]git.DivergeObject) {
+ req := NewRequest(t, "GET", branchesURL)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+
+ branchNodes := htmlDoc.doc.Find(".branch-name").Nodes
+ branchNames := []string{}
+ for _, node := range branchNodes {
+ branchNames = append(branchNames, node.FirstChild.Data)
+ }
+
+ expectBranchCount := len(expectedBranchToDivergence)
+
+ assert.Len(t, branchNames, expectBranchCount+1)
+ assert.Equal(t, expectedDefaultBranch, branchNames[0])
+
+ allCountBehindNodes := htmlDoc.doc.Find(".count-behind").Nodes
+ allCountAheadNodes := htmlDoc.doc.Find(".count-ahead").Nodes
+
+ assert.Len(t, allCountAheadNodes, expectBranchCount)
+ assert.Len(t, allCountBehindNodes, expectBranchCount)
+
+ for i := range expectBranchCount {
+ branchName := branchNames[i+1]
+ assert.Contains(t, expectedBranchToDivergence, branchName)
+
+ expectedCountAhead := expectedBranchToDivergence[branchName].Ahead
+ expectedCountBehind := expectedBranchToDivergence[branchName].Behind
+ countAhead, err := strconv.Atoi(allCountAheadNodes[i].FirstChild.Data)
+ assert.NoError(t, err)
+ countBehind, err := strconv.Atoi(allCountBehindNodes[i].FirstChild.Data)
+ assert.NoError(t, err)
+
+ assert.Equal(t, expectedCountAhead, countAhead)
+ assert.Equal(t, expectedCountBehind, countBehind)
+ }
+}
+
+func TestChangeDefaultBranchDivergence(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 16})
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ session := loginUser(t, owner.Name)
+ branchesURL := fmt.Sprintf("/%s/%s/branches", owner.Name, repo.Name)
+ settingsBranchesURL := fmt.Sprintf("/%s/%s/settings/branches", owner.Name, repo.Name)
+
+ // check branch divergence before switching default branch
+ expectedBranchToDivergenceBefore := map[string]git.DivergeObject{
+ "not-signed": {
+ Ahead: 0,
+ Behind: 0,
+ },
+ "good-sign-not-yet-validated": {
+ Ahead: 0,
+ Behind: 1,
+ },
+ "good-sign": {
+ Ahead: 1,
+ Behind: 3,
+ },
+ }
+ checkDivergence(t, session, branchesURL, "master", expectedBranchToDivergenceBefore)
+
+ // switch default branch
+ newDefaultBranch := "good-sign-not-yet-validated"
+ csrf := GetUserCSRFToken(t, session)
+ req := NewRequestWithValues(t, "POST", settingsBranchesURL, map[string]string{
+ "_csrf": csrf,
+ "action": "default_branch",
+ "branch": newDefaultBranch,
+ })
+ session.MakeRequest(t, req, http.StatusSeeOther)
+
+ // check branch divergence after switching default branch
+ expectedBranchToDivergenceAfter := map[string]git.DivergeObject{
+ "master": {
+ Ahead: 1,
+ Behind: 0,
+ },
+ "not-signed": {
+ Ahead: 1,
+ Behind: 0,
+ },
+ "good-sign": {
+ Ahead: 1,
+ Behind: 2,
+ },
+ }
+ checkDivergence(t, session, branchesURL, newDefaultBranch, expectedBranchToDivergenceAfter)
+}
diff --git a/web_src/js/features/common-button.test.ts b/web_src/js/features/common-button.test.ts
new file mode 100644
index 0000000000..f41bafbc79
--- /dev/null
+++ b/web_src/js/features/common-button.test.ts
@@ -0,0 +1,14 @@
+import {assignElementProperty} from './common-button.ts';
+
+test('assignElementProperty', () => {
+ const elForm = document.createElement('form');
+ assignElementProperty(elForm, 'action', '/test-link');
+ expect(elForm.action).contains('/test-link'); // the DOM always returns absolute URL
+ assignElementProperty(elForm, 'text-content', 'dummy');
+ expect(elForm.textContent).toBe('dummy');
+
+ const elInput = document.createElement('input');
+ expect(elInput.readOnly).toBe(false);
+ assignElementProperty(elInput, 'read-only', 'true');
+ expect(elInput.readOnly).toBe(true);
+});
diff --git a/web_src/js/features/common-button.ts b/web_src/js/features/common-button.ts
index 003bfbce5d..ae399e48b3 100644
--- a/web_src/js/features/common-button.ts
+++ b/web_src/js/features/common-button.ts
@@ -1,5 +1,5 @@
import {POST} from '../modules/fetch.ts';
-import {addDelegatedEventListener, hideElem, showElem, toggleElem} from '../utils/dom.ts';
+import {addDelegatedEventListener, hideElem, isElemVisible, showElem, toggleElem} from '../utils/dom.ts';
import {fomanticQuery} from '../modules/fomantic/base.ts';
import {camelize} from 'vue';
@@ -79,10 +79,11 @@ function onShowPanelClick(el: HTMLElement, e: MouseEvent) {
// if it has "toggle" class, it toggles the panel
e.preventDefault();
const sel = el.getAttribute('data-panel');
- if (el.classList.contains('toggle')) {
- toggleElem(sel);
- } else {
- showElem(sel);
+ const elems = el.classList.contains('toggle') ? toggleElem(sel) : showElem(sel);
+ for (const elem of elems) {
+ if (isElemVisible(elem as HTMLElement)) {
+ elem.querySelector<HTMLElement>('[autofocus]')?.focus();
+ }
}
}
@@ -102,6 +103,21 @@ function onHidePanelClick(el: HTMLElement, e: MouseEvent) {
throw new Error('no panel to hide'); // should never happen, otherwise there is a bug in code
}
+export function assignElementProperty(el: any, name: string, val: string) {
+ name = camelize(name);
+ const old = el[name];
+ if (typeof old === 'boolean') {
+ el[name] = val === 'true';
+ } else if (typeof old === 'number') {
+ el[name] = parseFloat(val);
+ } else if (typeof old === 'string') {
+ el[name] = val;
+ } else {
+ // in the future, we could introduce a better typing system like `data-modal-form.action:string="..."`
+ throw new Error(`cannot assign element property ${name} by value ${val}`);
+ }
+}
+
function onShowModalClick(el: HTMLElement, e: MouseEvent) {
// A ".show-modal" button will show a modal dialog defined by its "data-modal" attribute.
// Each "data-modal-{target}" attribute will be filled to target element's value or text-content.
@@ -109,7 +125,7 @@ function onShowModalClick(el: HTMLElement, e: MouseEvent) {
// * Then, try to query '[name=target]'
// * Then, try to query '.target'
// * Then, try to query 'target' as HTML tag
- // If there is a ".{attr}" part like "data-modal-form.action", then the form's "action" attribute will be set.
+ // If there is a ".{prop-name}" part like "data-modal-form.action", the "form" element's "action" property will be set, the "prop-name" will be camel-cased to "propName".
e.preventDefault();
const modalSelector = el.getAttribute('data-modal');
const elModal = document.querySelector(modalSelector);
@@ -122,7 +138,7 @@ function onShowModalClick(el: HTMLElement, e: MouseEvent) {
}
const attrTargetCombo = attrib.name.substring(modalAttrPrefix.length);
- const [attrTargetName, attrTargetAttr] = attrTargetCombo.split('.');
+ const [attrTargetName, attrTargetProp] = attrTargetCombo.split('.');
// try to find target by: "#target" -> "[name=target]" -> ".target" -> "<target> tag"
const attrTarget = elModal.querySelector(`#${attrTargetName}`) ||
elModal.querySelector(`[name=${attrTargetName}]`) ||
@@ -133,8 +149,8 @@ function onShowModalClick(el: HTMLElement, e: MouseEvent) {
continue;
}
- if (attrTargetAttr) {
- (attrTarget as any)[camelize(attrTargetAttr)] = attrib.value;
+ if (attrTargetProp) {
+ assignElementProperty(attrTarget, attrTargetProp, attrib.value);
} else if (attrTarget.matches('input, textarea')) {
(attrTarget as HTMLInputElement | HTMLTextAreaElement).value = attrib.value; // FIXME: add more supports like checkbox
} else {
diff --git a/web_src/js/features/common-issue-list.ts b/web_src/js/features/common-issue-list.ts
index e207364794..037529bd10 100644
--- a/web_src/js/features/common-issue-list.ts
+++ b/web_src/js/features/common-issue-list.ts
@@ -1,4 +1,4 @@
-import {isElemHidden, onInputDebounce, submitEventSubmitter, toggleElem} from '../utils/dom.ts';
+import {isElemVisible, onInputDebounce, submitEventSubmitter, toggleElem} from '../utils/dom.ts';
import {GET} from '../modules/fetch.ts';
const {appSubUrl} = window.config;
@@ -28,7 +28,7 @@ export function parseIssueListQuickGotoLink(repoLink: string, searchText: string
}
export function initCommonIssueListQuickGoto() {
- const goto = document.querySelector('#issue-list-quick-goto');
+ const goto = document.querySelector<HTMLElement>('#issue-list-quick-goto');
if (!goto) return;
const form = goto.closest('form');
@@ -37,7 +37,7 @@ export function initCommonIssueListQuickGoto() {
form.addEventListener('submit', (e) => {
// if there is no goto button, or the form is submitted by non-quick-goto elements, submit the form directly
- let doQuickGoto = !isElemHidden(goto);
+ let doQuickGoto = isElemVisible(goto);
const submitter = submitEventSubmitter(e);
if (submitter !== form && submitter !== input && submitter !== goto) doQuickGoto = false;
if (!doQuickGoto) return;
diff --git a/web_src/js/features/repo-issue-list.ts b/web_src/js/features/repo-issue-list.ts
index 8cd4483357..3ea5fb70c0 100644
--- a/web_src/js/features/repo-issue-list.ts
+++ b/web_src/js/features/repo-issue-list.ts
@@ -1,5 +1,5 @@
import {updateIssuesMeta} from './repo-common.ts';
-import {toggleElem, isElemHidden, queryElems} from '../utils/dom.ts';
+import {toggleElem, queryElems, isElemVisible} from '../utils/dom.ts';
import {htmlEscape} from 'escape-goat';
import {confirmModal} from './comp/ConfirmModal.ts';
import {showErrorToast} from '../modules/toast.ts';
@@ -33,8 +33,8 @@ function initRepoIssueListCheckboxes() {
toggleElem('#issue-filters', !anyChecked);
toggleElem('#issue-actions', anyChecked);
// there are two panels but only one select-all checkbox, so move the checkbox to the visible panel
- const panels = document.querySelectorAll('#issue-filters, #issue-actions');
- const visiblePanel = Array.from(panels).find((el) => !isElemHidden(el));
+ const panels = document.querySelectorAll<HTMLElement>('#issue-filters, #issue-actions');
+ const visiblePanel = Array.from(panels).find((el) => isElemVisible(el));
const toolbarLeft = visiblePanel.querySelector('.issue-list-toolbar-left');
toolbarLeft.prepend(issueSelectAll);
};
diff --git a/web_src/js/utils/dom.test.ts b/web_src/js/utils/dom.test.ts
index 6a3af91556..057ea9808c 100644
--- a/web_src/js/utils/dom.test.ts
+++ b/web_src/js/utils/dom.test.ts
@@ -25,10 +25,14 @@ test('createElementFromAttrs', () => {
});
test('querySingleVisibleElem', () => {
- let el = createElementFromHTML('<div><span>foo</span></div>');
+ let el = createElementFromHTML('<div></div>');
+ expect(querySingleVisibleElem(el, 'span')).toBeNull();
+ el = createElementFromHTML('<div><span>foo</span></div>');
expect(querySingleVisibleElem(el, 'span').textContent).toEqual('foo');
el = createElementFromHTML('<div><span style="display: none;">foo</span><span>bar</span></div>');
expect(querySingleVisibleElem(el, 'span').textContent).toEqual('bar');
+ el = createElementFromHTML('<div><span class="some-class tw-hidden">foo</span><span>bar</span></div>');
+ expect(querySingleVisibleElem(el, 'span').textContent).toEqual('bar');
el = createElementFromHTML('<div><span>foo</span><span>bar</span></div>');
expect(() => querySingleVisibleElem(el, 'span')).toThrowError('Expected exactly one visible element');
});
diff --git a/web_src/js/utils/dom.ts b/web_src/js/utils/dom.ts
index 4386d38632..83a0d9c8df 100644
--- a/web_src/js/utils/dom.ts
+++ b/web_src/js/utils/dom.ts
@@ -9,24 +9,24 @@ type ElementsCallback<T extends Element> = (el: T) => Promisable<any>;
type ElementsCallbackWithArgs = (el: Element, ...args: any[]) => Promisable<any>;
export type DOMEvent<E extends Event, T extends Element = HTMLElement> = E & { target: Partial<T>; };
-function elementsCall(el: ElementArg, func: ElementsCallbackWithArgs, ...args: any[]) {
+function elementsCall(el: ElementArg, func: ElementsCallbackWithArgs, ...args: any[]): ArrayLikeIterable<Element> {
if (typeof el === 'string' || el instanceof String) {
el = document.querySelectorAll(el as string);
}
if (el instanceof Node) {
func(el, ...args);
+ return [el];
} else if (el.length !== undefined) {
// this works for: NodeList, HTMLCollection, Array, jQuery
- for (const e of (el as ArrayLikeIterable<Element>)) {
- func(e, ...args);
- }
- } else {
- throw new Error('invalid argument to be shown/hidden');
+ const elems = el as ArrayLikeIterable<Element>;
+ for (const elem of elems) func(elem, ...args);
+ return elems;
}
+ throw new Error('invalid argument to be shown/hidden');
}
-export function toggleClass(el: ElementArg, className: string, force?: boolean) {
- elementsCall(el, (e: Element) => {
+export function toggleClass(el: ElementArg, className: string, force?: boolean): ArrayLikeIterable<Element> {
+ return elementsCall(el, (e: Element) => {
if (force === true) {
e.classList.add(className);
} else if (force === false) {
@@ -43,23 +43,16 @@ export function toggleClass(el: ElementArg, className: string, force?: boolean)
* @param el ElementArg
* @param force force=true to show or force=false to hide, undefined to toggle
*/
-export function toggleElem(el: ElementArg, force?: boolean) {
- toggleClass(el, 'tw-hidden', force === undefined ? force : !force);
-}
-
-export function showElem(el: ElementArg) {
- toggleElem(el, true);
+export function toggleElem(el: ElementArg, force?: boolean): ArrayLikeIterable<Element> {
+ return toggleClass(el, 'tw-hidden', force === undefined ? force : !force);
}
-export function hideElem(el: ElementArg) {
- toggleElem(el, false);
+export function showElem(el: ElementArg): ArrayLikeIterable<Element> {
+ return toggleElem(el, true);
}
-export function isElemHidden(el: ElementArg) {
- const res: boolean[] = [];
- elementsCall(el, (e) => res.push(e.classList.contains('tw-hidden')));
- if (res.length > 1) throw new Error(`isElemHidden doesn't work for multiple elements`);
- return res[0];
+export function hideElem(el: ElementArg): ArrayLikeIterable<Element> {
+ return toggleElem(el, false);
}
function applyElemsCallback<T extends Element>(elems: ArrayLikeIterable<T>, fn?: ElementsCallback<T>): ArrayLikeIterable<T> {
@@ -275,14 +268,12 @@ export function initSubmitEventPolyfill() {
document.body.addEventListener('focus', submitEventPolyfillListener);
}
-/**
- * Check if an element is visible, equivalent to jQuery's `:visible` pseudo.
- * Note: This function doesn't account for all possible visibility scenarios.
- */
-export function isElemVisible(element: HTMLElement): boolean {
- if (!element) return false;
- // checking element.style.display is not necessary for browsers, but it is required by some tests with happy-dom because happy-dom doesn't really do layout
- return Boolean((element.offsetWidth || element.offsetHeight || element.getClientRects().length) && element.style.display !== 'none');
+export function isElemVisible(el: HTMLElement): boolean {
+ // Check if an element is visible, equivalent to jQuery's `:visible` pseudo.
+ // This function DOESN'T account for all possible visibility scenarios, its behavior is covered by the tests of "querySingleVisibleElem"
+ if (!el) return false;
+ // checking el.style.display is not necessary for browsers, but it is required by some tests with happy-dom because happy-dom doesn't really do layout
+ return !el.classList.contains('tw-hidden') && Boolean((el.offsetWidth || el.offsetHeight || el.getClientRects().length) && el.style.display !== 'none');
}
// replace selected text in a textarea while preserving editor history, e.g. CTRL-Z works after this