mirror of
https://github.com/go-gitea/gitea
synced 2024-12-22 16:44:26 +00:00
Refactor sidebar label selector (#32460)
Introduce `issueSidebarLabelsData` to handle all sidebar labels related data.
This commit is contained in:
parent
b55a31eb6a
commit
58c634b854
@ -788,7 +788,11 @@ func CompareDiff(ctx *context.Context) {
|
|||||||
|
|
||||||
if !nothingToCompare {
|
if !nothingToCompare {
|
||||||
// Setup information for new form.
|
// Setup information for new form.
|
||||||
RetrieveRepoMetas(ctx, ctx.Repo.Repository, true)
|
retrieveRepoMetasForIssueWriter(ctx, ctx.Repo.Repository, true)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
labelsData := retrieveRepoLabels(ctx, ctx.Repo.Repository, 0, true)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -796,6 +800,10 @@ func CompareDiff(ctx *context.Context) {
|
|||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
_, templateErrs := setTemplateIfExists(ctx, pullRequestTemplateKey, pullRequestTemplateCandidates, labelsData)
|
||||||
|
if len(templateErrs) > 0 {
|
||||||
|
ctx.Flash.Warning(renderErrorOfTemplates(ctx, templateErrs), true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
beforeCommitID := ctx.Data["BeforeCommitID"].(string)
|
beforeCommitID := ctx.Data["BeforeCommitID"].(string)
|
||||||
@ -808,11 +816,6 @@ func CompareDiff(ctx *context.Context) {
|
|||||||
ctx.Data["Title"] = "Comparing " + base.ShortSha(beforeCommitID) + separator + base.ShortSha(afterCommitID)
|
ctx.Data["Title"] = "Comparing " + base.ShortSha(beforeCommitID) + separator + base.ShortSha(afterCommitID)
|
||||||
|
|
||||||
ctx.Data["IsDiffCompare"] = true
|
ctx.Data["IsDiffCompare"] = true
|
||||||
_, templateErrs := setTemplateIfExists(ctx, pullRequestTemplateKey, pullRequestTemplateCandidates)
|
|
||||||
|
|
||||||
if len(templateErrs) > 0 {
|
|
||||||
ctx.Flash.Warning(renderErrorOfTemplates(ctx, templateErrs), true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if content, ok := ctx.Data["content"].(string); ok && content != "" {
|
if content, ok := ctx.Data["content"].(string); ok && content != "" {
|
||||||
// If a template content is set, prepend the "content". In this case that's only
|
// If a template content is set, prepend the "content". In this case that's only
|
||||||
|
@ -870,51 +870,112 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
|
|||||||
ctx.Data["IssueSidebarReviewersData"] = data
|
ctx.Data["IssueSidebarReviewersData"] = data
|
||||||
}
|
}
|
||||||
|
|
||||||
// RetrieveRepoMetas find all the meta information of a repository
|
type issueSidebarLabelsData struct {
|
||||||
func RetrieveRepoMetas(ctx *context.Context, repo *repo_model.Repository, isPull bool) []*issues_model.Label {
|
Repository *repo_model.Repository
|
||||||
if !ctx.Repo.CanWriteIssuesOrPulls(isPull) {
|
RepoLink string
|
||||||
return nil
|
IssueID int64
|
||||||
|
IsPullRequest bool
|
||||||
|
AllLabels []*issues_model.Label
|
||||||
|
RepoLabels []*issues_model.Label
|
||||||
|
OrgLabels []*issues_model.Label
|
||||||
|
SelectedLabelIDs string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeSelectedStringIDs[KeyType, ItemType comparable](
|
||||||
|
allLabels []*issues_model.Label, candidateKey func(candidate *issues_model.Label) KeyType,
|
||||||
|
selectedItems []ItemType, selectedKey func(selected ItemType) KeyType,
|
||||||
|
) string {
|
||||||
|
selectedIDSet := make(container.Set[string])
|
||||||
|
allLabelMap := map[KeyType]*issues_model.Label{}
|
||||||
|
for _, label := range allLabels {
|
||||||
|
allLabelMap[candidateKey(label)] = label
|
||||||
|
}
|
||||||
|
for _, item := range selectedItems {
|
||||||
|
if label, ok := allLabelMap[selectedKey(item)]; ok {
|
||||||
|
label.IsChecked = true
|
||||||
|
selectedIDSet.Add(strconv.FormatInt(label.ID, 10))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ids := selectedIDSet.Values()
|
||||||
|
sort.Strings(ids)
|
||||||
|
return strings.Join(ids, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *issueSidebarLabelsData) SetSelectedLabels(labels []*issues_model.Label) {
|
||||||
|
d.SelectedLabelIDs = makeSelectedStringIDs(
|
||||||
|
d.AllLabels, func(label *issues_model.Label) int64 { return label.ID },
|
||||||
|
labels, func(label *issues_model.Label) int64 { return label.ID },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *issueSidebarLabelsData) SetSelectedLabelNames(labelNames []string) {
|
||||||
|
d.SelectedLabelIDs = makeSelectedStringIDs(
|
||||||
|
d.AllLabels, func(label *issues_model.Label) string { return strings.ToLower(label.Name) },
|
||||||
|
labelNames, strings.ToLower,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *issueSidebarLabelsData) SetSelectedLabelIDs(labelIDs []int64) {
|
||||||
|
d.SelectedLabelIDs = makeSelectedStringIDs(
|
||||||
|
d.AllLabels, func(label *issues_model.Label) int64 { return label.ID },
|
||||||
|
labelIDs, func(labelID int64) int64 { return labelID },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func retrieveRepoLabels(ctx *context.Context, repo *repo_model.Repository, issueID int64, isPull bool) *issueSidebarLabelsData {
|
||||||
|
labelsData := &issueSidebarLabelsData{
|
||||||
|
Repository: repo,
|
||||||
|
RepoLink: ctx.Repo.RepoLink,
|
||||||
|
IssueID: issueID,
|
||||||
|
IsPullRequest: isPull,
|
||||||
|
}
|
||||||
|
ctx.Data["IssueSidebarLabelsData"] = labelsData
|
||||||
|
|
||||||
labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{})
|
labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetLabelsByRepoID", err)
|
ctx.ServerError("GetLabelsByRepoID", err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
ctx.Data["Labels"] = labels
|
labelsData.RepoLabels = labels
|
||||||
|
|
||||||
if repo.Owner.IsOrganization() {
|
if repo.Owner.IsOrganization() {
|
||||||
orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
|
orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
labelsData.OrgLabels = orgLabels
|
||||||
|
}
|
||||||
|
labelsData.AllLabels = append(labelsData.AllLabels, labelsData.RepoLabels...)
|
||||||
|
labelsData.AllLabels = append(labelsData.AllLabels, labelsData.OrgLabels...)
|
||||||
|
return labelsData
|
||||||
|
}
|
||||||
|
|
||||||
ctx.Data["OrgLabels"] = orgLabels
|
// retrieveRepoMetasForIssueWriter finds some the meta information of a repository for an issue/pr writer
|
||||||
labels = append(labels, orgLabels...)
|
func retrieveRepoMetasForIssueWriter(ctx *context.Context, repo *repo_model.Repository, isPull bool) {
|
||||||
|
if !ctx.Repo.CanWriteIssuesOrPulls(isPull) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
RetrieveRepoMilestonesAndAssignees(ctx, repo)
|
RetrieveRepoMilestonesAndAssignees(ctx, repo)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
retrieveProjects(ctx, repo)
|
retrieveProjects(ctx, repo)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
PrepareBranchList(ctx)
|
PrepareBranchList(ctx)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Contains true if the user can create issue dependencies
|
// Contains true if the user can create issue dependencies
|
||||||
ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx, ctx.Doer, isPull)
|
ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx, ctx.Doer, isPull)
|
||||||
|
|
||||||
return labels
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tries to load and set an issue template. The first return value indicates if a template was loaded.
|
// Tries to load and set an issue template. The first return value indicates if a template was loaded.
|
||||||
func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles []string) (bool, map[string]error) {
|
func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles []string, labelsData *issueSidebarLabelsData) (bool, map[string]error) {
|
||||||
commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
|
commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, nil
|
return false, nil
|
||||||
@ -951,26 +1012,9 @@ func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles
|
|||||||
ctx.Data["Fields"] = template.Fields
|
ctx.Data["Fields"] = template.Fields
|
||||||
ctx.Data["TemplateFile"] = template.FileName
|
ctx.Data["TemplateFile"] = template.FileName
|
||||||
}
|
}
|
||||||
labelIDs := make([]string, 0, len(template.Labels))
|
|
||||||
if repoLabels, err := issues_model.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, "", db.ListOptions{}); err == nil {
|
|
||||||
ctx.Data["Labels"] = repoLabels
|
|
||||||
if ctx.Repo.Owner.IsOrganization() {
|
|
||||||
if orgLabels, err := issues_model.GetLabelsByOrgID(ctx, ctx.Repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}); err == nil {
|
|
||||||
ctx.Data["OrgLabels"] = orgLabels
|
|
||||||
repoLabels = append(repoLabels, orgLabels...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, metaLabel := range template.Labels {
|
labelsData.SetSelectedLabelNames(template.Labels)
|
||||||
for _, repoLabel := range repoLabels {
|
|
||||||
if strings.EqualFold(repoLabel.Name, metaLabel) {
|
|
||||||
repoLabel.IsChecked = true
|
|
||||||
labelIDs = append(labelIDs, strconv.FormatInt(repoLabel.ID, 10))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
selectedAssigneeIDs := make([]int64, 0, len(template.Assignees))
|
selectedAssigneeIDs := make([]int64, 0, len(template.Assignees))
|
||||||
selectedAssigneeIDStrings := make([]string, 0, len(template.Assignees))
|
selectedAssigneeIDStrings := make([]string, 0, len(template.Assignees))
|
||||||
if userIDs, err := user_model.GetUserIDsByNames(ctx, template.Assignees, false); err == nil {
|
if userIDs, err := user_model.GetUserIDsByNames(ctx, template.Assignees, false); err == nil {
|
||||||
@ -983,8 +1027,7 @@ func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles
|
|||||||
if template.Ref != "" && !strings.HasPrefix(template.Ref, "refs/") { // Assume that the ref intended is always a branch - for tags users should use refs/tags/<ref>
|
if template.Ref != "" && !strings.HasPrefix(template.Ref, "refs/") { // Assume that the ref intended is always a branch - for tags users should use refs/tags/<ref>
|
||||||
template.Ref = git.BranchPrefix + template.Ref
|
template.Ref = git.BranchPrefix + template.Ref
|
||||||
}
|
}
|
||||||
ctx.Data["HasSelectedLabel"] = len(labelIDs) > 0
|
|
||||||
ctx.Data["label_ids"] = strings.Join(labelIDs, ",")
|
|
||||||
ctx.Data["HasSelectedAssignee"] = len(selectedAssigneeIDs) > 0
|
ctx.Data["HasSelectedAssignee"] = len(selectedAssigneeIDs) > 0
|
||||||
ctx.Data["assignee_ids"] = strings.Join(selectedAssigneeIDStrings, ",")
|
ctx.Data["assignee_ids"] = strings.Join(selectedAssigneeIDStrings, ",")
|
||||||
ctx.Data["SelectedAssigneeIDs"] = selectedAssigneeIDs
|
ctx.Data["SelectedAssigneeIDs"] = selectedAssigneeIDs
|
||||||
@ -1042,8 +1085,14 @@ func NewIssue(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RetrieveRepoMetas(ctx, ctx.Repo.Repository, false)
|
retrieveRepoMetasForIssueWriter(ctx, ctx.Repo.Repository, false)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
labelsData := retrieveRepoLabels(ctx, ctx.Repo.Repository, 0, false)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
|
tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetTagNamesByRepoID", err)
|
ctx.ServerError("GetTagNamesByRepoID", err)
|
||||||
@ -1052,7 +1101,7 @@ func NewIssue(ctx *context.Context) {
|
|||||||
ctx.Data["Tags"] = tags
|
ctx.Data["Tags"] = tags
|
||||||
|
|
||||||
ret := issue_service.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
|
ret := issue_service.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
|
||||||
templateLoaded, errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates)
|
templateLoaded, errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates, labelsData)
|
||||||
for k, v := range errs {
|
for k, v := range errs {
|
||||||
ret.TemplateErrors[k] = v
|
ret.TemplateErrors[k] = v
|
||||||
}
|
}
|
||||||
@ -1161,33 +1210,24 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull
|
|||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
labels := RetrieveRepoMetas(ctx, ctx.Repo.Repository, isPull)
|
retrieveRepoMetasForIssueWriter(ctx, ctx.Repo.Repository, isPull)
|
||||||
|
if ctx.Written() {
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
labelsData := retrieveRepoLabels(ctx, ctx.Repo.Repository, 0, isPull)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
var labelIDs []int64
|
var labelIDs []int64
|
||||||
hasSelected := false
|
|
||||||
// Check labels.
|
// Check labels.
|
||||||
if len(form.LabelIDs) > 0 {
|
if len(form.LabelIDs) > 0 {
|
||||||
labelIDs, err = base.StringsToInt64s(strings.Split(form.LabelIDs, ","))
|
labelIDs, err = base.StringsToInt64s(strings.Split(form.LabelIDs, ","))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
labelIDMark := make(container.Set[int64])
|
labelsData.SetSelectedLabelIDs(labelIDs)
|
||||||
labelIDMark.AddMultiple(labelIDs...)
|
|
||||||
|
|
||||||
for i := range labels {
|
|
||||||
if labelIDMark.Contains(labels[i].ID) {
|
|
||||||
labels[i].IsChecked = true
|
|
||||||
hasSelected = true
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Data["Labels"] = labels
|
|
||||||
ctx.Data["HasSelectedLabel"] = hasSelected
|
|
||||||
ctx.Data["label_ids"] = form.LabelIDs
|
|
||||||
|
|
||||||
// Check milestone.
|
// Check milestone.
|
||||||
milestoneID := form.MilestoneID
|
milestoneID := form.MilestoneID
|
||||||
@ -1579,38 +1619,15 @@ func ViewIssue(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metas.
|
retrieveRepoMetasForIssueWriter(ctx, repo, issue.IsPull)
|
||||||
// Check labels.
|
if ctx.Written() {
|
||||||
labelIDMark := make(container.Set[int64])
|
|
||||||
for _, label := range issue.Labels {
|
|
||||||
labelIDMark.Add(label.ID)
|
|
||||||
}
|
|
||||||
labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{})
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("GetLabelsByRepoID", err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Data["Labels"] = labels
|
labelsData := retrieveRepoLabels(ctx, repo, issue.ID, issue.IsPull)
|
||||||
|
if ctx.Written() {
|
||||||
if repo.Owner.IsOrganization() {
|
|
||||||
orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("GetLabelsByOrgID", err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Data["OrgLabels"] = orgLabels
|
labelsData.SetSelectedLabels(issue.Labels)
|
||||||
|
|
||||||
labels = append(labels, orgLabels...)
|
|
||||||
}
|
|
||||||
|
|
||||||
hasSelected := false
|
|
||||||
for i := range labels {
|
|
||||||
if labelIDMark.Contains(labels[i].ID) {
|
|
||||||
labels[i].IsChecked = true
|
|
||||||
hasSelected = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx.Data["HasSelectedLabel"] = hasSelected
|
|
||||||
|
|
||||||
// Check milestone and assignee.
|
// Check milestone and assignee.
|
||||||
if ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
|
if ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
|
||||||
|
@ -53,11 +53,11 @@ func InitializeLabels(ctx *context.Context) {
|
|||||||
ctx.Redirect(ctx.Repo.RepoLink + "/labels")
|
ctx.Redirect(ctx.Repo.RepoLink + "/labels")
|
||||||
}
|
}
|
||||||
|
|
||||||
// RetrieveLabels find all the labels of a repository and organization
|
// RetrieveLabelsForList find all the labels of a repository and organization, it is only used by "/labels" page to list all labels
|
||||||
func RetrieveLabels(ctx *context.Context) {
|
func RetrieveLabelsForList(ctx *context.Context) {
|
||||||
labels, err := issues_model.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, ctx.FormString("sort"), db.ListOptions{})
|
labels, err := issues_model.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, ctx.FormString("sort"), db.ListOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("RetrieveLabels.GetLabels", err)
|
ctx.ServerError("RetrieveLabelsForList.GetLabels", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,7 +62,7 @@ func TestRetrieveLabels(t *testing.T) {
|
|||||||
contexttest.LoadUser(t, ctx, 2)
|
contexttest.LoadUser(t, ctx, 2)
|
||||||
contexttest.LoadRepo(t, ctx, testCase.RepoID)
|
contexttest.LoadRepo(t, ctx, testCase.RepoID)
|
||||||
ctx.Req.Form.Set("sort", testCase.Sort)
|
ctx.Req.Form.Set("sort", testCase.Sort)
|
||||||
RetrieveLabels(ctx)
|
RetrieveLabelsForList(ctx)
|
||||||
assert.False(t, ctx.Written())
|
assert.False(t, ctx.Written())
|
||||||
labels, ok := ctx.Data["Labels"].([]*issues_model.Label)
|
labels, ok := ctx.Data["Labels"].([]*issues_model.Label)
|
||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
|
@ -1163,7 +1163,7 @@ func registerRoutes(m *web.Router) {
|
|||||||
m.Get("/issues/posters", repo.IssuePosters) // it can't use {type:issues|pulls} because it would conflict with other routes like "/pulls/{index}"
|
m.Get("/issues/posters", repo.IssuePosters) // it can't use {type:issues|pulls} because it would conflict with other routes like "/pulls/{index}"
|
||||||
m.Get("/pulls/posters", repo.PullPosters)
|
m.Get("/pulls/posters", repo.PullPosters)
|
||||||
m.Get("/comments/{id}/attachments", repo.GetCommentAttachments)
|
m.Get("/comments/{id}/attachments", repo.GetCommentAttachments)
|
||||||
m.Get("/labels", repo.RetrieveLabels, repo.Labels)
|
m.Get("/labels", repo.RetrieveLabelsForList, repo.Labels)
|
||||||
m.Get("/milestones", repo.Milestones)
|
m.Get("/milestones", repo.Milestones)
|
||||||
m.Get("/milestone/{id}", context.RepoRef(), repo.MilestoneIssuesAndPulls)
|
m.Get("/milestone/{id}", context.RepoRef(), repo.MilestoneIssuesAndPulls)
|
||||||
m.Group("/{type:issues|pulls}", func() {
|
m.Group("/{type:issues|pulls}", func() {
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
<a
|
|
||||||
class="item {{if not .label.IsChecked}}tw-hidden{{end}}"
|
|
||||||
id="label_{{.label.ID}}"
|
|
||||||
href="{{.root.RepoLink}}/{{if or .root.IsPull .root.Issue.IsPull}}pulls{{else}}issues{{end}}?labels={{.label.ID}}"{{/* FIXME: use .root.Issue.Link or create .root.Link */}}
|
|
||||||
>
|
|
||||||
{{- ctx.RenderUtils.RenderLabel .label -}}
|
|
||||||
</a>
|
|
@ -1,46 +0,0 @@
|
|||||||
<div class="ui {{if or (not .HasIssuesOrPullsWritePermission) .Repository.IsArchived}}disabled{{end}} floating jump select-label dropdown">
|
|
||||||
<span class="text muted flex-text-block">
|
|
||||||
<strong>{{ctx.Locale.Tr "repo.issues.new.labels"}}</strong>
|
|
||||||
{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
|
|
||||||
{{svg "octicon-gear" 16 "tw-ml-1"}}
|
|
||||||
{{end}}
|
|
||||||
</span>
|
|
||||||
<div class="filter menu" {{if .Issue}}data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/labels"{{else}}data-id="#label_ids"{{end}}>
|
|
||||||
{{if or .Labels .OrgLabels}}
|
|
||||||
<div class="ui icon search input">
|
|
||||||
<i class="icon">{{svg "octicon-search" 16}}</i>
|
|
||||||
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_labels"}}">
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
<a class="no-select item" href="#">{{ctx.Locale.Tr "repo.issues.new.clear_labels"}}</a>
|
|
||||||
{{if or .Labels .OrgLabels}}
|
|
||||||
{{$previousExclusiveScope := "_no_scope"}}
|
|
||||||
{{range .Labels}}
|
|
||||||
{{$exclusiveScope := .ExclusiveScope}}
|
|
||||||
{{if and (ne $previousExclusiveScope "_no_scope") (ne $previousExclusiveScope $exclusiveScope)}}
|
|
||||||
<div class="divider"></div>
|
|
||||||
{{end}}
|
|
||||||
{{$previousExclusiveScope = $exclusiveScope}}
|
|
||||||
<a class="{{if .IsChecked}}checked{{end}} item" href="#" data-id="{{.ID}}" {{if .IsArchived}}data-is-archived{{end}} data-id-selector="#label_{{.ID}}" data-scope="{{$exclusiveScope}}"><span class="octicon-check {{if not .IsChecked}}tw-invisible{{end}}">{{svg (Iif $exclusiveScope "octicon-dot-fill" "octicon-check")}}</span> {{ctx.RenderUtils.RenderLabel .}}
|
|
||||||
{{if .Description}}<br><small class="desc">{{.Description | ctx.RenderUtils.RenderEmoji}}</small>{{end}}
|
|
||||||
<p class="archived-label-hint">{{template "repo/issue/labels/label_archived" .}}</p>
|
|
||||||
</a>
|
|
||||||
{{end}}
|
|
||||||
<div class="divider"></div>
|
|
||||||
{{$previousExclusiveScope = "_no_scope"}}
|
|
||||||
{{range .OrgLabels}}
|
|
||||||
{{$exclusiveScope := .ExclusiveScope}}
|
|
||||||
{{if and (ne $previousExclusiveScope "_no_scope") (ne $previousExclusiveScope $exclusiveScope)}}
|
|
||||||
<div class="divider"></div>
|
|
||||||
{{end}}
|
|
||||||
{{$previousExclusiveScope = $exclusiveScope}}
|
|
||||||
<a class="{{if .IsChecked}}checked{{end}} item" href="#" data-id="{{.ID}}" {{if .IsArchived}}data-is-archived{{end}} data-id-selector="#label_{{.ID}}" data-scope="{{$exclusiveScope}}"><span class="octicon-check {{if not .IsChecked}}tw-invisible{{end}}">{{svg (Iif $exclusiveScope "octicon-dot-fill" "octicon-check")}}</span> {{ctx.RenderUtils.RenderLabel .}}
|
|
||||||
{{if .Description}}<br><small class="desc">{{.Description | ctx.RenderUtils.RenderEmoji}}</small>{{end}}
|
|
||||||
<p class="archived-label-hint">{{template "repo/issue/labels/label_archived" .}}</p>
|
|
||||||
</a>
|
|
||||||
{{end}}
|
|
||||||
{{else}}
|
|
||||||
<div class="disabled item">{{ctx.Locale.Tr "repo.issues.new.no_items"}}</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,11 +0,0 @@
|
|||||||
<div class="ui labels list">
|
|
||||||
<span class="labels-list">
|
|
||||||
<span class="no-select {{if .root.HasSelectedLabel}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_label"}}</span>
|
|
||||||
{{range .root.Labels}}
|
|
||||||
{{template "repo/issue/labels/label" dict "root" $.root "label" .}}
|
|
||||||
{{end}}
|
|
||||||
{{range .root.OrgLabels}}
|
|
||||||
{{template "repo/issue/labels/label" dict "root" $.root "label" .}}
|
|
||||||
{{end}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
@ -18,15 +18,15 @@
|
|||||||
<input type="hidden" name="template-file" value="{{.TemplateFile}}">
|
<input type="hidden" name="template-file" value="{{.TemplateFile}}">
|
||||||
{{range .Fields}}
|
{{range .Fields}}
|
||||||
{{if eq .Type "input"}}
|
{{if eq .Type "input"}}
|
||||||
{{template "repo/issue/fields/input" "item" .}}
|
{{template "repo/issue/fields/input" dict "item" .}}
|
||||||
{{else if eq .Type "markdown"}}
|
{{else if eq .Type "markdown"}}
|
||||||
{{template "repo/issue/fields/markdown" "item" .}}
|
{{template "repo/issue/fields/markdown" dict "item" .}}
|
||||||
{{else if eq .Type "textarea"}}
|
{{else if eq .Type "textarea"}}
|
||||||
{{template "repo/issue/fields/textarea" "item" . "root" $}}
|
{{template "repo/issue/fields/textarea" dict "item" . "root" $}}
|
||||||
{{else if eq .Type "dropdown"}}
|
{{else if eq .Type "dropdown"}}
|
||||||
{{template "repo/issue/fields/dropdown" "item" .}}
|
{{template "repo/issue/fields/dropdown" dict "item" .}}
|
||||||
{{else if eq .Type "checkboxes"}}
|
{{else if eq .Type "checkboxes"}}
|
||||||
{{template "repo/issue/fields/checkboxes" "item" .}}
|
{{template "repo/issue/fields/checkboxes" dict "item" .}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{else}}
|
{{else}}
|
||||||
@ -49,13 +49,11 @@
|
|||||||
<div class="issue-content-right ui segment">
|
<div class="issue-content-right ui segment">
|
||||||
{{template "repo/issue/branch_selector_field" $}}
|
{{template "repo/issue/branch_selector_field" $}}
|
||||||
{{if .PageIsComparePull}}
|
{{if .PageIsComparePull}}
|
||||||
{{template "repo/issue/sidebar/reviewer_list" dict "IssueSidebarReviewersData" $.IssueSidebarReviewersData}}
|
{{template "repo/issue/sidebar/reviewer_list" $.IssueSidebarReviewersData}}
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<input id="label_ids" name="label_ids" type="hidden" value="{{.label_ids}}">
|
{{template "repo/issue/sidebar/label_list" $.IssueSidebarLabelsData}}
|
||||||
{{template "repo/issue/labels/labels_selector_field" .}}
|
|
||||||
{{template "repo/issue/labels/labels_sidebar" dict "root" $}}
|
|
||||||
|
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
51
templates/repo/issue/sidebar/label_list.tmpl
Normal file
51
templates/repo/issue/sidebar/label_list.tmpl
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
{{$data := .}}
|
||||||
|
{{$canChange := and ctx.RootData.HasIssuesOrPullsWritePermission (not $data.Repository.IsArchived)}}
|
||||||
|
<div class="issue-sidebar-combo" {{if $data.IssueID}}data-update-url="{{$data.RepoLink}}/issues/labels?issue_ids={{$data.IssueID}}"{{end}}>
|
||||||
|
<input class="combo-value" name="label_ids" type="hidden" value="{{$data.SelectedLabelIDs}}">
|
||||||
|
<div class="ui dropdown {{if not $canChange}}disabled{{end}}">
|
||||||
|
<a class="text muted">
|
||||||
|
<strong>{{ctx.Locale.Tr "repo.issues.new.labels"}}</strong> {{if $canChange}}{{svg "octicon-gear"}}{{end}}
|
||||||
|
</a>
|
||||||
|
<div class="menu">
|
||||||
|
{{if not $data.AllLabels}}
|
||||||
|
<div class="item disabled">{{ctx.Locale.Tr "repo.issues.new.no_items"}}</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="ui icon search input">
|
||||||
|
<i class="icon">{{svg "octicon-search" 16}}</i>
|
||||||
|
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_labels"}}">
|
||||||
|
</div>
|
||||||
|
<a class="item clear-selection" href="#">{{ctx.Locale.Tr "repo.issues.new.clear_labels"}}</a>
|
||||||
|
{{$previousExclusiveScope := "_no_scope"}}
|
||||||
|
{{range .RepoLabels}}
|
||||||
|
{{$exclusiveScope := .ExclusiveScope}}
|
||||||
|
{{if and (ne $previousExclusiveScope "_no_scope") (ne $previousExclusiveScope $exclusiveScope)}}
|
||||||
|
<div class="divider"></div>
|
||||||
|
{{end}}
|
||||||
|
{{$previousExclusiveScope = $exclusiveScope}}
|
||||||
|
{{template "repo/issue/sidebar/label_list_item" dict "Label" .}}
|
||||||
|
{{end}}
|
||||||
|
<div class="divider"></div>
|
||||||
|
{{$previousExclusiveScope = "_no_scope"}}
|
||||||
|
{{range .OrgLabels}}
|
||||||
|
{{$exclusiveScope := .ExclusiveScope}}
|
||||||
|
{{if and (ne $previousExclusiveScope "_no_scope") (ne $previousExclusiveScope $exclusiveScope)}}
|
||||||
|
<div class="divider"></div>
|
||||||
|
{{end}}
|
||||||
|
{{$previousExclusiveScope = $exclusiveScope}}
|
||||||
|
{{template "repo/issue/sidebar/label_list_item" dict "Label" .}}
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ui list labels-list tw-my-2 tw-flex tw-gap-2">
|
||||||
|
<span class="item empty-list {{if $data.SelectedLabelIDs}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_label"}}</span>
|
||||||
|
{{range $data.AllLabels}}
|
||||||
|
{{if .IsChecked}}
|
||||||
|
<a class="item" href="{{$data.RepoLink}}/{{if $data.IsPullRequest}}pulls{{else}}issues{{end}}?labels={{.ID}}">
|
||||||
|
{{- ctx.RenderUtils.RenderLabel . -}}
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
11
templates/repo/issue/sidebar/label_list_item.tmpl
Normal file
11
templates/repo/issue/sidebar/label_list_item.tmpl
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{{$label := .Label}}
|
||||||
|
<a class="item {{if $label.IsChecked}}checked{{else if $label.IsArchived}}tw-hidden{{end}}" href="#"
|
||||||
|
data-scope="{{$label.ExclusiveScope}}" data-value="{{$label.ID}}" {{if $label.IsArchived}}data-is-archived{{end}}
|
||||||
|
>
|
||||||
|
<span class="item-check-mark">{{svg (Iif $label.ExclusiveScope "octicon-dot-fill" "octicon-check")}}</span>
|
||||||
|
{{ctx.RenderUtils.RenderLabel $label}}
|
||||||
|
<div class="item-secondary-info">
|
||||||
|
{{if $label.Description}}<div class="tw-pl-[20px]"><small>{{$label.Description | ctx.RenderUtils.RenderEmoji}}</small></div>{{end}}
|
||||||
|
<div class="archived-label-hint">{{template "repo/issue/labels/label_archived" $label}}</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
@ -1,11 +1,9 @@
|
|||||||
{{$data := .IssueSidebarReviewersData}}
|
{{$data := .}}
|
||||||
{{$hasCandidates := or $data.Reviewers $data.TeamReviewers}}
|
{{$hasCandidates := or $data.Reviewers $data.TeamReviewers}}
|
||||||
<div class="issue-sidebar-combo" data-sidebar-combo-for="reviewers"
|
<div class="issue-sidebar-combo" {{if $data.IssueID}}data-update-url="{{$data.RepoLink}}/issues/request_review?issue_ids={{$data.IssueID}}"{{end}}>
|
||||||
{{if $data.IssueID}}data-update-url="{{$data.RepoLink}}/issues/request_review?issue_ids={{$data.IssueID}}"{{end}}
|
|
||||||
>
|
|
||||||
<input type="hidden" class="combo-value" name="reviewer_ids">{{/* match CreateIssueForm */}}
|
<input type="hidden" class="combo-value" name="reviewer_ids">{{/* match CreateIssueForm */}}
|
||||||
<div class="ui dropdown custom {{if or (not $hasCandidates) (not $data.CanChooseReviewer)}}disabled{{end}}">
|
<div class="ui dropdown {{if or (not $hasCandidates) (not $data.CanChooseReviewer)}}disabled{{end}}">
|
||||||
<a class="muted text">
|
<a class="text muted">
|
||||||
<strong>{{ctx.Locale.Tr "repo.issues.review.reviewers"}}</strong> {{if and $data.CanChooseReviewer}}{{svg "octicon-gear"}}{{end}}
|
<strong>{{ctx.Locale.Tr "repo.issues.review.reviewers"}}</strong> {{if and $data.CanChooseReviewer}}{{svg "octicon-gear"}}{{end}}
|
||||||
</a>
|
</a>
|
||||||
<div class="menu flex-items-menu">
|
<div class="menu flex-items-menu">
|
||||||
@ -19,7 +17,8 @@
|
|||||||
{{if .User}}
|
{{if .User}}
|
||||||
<a class="item muted {{if .Requested}}checked{{end}}" href="{{.User.HomeLink}}" data-value="{{.ItemID}}" data-can-change="{{.CanChange}}"
|
<a class="item muted {{if .Requested}}checked{{end}}" href="{{.User.HomeLink}}" data-value="{{.ItemID}}" data-can-change="{{.CanChange}}"
|
||||||
{{if not .CanChange}}data-tooltip-content="{{ctx.Locale.Tr "repo.issues.remove_request_review_block"}}"{{end}}>
|
{{if not .CanChange}}data-tooltip-content="{{ctx.Locale.Tr "repo.issues.remove_request_review_block"}}"{{end}}>
|
||||||
{{svg "octicon-check"}} {{ctx.AvatarUtils.Avatar .User 20}} {{template "repo/search_name" .User}}
|
<span class="item-check-mark">{{svg "octicon-check"}}</span>
|
||||||
|
{{ctx.AvatarUtils.Avatar .User 20}} {{template "repo/search_name" .User}}
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
@ -29,7 +28,8 @@
|
|||||||
{{if .Team}}
|
{{if .Team}}
|
||||||
<a class="item muted {{if .Requested}}checked{{end}}" href="#" data-value="{{.ItemID}}" data-can-change="{{.CanChange}}"
|
<a class="item muted {{if .Requested}}checked{{end}}" href="#" data-value="{{.ItemID}}" data-can-change="{{.CanChange}}"
|
||||||
{{if not .CanChange}} data-tooltip-content="{{ctx.Locale.Tr "repo.issues.remove_request_review_block"}}"{{end}}>
|
{{if not .CanChange}} data-tooltip-content="{{ctx.Locale.Tr "repo.issues.remove_request_review_block"}}"{{end}}>
|
||||||
{{svg "octicon-check"}} {{svg "octicon-people" 20}} {{$data.RepoOwnerName}}/{{.Team.Name}}
|
<span class="item-check-mark">{{svg "octicon-check"}}</span>
|
||||||
|
{{svg "octicon-people" 20}} {{$data.RepoOwnerName}}/{{.Team.Name}}
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@ -2,13 +2,12 @@
|
|||||||
{{template "repo/issue/branch_selector_field" $}}
|
{{template "repo/issue/branch_selector_field" $}}
|
||||||
|
|
||||||
{{if .Issue.IsPull}}
|
{{if .Issue.IsPull}}
|
||||||
{{template "repo/issue/sidebar/reviewer_list" dict "IssueSidebarReviewersData" $.IssueSidebarReviewersData}}
|
{{template "repo/issue/sidebar/reviewer_list" $.IssueSidebarReviewersData}}
|
||||||
{{template "repo/issue/sidebar/wip_switch" $}}
|
{{template "repo/issue/sidebar/wip_switch" $}}
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{template "repo/issue/labels/labels_selector_field" $}}
|
{{template "repo/issue/sidebar/label_list" $.IssueSidebarLabelsData}}
|
||||||
{{template "repo/issue/labels/labels_sidebar" dict "root" $}}
|
|
||||||
|
|
||||||
{{template "repo/issue/sidebar/milestone_list" $}}
|
{{template "repo/issue/sidebar/milestone_list" $}}
|
||||||
{{template "repo/issue/sidebar/project_list" $}}
|
{{template "repo/issue/sidebar/project_list" $}}
|
||||||
|
@ -50,7 +50,7 @@
|
|||||||
width: 300px;
|
width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.issue-sidebar-combo .ui.dropdown .item:not(.checked) svg.octicon-check {
|
.issue-sidebar-combo .ui.dropdown .item:not(.checked) .item-check-mark {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
/* ideally, we should move these styles to ".ui.dropdown .menu.flex-items-menu > .item ...", could be done later */
|
/* ideally, we should move these styles to ".ui.dropdown .menu.flex-items-menu > .item ...", could be done later */
|
||||||
@ -62,6 +62,8 @@
|
|||||||
.issue-content-right .dropdown > .menu {
|
.issue-content-right .dropdown > .menu {
|
||||||
max-width: 270px;
|
max-width: 270px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
max-height: 500px;
|
||||||
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
@media (max-width: 767.98px) {
|
||||||
@ -110,10 +112,6 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repository .select-label .desc {
|
|
||||||
padding-left: 23px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* For the secondary pointing menu, respect its own border-bottom */
|
/* For the secondary pointing menu, respect its own border-bottom */
|
||||||
/* style reference: https://semantic-ui.com/collections/menu.html#pointing */
|
/* style reference: https://semantic-ui.com/collections/menu.html#pointing */
|
||||||
.repository .ui.tabs.container .ui.menu:not(.secondary.pointing) {
|
.repository .ui.tabs.container .ui.menu:not(.secondary.pointing) {
|
||||||
|
@ -47,6 +47,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.archived-label-hint {
|
.archived-label-hint {
|
||||||
float: right;
|
position: absolute;
|
||||||
margin: -12px;
|
top: 10px;
|
||||||
|
right: 5px;
|
||||||
}
|
}
|
||||||
|
@ -32,13 +32,13 @@ export function initGlobalDropdown() {
|
|||||||
const $uiDropdowns = fomanticQuery('.ui.dropdown');
|
const $uiDropdowns = fomanticQuery('.ui.dropdown');
|
||||||
|
|
||||||
// do not init "custom" dropdowns, "custom" dropdowns are managed by their own code.
|
// do not init "custom" dropdowns, "custom" dropdowns are managed by their own code.
|
||||||
$uiDropdowns.filter(':not(.custom)').dropdown();
|
$uiDropdowns.filter(':not(.custom)').dropdown({hideDividers: 'empty'});
|
||||||
|
|
||||||
// The "jump" means this dropdown is mainly used for "menu" purpose,
|
// The "jump" means this dropdown is mainly used for "menu" purpose,
|
||||||
// clicking an item will jump to somewhere else or trigger an action/function.
|
// clicking an item will jump to somewhere else or trigger an action/function.
|
||||||
// When a dropdown is used for non-refresh actions with tippy,
|
// When a dropdown is used for non-refresh actions with tippy,
|
||||||
// it must have this "jump" class to hide the tippy when dropdown is closed.
|
// it must have this "jump" class to hide the tippy when dropdown is closed.
|
||||||
$uiDropdowns.filter('.jump').dropdown({
|
$uiDropdowns.filter('.jump').dropdown('setting', {
|
||||||
action: 'hide',
|
action: 'hide',
|
||||||
onShow() {
|
onShow() {
|
||||||
// hide associated tooltip while dropdown is open
|
// hide associated tooltip while dropdown is open
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||||
import {POST} from '../modules/fetch.ts';
|
import {POST} from '../modules/fetch.ts';
|
||||||
import {queryElemChildren, toggleElem} from '../utils/dom.ts';
|
import {queryElemChildren, queryElems, toggleElem} from '../utils/dom.ts';
|
||||||
|
|
||||||
// if there are draft comments, confirm before reloading, to avoid losing comments
|
// if there are draft comments, confirm before reloading, to avoid losing comments
|
||||||
export function issueSidebarReloadConfirmDraftComment() {
|
export function issueSidebarReloadConfirmDraftComment() {
|
||||||
@ -27,20 +27,37 @@ function collectCheckedValues(elDropdown: HTMLElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function initIssueSidebarComboList(container: HTMLElement) {
|
export function initIssueSidebarComboList(container: HTMLElement) {
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
const updateUrl = container.getAttribute('data-update-url');
|
const updateUrl = container.getAttribute('data-update-url');
|
||||||
const elDropdown = container.querySelector<HTMLElement>(':scope > .ui.dropdown');
|
const elDropdown = container.querySelector<HTMLElement>(':scope > .ui.dropdown');
|
||||||
const elList = container.querySelector<HTMLElement>(':scope > .ui.list');
|
const elList = container.querySelector<HTMLElement>(':scope > .ui.list');
|
||||||
const elComboValue = container.querySelector<HTMLInputElement>(':scope > .combo-value');
|
const elComboValue = container.querySelector<HTMLInputElement>(':scope > .combo-value');
|
||||||
const initialValues = collectCheckedValues(elDropdown);
|
let initialValues = collectCheckedValues(elDropdown);
|
||||||
|
|
||||||
elDropdown.addEventListener('click', (e) => {
|
elDropdown.addEventListener('click', (e) => {
|
||||||
const elItem = (e.target as HTMLElement).closest('.item');
|
const elItem = (e.target as HTMLElement).closest('.item');
|
||||||
if (!elItem) return;
|
if (!elItem) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (elItem.getAttribute('data-can-change') !== 'true') return;
|
if (elItem.hasAttribute('data-can-change') && elItem.getAttribute('data-can-change') !== 'true') return;
|
||||||
|
|
||||||
|
if (elItem.matches('.clear-selection')) {
|
||||||
|
queryElems(elDropdown, '.menu > .item', (el) => el.classList.remove('checked'));
|
||||||
|
elComboValue.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scope = elItem.getAttribute('data-scope');
|
||||||
|
if (scope) {
|
||||||
|
// scoped items could only be checked one at a time
|
||||||
|
const elSelected = elDropdown.querySelector<HTMLElement>(`.menu > .item.checked[data-scope="${CSS.escape(scope)}"]`);
|
||||||
|
if (elSelected === elItem) {
|
||||||
elItem.classList.toggle('checked');
|
elItem.classList.toggle('checked');
|
||||||
|
} else {
|
||||||
|
queryElems(elDropdown, `.menu > .item[data-scope="${CSS.escape(scope)}"]`, (el) => el.classList.remove('checked'));
|
||||||
|
elItem.classList.toggle('checked', true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
elItem.classList.toggle('checked');
|
||||||
|
}
|
||||||
elComboValue.value = collectCheckedValues(elDropdown).join(',');
|
elComboValue.value = collectCheckedValues(elDropdown).join(',');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -61,29 +78,28 @@ export function initIssueSidebarComboList(container: HTMLElement) {
|
|||||||
if (changed) issueSidebarReloadConfirmDraftComment();
|
if (changed) issueSidebarReloadConfirmDraftComment();
|
||||||
};
|
};
|
||||||
|
|
||||||
const syncList = (changedValues) => {
|
const syncUiList = (changedValues) => {
|
||||||
const elEmptyTip = elList.querySelector('.item.empty-list');
|
const elEmptyTip = elList.querySelector('.item.empty-list');
|
||||||
queryElemChildren(elList, '.item:not(.empty-list)', (el) => el.remove());
|
queryElemChildren(elList, '.item:not(.empty-list)', (el) => el.remove());
|
||||||
for (const value of changedValues) {
|
for (const value of changedValues) {
|
||||||
const el = elDropdown.querySelector<HTMLElement>(`.menu > .item[data-value="${value}"]`);
|
const el = elDropdown.querySelector<HTMLElement>(`.menu > .item[data-value="${CSS.escape(value)}"]`);
|
||||||
const listItem = el.cloneNode(true) as HTMLElement;
|
const listItem = el.cloneNode(true) as HTMLElement;
|
||||||
listItem.querySelector('svg.octicon-check')?.remove();
|
queryElems(listItem, '.item-check-mark, .item-secondary-info', (el) => el.remove());
|
||||||
elList.append(listItem);
|
elList.append(listItem);
|
||||||
}
|
}
|
||||||
const hasItems = Boolean(elList.querySelector('.item:not(.empty-list)'));
|
const hasItems = Boolean(elList.querySelector('.item:not(.empty-list)'));
|
||||||
toggleElem(elEmptyTip, !hasItems);
|
toggleElem(elEmptyTip, !hasItems);
|
||||||
};
|
};
|
||||||
|
|
||||||
fomanticQuery(elDropdown).dropdown({
|
fomanticQuery(elDropdown).dropdown('setting', {
|
||||||
action: 'nothing', // do not hide the menu if user presses Enter
|
action: 'nothing', // do not hide the menu if user presses Enter
|
||||||
fullTextSearch: 'exact',
|
fullTextSearch: 'exact',
|
||||||
async onHide() {
|
async onHide() {
|
||||||
|
// TODO: support "Esc" to cancel the selection. Use partial page loading to avoid losing inputs.
|
||||||
const changedValues = collectCheckedValues(elDropdown);
|
const changedValues = collectCheckedValues(elDropdown);
|
||||||
if (updateUrl) {
|
syncUiList(changedValues);
|
||||||
await updateToBackend(changedValues); // send requests to backend and reload the page
|
if (updateUrl) await updateToBackend(changedValues);
|
||||||
} else {
|
initialValues = changedValues;
|
||||||
syncList(changedValues); // only update the list in the sidebar
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
27
web_src/js/features/repo-issue-sidebar.md
Normal file
27
web_src/js/features/repo-issue-sidebar.md
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
A sidebar combo (dropdown+list) is like this:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="issue-sidebar-combo" data-update-url="...">
|
||||||
|
<input class="combo-value" name="..." type="hidden" value="...">
|
||||||
|
<div class="ui dropdown">
|
||||||
|
<div class="menu">
|
||||||
|
<div class="item clear-selection">clear</div>
|
||||||
|
<div class="item" data-value="..." data-scope="...">
|
||||||
|
<span class="item-check-mark">...</span>
|
||||||
|
...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ui list">
|
||||||
|
<span class="item empty-list">no item</span>
|
||||||
|
<span class="item">...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
When the selected items change, the `combo-value` input will be updated.
|
||||||
|
If there is `data-update-url`, it also calls backend to attach/detach the changed items.
|
||||||
|
|
||||||
|
Also, the changed items will be syncronized to the `ui list` items.
|
||||||
|
|
||||||
|
The items with the same data-scope only allow one selected at a time.
|
@ -3,7 +3,7 @@ import {POST} from '../modules/fetch.ts';
|
|||||||
import {updateIssuesMeta} from './repo-common.ts';
|
import {updateIssuesMeta} from './repo-common.ts';
|
||||||
import {svg} from '../svg.ts';
|
import {svg} from '../svg.ts';
|
||||||
import {htmlEscape} from 'escape-goat';
|
import {htmlEscape} from 'escape-goat';
|
||||||
import {toggleElem} from '../utils/dom.ts';
|
import {queryElems, toggleElem} from '../utils/dom.ts';
|
||||||
import {initIssueSidebarComboList, issueSidebarReloadConfirmDraftComment} from './repo-issue-sidebar-combolist.ts';
|
import {initIssueSidebarComboList, issueSidebarReloadConfirmDraftComment} from './repo-issue-sidebar-combolist.ts';
|
||||||
|
|
||||||
function initBranchSelector() {
|
function initBranchSelector() {
|
||||||
@ -28,7 +28,7 @@ function initBranchSelector() {
|
|||||||
} else {
|
} else {
|
||||||
// for new issue, only update UI&form, do not send request/reload
|
// for new issue, only update UI&form, do not send request/reload
|
||||||
const selectedHiddenSelector = this.getAttribute('data-id-selector');
|
const selectedHiddenSelector = this.getAttribute('data-id-selector');
|
||||||
document.querySelector(selectedHiddenSelector).value = selectedValue;
|
document.querySelector<HTMLInputElement>(selectedHiddenSelector).value = selectedValue;
|
||||||
elSelectBranch.querySelector('.text-branch-name').textContent = selectedText;
|
elSelectBranch.querySelector('.text-branch-name').textContent = selectedText;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -53,7 +53,7 @@ function initListSubmits(selector, outerSelector) {
|
|||||||
for (const [elementId, item] of itemEntries) {
|
for (const [elementId, item] of itemEntries) {
|
||||||
await updateIssuesMeta(
|
await updateIssuesMeta(
|
||||||
item['update-url'],
|
item['update-url'],
|
||||||
item.action,
|
item['action'],
|
||||||
item['issue-id'],
|
item['issue-id'],
|
||||||
elementId,
|
elementId,
|
||||||
);
|
);
|
||||||
@ -80,14 +80,14 @@ function initListSubmits(selector, outerSelector) {
|
|||||||
if (scope) {
|
if (scope) {
|
||||||
// Enable only clicked item for scoped labels
|
// Enable only clicked item for scoped labels
|
||||||
if (this.getAttribute('data-scope') !== scope) {
|
if (this.getAttribute('data-scope') !== scope) {
|
||||||
return true;
|
return;
|
||||||
}
|
}
|
||||||
if (this !== clickedItem && !this.classList.contains('checked')) {
|
if (this !== clickedItem && !this.classList.contains('checked')) {
|
||||||
return true;
|
return;
|
||||||
}
|
}
|
||||||
} else if (this !== clickedItem) {
|
} else if (this !== clickedItem) {
|
||||||
// Toggle for other labels
|
// Toggle for other labels
|
||||||
return true;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.classList.contains('checked')) {
|
if (this.classList.contains('checked')) {
|
||||||
@ -258,13 +258,13 @@ export function initRepoIssueSidebar() {
|
|||||||
initRepoIssueDue();
|
initRepoIssueDue();
|
||||||
|
|
||||||
// TODO: refactor the legacy initListSubmits&selectItem to initIssueSidebarComboList
|
// TODO: refactor the legacy initListSubmits&selectItem to initIssueSidebarComboList
|
||||||
initListSubmits('select-label', 'labels');
|
|
||||||
initListSubmits('select-assignees', 'assignees');
|
initListSubmits('select-assignees', 'assignees');
|
||||||
initListSubmits('select-assignees-modify', 'assignees');
|
initListSubmits('select-assignees-modify', 'assignees');
|
||||||
selectItem('.select-project', '#project_id');
|
|
||||||
selectItem('.select-milestone', '#milestone_id');
|
|
||||||
selectItem('.select-assignee', '#assignee_id');
|
selectItem('.select-assignee', '#assignee_id');
|
||||||
|
|
||||||
// init the combo list: a dropdown for selecting reviewers, and a list for showing selected reviewers and related actions
|
selectItem('.select-project', '#project_id');
|
||||||
initIssueSidebarComboList(document.querySelector('.issue-sidebar-combo[data-sidebar-combo-for="reviewers"]'));
|
selectItem('.select-milestone', '#milestone_id');
|
||||||
|
|
||||||
|
// init the combo list: a dropdown for selecting items, and a list for showing selected items and related actions
|
||||||
|
queryElems<HTMLElement>(document, '.issue-sidebar-combo', (el) => initIssueSidebarComboList(el));
|
||||||
}
|
}
|
||||||
|
@ -98,6 +98,7 @@ export function initRepoIssueSidebarList() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// FIXME: it is wrong place to init ".ui.dropdown.label-filter"
|
||||||
$('.menu .ui.dropdown.label-filter').on('keydown', (e) => {
|
$('.menu .ui.dropdown.label-filter').on('keydown', (e) => {
|
||||||
if (e.altKey && e.key === 'Enter') {
|
if (e.altKey && e.key === 'Enter') {
|
||||||
const selectedItem = document.querySelector('.menu .ui.dropdown.label-filter .menu .item.selected');
|
const selectedItem = document.querySelector('.menu .ui.dropdown.label-filter .menu .item.selected');
|
||||||
@ -106,7 +107,6 @@ export function initRepoIssueSidebarList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$('.ui.dropdown.label-filter, .ui.dropdown.select-label').dropdown('setting', {'hideDividers': 'empty'}).dropdown('refreshItems');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initRepoIssueCommentDelete() {
|
export function initRepoIssueCommentDelete() {
|
||||||
@ -652,19 +652,6 @@ function initIssueTemplateCommentEditors($commentForm) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function used to show and hide archived label on issue/pr
|
|
||||||
// page in the sidebar where we select the labels
|
|
||||||
// If we have any archived label tagged to issue and pr. We will show that
|
|
||||||
// archived label with checked classed otherwise we will hide it
|
|
||||||
// with the help of this function.
|
|
||||||
// This function runs globally.
|
|
||||||
export function initArchivedLabelHandler() {
|
|
||||||
if (!document.querySelector('.archived-label-hint')) return;
|
|
||||||
for (const label of document.querySelectorAll('[data-is-archived]')) {
|
|
||||||
toggleElem(label, label.classList.contains('checked'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function initRepoCommentFormAndSidebar() {
|
export function initRepoCommentFormAndSidebar() {
|
||||||
const $commentForm = $('.comment.form');
|
const $commentForm = $('.comment.form');
|
||||||
if (!$commentForm.length) return;
|
if (!$commentForm.length) return;
|
||||||
|
@ -30,7 +30,7 @@ import {
|
|||||||
initRepoIssueWipTitle,
|
initRepoIssueWipTitle,
|
||||||
initRepoPullRequestMergeInstruction,
|
initRepoPullRequestMergeInstruction,
|
||||||
initRepoPullRequestAllowMaintainerEdit,
|
initRepoPullRequestAllowMaintainerEdit,
|
||||||
initRepoPullRequestReview, initRepoIssueSidebarList, initArchivedLabelHandler,
|
initRepoPullRequestReview, initRepoIssueSidebarList,
|
||||||
} from './features/repo-issue.ts';
|
} from './features/repo-issue.ts';
|
||||||
import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts';
|
import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts';
|
||||||
import {initRepoTopicBar} from './features/repo-home.ts';
|
import {initRepoTopicBar} from './features/repo-home.ts';
|
||||||
@ -182,7 +182,6 @@ onDomReady(() => {
|
|||||||
initRepoIssueContentHistory,
|
initRepoIssueContentHistory,
|
||||||
initRepoIssueList,
|
initRepoIssueList,
|
||||||
initRepoIssueSidebarList,
|
initRepoIssueSidebarList,
|
||||||
initArchivedLabelHandler,
|
|
||||||
initRepoIssueReferenceRepositorySearch,
|
initRepoIssueReferenceRepositorySearch,
|
||||||
initRepoIssueTimeTracking,
|
initRepoIssueTimeTracking,
|
||||||
initRepoIssueWipTitle,
|
initRepoIssueWipTitle,
|
||||||
|
@ -3,7 +3,7 @@ import type {Promisable} from 'type-fest';
|
|||||||
import type $ from 'jquery';
|
import type $ from 'jquery';
|
||||||
|
|
||||||
type ElementArg = Element | string | NodeListOf<Element> | Array<Element> | ReturnType<typeof $>;
|
type ElementArg = Element | string | NodeListOf<Element> | Array<Element> | ReturnType<typeof $>;
|
||||||
type ElementsCallback = (el: Element) => Promisable<any>;
|
type ElementsCallback<T extends Element> = (el: T) => Promisable<any>;
|
||||||
type ElementsCallbackWithArgs = (el: Element, ...args: any[]) => Promisable<any>;
|
type ElementsCallbackWithArgs = (el: Element, ...args: any[]) => Promisable<any>;
|
||||||
type ArrayLikeIterable<T> = ArrayLike<T> & Iterable<T>; // for NodeListOf and Array
|
type ArrayLikeIterable<T> = ArrayLike<T> & Iterable<T>; // for NodeListOf and Array
|
||||||
|
|
||||||
@ -58,7 +58,7 @@ export function isElemHidden(el: ElementArg) {
|
|||||||
return res[0];
|
return res[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyElemsCallback<T extends Element>(elems: ArrayLikeIterable<T>, fn?: ElementsCallback): ArrayLikeIterable<T> {
|
function applyElemsCallback<T extends Element>(elems: ArrayLikeIterable<T>, fn?: ElementsCallback<T>): ArrayLikeIterable<T> {
|
||||||
if (fn) {
|
if (fn) {
|
||||||
for (const el of elems) {
|
for (const el of elems) {
|
||||||
fn(el);
|
fn(el);
|
||||||
@ -67,7 +67,7 @@ function applyElemsCallback<T extends Element>(elems: ArrayLikeIterable<T>, fn?:
|
|||||||
return elems;
|
return elems;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function queryElemSiblings<T extends Element>(el: Element, selector = '*', fn?: ElementsCallback): ArrayLikeIterable<T> {
|
export function queryElemSiblings<T extends Element>(el: Element, selector = '*', fn?: ElementsCallback<T>): ArrayLikeIterable<T> {
|
||||||
const elems = Array.from(el.parentNode.children) as T[];
|
const elems = Array.from(el.parentNode.children) as T[];
|
||||||
return applyElemsCallback<T>(elems.filter((child: Element) => {
|
return applyElemsCallback<T>(elems.filter((child: Element) => {
|
||||||
return child !== el && child.matches(selector);
|
return child !== el && child.matches(selector);
|
||||||
@ -75,13 +75,13 @@ export function queryElemSiblings<T extends Element>(el: Element, selector = '*'
|
|||||||
}
|
}
|
||||||
|
|
||||||
// it works like jQuery.children: only the direct children are selected
|
// it works like jQuery.children: only the direct children are selected
|
||||||
export function queryElemChildren<T extends Element>(parent: Element | ParentNode, selector = '*', fn?: ElementsCallback): ArrayLikeIterable<T> {
|
export function queryElemChildren<T extends Element>(parent: Element | ParentNode, selector = '*', fn?: ElementsCallback<T>): ArrayLikeIterable<T> {
|
||||||
return applyElemsCallback<T>(parent.querySelectorAll(`:scope > ${selector}`), fn);
|
return applyElemsCallback<T>(parent.querySelectorAll(`:scope > ${selector}`), fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
// it works like parent.querySelectorAll: all descendants are selected
|
// it works like parent.querySelectorAll: all descendants are selected
|
||||||
// in the future, all "queryElems(document, ...)" should be refactored to use a more specific parent
|
// in the future, all "queryElems(document, ...)" should be refactored to use a more specific parent
|
||||||
export function queryElems<T extends Element>(parent: Element | ParentNode, selector: string, fn?: ElementsCallback): ArrayLikeIterable<T> {
|
export function queryElems<T extends Element>(parent: Element | ParentNode, selector: string, fn?: ElementsCallback<T>): ArrayLikeIterable<T> {
|
||||||
return applyElemsCallback<T>(parent.querySelectorAll(selector), fn);
|
return applyElemsCallback<T>(parent.querySelectorAll(selector), fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user