diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000000..125f92a253 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +/public/js/semantic.dropdown.custom.js diff --git a/custom/conf/app.ini.sample b/custom/conf/app.ini.sample index 33cd0506ed..17fcc0de23 100644 --- a/custom/conf/app.ini.sample +++ b/custom/conf/app.ini.sample @@ -511,6 +511,10 @@ DELIVER_TIMEOUT = 5 SKIP_TLS_VERIFY = false ; Number of history information in each page PAGING_NUM = 10 +; Proxy server URL, support http://, https//, socks://, blank will follow environment http_proxy/https_proxy +PROXY_URL = +; Comma separated list of host names requiring proxy. Glob patterns (*) are accepted; use ** to match all hosts. +PROXY_HOSTS = [mailer] ENABLED = false diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index 1e24255d8d..96b529c0bc 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -312,6 +312,8 @@ relation to port exhaustion. - `DELIVER_TIMEOUT`: **5**: Delivery timeout (sec) for shooting webhooks. - `SKIP_TLS_VERIFY`: **false**: Allow insecure certification. - `PAGING_NUM`: **10**: Number of webhook history events that are shown in one page. +- `PROXY_URL`: ****: Proxy server URL, support http://, https//, socks://, blank will follow environment http_proxy/https_proxy +- `PROXY_HOSTS`: ****: Comma separated list of host names requiring proxy. Glob patterns (*) are accepted; use ** to match all hosts. ## Mailer (`mailer`) diff --git a/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md b/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md index 53426ed983..b545d9a99d 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md +++ b/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md @@ -129,6 +129,8 @@ menu: - `DELIVER_TIMEOUT`: 请求webhooks的超时时间,单位秒。 - `SKIP_TLS_VERIFY`: 是否允许不安全的证书。 - `PAGING_NUM`: 每页显示的Webhook 历史数量。 +- `PROXY_URL`: ****: 代理服务器网址,支持 http://, https//, socks://, 为空将使用环境变量中的 http_proxy/https_proxy 设置。 +- `PROXY_HOSTS`: ****: 逗号分隔的需要代理的域名或IP地址。支持 * 号匹配符,使用 ** 匹配所有域名和IP地址。 ## Mailer (`mailer`) diff --git a/docs/content/doc/advanced/third-party-tools.zh-cn.md b/docs/content/doc/advanced/third-party-tools.zh-cn.md new file mode 100644 index 0000000000..be7b17c765 --- /dev/null +++ b/docs/content/doc/advanced/third-party-tools.zh-cn.md @@ -0,0 +1,36 @@ +--- +date: "2019-03-11T21:45:00+00:00" +title: "高级: 第三方工具" +slug: "third-party-tools" +weight: 50 +toc: true +draft: false +menu: + sidebar: + parent: "advanced" + name: "第三方工具" + weight: 50 + identifier: "third-party-tools" +--- + +# 第三方工具列表 +**注意:** 这些工具并没有经过Gitea的检验,在这里列出它们只是为了便捷. + +*此列表并不是完整的列表,可以随时咨询如何添加!* + +### 持续集成 +[BuildKite 连接器](https://github.com/techknowlogick/gitea-buildkite-connector) +[Jenkins 插件](https://github.com/jenkinsci/gitea-plugin) +[Gitea搭配Drone](https://docs.drone.io/installation/gitea) + + +### 迁移 +[Gitea安装脚本](https://git.coolaj86.com/coolaj86/gitea-installer.sh) +[GitHub迁移](https://gitea.com/gitea/migrator) + + +### 移动端 +[安卓客户端GitNex](https://gitlab.com/mmarif4u/gitnex) + +### 编辑器扩展 + - [Gitea的Visual Studio扩展](https://github.com/maikebing/Gitea.VisualStudio) 从 [Visual Studio 扩展市场](https://marketplace.visualstudio.com/items?itemName=MysticBoy.GiteaExtensionforVisualStudio) 下载 diff --git a/docs/content/doc/help/faq.en-us.md b/docs/content/doc/help/faq.en-us.md index 856e756db1..8a65b522f5 100644 --- a/docs/content/doc/help/faq.en-us.md +++ b/docs/content/doc/help/faq.en-us.md @@ -43,6 +43,8 @@ Also see [Support Options]({{< relref "doc/help/seek-help.en-us.md" >}}) * [Missing releases after migration repository with tags](#missing-releases-after-migrating-repository-with-tags) * [LFS Issues](#lfs-issues) * [How can I create users before starting Gitea](#how-can-i-create-users-before-starting-gitea) +* [How can I enable password reset](#how-can-i-enable-password-reset) +* [How can a user's password be changed](#how-can-a-user-s-password-be-changed) ## Difference between 1.x and 1.x.x downloads @@ -275,4 +277,17 @@ By default, your LFS token will expire after 20 minutes. If you have a slow conn You may want to set this value to `60m` or `120m`. ## How can I create users before starting Gitea -Gitea provides a sub-command `gitea migrate` to initialize the database, after which you can use the [admin CLI commands]({{< relref "doc/usage/command-line.en-us.md" >}}) to add users like normal. +Gitea provides a sub-command `gitea migrate` to initialize the database, after which you can use the [admin CLI commands]({{< relref "doc/usage/command-line.en-us.md#admin" >}}) to add users like normal. + +## How can I enable password reset +There is no setting for password resets. It is enabled when a [mail service]({{< relref "doc/usage/email-setup.en-us.md" >}}) is configured, and disabled otherwise. + +## How can a user's password be changed +- As an **admin**, you can change any user's password (and optionally force them to change it on next login)... + - By navigating to your `Site Administration -> User Accounts` page and editing a user. + - By using the [admin CLI commands]({{< relref "doc/usage/command-line.en-us.md#admin" >}}). + Keep in mind most commands will also need a [global flag]({{< relref "doc/usage/command-line.en-us.md#global-options" >}}) to point the CLI at the correct configuration. +- As a **user** you can change it... + - In your account `Settings -> Account` page (this method **requires** you to know your current password). + - By using the `Forgot Password` link. + If the `Forgot Password/Account Recovery` page is disabled, please contact your administrator to configure a [mail service]({{< relref "doc/usage/email-setup.en-us.md" >}}). diff --git a/docs/content/doc/usage/email-setup.md b/docs/content/doc/usage/email-setup.en-us.md similarity index 100% rename from docs/content/doc/usage/email-setup.md rename to docs/content/doc/usage/email-setup.en-us.md diff --git a/integrations/api_repo_test.go b/integrations/api_repo_test.go index 60fe4a3649..a2683d4af4 100644 --- a/integrations/api_repo_test.go +++ b/integrations/api_repo_test.go @@ -334,7 +334,7 @@ func testAPIRepoMigrateConflict(t *testing.T, u *url.URL) { resp := httpContext.Session.MakeRequest(t, req, http.StatusConflict) respJSON := map[string]string{} DecodeJSON(t, resp, &respJSON) - assert.Equal(t, respJSON["message"], "The repository with the same name already exists.") + assert.Equal(t, "The repository with the same name already exists.", respJSON["message"]) }) } diff --git a/integrations/signup_test.go b/integrations/signup_test.go index 325c906326..e122efa39c 100644 --- a/integrations/signup_test.go +++ b/integrations/signup_test.go @@ -19,8 +19,8 @@ func TestSignup(t *testing.T) { req := NewRequestWithValues(t, "POST", "/user/sign_up", map[string]string{ "user_name": "exampleUser", "email": "exampleUser@example.com", - "password": "examplePassword", - "retype": "examplePassword", + "password": "examplePassword!1", + "retype": "examplePassword!1", }) MakeRequest(t, req, http.StatusFound) diff --git a/models/action.go b/models/action.go index b651c658d5..ddb82e0f4c 100644 --- a/models/action.go +++ b/models/action.go @@ -283,49 +283,6 @@ func (a *Action) GetIssueContent() string { return issue.Content } -func newRepoAction(e Engine, u *User, repo *Repository) (err error) { - if err = notifyWatchers(e, &Action{ - ActUserID: u.ID, - ActUser: u, - OpType: ActionCreateRepo, - RepoID: repo.ID, - Repo: repo, - IsPrivate: repo.IsPrivate, - }); err != nil { - return fmt.Errorf("notify watchers '%d/%d': %v", u.ID, repo.ID, err) - } - - log.Trace("action.newRepoAction: %s/%s", u.Name, repo.Name) - return err -} - -// NewRepoAction adds new action for creating repository. -func NewRepoAction(u *User, repo *Repository) (err error) { - return newRepoAction(x, u, repo) -} - -func renameRepoAction(e Engine, actUser *User, oldRepoName string, repo *Repository) (err error) { - if err = notifyWatchers(e, &Action{ - ActUserID: actUser.ID, - ActUser: actUser, - OpType: ActionRenameRepo, - RepoID: repo.ID, - Repo: repo, - IsPrivate: repo.IsPrivate, - Content: oldRepoName, - }); err != nil { - return fmt.Errorf("notify watchers: %v", err) - } - - log.Trace("action.renameRepoAction: %s/%s", actUser.Name, repo.Name) - return nil -} - -// RenameRepoAction adds new action for renaming a repository. -func RenameRepoAction(actUser *User, oldRepoName string, repo *Repository) error { - return renameRepoAction(x, actUser, oldRepoName, repo) -} - // PushCommit represents a commit in a push operation. type PushCommit struct { Sha1 string diff --git a/models/action_test.go b/models/action_test.go index df41556850..5eb89aac21 100644 --- a/models/action_test.go +++ b/models/action_test.go @@ -2,7 +2,6 @@ package models import ( "path" - "strings" "testing" "code.gitea.io/gitea/modules/setting" @@ -28,58 +27,6 @@ func TestAction_GetRepoLink(t *testing.T) { assert.Equal(t, expected, action.GetRepoLink()) } -func TestNewRepoAction(t *testing.T) { - assert.NoError(t, PrepareTestDatabase()) - - user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User) - repo := AssertExistsAndLoadBean(t, &Repository{OwnerID: user.ID}).(*Repository) - repo.Owner = user - - actionBean := &Action{ - OpType: ActionCreateRepo, - ActUserID: user.ID, - RepoID: repo.ID, - ActUser: user, - Repo: repo, - IsPrivate: repo.IsPrivate, - } - - AssertNotExistsBean(t, actionBean) - assert.NoError(t, NewRepoAction(user, repo)) - AssertExistsAndLoadBean(t, actionBean) - CheckConsistencyFor(t, &Action{}) -} - -func TestRenameRepoAction(t *testing.T) { - assert.NoError(t, PrepareTestDatabase()) - - user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User) - repo := AssertExistsAndLoadBean(t, &Repository{OwnerID: user.ID}).(*Repository) - repo.Owner = user - - oldRepoName := repo.Name - const newRepoName = "newRepoName" - repo.Name = newRepoName - repo.LowerName = strings.ToLower(newRepoName) - - actionBean := &Action{ - OpType: ActionRenameRepo, - ActUserID: user.ID, - ActUser: user, - RepoID: repo.ID, - Repo: repo, - IsPrivate: repo.IsPrivate, - Content: oldRepoName, - } - AssertNotExistsBean(t, actionBean) - assert.NoError(t, RenameRepoAction(user, oldRepoName, repo)) - AssertExistsAndLoadBean(t, actionBean) - - _, err := x.ID(repo.ID).Cols("name", "lower_name").Update(repo) - assert.NoError(t, err) - CheckConsistencyFor(t, &Action{}) -} - func TestPushCommits_ToAPIPayloadCommits(t *testing.T) { pushCommits := NewPushCommits() pushCommits.Commits = []*PushCommit{ diff --git a/models/issue.go b/models/issue.go index 1e9d973186..3893048533 100644 --- a/models/issue.go +++ b/models/issue.go @@ -1075,7 +1075,8 @@ func sortIssuesSession(sess *xorm.Session, sortType string, priorityRepoID int64 case "priority": sess.Desc("issue.priority") case "nearduedate": - sess.Asc("issue.deadline_unix") + // 253370764800 is 01/01/9999 @ 12:00am (UTC) + sess.OrderBy("CASE WHEN issue.deadline_unix = 0 THEN 253370764800 ELSE issue.deadline_unix END ASC") case "farduedate": sess.Desc("issue.deadline_unix") case "priorityrepo": diff --git a/models/org_team.go b/models/org_team.go index d740e1c240..126a8c896a 100644 --- a/models/org_team.go +++ b/models/org_team.go @@ -243,6 +243,21 @@ func (t *Team) addAllRepositories(e Engine) error { return nil } +// AddAllRepositories adds all repositories to the team +func (t *Team) AddAllRepositories() (err error) { + sess := x.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return err + } + + if err = t.addAllRepositories(sess); err != nil { + return err + } + + return sess.Commit() +} + // AddRepository adds new repository to team of organization. func (t *Team) AddRepository(repo *Repository) (err error) { if repo.OwnerID != t.OrgID { @@ -264,6 +279,69 @@ func (t *Team) AddRepository(repo *Repository) (err error) { return sess.Commit() } +// RemoveAllRepositories removes all repositories from team and recalculates access +func (t *Team) RemoveAllRepositories() (err error) { + if t.IncludesAllRepositories { + return nil + } + + sess := x.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return err + } + + if err = t.removeAllRepositories(sess); err != nil { + return err + } + + return sess.Commit() +} + +// removeAllRepositories removes all repositories from team and recalculates access +// Note: Shall not be called if team includes all repositories +func (t *Team) removeAllRepositories(e Engine) (err error) { + // Delete all accesses. + for _, repo := range t.Repos { + if err := repo.recalculateTeamAccesses(e, t.ID); err != nil { + return err + } + + // Remove watches from all users and now unaccessible repos + for _, user := range t.Members { + has, err := hasAccess(e, user.ID, repo) + if err != nil { + return err + } else if has { + continue + } + + if err = watchRepo(e, user.ID, repo.ID, false); err != nil { + return err + } + + // Remove all IssueWatches a user has subscribed to in the repositories + if err = removeIssueWatchersByRepoID(e, user.ID, repo.ID); err != nil { + return err + } + } + } + + // Delete team-repo + if _, err := e. + Where("team_id=?", t.ID). + Delete(new(TeamRepo)); err != nil { + return err + } + + t.NumRepos = 0 + if _, err = e.ID(t.ID).Cols("num_repos").Update(t); err != nil { + return err + } + + return nil +} + // removeRepository removes a repository from a team and recalculates access // Note: Repository shall not be removed from team if it includes all repositories (unless the repository is deleted) func (t *Team) removeRepository(e Engine, repo *Repository, recalculate bool) (err error) { @@ -577,36 +655,7 @@ func DeleteTeam(t *Team) error { return err } - // Delete all accesses. - for _, repo := range t.Repos { - if err := repo.recalculateTeamAccesses(sess, t.ID); err != nil { - return err - } - - // Remove watches from all users and now unaccessible repos - for _, user := range t.Members { - has, err := hasAccess(sess, user.ID, repo) - if err != nil { - return err - } else if has { - continue - } - - if err = watchRepo(sess, user.ID, repo.ID, false); err != nil { - return err - } - - // Remove all IssueWatches a user has subscribed to in the repositories - if err = removeIssueWatchersByRepoID(sess, user.ID, repo.ID); err != nil { - return err - } - } - } - - // Delete team-repo - if _, err := sess. - Where("team_id=?", t.ID). - Delete(new(TeamRepo)); err != nil { + if err := t.removeAllRepositories(sess); err != nil { return err } diff --git a/models/repo.go b/models/repo.go index 89e579d1ec..812460e92f 100644 --- a/models/repo.go +++ b/models/repo.go @@ -1469,8 +1469,15 @@ func createRepository(e *xorm.Session, doer, u *User, repo *Repository) (err err return fmt.Errorf("watchRepo: %v", err) } } - if err = newRepoAction(e, doer, repo); err != nil { - return fmt.Errorf("newRepoAction: %v", err) + if err = notifyWatchers(e, &Action{ + ActUserID: doer.ID, + ActUser: doer, + OpType: ActionCreateRepo, + RepoID: repo.ID, + Repo: repo, + IsPrivate: repo.IsPrivate, + }); err != nil { + return fmt.Errorf("notify watchers '%d/%d': %v", doer.ID, repo.ID, err) } if err = copyDefaultWebhooksToRepo(e, repo.ID); err != nil { @@ -2833,3 +2840,9 @@ func (repo *Repository) GetTreePathLock(treePath string) (*LFSLock, error) { } return nil, nil } + +// UpdateRepositoryCols updates repository's columns +func UpdateRepositoryCols(repo *Repository, cols ...string) error { + _, err := x.ID(repo.ID).Cols(cols...).Update(repo) + return err +} diff --git a/modules/migrations/github_test.go b/modules/migrations/github_test.go index 2a0a4edf32..d9976d1132 100644 --- a/modules/migrations/github_test.go +++ b/modules/migrations/github_test.go @@ -118,12 +118,6 @@ func TestGitHubDownloadRepo(t *testing.T) { "2018-09-05 16:34:22 +0000 UTC", "2018-08-11 08:45:01 +0000 UTC", "closed", milestone) - case "1.6.0": - assertMilestoneEqual(t, "1.6.0", "2018-09-25 07:00:00 +0000 UTC", - "2018-05-11 05:37:01 +0000 UTC", - "2019-01-27 19:21:22 +0000 UTC", - "2018-11-23 13:23:16 +0000 UTC", - "closed", milestone) case "1.7.0": assertMilestoneEqual(t, "1.7.0", "2018-12-25 08:00:00 +0000 UTC", "2018-08-28 14:20:14 +0000 UTC", diff --git a/modules/notification/action/action.go b/modules/notification/action/action.go index 15228f65e7..52471c1107 100644 --- a/modules/notification/action/action.go +++ b/modules/notification/action/action.go @@ -20,7 +20,7 @@ var ( _ base.Notifier = &actionNotifier{} ) -// NewNotifier create a new webhookNotifier notifier +// NewNotifier create a new actionNotifier notifier func NewNotifier() base.Notifier { return &actionNotifier{} } @@ -75,3 +75,19 @@ func (a *actionNotifier) NotifyNewPullRequest(pull *models.PullRequest) { log.Error("NotifyWatchers: %v", err) } } + +func (a *actionNotifier) NotifyRenameRepository(doer *models.User, repo *models.Repository, oldName string) { + if err := models.NotifyWatchers(&models.Action{ + ActUserID: doer.ID, + ActUser: doer, + OpType: models.ActionRenameRepo, + RepoID: repo.ID, + Repo: repo, + IsPrivate: repo.IsPrivate, + Content: oldName, + }); err != nil { + log.Error("notify watchers: %v", err) + } else { + log.Trace("action.renameRepoAction: %s/%s", doer.Name, repo.Name) + } +} diff --git a/modules/notification/action/action_test.go b/modules/notification/action/action_test.go new file mode 100644 index 0000000000..cdfe4bd663 --- /dev/null +++ b/modules/notification/action/action_test.go @@ -0,0 +1,47 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package action + +import ( + "path/filepath" + "strings" + "testing" + + "code.gitea.io/gitea/models" + "github.com/stretchr/testify/assert" +) + +func TestMain(m *testing.M) { + models.MainTest(m, filepath.Join("..", "..", "..")) +} + +func TestRenameRepoAction(t *testing.T) { + assert.NoError(t, models.PrepareTestDatabase()) + + user := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) + repo := models.AssertExistsAndLoadBean(t, &models.Repository{OwnerID: user.ID}).(*models.Repository) + repo.Owner = user + + oldRepoName := repo.Name + const newRepoName = "newRepoName" + repo.Name = newRepoName + repo.LowerName = strings.ToLower(newRepoName) + + actionBean := &models.Action{ + OpType: models.ActionRenameRepo, + ActUserID: user.ID, + ActUser: user, + RepoID: repo.ID, + Repo: repo, + IsPrivate: repo.IsPrivate, + Content: oldRepoName, + } + models.AssertNotExistsBean(t, actionBean) + + NewNotifier().NotifyRenameRepository(user, repo, oldRepoName) + + models.AssertExistsAndLoadBean(t, actionBean) + models.CheckConsistencyFor(t, &models.Action{}) +} diff --git a/modules/notification/base/notifier.go b/modules/notification/base/notifier.go index 72bf52c938..9510afc978 100644 --- a/modules/notification/base/notifier.go +++ b/modules/notification/base/notifier.go @@ -17,6 +17,7 @@ type Notifier interface { NotifyMigrateRepository(doer *models.User, u *models.User, repo *models.Repository) NotifyDeleteRepository(doer *models.User, repo *models.Repository) NotifyForkRepository(doer *models.User, oldRepo, repo *models.Repository) + NotifyRenameRepository(doer *models.User, repo *models.Repository, oldName string) NotifyNewIssue(*models.Issue) NotifyIssueChangeStatus(*models.User, *models.Issue, bool) diff --git a/modules/notification/base/null.go b/modules/notification/base/null.go index a9d9d6a164..2341b8d2a7 100644 --- a/modules/notification/base/null.go +++ b/modules/notification/base/null.go @@ -66,6 +66,10 @@ func (*NullNotifier) NotifyDeleteRepository(doer *models.User, repo *models.Repo func (*NullNotifier) NotifyForkRepository(doer *models.User, oldRepo, repo *models.Repository) { } +// NotifyRenameRepository places a place holder function +func (*NullNotifier) NotifyRenameRepository(doer *models.User, repo *models.Repository, oldName string) { +} + // NotifyNewRelease places a place holder function func (*NullNotifier) NotifyNewRelease(rel *models.Release) { } diff --git a/modules/notification/notification.go b/modules/notification/notification.go index 5ac09a72e5..fdfcc62ffe 100644 --- a/modules/notification/notification.go +++ b/modules/notification/notification.go @@ -115,6 +115,13 @@ func NotifyForkRepository(doer *models.User, oldRepo, repo *models.Repository) { } } +// NotifyRenameRepository notifies repository renamed +func NotifyRenameRepository(doer *models.User, repo *models.Repository, oldName string) { + for _, notifier := range notifiers { + notifier.NotifyRenameRepository(doer, repo, oldName) + } +} + // NotifyNewRelease notifies new release to notifiers func NotifyNewRelease(rel *models.Release) { for _, notifier := range notifiers { diff --git a/modules/references/references.go b/modules/references/references.go index 58a8da2895..af0fe1aa0d 100644 --- a/modules/references/references.go +++ b/modules/references/references.go @@ -27,7 +27,7 @@ var ( // TODO: fix invalid linking issue // mentionPattern matches all mentions in the form of "@user" - mentionPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(@[0-9a-zA-Z-_\.]+)(?:\s|$|\)|\])`) + mentionPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(@[0-9a-zA-Z-_]+|@[0-9a-zA-Z-_][0-9a-zA-Z-_.]+[0-9a-zA-Z-_])(?:\s|[:,;.?!]\s|[:,;.?!]?$|\)|\])`) // issueNumericPattern matches string that references to a numeric issue, e.g. #1287 issueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(#[0-9]+)(?:\s|$|\)|\]|:|\.(\s|$))`) // issueAlphanumericPattern matches string that references to an alphanumeric issue, e.g. ABC-1234 diff --git a/modules/references/references_test.go b/modules/references/references_test.go index 52e9b4ff52..d46c5e85d7 100644 --- a/modules/references/references_test.go +++ b/modules/references/references_test.go @@ -208,14 +208,32 @@ func testFixtures(t *testing.T, fixtures []testFixture, context string) { } func TestRegExp_mentionPattern(t *testing.T) { - trueTestCases := []string{ - "@Unknwon", - "@ANT_123", - "@xxx-DiN0-z-A..uru..s-xxx", - " @lol ", - " @Te-st", - "(@gitea)", - "[@gitea]", + trueTestCases := []struct { + pat string + exp string + }{ + {"@Unknwon", "@Unknwon"}, + {"@ANT_123", "@ANT_123"}, + {"@xxx-DiN0-z-A..uru..s-xxx", "@xxx-DiN0-z-A..uru..s-xxx"}, + {" @lol ", "@lol"}, + {" @Te-st", "@Te-st"}, + {"(@gitea)", "@gitea"}, + {"[@gitea]", "@gitea"}, + {"@gitea! this", "@gitea"}, + {"@gitea? this", "@gitea"}, + {"@gitea. this", "@gitea"}, + {"@gitea, this", "@gitea"}, + {"@gitea; this", "@gitea"}, + {"@gitea!\nthis", "@gitea"}, + {"\n@gitea?\nthis", "@gitea"}, + {"\t@gitea.\nthis", "@gitea"}, + {"@gitea,\nthis", "@gitea"}, + {"@gitea;\nthis", "@gitea"}, + {"@gitea!", "@gitea"}, + {"@gitea?", "@gitea"}, + {"@gitea.", "@gitea"}, + {"@gitea,", "@gitea"}, + {"@gitea;", "@gitea"}, } falseTestCases := []string{ "@ 0", @@ -223,17 +241,24 @@ func TestRegExp_mentionPattern(t *testing.T) { "@", "", "ABC", + "@.ABC", "/home/gitea/@gitea", "\"@gitea\"", + "@@gitea", + "@gitea!this", + "@gitea?this", + "@gitea,this", + "@gitea;this", } for _, testCase := range trueTestCases { - res := mentionPattern.MatchString(testCase) - assert.True(t, res) + found := mentionPattern.FindStringSubmatch(testCase.pat) + assert.Len(t, found, 2) + assert.Equal(t, testCase.exp, found[1]) } for _, testCase := range falseTestCases { res := mentionPattern.MatchString(testCase) - assert.False(t, res) + assert.False(t, res, "[%s] should be false", testCase) } } diff --git a/modules/setting/webhook.go b/modules/setting/webhook.go index b0e7d66ad2..4a953616f1 100644 --- a/modules/setting/webhook.go +++ b/modules/setting/webhook.go @@ -4,6 +4,12 @@ package setting +import ( + "net/url" + + "code.gitea.io/gitea/modules/log" +) + var ( // Webhook settings Webhook = struct { @@ -12,11 +18,16 @@ var ( SkipTLSVerify bool Types []string PagingNum int + ProxyURL string + ProxyURLFixed *url.URL + ProxyHosts []string }{ QueueLength: 1000, DeliverTimeout: 5, SkipTLSVerify: false, PagingNum: 10, + ProxyURL: "", + ProxyHosts: []string{}, } ) @@ -27,4 +38,14 @@ func newWebhookService() { Webhook.SkipTLSVerify = sec.Key("SKIP_TLS_VERIFY").MustBool() Webhook.Types = []string{"gitea", "gogs", "slack", "discord", "dingtalk", "telegram", "msteams"} Webhook.PagingNum = sec.Key("PAGING_NUM").MustInt(10) + Webhook.ProxyURL = sec.Key("PROXY_URL").MustString("") + if Webhook.ProxyURL != "" { + var err error + Webhook.ProxyURLFixed, err = url.Parse(Webhook.ProxyURL) + if err != nil { + log.Error("Webhook PROXY_URL is not valid") + Webhook.ProxyURL = "" + } + } + Webhook.ProxyHosts = sec.Key("PROXY_HOSTS").Strings(",") } diff --git a/modules/task/migrate.go b/modules/task/migrate.go index 5d15a506d7..247403d7be 100644 --- a/modules/task/migrate.go +++ b/modules/task/migrate.go @@ -97,8 +97,6 @@ func runMigrateTask(t *models.Task) (err error) { opts.MigrateToRepoID = t.RepoID repo, err := migrations.MigrateRepository(t.Doer, t.Owner.Name, *opts) if err == nil { - notification.NotifyMigrateRepository(t.Doer, t.Owner, repo) - log.Trace("Repository migrated [%d]: %s/%s", repo.ID, t.Owner.Name, repo.Name) return nil } diff --git a/modules/webhook/deliver.go b/modules/webhook/deliver.go index 54f20171fa..b262505cea 100644 --- a/modules/webhook/deliver.go +++ b/modules/webhook/deliver.go @@ -12,11 +12,13 @@ import ( "net/http" "net/url" "strings" + "sync" "time" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "github.com/gobwas/glob" "github.com/unknwon/com" ) @@ -182,7 +184,36 @@ func DeliverHooks() { } } -var webhookHTTPClient *http.Client +var ( + webhookHTTPClient *http.Client + once sync.Once + hostMatchers []glob.Glob +) + +func webhookProxy() func(req *http.Request) (*url.URL, error) { + if setting.Webhook.ProxyURL == "" { + return http.ProxyFromEnvironment + } + + once.Do(func() { + for _, h := range setting.Webhook.ProxyHosts { + if g, err := glob.Compile(h); err == nil { + hostMatchers = append(hostMatchers, g) + } else { + log.Error("glob.Compile %s failed: %v", h, err) + } + } + }) + + return func(req *http.Request) (*url.URL, error) { + for _, v := range hostMatchers { + if v.Match(req.URL.Host) { + return http.ProxyURL(setting.Webhook.ProxyURLFixed)(req) + } + } + return http.ProxyFromEnvironment(req) + } +} // InitDeliverHooks starts the hooks delivery thread func InitDeliverHooks() { @@ -191,7 +222,7 @@ func InitDeliverHooks() { webhookHTTPClient = &http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: setting.Webhook.SkipTLSVerify}, - Proxy: http.ProxyFromEnvironment, + Proxy: webhookProxy(), Dial: func(netw, addr string) (net.Conn, error) { conn, err := net.DialTimeout(netw, addr, timeout) if err != nil { @@ -199,7 +230,6 @@ func InitDeliverHooks() { } return conn, conn.SetDeadline(time.Now().Add(timeout)) - }, }, } diff --git a/modules/webhook/deliver_test.go b/modules/webhook/deliver_test.go new file mode 100644 index 0000000000..cfc99d796a --- /dev/null +++ b/modules/webhook/deliver_test.go @@ -0,0 +1,39 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package webhook + +import ( + "net/http" + "net/url" + "testing" + + "code.gitea.io/gitea/modules/setting" + "github.com/stretchr/testify/assert" +) + +func TestWebhookProxy(t *testing.T) { + setting.Webhook.ProxyURL = "http://localhost:8080" + setting.Webhook.ProxyURLFixed, _ = url.Parse(setting.Webhook.ProxyURL) + setting.Webhook.ProxyHosts = []string{"*.discordapp.com", "discordapp.com"} + + var kases = map[string]string{ + "https://discordapp.com/api/webhooks/xxxxxxxxx/xxxxxxxxxxxxxxxxxxx": "http://localhost:8080", + "http://s.discordapp.com/assets/xxxxxx": "http://localhost:8080", + "http://github.com/a/b": "", + } + + for reqURL, proxyURL := range kases { + req, err := http.NewRequest("POST", reqURL, nil) + assert.NoError(t, err) + + u, err := webhookProxy()(req) + assert.NoError(t, err) + if proxyURL == "" { + assert.Nil(t, u) + } else { + assert.EqualValues(t, proxyURL, u.String()) + } + } +} diff --git a/options/locale/locale_bg-BG.ini b/options/locale/locale_bg-BG.ini index 40dabfae9f..37adbbf9fc 100644 --- a/options/locale/locale_bg-BG.ini +++ b/options/locale/locale_bg-BG.ini @@ -550,8 +550,6 @@ teams.members=Участници в екипа teams.update_settings=Запази настройките teams.add_team_member=Добави участник в екипа teams.repositories=Хранилища на екипа -teams.add_team_repository=Добави хранилище на екипа -teams.remove_repo=Премахни teams.add_nonexistent_repo=Хранилището, което се опитвате да добавите не съществува. Моля първо го създайте! [admin] diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index 24680cea5b..e95d786120 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -1527,8 +1527,6 @@ teams.write_permission_desc=Členství v tom týmu poskytuje právo záp teams.admin_permission_desc=Členství v tom týmu poskytuje právo správce: členové mohou číst z, nahrávat do a přidávat spolupracovníky do repozitářů týmu. teams.repositories=Repozitáře týmu teams.search_repo_placeholder=Hledat repozitář… -teams.add_team_repository=Přidat repozitář týmu -teams.remove_repo=Smazat teams.add_nonexistent_repo=Repozitář, který se snažíte přidat, neexistuje. Prosím, nejdříve jej vytvořte. teams.add_duplicate_users=Uživatel je již členem týmu. teams.repos.none=Tento tým nemůže přistoupit k žádným repozitářům. diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index dc32a9f154..9897696fb4 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -1582,8 +1582,6 @@ teams.write_permission_desc=Dieses Team hat Schreibzugriff: Mit teams.admin_permission_desc=Dieses Team hat Adminzugriff: Mitglieder dieses Teams können Team-Repositories ansehen, auf sie pushen und Mitarbeiter hinzufügen. teams.repositories=Team-Repositories teams.search_repo_placeholder=Repository durchsuchen… -teams.add_team_repository=Team-Repository hinzufügen -teams.remove_repo=Entfernen teams.add_nonexistent_repo=Das Repository, das du hinzufügen möchten, existiert nicht. Bitte erstelle es zuerst. teams.add_duplicate_users=Dieser Benutzer ist bereits ein Teammitglied. teams.repos.none=Dieses Team hat Zugang zu keinem Repository. diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 932d0bceac..4433c5bb2a 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -68,6 +68,10 @@ pull_requests = Pull Requests issues = Issues cancel = Cancel +add = Add +add_all = Add All +remove = Remove +remove_all = Remove All write = Write preview = Preview @@ -1583,8 +1587,10 @@ teams.write_permission_desc = This team grants Write access: me teams.admin_permission_desc = This team grants Admin access: members can read from, push to and add collaborators to team repositories. teams.repositories = Team Repositories teams.search_repo_placeholder = Search repository… -teams.add_team_repository = Add Team Repository -teams.remove_repo = Remove +teams.remove_all_repos_title = Remove all team repositories +teams.remove_all_repos_desc = This will remove all repositories from the team. +teams.add_all_repos_title = Add all repositories +teams.add_all_repos_desc = This will add all the organization's repositories to the team. teams.add_nonexistent_repo = "The repository you're trying to add does not exist; please create it first." teams.add_duplicate_users = User is already a team member. teams.repos.none = No repositories could be accessed by this team. diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index a33b78f71b..593843c51e 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -1561,8 +1561,6 @@ teams.write_permission_desc=Este equipo tiene permisos de EscrituraAdministración: los miembros pueden ver, hacer push y añadir colaboradores a los repositorios del equipo. teams.repositories=Repositorios del equipo teams.search_repo_placeholder=Buscar repositorio… -teams.add_team_repository=Añadir repositorio al equipo -teams.remove_repo=Eliminar teams.add_nonexistent_repo=El repositorio que estás intentando añadir no existe, por favor, créalo primero. teams.add_duplicate_users=El usuario ya es miembro del equipo. teams.repos.none=Este equipo no tiene repositorios accesibles. diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini index a0551ed71d..7f14f280d9 100644 --- a/options/locale/locale_fa-IR.ini +++ b/options/locale/locale_fa-IR.ini @@ -1563,8 +1563,6 @@ teams.write_permission_desc=این تیم دسترسی نوشتننوشتن خواهد داشت: اعضا خواهند توانست مخازن تیم را خوانده ، تغییراتی در آنها اعمال کرده و یا همکارانشان را به مخازن اضافه نمایند. teams.repositories=مخازن تیم teams.search_repo_placeholder=جستجوی مخزن... -teams.add_team_repository=افزودن مخزن تیمی -teams.remove_repo=حذف teams.add_nonexistent_repo=مخزنی را که شما قصد افزودن آن را دارید موجود نیست، لطفا ابتدا آن را ایجاد کنید. teams.add_duplicate_users=این کاربر پیش از این عضو تیم بوده است. teams.repos.none=این تیم به هیچ مخزنی دسترسی ندارد. diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini index 75a80de528..fc79584d1a 100644 --- a/options/locale/locale_fi-FI.ini +++ b/options/locale/locale_fi-FI.ini @@ -609,8 +609,6 @@ teams.members=Ryhmän jäsenet teams.update_settings=Päivitä asetukset teams.add_team_member=Lisää tiimin jäsen teams.repositories=Tiimin repot -teams.add_team_repository=Lisää tiimirepo -teams.remove_repo=Poista teams.add_nonexistent_repo=Repo jota yrität lisätä ei ole vielä olemassa, ole hyvä ja luo se ensin. [admin] diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index c1509b1089..1f14967002 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -1550,8 +1550,6 @@ teams.write_permission_desc=Cette équipe permet l'accès en écritureadministrateur : les membres peuvent voir, participer et ajouter des collaborateurs à ses dépôts. teams.repositories=Dépôts de l'Équipe teams.search_repo_placeholder=Rechercher dans le dépôt… -teams.add_team_repository=Ajouter un Dépôt à l'Équipe -teams.remove_repo=Supprimer teams.add_nonexistent_repo=Dépôt inexistant, veuillez d'abord le créer. teams.add_duplicate_users=L’utilisateur est déjà un membre de l’équipe. teams.repos.none=Aucun dépôt n'est accessible par cette équipe. diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini index 2d90909d1e..4992dbbef7 100644 --- a/options/locale/locale_hu-HU.ini +++ b/options/locale/locale_hu-HU.ini @@ -699,8 +699,6 @@ teams.add_team_member=Csapattag hozzáadása teams.delete_team_success=A csoport törölve lett. teams.repositories=Csoport tárolói teams.search_repo_placeholder=Tároló keresése… -teams.add_team_repository=Új csoport tároló -teams.remove_repo=Eltávolítás teams.add_nonexistent_repo=A tároló, melybe feltölteni szeretne, még nem létezik; először hozza létre. [admin] diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini index bd7d0eeb63..bbe4870cbb 100644 --- a/options/locale/locale_id-ID.ini +++ b/options/locale/locale_id-ID.ini @@ -745,8 +745,6 @@ teams.add_team_member=Tambahkan Anggota Tim teams.delete_team_success=Tim sudah di hapus. teams.repositories=Tim repositori teams.search_repo_placeholder=Cari repositori… -teams.add_team_repository=Tambahkan Tim Repositori -teams.remove_repo=Menghapus teams.add_nonexistent_repo=Repositori yang ingin Anda tambahkan tidak ada; Silahkan buat terlebih dahulu. [admin] diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini index 7829406297..00b6f3b378 100644 --- a/options/locale/locale_it-IT.ini +++ b/options/locale/locale_it-IT.ini @@ -1195,8 +1195,6 @@ teams.write_permission_desc=Questo team concede l'accesso di ScritturaAmministratore: i membri possono leggere da, pushare su e aggiungere collaboratori ai repository del team. teams.repositories=Repository di Squadra teams.search_repo_placeholder=Ricerca repository… -teams.add_team_repository=Aggiungere Repository di Squadra -teams.remove_repo=Rimuovi teams.add_nonexistent_repo=Il repository che stai tentando di aggiungere non esiste, crealo prima. [admin] diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index a006e17ccc..1802108a50 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -68,6 +68,10 @@ pull_requests=プルリクエスト issues=課題 cancel=キャンセル +add=追加 +add_all=すべて追加 +remove=除去 +remove_all=すべて除去 write=書き込み preview=プレビュー @@ -1514,6 +1518,7 @@ team_name=チーム名 team_desc=説明 team_name_helper=チーム名は短く覚えやすいものにしましょう。 team_desc_helper=チームの目的や役割を説明します。 +team_access_desc=リポジトリアクセス team_permission_desc=権限 team_unit_desc=リポジトリのセクションへのアクセスを許可 @@ -1581,12 +1586,21 @@ teams.write_permission_desc=このチームは書き込みア teams.admin_permission_desc=このチームは管理者アクセス権を持ちます: メンバーはチームリポジトリの読み取り、プッシュ、共同作業者の追加が可能です。 teams.repositories=チームのリポジトリ teams.search_repo_placeholder=リポジトリを検索… -teams.add_team_repository=チームのリポジトリを追加 -teams.remove_repo=削除 +teams.remove_all_repos_title=チームリポジトリをすべて除去 +teams.remove_all_repos_desc=チームからすべてのリポジトリを除去します。 +teams.add_all_repos_title=すべてのリポジトリを追加 +teams.add_all_repos_desc=組織のすべてのリポジトリをチームに追加します。 teams.add_nonexistent_repo=追加しようとしているリポジトリは存在しません。 先にリポジトリを作成してください。 teams.add_duplicate_users=ユーザーは既にチームのメンバーです。 teams.repos.none=このチームがアクセスできるリポジトリはありません。 teams.members.none=このチームにはメンバーがいません。 +teams.specific_repositories=指定したリポジトリ +teams.specific_repositories_helper=メンバーは、明示的にチームへ追加したリポジトリにのみアクセスできます。 これを選択しても、すでにすべてのリポジトリで追加されたリポジトリは自動的に除去されません。 +teams.all_repositories=すべてのリポジトリ +teams.all_repositories_helper=チームはすべてのリポジトリにアクセスできます。 これを選択すると、既存のすべてのリポジトリをチームに追加します。 +teams.all_repositories_read_permission_desc=このチームはすべてのリポジトリ読み取りアクセス権を持ちます: メンバーはリポジトリの閲覧とクローンが可能です。 +teams.all_repositories_write_permission_desc=このチームはすべてのリポジトリ書き込みアクセス権を持ちます: メンバーはリポジトリの読み取りとプッシュが可能です。 +teams.all_repositories_admin_permission_desc=このチームはすべてのリポジトリ管理者アクセス権を持ちます: メンバーはリポジトリの読み取り、プッシュ、共同作業者の追加が可能です。 [admin] dashboard=ダッシュボード diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini index f0ce4cdb30..dbc74b22ce 100644 --- a/options/locale/locale_ko-KR.ini +++ b/options/locale/locale_ko-KR.ini @@ -570,8 +570,6 @@ teams.update_settings=설정 업데이트 teams.add_team_member=팀 구성원 추가 teams.delete_team_success=팀이 삭제되었습니다. teams.repositories=팀 저장소 -teams.add_team_repository=팀 저장소 추가 -teams.remove_repo=삭제 teams.add_nonexistent_repo=추가하려는 저장소를 존재하지 않습니다. 먼저 생성해주세요. [admin] diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index 96fe30a7a4..f7491f1a89 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -1539,8 +1539,6 @@ teams.write_permission_desc=Šai komandai ir rakstīšanas ties teams.admin_permission_desc=Šai komandai ir administratora tiesības: dalībnieki var lasīt, rakstīt un pievienot citus dalībniekus komandas repozitorijiem. teams.repositories=Komandas repozitoriji teams.search_repo_placeholder=Meklēt repozitorijā… -teams.add_team_repository=Pievienot komandas repozitoriju -teams.remove_repo=Noņemt teams.add_nonexistent_repo=Repozitorijs, kuram Jūs mēģinat pievienot neeksistē, sākumā izveidojiet to. teams.add_duplicate_users=Lietotājs jau ir šajā komandā. teams.repos.none=Šai komandai nav piekļuves nevienam repozitorijam. diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini index 66f8efe031..5aac7b0f1a 100644 --- a/options/locale/locale_nl-NL.ini +++ b/options/locale/locale_nl-NL.ini @@ -1156,8 +1156,6 @@ teams.delete_team_title=Verwijder team teams.delete_team_success=Het team is verwijderd. teams.repositories=Teamrepositories teams.search_repo_placeholder=Repository zoeken… -teams.add_team_repository=Nieuwe teamrepositorie aanmaken -teams.remove_repo=Verwijder teams.add_nonexistent_repo=De opslagplaats die u probeert toe te voegen bestaat niet: maak deze eerst aan. teams.add_duplicate_users=Gebruiker is al een teamlid. diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini index 71c9522921..0fdbaced6b 100644 --- a/options/locale/locale_pl-PL.ini +++ b/options/locale/locale_pl-PL.ini @@ -1468,8 +1468,6 @@ teams.write_permission_desc=Ten zespół udziela dostępu z zapisemadministratora: członkowie mogą wyświetlać i wypychać zmiany oraz dodawać współpracowników do repozytoriów zespołu. teams.repositories=Repozytoria zespołu teams.search_repo_placeholder=Szukaj repozytorium… -teams.add_team_repository=Dodaj repozytorium zespołu -teams.remove_repo=Usuń teams.add_nonexistent_repo=Repozytorium, które próbujesz dodać, nie istnieje. Proszę je najpierw utworzyć. teams.add_duplicate_users=Użytkownik jest już członkiem zespołu. teams.repos.none=Ten zespół nie ma dostępu do żadnego repozytorium. diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index 33da0ed0c4..e937d7091a 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -1582,8 +1582,6 @@ teams.write_permission_desc=Esta equipe concede acesso para escritaAdministrador: Membros podem ler, fazer push e adicionar outros colaboradores para os repositórios da equipe. teams.repositories=Repositórios da equipe teams.search_repo_placeholder=Pesquisar repositório... -teams.add_team_repository=Adicionar repositório da equipe -teams.remove_repo=Remover teams.add_nonexistent_repo=O repositório que você está tentando adicionar não existe, por favor, crie-o primeiro. teams.add_duplicate_users=Usuário já é um membro da equipe. teams.repos.none=Nenhum repositório pode ser acessado por essa equipe. diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index 9102603425..341ee4719a 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -1423,8 +1423,6 @@ teams.write_permission_desc=Эта команда предоставляет д teams.admin_permission_desc=Эта команда дает административный доступ: участники могут читать, пушить и добавлять соавторов к ее репозиториям. teams.repositories=Репозитории группы разработки teams.search_repo_placeholder=Поиск репозитория… -teams.add_team_repository=Добавить репозиторий группы разработки -teams.remove_repo=Удалить teams.add_nonexistent_repo=Вы добавляете в отсутствующий репозиторий, пожалуйста сначала его создайте. teams.add_duplicate_users=Пользователь уже состоит в команде. teams.repos.none=Для этой команды нет доступных репозиториев. diff --git a/options/locale/locale_sr-SP.ini b/options/locale/locale_sr-SP.ini index a4072b7dfa..f8eed4333c 100644 --- a/options/locale/locale_sr-SP.ini +++ b/options/locale/locale_sr-SP.ini @@ -488,8 +488,6 @@ teams.members=Чланови тима teams.update_settings=Примени промене teams.add_team_member=Додај члан тиму teams.repositories=Тимска спремишта -teams.add_team_repository=Додај тимско спремиште -teams.remove_repo=Уклони teams.add_nonexistent_repo=Овакво спремиште не постоји, молим вас прво да га направите. [admin] diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini index 37d6621642..7b737bded6 100644 --- a/options/locale/locale_sv-SE.ini +++ b/options/locale/locale_sv-SE.ini @@ -1231,8 +1231,6 @@ teams.write_permission_desc=Medlemskap i detta team ger skrivrättighete teams.admin_permission_desc=Medlemskap i detta team ger administratörsrättigheter: medlemmar kan läsa, pusha och lägga till medarbetare till teamets utvecklingskataloger. teams.repositories=Teamförråd teams.search_repo_placeholder=Sök utvecklingskatalog… -teams.add_team_repository=Lägg till teamförråd -teams.remove_repo=Ta bort teams.add_nonexistent_repo=Förrådet du försöka lägga till finns inte, vänligen skapa det först. [admin] diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index 5a6af8def4..bfd42b40d8 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -1517,8 +1517,6 @@ teams.write_permission_desc=Bu takım Yazma erişimi veriyor. teams.admin_permission_desc=Bu takım Yönetici erişimi veriyor. Üyeler takım depolarını okuyabilir, itebilir ve katkıcı ekleyebilir. teams.repositories=Ekip Depoları teams.search_repo_placeholder=Depo ara… -teams.add_team_repository=Ekip Deposu Ekle -teams.remove_repo=Kaldır teams.add_nonexistent_repo=Eklemeye çalıştığınz depo mevcut değil. Lütfen önce oluşturun. teams.add_duplicate_users=Kullanıcı zaten takımın üyesi. teams.repos.none=Bu takım tarafından hiçbir depoya erişilemedi. diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini index 51e52b2785..2c57b08aff 100644 --- a/options/locale/locale_uk-UA.ini +++ b/options/locale/locale_uk-UA.ini @@ -1294,8 +1294,6 @@ teams.write_permission_desc=Ця команда надає доступ на адміністраторський доступ: учасники можуть читати, виконувати push команди та додавати співробітників до репозиторію. teams.repositories=Репозиторії команди teams.search_repo_placeholder=Пошук репозиторію… -teams.add_team_repository=Додати репозиторій команди -teams.remove_repo=Видалити teams.add_nonexistent_repo=Ви намагаєтеся додати у репозиторій якого не існує. Будь ласка, спочатку створіть його. teams.add_duplicate_users=Користувач уже є членом команди. diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 594231262f..edcd99f525 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -1577,8 +1577,6 @@ teams.write_permission_desc=该团队拥有对所属仓库的 读取管理 权限,团队成员可以读取、克隆、推送以及添加其它仓库协作者。 teams.repositories=团队仓库 teams.search_repo_placeholder=搜索仓库... -teams.add_team_repository=添加团队仓库 -teams.remove_repo=移除仓库 teams.add_nonexistent_repo=您尝试添加到团队的仓库不存在,请先创建仓库! teams.add_duplicate_users=用户已经是团队成员。 teams.repos.none=此团队无法访问任何仓库。 diff --git a/options/locale/locale_zh-HK.ini b/options/locale/locale_zh-HK.ini index 43e9b3eea5..f6bfebabc9 100644 --- a/options/locale/locale_zh-HK.ini +++ b/options/locale/locale_zh-HK.ini @@ -588,8 +588,6 @@ teams.update_settings=更新團隊設定 teams.add_team_member=新增團隊成員 teams.delete_team_success=該團隊已被刪除。 teams.repositories=團隊儲存庫 -teams.add_team_repository=新增團隊儲存庫 -teams.remove_repo=移除儲存庫 teams.add_nonexistent_repo=您嘗試新增到團隊的儲存庫不存在,請先建立儲存庫! [admin] diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini index de787b6b47..41b174ebdb 100644 --- a/options/locale/locale_zh-TW.ini +++ b/options/locale/locale_zh-TW.ini @@ -1037,8 +1037,6 @@ teams.delete_team_title=刪除團隊 teams.delete_team_success=該團隊已被刪除。 teams.repositories=團隊儲存庫 teams.search_repo_placeholder=搜尋儲存庫... -teams.add_team_repository=新增團隊儲存庫 -teams.remove_repo=移除儲存庫 teams.add_nonexistent_repo=您嘗試新增到團隊的儲存庫不存在,請先建立儲存庫! [admin] diff --git a/public/css/index.css b/public/css/index.css index 3ec47eb85c..1a6032cf3e 100644 --- a/public/css/index.css +++ b/public/css/index.css @@ -945,8 +945,9 @@ tbody.commit-list{vertical-align:baseline} .organization.teams .members .item,.organization.teams .repositories .item{padding:10px 20px;line-height:32px} .organization.teams .members .item:not(:last-child),.organization.teams .repositories .item:not(:last-child){border-bottom:1px solid #ddd} .organization.teams .members .item .button,.organization.teams .repositories .item .button{padding:9px 10px} -.organization.teams #add-member-form input,.organization.teams #add-repo-form input{margin-left:0} -.organization.teams #add-member-form .ui.button,.organization.teams #add-repo-form .ui.button{margin-left:5px;margin-top:-3px} +.organization.teams #add-member-form input,.organization.teams #add-repo-form input,.organization.teams #repo-multiple-form input{margin-left:0} +.organization.teams #add-member-form .ui.button,.organization.teams #add-repo-form .ui.button,.organization.teams #repo-multiple-form .ui.button{margin-left:5px;margin-top:-3px} +.organization.teams #repo-top-segment{height:60px} .user:not(.icon){padding-top:15px} .user.profile .ui.card .username{display:block} .user.profile .ui.card .extra.content{padding:0} diff --git a/public/js/index.js b/public/js/index.js index 53650890f0..93ac6a2178 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -2263,6 +2263,7 @@ $(document).ready(function () { // Helpers. $('.delete-button').click(showDeletePopup); + $('.add-all-button').click(showAddAllPopup); $('.delete-branch-button').click(showDeletePopup); @@ -2501,6 +2502,35 @@ function showDeletePopup() { return false; } +function showAddAllPopup() { + const $this = $(this); + let filter = ""; + if ($this.attr("id")) { + filter += "#" + $this.attr("id") + } + + const dialog = $('.addall.modal' + filter); + dialog.find('.name').text($this.data('name')); + + dialog.modal({ + closable: false, + onApprove: function() { + if ($this.data('type') == "form") { + $($this.data('form')).submit(); + return; + } + + $.post($this.data('url'), { + "_csrf": csrf, + "id": $this.data("id") + }).done(function(data) { + window.location.href = data.redirect; + }); + } + }).modal('show'); + return false; +} + function initVueComponents(){ const vueDelimeters = ['${', '}']; diff --git a/public/js/semantic.dropdown.custom.js b/public/js/semantic.dropdown.custom.js new file mode 100644 index 0000000000..1745869fbf --- /dev/null +++ b/public/js/semantic.dropdown.custom.js @@ -0,0 +1,4023 @@ +/*! + * # Semantic UI 2.3.1 - Dropdown + * http://github.com/semantic-org/semantic-ui/ + * + * + * Released under the MIT license + * http://opensource.org/licenses/MIT + * + */ + +/* + * Copyright 2019 The Gitea Authors + * Released under the MIT license + * http://opensource.org/licenses/MIT + * This version has been modified by Gitea to improve accessibility. + */ + +;(function ($, window, document, undefined) { + +'use strict'; + +window = (typeof window != 'undefined' && window.Math == Math) + ? window + : (typeof self != 'undefined' && self.Math == Math) + ? self + : Function('return this')() +; + +$.fn.dropdown = function(parameters) { + var + $allModules = $(this), + $document = $(document), + + moduleSelector = $allModules.selector || '', + + hasTouch = ('ontouchstart' in document.documentElement), + time = new Date().getTime(), + performance = [], + + query = arguments[0], + methodInvoked = (typeof query == 'string'), + queryArguments = [].slice.call(arguments, 1), + lastAriaID = 1, + returnedValue + ; + + $allModules + .each(function(elementIndex) { + var + settings = ( $.isPlainObject(parameters) ) + ? $.extend(true, {}, $.fn.dropdown.settings, parameters) + : $.extend({}, $.fn.dropdown.settings), + + className = settings.className, + message = settings.message, + fields = settings.fields, + keys = settings.keys, + metadata = settings.metadata, + namespace = settings.namespace, + regExp = settings.regExp, + selector = settings.selector, + error = settings.error, + templates = settings.templates, + + eventNamespace = '.' + namespace, + moduleNamespace = 'module-' + namespace, + + $module = $(this), + $context = $(settings.context), + $text = $module.find(selector.text), + $search = $module.find(selector.search), + $sizer = $module.find(selector.sizer), + $input = $module.find(selector.input), + $icon = $module.find(selector.icon), + + $combo = ($module.prev().find(selector.text).length > 0) + ? $module.prev().find(selector.text) + : $module.prev(), + + $menu = $module.children(selector.menu), + $item = $menu.find(selector.item), + + activated = false, + itemActivated = false, + internalChange = false, + element = this, + instance = $module.data(moduleNamespace), + + initialLoad, + pageLostFocus, + willRefocus, + elementNamespace, + id, + selectObserver, + menuObserver, + module + ; + + module = { + + initialize: function() { + module.debug('Initializing dropdown', settings); + + if( module.is.alreadySetup() ) { + module.setup.reference(); + } + else { + + module.setup.layout(); + + if(settings.values) { + module.change.values(settings.values); + } + + module.refreshData(); + + module.save.defaults(); + module.restore.selected(); + + module.create.id(); + module.bind.events(); + + module.observeChanges(); + module.instantiate(); + + module.aria.setup(); + } + + }, + + instantiate: function() { + module.verbose('Storing instance of dropdown', module); + instance = module; + $module + .data(moduleNamespace, module) + ; + }, + + destroy: function() { + module.verbose('Destroying previous dropdown', $module); + module.remove.tabbable(); + $module + .off(eventNamespace) + .removeData(moduleNamespace) + ; + $menu + .off(eventNamespace) + ; + $document + .off(elementNamespace) + ; + module.disconnect.menuObserver(); + module.disconnect.selectObserver(); + }, + + observeChanges: function() { + if('MutationObserver' in window) { + selectObserver = new MutationObserver(module.event.select.mutation); + menuObserver = new MutationObserver(module.event.menu.mutation); + module.debug('Setting up mutation observer', selectObserver, menuObserver); + module.observe.select(); + module.observe.menu(); + } + }, + + disconnect: { + menuObserver: function() { + if(menuObserver) { + menuObserver.disconnect(); + } + }, + selectObserver: function() { + if(selectObserver) { + selectObserver.disconnect(); + } + } + }, + observe: { + select: function() { + if(module.has.input()) { + selectObserver.observe($module[0], { + childList : true, + subtree : true + }); + } + }, + menu: function() { + if(module.has.menu()) { + menuObserver.observe($menu[0], { + childList : true, + subtree : true + }); + } + } + }, + + create: { + id: function() { + id = (Math.random().toString(16) + '000000000').substr(2, 8); + elementNamespace = '.' + id; + module.verbose('Creating unique id for element', id); + }, + userChoice: function(values) { + var + $userChoices, + $userChoice, + isUserValue, + html + ; + values = values || module.get.userValues(); + if(!values) { + return false; + } + values = $.isArray(values) + ? values + : [values] + ; + $.each(values, function(index, value) { + if(module.get.item(value) === false) { + html = settings.templates.addition( module.add.variables(message.addResult, value) ); + $userChoice = $('
') + .html(html) + .attr('data-' + metadata.value, value) + .attr('data-' + metadata.text, value) + .addClass(className.addition) + .addClass(className.item) + ; + if(settings.hideAdditions) { + $userChoice.addClass(className.hidden); + } + $userChoices = ($userChoices === undefined) + ? $userChoice + : $userChoices.add($userChoice) + ; + module.verbose('Creating user choices for value', value, $userChoice); + } + }); + return $userChoices; + }, + userLabels: function(value) { + var + userValues = module.get.userValues() + ; + if(userValues) { + module.debug('Adding user labels', userValues); + $.each(userValues, function(index, value) { + module.verbose('Adding custom user value'); + module.add.label(value, value); + }); + } + }, + menu: function() { + $menu = $('
') + .addClass(className.menu) + .appendTo($module) + ; + }, + sizer: function() { + $sizer = $('') + .addClass(className.sizer) + .insertAfter($search) + ; + } + }, + + search: function(query) { + query = (query !== undefined) + ? query + : module.get.query() + ; + module.verbose('Searching for query', query); + if(module.has.minCharacters(query)) { + module.filter(query); + } + else { + module.hide(); + } + }, + + select: { + firstUnfiltered: function() { + module.verbose('Selecting first non-filtered element'); + module.remove.selectedItem(); + $item + .not(selector.unselectable) + .not(selector.addition + selector.hidden) + .eq(0) + .addClass(className.selected) + ; + }, + nextAvailable: function($selected) { + $selected = $selected.eq(0); + var + $nextAvailable = $selected.nextAll(selector.item).not(selector.unselectable).eq(0), + $prevAvailable = $selected.prevAll(selector.item).not(selector.unselectable).eq(0), + hasNext = ($nextAvailable.length > 0) + ; + if(hasNext) { + module.verbose('Moving selection to', $nextAvailable); + $nextAvailable.addClass(className.selected); + } + else { + module.verbose('Moving selection to', $prevAvailable); + $prevAvailable.addClass(className.selected); + } + } + }, + + aria: { + setup: function() { + var role = module.aria.guessRole(); + if( role !== 'menu' ) { + return; + } + $module.attr('aria-busy', 'true'); + $module.attr('role', 'menu'); + $module.attr('aria-haspopup', 'menu'); + $module.attr('aria-expanded', 'false'); + $menu.find('.divider').attr('role', 'separator'); + $item.attr('role', 'menuitem'); + $item.each(function (index, item) { + if( !item.id ) { + item.id = module.aria.nextID('menuitem'); + } + }); + $text = $module + .find('> .text') + .eq(0) + ; + if( $module.data('content') ) { + $text.attr('aria-hidden'); + $module.attr('aria-label', $module.data('content')); + } + else { + $text.attr('id', module.aria.nextID('menutext')); + $module.attr('aria-labelledby', $text.attr('id')); + } + $module.attr('aria-busy', 'false'); + }, + nextID: function(prefix) { + var nextID; + do { + nextID = prefix + '_' + lastAriaID++; + } while( document.getElementById(nextID) ); + return nextID; + }, + setExpanded: function(expanded) { + if( $module.attr('aria-haspopup') ) { + $module.attr('aria-expanded', expanded); + } + }, + refreshDescendant: function() { + if( $module.attr('aria-haspopup') !== 'menu' ) { + return; + } + var + $currentlySelected = $item.not(selector.unselectable).filter('.' + className.selected).eq(0), + $activeItem = $menu.children('.' + className.active).eq(0), + $selectedItem = ($currentlySelected.length > 0) + ? $currentlySelected + : $activeItem + ; + if( $selectedItem ) { + $module.attr('aria-activedescendant', $selectedItem.attr('id')); + } + else { + module.aria.removeDescendant(); + } + }, + removeDescendant: function() { + if( $module.attr('aria-haspopup') == 'menu' ) { + $module.removeAttr('aria-activedescendant'); + } + }, + guessRole: function() { + var + isIcon = $module.hasClass('icon'), + hasSearch = module.has.search(), + hasInput = ($input.length > 0), + isMultiple = module.is.multiple() + ; + if ( !isIcon && !hasSearch && !hasInput && !isMultiple ) { + return 'menu'; + } + return 'unknown'; + } + }, + + setup: { + api: function() { + var + apiSettings = { + debug : settings.debug, + urlData : { + value : module.get.value(), + query : module.get.query() + }, + on : false + } + ; + module.verbose('First request, initializing API'); + $module + .api(apiSettings) + ; + }, + layout: function() { + if( $module.is('select') ) { + module.setup.select(); + module.setup.returnedObject(); + } + if( !module.has.menu() ) { + module.create.menu(); + } + if( module.is.search() && !module.has.search() ) { + module.verbose('Adding search input'); + $search = $('') + .addClass(className.search) + .prop('autocomplete', 'off') + .insertBefore($text) + ; + } + if( module.is.multiple() && module.is.searchSelection() && !module.has.sizer()) { + module.create.sizer(); + } + if(settings.allowTab) { + module.set.tabbable(); + } + $item.attr('tabindex', '-1'); + }, + select: function() { + var + selectValues = module.get.selectValues() + ; + module.debug('Dropdown initialized on a select', selectValues); + if( $module.is('select') ) { + $input = $module; + } + // see if select is placed correctly already + if($input.parent(selector.dropdown).length > 0) { + module.debug('UI dropdown already exists. Creating dropdown menu only'); + $module = $input.closest(selector.dropdown); + if( !module.has.menu() ) { + module.create.menu(); + } + $menu = $module.children(selector.menu); + module.setup.menu(selectValues); + } + else { + module.debug('Creating entire dropdown from select'); + $module = $('
') + .attr('class', $input.attr('class') ) + .addClass(className.selection) + .addClass(className.dropdown) + .html( templates.dropdown(selectValues) ) + .insertBefore($input) + ; + if($input.hasClass(className.multiple) && $input.prop('multiple') === false) { + module.error(error.missingMultiple); + $input.prop('multiple', true); + } + if($input.is('[multiple]')) { + module.set.multiple(); + } + if ($input.prop('disabled')) { + module.debug('Disabling dropdown'); + $module.addClass(className.disabled); + } + $input + .removeAttr('class') + .detach() + .prependTo($module) + ; + } + module.refresh(); + }, + menu: function(values) { + $menu.html( templates.menu(values, fields)); + $item = $menu.find(selector.item); + }, + reference: function() { + module.debug('Dropdown behavior was called on select, replacing with closest dropdown'); + // replace module reference + $module = $module.parent(selector.dropdown); + instance = $module.data(moduleNamespace); + element = $module.get(0); + module.refresh(); + module.setup.returnedObject(); + }, + returnedObject: function() { + var + $firstModules = $allModules.slice(0, elementIndex), + $lastModules = $allModules.slice(elementIndex + 1) + ; + // adjust all modules to use correct reference + $allModules = $firstModules.add($module).add($lastModules); + } + }, + + refresh: function() { + module.refreshSelectors(); + module.refreshData(); + }, + + refreshItems: function() { + $item = $menu.find(selector.item); + }, + + refreshSelectors: function() { + module.verbose('Refreshing selector cache'); + $text = $module.find(selector.text); + $search = $module.find(selector.search); + $input = $module.find(selector.input); + $icon = $module.find(selector.icon); + $combo = ($module.prev().find(selector.text).length > 0) + ? $module.prev().find(selector.text) + : $module.prev() + ; + $menu = $module.children(selector.menu); + $item = $menu.find(selector.item); + }, + + refreshData: function() { + module.verbose('Refreshing cached metadata'); + $item + .removeData(metadata.text) + .removeData(metadata.value) + ; + }, + + clearData: function() { + module.verbose('Clearing metadata'); + $item + .removeData(metadata.text) + .removeData(metadata.value) + ; + $module + .removeData(metadata.defaultText) + .removeData(metadata.defaultValue) + .removeData(metadata.placeholderText) + ; + }, + + toggle: function() { + module.verbose('Toggling menu visibility'); + if( !module.is.active() ) { + module.show(); + } + else { + module.hide(); + } + }, + + show: function(callback) { + callback = $.isFunction(callback) + ? callback + : function(){} + ; + if(!module.can.show() && module.is.remote()) { + module.debug('No API results retrieved, searching before show'); + module.queryRemote(module.get.query(), module.show); + } + if( module.can.show() && !module.is.active() ) { + module.debug('Showing dropdown'); + if(module.has.message() && !(module.has.maxSelections() || module.has.allResultsFiltered()) ) { + module.remove.message(); + } + if(module.is.allFiltered()) { + return true; + } + if(settings.onShow.call(element) !== false) { + module.aria.setExpanded(true); + module.aria.refreshDescendant(); + module.animate.show(function() { + if( module.can.click() ) { + module.bind.intent(); + } + if(module.has.menuSearch()) { + module.focusSearch(); + } + module.set.visible(); + callback.call(element); + }); + } + } + }, + + hide: function(callback) { + callback = $.isFunction(callback) + ? callback + : function(){} + ; + if( module.is.active() && !module.is.animatingOutward() ) { + module.debug('Hiding dropdown'); + if(settings.onHide.call(element) !== false) { + module.aria.setExpanded(false); + module.aria.removeDescendant(); + module.animate.hide(function() { + module.remove.visible(); + callback.call(element); + }); + } + } + }, + + hideOthers: function() { + module.verbose('Finding other dropdowns to hide'); + $allModules + .not($module) + .has(selector.menu + '.' + className.visible) + .dropdown('hide') + ; + }, + + hideMenu: function() { + module.verbose('Hiding menu instantaneously'); + module.remove.active(); + module.remove.visible(); + $menu.transition('hide'); + }, + + hideSubMenus: function() { + var + $subMenus = $menu.children(selector.item).find(selector.menu) + ; + module.verbose('Hiding sub menus', $subMenus); + $subMenus.transition('hide'); + }, + + bind: { + events: function() { + if(hasTouch) { + module.bind.touchEvents(); + } + module.bind.keyboardEvents(); + module.bind.inputEvents(); + module.bind.mouseEvents(); + }, + touchEvents: function() { + module.debug('Touch device detected binding additional touch events'); + if( module.is.searchSelection() ) { + // do nothing special yet + } + else if( module.is.single() ) { + $module + .on('touchstart' + eventNamespace, module.event.test.toggle) + ; + } + $menu + .on('touchstart' + eventNamespace, selector.item, module.event.item.mouseenter) + ; + }, + keyboardEvents: function() { + module.verbose('Binding keyboard events'); + $module + .on('keydown' + eventNamespace, module.event.keydown) + ; + if( module.has.search() ) { + $module + .on(module.get.inputEvent() + eventNamespace, selector.search, module.event.input) + ; + } + if( module.is.multiple() ) { + $document + .on('keydown' + elementNamespace, module.event.document.keydown) + ; + } + }, + inputEvents: function() { + module.verbose('Binding input change events'); + $module + .on('change' + eventNamespace, selector.input, module.event.change) + ; + }, + mouseEvents: function() { + module.verbose('Binding mouse events'); + if(module.is.multiple()) { + $module + .on('click' + eventNamespace, selector.label, module.event.label.click) + .on('click' + eventNamespace, selector.remove, module.event.remove.click) + ; + } + if( module.is.searchSelection() ) { + $module + .on('mousedown' + eventNamespace, module.event.mousedown) + .on('mouseup' + eventNamespace, module.event.mouseup) + .on('mousedown' + eventNamespace, selector.menu, module.event.menu.mousedown) + .on('mouseup' + eventNamespace, selector.menu, module.event.menu.mouseup) + .on('click' + eventNamespace, selector.icon, module.event.icon.click) + .on('focus' + eventNamespace, selector.search, module.event.search.focus) + .on('click' + eventNamespace, selector.search, module.event.search.focus) + .on('blur' + eventNamespace, selector.search, module.event.search.blur) + .on('click' + eventNamespace, selector.text, module.event.text.focus) + ; + if(module.is.multiple()) { + $module + .on('click' + eventNamespace, module.event.click) + ; + } + } + else { + if(settings.on == 'click') { + $module + .on('click' + eventNamespace, selector.icon, module.event.icon.click) + .on('click' + eventNamespace, module.event.test.toggle) + ; + } + else if(settings.on == 'hover') { + $module + .on('mouseenter' + eventNamespace, module.delay.show) + .on('mouseleave' + eventNamespace, module.delay.hide) + ; + } + else { + $module + .on(settings.on + eventNamespace, module.toggle) + ; + } + $module + .on('mousedown' + eventNamespace, module.event.mousedown) + .on('mouseup' + eventNamespace, module.event.mouseup) + .on('focus' + eventNamespace, module.event.focus) + ; + if(module.has.menuSearch() ) { + $module + .on('blur' + eventNamespace, selector.search, module.event.search.blur) + ; + } + else { + $module + .on('blur' + eventNamespace, module.event.blur) + ; + } + } + $menu + .on('mouseenter' + eventNamespace, selector.item, module.event.item.mouseenter) + .on('mouseleave' + eventNamespace, selector.item, module.event.item.mouseleave) + .on('click' + eventNamespace, selector.item, module.event.item.click) + ; + }, + intent: function() { + module.verbose('Binding hide intent event to document'); + if(hasTouch) { + $document + .on('touchstart' + elementNamespace, module.event.test.touch) + .on('touchmove' + elementNamespace, module.event.test.touch) + ; + } + $document + .on('click' + elementNamespace, module.event.test.hide) + ; + } + }, + + unbind: { + intent: function() { + module.verbose('Removing hide intent event from document'); + if(hasTouch) { + $document + .off('touchstart' + elementNamespace) + .off('touchmove' + elementNamespace) + ; + } + $document + .off('click' + elementNamespace) + ; + } + }, + + filter: function(query) { + var + searchTerm = (query !== undefined) + ? query + : module.get.query(), + afterFiltered = function() { + if(module.is.multiple()) { + module.filterActive(); + } + if(query || (!query && module.get.activeItem().length == 0)) { + module.select.firstUnfiltered(); + } + if( module.has.allResultsFiltered() ) { + if( settings.onNoResults.call(element, searchTerm) ) { + if(settings.allowAdditions) { + if(settings.hideAdditions) { + module.verbose('User addition with no menu, setting empty style'); + module.set.empty(); + module.hideMenu(); + } + } + else { + module.verbose('All items filtered, showing message', searchTerm); + module.add.message(message.noResults); + } + } + else { + module.verbose('All items filtered, hiding dropdown', searchTerm); + module.hideMenu(); + } + } + else { + module.remove.empty(); + module.remove.message(); + } + if(settings.allowAdditions) { + module.add.userSuggestion(query); + } + if(module.is.searchSelection() && module.can.show() && module.is.focusedOnSearch() ) { + module.show(); + } + } + ; + if(settings.useLabels && module.has.maxSelections()) { + return; + } + if(settings.apiSettings) { + if( module.can.useAPI() ) { + module.queryRemote(searchTerm, function() { + if(settings.filterRemoteData) { + module.filterItems(searchTerm); + } + afterFiltered(); + }); + } + else { + module.error(error.noAPI); + } + } + else { + module.filterItems(searchTerm); + afterFiltered(); + } + }, + + queryRemote: function(query, callback) { + var + apiSettings = { + errorDuration : false, + cache : 'local', + throttle : settings.throttle, + urlData : { + query: query + }, + onError: function() { + module.add.message(message.serverError); + callback(); + }, + onFailure: function() { + module.add.message(message.serverError); + callback(); + }, + onSuccess : function(response) { + module.remove.message(); + module.setup.menu({ + values: response[fields.remoteValues] + }); + callback(); + } + } + ; + if( !$module.api('get request') ) { + module.setup.api(); + } + apiSettings = $.extend(true, {}, apiSettings, settings.apiSettings); + $module + .api('setting', apiSettings) + .api('query') + ; + }, + + filterItems: function(query) { + var + searchTerm = (query !== undefined) + ? query + : module.get.query(), + results = null, + escapedTerm = module.escape.string(searchTerm), + beginsWithRegExp = new RegExp('^' + escapedTerm, 'igm') + ; + // avoid loop if we're matching nothing + if( module.has.query() ) { + results = []; + + module.verbose('Searching for matching values', searchTerm); + $item + .each(function(){ + var + $choice = $(this), + text, + value + ; + if(settings.match == 'both' || settings.match == 'text') { + text = String(module.get.choiceText($choice, false)); + if(text.search(beginsWithRegExp) !== -1) { + results.push(this); + return true; + } + else if (settings.fullTextSearch === 'exact' && module.exactSearch(searchTerm, text)) { + results.push(this); + return true; + } + else if (settings.fullTextSearch === true && module.fuzzySearch(searchTerm, text)) { + results.push(this); + return true; + } + } + if(settings.match == 'both' || settings.match == 'value') { + value = String(module.get.choiceValue($choice, text)); + if(value.search(beginsWithRegExp) !== -1) { + results.push(this); + return true; + } + else if (settings.fullTextSearch === 'exact' && module.exactSearch(searchTerm, value)) { + results.push(this); + return true; + } + else if (settings.fullTextSearch === true && module.fuzzySearch(searchTerm, value)) { + results.push(this); + return true; + } + } + }) + ; + } + module.debug('Showing only matched items', searchTerm); + module.remove.filteredItem(); + if(results) { + $item + .not(results) + .addClass(className.filtered) + ; + } + }, + + fuzzySearch: function(query, term) { + var + termLength = term.length, + queryLength = query.length + ; + query = query.toLowerCase(); + term = term.toLowerCase(); + if(queryLength > termLength) { + return false; + } + if(queryLength === termLength) { + return (query === term); + } + search: for (var characterIndex = 0, nextCharacterIndex = 0; characterIndex < queryLength; characterIndex++) { + var + queryCharacter = query.charCodeAt(characterIndex) + ; + while(nextCharacterIndex < termLength) { + if(term.charCodeAt(nextCharacterIndex++) === queryCharacter) { + continue search; + } + } + return false; + } + return true; + }, + exactSearch: function (query, term) { + query = query.toLowerCase(); + term = term.toLowerCase(); + if(term.indexOf(query) > -1) { + return true; + } + return false; + }, + filterActive: function() { + if(settings.useLabels) { + $item.filter('.' + className.active) + .addClass(className.filtered) + ; + } + }, + + focusSearch: function(skipHandler) { + if( module.has.search() && !module.is.focusedOnSearch() ) { + if(skipHandler) { + $module.off('focus' + eventNamespace, selector.search); + $search.focus(); + $module.on('focus' + eventNamespace, selector.search, module.event.search.focus); + } + else { + $search.focus(); + } + } + }, + + forceSelection: function() { + var + $currentlySelected = $item.not(className.filtered).filter('.' + className.selected).eq(0), + $activeItem = $item.not(className.filtered).filter('.' + className.active).eq(0), + $selectedItem = ($currentlySelected.length > 0) + ? $currentlySelected + : $activeItem, + hasSelected = ($selectedItem.length > 0) + ; + if(hasSelected && !module.is.multiple()) { + module.debug('Forcing partial selection to selected item', $selectedItem); + $selectedItem[0].click(); + return; + } + else { + if(settings.allowAdditions) { + module.set.selected(module.get.query()); + module.remove.searchTerm(); + } + else { + module.remove.searchTerm(); + } + } + }, + + change: { + values: function(values) { + if(!settings.allowAdditions) { + module.clear(); + } + module.debug('Creating dropdown with specified values', values); + module.setup.menu({values: values}); + $.each(values, function(index, item) { + if(item.selected == true) { + module.debug('Setting initial selection to', item.value); + module.set.selected(item.value); + return true; + } + }); + } + }, + + event: { + change: function() { + if(!internalChange) { + module.debug('Input changed, updating selection'); + module.set.selected(); + } + }, + focus: function() { + if(settings.showOnFocus && !activated && module.is.hidden() && !pageLostFocus) { + module.show(); + } + }, + blur: function(event) { + pageLostFocus = (document.activeElement === this); + if(!activated && !pageLostFocus) { + module.remove.activeLabel(); + module.hide(); + } + }, + mousedown: function() { + if(module.is.searchSelection()) { + // prevent menu hiding on immediate re-focus + willRefocus = true; + } + else { + // prevents focus callback from occurring on mousedown + activated = true; + } + }, + mouseup: function() { + if(module.is.searchSelection()) { + // prevent menu hiding on immediate re-focus + willRefocus = false; + } + else { + activated = false; + } + }, + click: function(event) { + var + $target = $(event.target) + ; + // focus search + if($target.is($module)) { + if(!module.is.focusedOnSearch()) { + module.focusSearch(); + } + else { + module.show(); + } + } + }, + search: { + focus: function() { + activated = true; + if(module.is.multiple()) { + module.remove.activeLabel(); + } + if(settings.showOnFocus) { + module.search(); + } + }, + blur: function(event) { + pageLostFocus = (document.activeElement === this); + if(module.is.searchSelection() && !willRefocus) { + if(!itemActivated && !pageLostFocus) { + if(settings.forceSelection) { + module.forceSelection(); + } + module.hide(); + } + } + willRefocus = false; + } + }, + icon: { + click: function(event) { + module.toggle(); + } + }, + text: { + focus: function(event) { + activated = true; + module.focusSearch(); + } + }, + input: function(event) { + if(module.is.multiple() || module.is.searchSelection()) { + module.set.filtered(); + } + clearTimeout(module.timer); + module.timer = setTimeout(module.search, settings.delay.search); + }, + label: { + click: function(event) { + var + $label = $(this), + $labels = $module.find(selector.label), + $activeLabels = $labels.filter('.' + className.active), + $nextActive = $label.nextAll('.' + className.active), + $prevActive = $label.prevAll('.' + className.active), + $range = ($nextActive.length > 0) + ? $label.nextUntil($nextActive).add($activeLabels).add($label) + : $label.prevUntil($prevActive).add($activeLabels).add($label) + ; + if(event.shiftKey) { + $activeLabels.removeClass(className.active); + $range.addClass(className.active); + } + else if(event.ctrlKey) { + $label.toggleClass(className.active); + } + else { + $activeLabels.removeClass(className.active); + $label.addClass(className.active); + } + settings.onLabelSelect.apply(this, $labels.filter('.' + className.active)); + } + }, + remove: { + click: function() { + var + $label = $(this).parent() + ; + if( $label.hasClass(className.active) ) { + // remove all selected labels + module.remove.activeLabels(); + } + else { + // remove this label only + module.remove.activeLabels( $label ); + } + } + }, + test: { + toggle: function(event) { + var + toggleBehavior = (module.is.multiple()) + ? module.show + : module.toggle + ; + if(module.is.bubbledLabelClick(event) || module.is.bubbledIconClick(event)) { + return; + } + if( module.determine.eventOnElement(event, toggleBehavior) ) { + event.preventDefault(); + } + }, + touch: function(event) { + module.determine.eventOnElement(event, function() { + if(event.type == 'touchstart') { + module.timer = setTimeout(function() { + module.hide(); + }, settings.delay.touch); + } + else if(event.type == 'touchmove') { + clearTimeout(module.timer); + } + }); + event.stopPropagation(); + }, + hide: function(event) { + module.determine.eventInModule(event, module.hide); + } + }, + select: { + mutation: function(mutations) { + module.debug(' removing selected option', removedValue); + newValue = module.remove.arrayValue(removedValue, values); + module.remove.optionValue(removedValue); + } + else { + module.verbose('Removing from delimited values', removedValue); + newValue = module.remove.arrayValue(removedValue, values); + newValue = newValue.join(settings.delimiter); + } + if(settings.fireOnInit === false && module.is.initialLoad()) { + module.verbose('No callback on initial load', settings.onRemove); + } + else { + settings.onRemove.call(element, removedValue, removedText, $removedItem); + } + module.set.value(newValue, removedText, $removedItem); + module.check.maxSelections(); + }, + arrayValue: function(removedValue, values) { + if( !$.isArray(values) ) { + values = [values]; + } + values = $.grep(values, function(value){ + return (removedValue != value); + }); + module.verbose('Removed value from delimited string', removedValue, values); + return values; + }, + label: function(value, shouldAnimate) { + var + $labels = $module.find(selector.label), + $removedLabel = $labels.filter('[data-' + metadata.value + '="' + module.escape.string(value) +'"]') + ; + module.verbose('Removing label', $removedLabel); + $removedLabel.remove(); + }, + activeLabels: function($activeLabels) { + $activeLabels = $activeLabels || $module.find(selector.label).filter('.' + className.active); + module.verbose('Removing active label selections', $activeLabels); + module.remove.labels($activeLabels); + }, + labels: function($labels) { + $labels = $labels || $module.find(selector.label); + module.verbose('Removing labels', $labels); + $labels + .each(function(){ + var + $label = $(this), + value = $label.data(metadata.value), + stringValue = (value !== undefined) + ? String(value) + : value, + isUserValue = module.is.userValue(stringValue) + ; + if(settings.onLabelRemove.call($label, value) === false) { + module.debug('Label remove callback cancelled removal'); + return; + } + module.remove.message(); + if(isUserValue) { + module.remove.value(stringValue); + module.remove.label(stringValue); + } + else { + // selected will also remove label + module.remove.selected(stringValue); + } + }) + ; + }, + tabbable: function() { + if( module.is.searchSelection() ) { + module.debug('Searchable dropdown initialized'); + $search + .removeAttr('tabindex') + ; + $menu + .removeAttr('tabindex') + ; + } + else { + module.debug('Simple selection dropdown initialized'); + $module + .removeAttr('tabindex') + ; + $menu + .removeAttr('tabindex') + ; + } + } + }, + + has: { + menuSearch: function() { + return (module.has.search() && $search.closest($menu).length > 0); + }, + search: function() { + return ($search.length > 0); + }, + sizer: function() { + return ($sizer.length > 0); + }, + selectInput: function() { + return ( $input.is('select') ); + }, + minCharacters: function(searchTerm) { + if(settings.minCharacters) { + searchTerm = (searchTerm !== undefined) + ? String(searchTerm) + : String(module.get.query()) + ; + return (searchTerm.length >= settings.minCharacters); + } + return true; + }, + firstLetter: function($item, letter) { + var + text, + firstLetter + ; + if(!$item || $item.length === 0 || typeof letter !== 'string') { + return false; + } + text = module.get.choiceText($item, false); + letter = letter.toLowerCase(); + firstLetter = String(text).charAt(0).toLowerCase(); + return (letter == firstLetter); + }, + input: function() { + return ($input.length > 0); + }, + items: function() { + return ($item.length > 0); + }, + menu: function() { + return ($menu.length > 0); + }, + message: function() { + return ($menu.children(selector.message).length !== 0); + }, + label: function(value) { + var + escapedValue = module.escape.value(value), + $labels = $module.find(selector.label) + ; + if(settings.ignoreCase) { + escapedValue = escapedValue.toLowerCase(); + } + return ($labels.filter('[data-' + metadata.value + '="' + module.escape.string(escapedValue) +'"]').length > 0); + }, + maxSelections: function() { + return (settings.maxSelections && module.get.selectionCount() >= settings.maxSelections); + }, + allResultsFiltered: function() { + var + $normalResults = $item.not(selector.addition) + ; + return ($normalResults.filter(selector.unselectable).length === $normalResults.length); + }, + userSuggestion: function() { + return ($menu.children(selector.addition).length > 0); + }, + query: function() { + return (module.get.query() !== ''); + }, + value: function(value) { + return (settings.ignoreCase) + ? module.has.valueIgnoringCase(value) + : module.has.valueMatchingCase(value) + ; + }, + valueMatchingCase: function(value) { + var + values = module.get.values(), + hasValue = $.isArray(values) + ? values && ($.inArray(value, values) !== -1) + : (values == value) + ; + return (hasValue) + ? true + : false + ; + }, + valueIgnoringCase: function(value) { + var + values = module.get.values(), + hasValue = false + ; + if(!$.isArray(values)) { + values = [values]; + } + $.each(values, function(index, existingValue) { + if(String(value).toLowerCase() == String(existingValue).toLowerCase()) { + hasValue = true; + return false; + } + }); + return hasValue; + } + }, + + is: { + active: function() { + return $module.hasClass(className.active); + }, + animatingInward: function() { + return $menu.transition('is inward'); + }, + animatingOutward: function() { + return $menu.transition('is outward'); + }, + bubbledLabelClick: function(event) { + return $(event.target).is('select, input') && $module.closest('label').length > 0; + }, + bubbledIconClick: function(event) { + return $(event.target).closest($icon).length > 0; + }, + alreadySetup: function() { + return ($module.is('select') && $module.parent(selector.dropdown).data(moduleNamespace) !== undefined && $module.prev().length === 0); + }, + animating: function($subMenu) { + return ($subMenu) + ? $subMenu.transition && $subMenu.transition('is animating') + : $menu.transition && $menu.transition('is animating') + ; + }, + leftward: function($subMenu) { + var $selectedMenu = $subMenu || $menu; + return $selectedMenu.hasClass(className.leftward); + }, + disabled: function() { + return $module.hasClass(className.disabled); + }, + focused: function() { + return (document.activeElement === $module[0]); + }, + focusedOnSearch: function() { + return (document.activeElement === $search[0]); + }, + allFiltered: function() { + return( (module.is.multiple() || module.has.search()) && !(settings.hideAdditions == false && module.has.userSuggestion()) && !module.has.message() && module.has.allResultsFiltered() ); + }, + hidden: function($subMenu) { + return !module.is.visible($subMenu); + }, + initialLoad: function() { + return initialLoad; + }, + inObject: function(needle, object) { + var + found = false + ; + $.each(object, function(index, property) { + if(property == needle) { + found = true; + return true; + } + }); + return found; + }, + multiple: function() { + return $module.hasClass(className.multiple); + }, + remote: function() { + return settings.apiSettings && module.can.useAPI(); + }, + single: function() { + return !module.is.multiple(); + }, + selectMutation: function(mutations) { + var + selectChanged = false + ; + $.each(mutations, function(index, mutation) { + if(mutation.target && $(mutation.target).is('select')) { + selectChanged = true; + return true; + } + }); + return selectChanged; + }, + search: function() { + return $module.hasClass(className.search); + }, + searchSelection: function() { + return ( module.has.search() && $search.parent(selector.dropdown).length === 1 ); + }, + selection: function() { + return $module.hasClass(className.selection); + }, + userValue: function(value) { + return ($.inArray(value, module.get.userValues()) !== -1); + }, + upward: function($menu) { + var $element = $menu || $module; + return $element.hasClass(className.upward); + }, + visible: function($subMenu) { + return ($subMenu) + ? $subMenu.hasClass(className.visible) + : $menu.hasClass(className.visible) + ; + }, + verticallyScrollableContext: function() { + var + overflowY = ($context.get(0) !== window) + ? $context.css('overflow-y') + : false + ; + return (overflowY == 'auto' || overflowY == 'scroll'); + }, + horizontallyScrollableContext: function() { + var + overflowX = ($context.get(0) !== window) + ? $context.css('overflow-X') + : false + ; + return (overflowX == 'auto' || overflowX == 'scroll'); + } + }, + + can: { + activate: function($item) { + if(settings.useLabels) { + return true; + } + if(!module.has.maxSelections()) { + return true; + } + if(module.has.maxSelections() && $item.hasClass(className.active)) { + return true; + } + return false; + }, + openDownward: function($subMenu) { + var + $currentMenu = $subMenu || $menu, + canOpenDownward = true, + onScreen = {}, + calculations + ; + $currentMenu + .addClass(className.loading) + ; + calculations = { + context: { + offset : ($context.get(0) === window) + ? { top: 0, left: 0} + : $context.offset(), + scrollTop : $context.scrollTop(), + height : $context.outerHeight() + }, + menu : { + offset: $currentMenu.offset(), + height: $currentMenu.outerHeight() + } + }; + if(module.is.verticallyScrollableContext()) { + calculations.menu.offset.top += calculations.context.scrollTop; + } + onScreen = { + above : (calculations.context.scrollTop) <= calculations.menu.offset.top - calculations.context.offset.top - calculations.menu.height, + below : (calculations.context.scrollTop + calculations.context.height) >= calculations.menu.offset.top - calculations.context.offset.top + calculations.menu.height + }; + if(onScreen.below) { + module.verbose('Dropdown can fit in context downward', onScreen); + canOpenDownward = true; + } + else if(!onScreen.below && !onScreen.above) { + module.verbose('Dropdown cannot fit in either direction, favoring downward', onScreen); + canOpenDownward = true; + } + else { + module.verbose('Dropdown cannot fit below, opening upward', onScreen); + canOpenDownward = false; + } + $currentMenu.removeClass(className.loading); + return canOpenDownward; + }, + openRightward: function($subMenu) { + var + $currentMenu = $subMenu || $menu, + canOpenRightward = true, + isOffscreenRight = false, + calculations + ; + $currentMenu + .addClass(className.loading) + ; + calculations = { + context: { + offset : ($context.get(0) === window) + ? { top: 0, left: 0} + : $context.offset(), + scrollLeft : $context.scrollLeft(), + width : $context.outerWidth() + }, + menu: { + offset : $currentMenu.offset(), + width : $currentMenu.outerWidth() + } + }; + if(module.is.horizontallyScrollableContext()) { + calculations.menu.offset.left += calculations.context.scrollLeft; + } + isOffscreenRight = (calculations.menu.offset.left - calculations.context.offset.left + calculations.menu.width >= calculations.context.scrollLeft + calculations.context.width); + if(isOffscreenRight) { + module.verbose('Dropdown cannot fit in context rightward', isOffscreenRight); + canOpenRightward = false; + } + $currentMenu.removeClass(className.loading); + return canOpenRightward; + }, + click: function() { + return (hasTouch || settings.on == 'click'); + }, + extendSelect: function() { + return settings.allowAdditions || settings.apiSettings; + }, + show: function() { + return !module.is.disabled() && (module.has.items() || module.has.message()); + }, + useAPI: function() { + return $.fn.api !== undefined; + } + }, + + animate: { + show: function(callback, $subMenu) { + var + $currentMenu = $subMenu || $menu, + start = ($subMenu) + ? function() {} + : function() { + module.hideSubMenus(); + module.hideOthers(); + module.set.active(); + }, + transition + ; + callback = $.isFunction(callback) + ? callback + : function(){} + ; + module.verbose('Doing menu show animation', $currentMenu); + module.set.direction($subMenu); + transition = module.get.transition($subMenu); + if( module.is.selection() ) { + module.set.scrollPosition(module.get.selectedItem(), true); + } + if( module.is.hidden($currentMenu) || module.is.animating($currentMenu) ) { + if(transition == 'none') { + start(); + $currentMenu.transition('show'); + callback.call(element); + } + else if($.fn.transition !== undefined && $module.transition('is supported')) { + $currentMenu + .transition({ + animation : transition + ' in', + debug : settings.debug, + verbose : settings.verbose, + duration : settings.duration, + queue : true, + onStart : start, + onComplete : function() { + callback.call(element); + } + }) + ; + } + else { + module.error(error.noTransition, transition); + } + } + }, + hide: function(callback, $subMenu) { + var + $currentMenu = $subMenu || $menu, + duration = ($subMenu) + ? (settings.duration * 0.9) + : settings.duration, + start = ($subMenu) + ? function() {} + : function() { + if( module.can.click() ) { + module.unbind.intent(); + } + module.remove.active(); + }, + transition = module.get.transition($subMenu) + ; + callback = $.isFunction(callback) + ? callback + : function(){} + ; + if( module.is.visible($currentMenu) || module.is.animating($currentMenu) ) { + module.verbose('Doing menu hide animation', $currentMenu); + + if(transition == 'none') { + start(); + $currentMenu.transition('hide'); + callback.call(element); + } + else if($.fn.transition !== undefined && $module.transition('is supported')) { + $currentMenu + .transition({ + animation : transition + ' out', + duration : settings.duration, + debug : settings.debug, + verbose : settings.verbose, + queue : false, + onStart : start, + onComplete : function() { + callback.call(element); + } + }) + ; + } + else { + module.error(error.transition); + } + } + } + }, + + hideAndClear: function() { + module.remove.searchTerm(); + if( module.has.maxSelections() ) { + return; + } + if(module.has.search()) { + module.hide(function() { + module.remove.filteredItem(); + }); + } + else { + module.hide(); + } + }, + + delay: { + show: function() { + module.verbose('Delaying show event to ensure user intent'); + clearTimeout(module.timer); + module.timer = setTimeout(module.show, settings.delay.show); + }, + hide: function() { + module.verbose('Delaying hide event to ensure user intent'); + clearTimeout(module.timer); + module.timer = setTimeout(module.hide, settings.delay.hide); + } + }, + + escape: { + value: function(value) { + var + multipleValues = $.isArray(value), + stringValue = (typeof value === 'string'), + isUnparsable = (!stringValue && !multipleValues), + hasQuotes = (stringValue && value.search(regExp.quote) !== -1), + values = [] + ; + if(isUnparsable || !hasQuotes) { + return value; + } + module.debug('Encoding quote values for use in select', value); + if(multipleValues) { + $.each(value, function(index, value){ + values.push(value.replace(regExp.quote, '"')); + }); + return values; + } + return value.replace(regExp.quote, '"'); + }, + string: function(text) { + text = String(text); + return text.replace(regExp.escape, '\\$&'); + } + }, + + setting: function(name, value) { + module.debug('Changing setting', name, value); + if( $.isPlainObject(name) ) { + $.extend(true, settings, name); + } + else if(value !== undefined) { + if($.isPlainObject(settings[name])) { + $.extend(true, settings[name], value); + } + else { + settings[name] = value; + } + } + else { + return settings[name]; + } + }, + internal: function(name, value) { + if( $.isPlainObject(name) ) { + $.extend(true, module, name); + } + else if(value !== undefined) { + module[name] = value; + } + else { + return module[name]; + } + }, + debug: function() { + if(!settings.silent && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.debug.apply(console, arguments); + } + } + }, + verbose: function() { + if(!settings.silent && settings.verbose && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.verbose.apply(console, arguments); + } + } + }, + error: function() { + if(!settings.silent) { + module.error = Function.prototype.bind.call(console.error, console, settings.name + ':'); + module.error.apply(console, arguments); + } + }, + performance: { + log: function(message) { + var + currentTime, + executionTime, + previousTime + ; + if(settings.performance) { + currentTime = new Date().getTime(); + previousTime = time || currentTime; + executionTime = currentTime - previousTime; + time = currentTime; + performance.push({ + 'Name' : message[0], + 'Arguments' : [].slice.call(message, 1) || '', + 'Element' : element, + 'Execution Time' : executionTime + }); + } + clearTimeout(module.performance.timer); + module.performance.timer = setTimeout(module.performance.display, 500); + }, + display: function() { + var + title = settings.name + ':', + totalTime = 0 + ; + time = false; + clearTimeout(module.performance.timer); + $.each(performance, function(index, data) { + totalTime += data['Execution Time']; + }); + title += ' ' + totalTime + 'ms'; + if(moduleSelector) { + title += ' \'' + moduleSelector + '\''; + } + if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) { + console.groupCollapsed(title); + if(console.table) { + console.table(performance); + } + else { + $.each(performance, function(index, data) { + console.log(data['Name'] + ': ' + data['Execution Time']+'ms'); + }); + } + console.groupEnd(); + } + performance = []; + } + }, + invoke: function(query, passedArguments, context) { + var + object = instance, + maxDepth, + found, + response + ; + passedArguments = passedArguments || queryArguments; + context = element || context; + if(typeof query == 'string' && object !== undefined) { + query = query.split(/[\. ]/); + maxDepth = query.length - 1; + $.each(query, function(depth, value) { + var camelCaseValue = (depth != maxDepth) + ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1) + : query + ; + if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) { + object = object[camelCaseValue]; + } + else if( object[camelCaseValue] !== undefined ) { + found = object[camelCaseValue]; + return false; + } + else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) { + object = object[value]; + } + else if( object[value] !== undefined ) { + found = object[value]; + return false; + } + else { + module.error(error.method, query); + return false; + } + }); + } + if ( $.isFunction( found ) ) { + response = found.apply(context, passedArguments); + } + else if(found !== undefined) { + response = found; + } + if($.isArray(returnedValue)) { + returnedValue.push(response); + } + else if(returnedValue !== undefined) { + returnedValue = [returnedValue, response]; + } + else if(response !== undefined) { + returnedValue = response; + } + return found; + } + }; + + if(methodInvoked) { + if(instance === undefined) { + module.initialize(); + } + module.invoke(query); + } + else { + if(instance !== undefined) { + instance.invoke('destroy'); + } + module.initialize(); + } + }) + ; + return (returnedValue !== undefined) + ? returnedValue + : $allModules + ; +}; + +$.fn.dropdown.settings = { + + silent : false, + debug : false, + verbose : false, + performance : true, + + on : 'click', // what event should show menu action on item selection + action : 'activate', // action on item selection (nothing, activate, select, combo, hide, function(){}) + + values : false, // specify values to use for dropdown + + apiSettings : false, + selectOnKeydown : true, // Whether selection should occur automatically when keyboard shortcuts used + minCharacters : 0, // Minimum characters required to trigger API call + + filterRemoteData : false, // Whether API results should be filtered after being returned for query term + saveRemoteData : true, // Whether remote name/value pairs should be stored in sessionStorage to allow remote data to be restored on page refresh + + throttle : 200, // How long to wait after last user input to search remotely + + context : window, // Context to use when determining if on screen + direction : 'auto', // Whether dropdown should always open in one direction + keepOnScreen : true, // Whether dropdown should check whether it is on screen before showing + + match : 'both', // what to match against with search selection (both, text, or label) + fullTextSearch : false, // search anywhere in value (set to 'exact' to require exact matches) + + placeholder : 'auto', // whether to convert blank the values will be delimited with this character + + showOnFocus : true, // show menu on focus + allowReselection : false, // whether current value should trigger callbacks when reselected + allowTab : true, // add tabindex to element + allowCategorySelection : false, // allow elements with sub-menus to be selected + + fireOnInit : false, // Whether callbacks should fire when initializing dropdown values + + transition : 'auto', // auto transition will slide down or up based on direction + duration : 200, // duration of transition + + glyphWidth : 1.037, // widest glyph width in em (W is 1.037 em) used to calculate multiselect input width + + // label settings on multi-select + label: { + transition : 'scale', + duration : 200, + variation : false + }, + + // delay before event + delay : { + hide : 300, + show : 200, + search : 20, + touch : 50 + }, + + /* Callbacks */ + onChange : function(value, text, $selected){}, + onAdd : function(value, text, $selected){}, + onRemove : function(value, text, $selected){}, + + onLabelSelect : function($selectedLabels){}, + onLabelCreate : function(value, text) { return $(this); }, + onLabelRemove : function(value) { return true; }, + onNoResults : function(searchTerm) { return true; }, + onShow : function(){}, + onHide : function(){}, + + /* Component */ + name : 'Dropdown', + namespace : 'dropdown', + + message: { + addResult : 'Add {term}', + count : '{count} selected', + maxSelections : 'Max {maxCount} selections', + noResults : 'No results found.', + serverError : 'There was an error contacting the server' + }, + + error : { + action : 'You called a dropdown action that was not defined', + alreadySetup : 'Once a select has been initialized behaviors must be called on the created ui dropdown', + labels : 'Allowing user additions currently requires the use of labels.', + missingMultiple : ' +
+
+
+ {{.CsrfTokenHtml}} +
+
-
- - + + +
+
+
+ + +
+
{{end}}
{{range .Team.Repos}}
+ + + + + {{template "base/footer" .}} diff --git a/templates/pwa/serviceworker_js.tmpl b/templates/pwa/serviceworker_js.tmpl index 5a2756cf24..dc2453e3d8 100644 --- a/templates/pwa/serviceworker_js.tmpl +++ b/templates/pwa/serviceworker_js.tmpl @@ -6,6 +6,7 @@ var urlsToCache = [ '{{StaticUrlPrefix}}/vendor/plugins/jquery-migrate/jquery-migrate.min.js?v=3.0.1', '{{StaticUrlPrefix}}/vendor/plugins/semantic/semantic.min.js', '{{StaticUrlPrefix}}/js/index.js?v={{MD5 AppVer}}', + '{{StaticUrlPrefix}}/js/semantic.dropdown.custom.js?v={{MD5 AppVer}}', '{{StaticUrlPrefix}}/js/draw.js', '{{StaticUrlPrefix}}/vendor/plugins/clipboard/clipboard.min.js', '{{StaticUrlPrefix}}/vendor/plugins/gitgraph/gitgraph.js', diff --git a/templates/repo/issue/view_content.tmpl b/templates/repo/issue/view_content.tmpl index 29d48d7089..9599930d71 100644 --- a/templates/repo/issue/view_content.tmpl +++ b/templates/repo/issue/view_content.tmpl @@ -22,7 +22,7 @@