Refactor sidebar label selector (#32460)

Introduce `issueSidebarLabelsData` to handle all sidebar labels related data.
This commit is contained in:
wxiaoguang 2024-11-10 16:26:42 +08:00 committed by GitHub
parent b55a31eb6a
commit 58c634b854
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 276 additions and 233 deletions

View File

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

View File

@ -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,34 +1210,25 @@ 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
if milestoneID > 0 { if milestoneID > 0 {
@ -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() { return
orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
if err != nil {
ctx.ServerError("GetLabelsByOrgID", err)
return
}
ctx.Data["OrgLabels"] = orgLabels
labels = append(labels, orgLabels...)
} }
labelsData.SetSelectedLabels(issue.Labels)
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) {

View File

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

View File

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

View File

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

View File

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

View File

@ -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>&nbsp;&nbsp;{{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>&nbsp;&nbsp;{{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>

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

@ -47,6 +47,7 @@
} }
.archived-label-hint { .archived-label-hint {
float: right; position: absolute;
margin: -12px; top: 10px;
right: 5px;
} }

View File

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

View File

@ -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;
elItem.classList.toggle('checked');
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');
} 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
}
}, },
}); });
} }

View 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.

View File

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

View File

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

View File

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

View File

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