mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-27 05:55:21 +08: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 { | ||||
| 			// 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() { | ||||
| 				return | ||||
| 			} | ||||
| @ -796,6 +800,10 @@ func CompareDiff(ctx *context.Context) { | ||||
| 			if ctx.Written() { | ||||
| 				return | ||||
| 			} | ||||
| 			_, templateErrs := setTemplateIfExists(ctx, pullRequestTemplateKey, pullRequestTemplateCandidates, labelsData) | ||||
| 			if len(templateErrs) > 0 { | ||||
| 				ctx.Flash.Warning(renderErrorOfTemplates(ctx, templateErrs), true) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	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["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 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 | ||||
| } | ||||
| 
 | ||||
| // RetrieveRepoMetas find all the meta information of a repository
 | ||||
| func RetrieveRepoMetas(ctx *context.Context, repo *repo_model.Repository, isPull bool) []*issues_model.Label { | ||||
| 	if !ctx.Repo.CanWriteIssuesOrPulls(isPull) { | ||||
| 		return nil | ||||
| type issueSidebarLabelsData struct { | ||||
| 	Repository       *repo_model.Repository | ||||
| 	RepoLink         string | ||||
| 	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{}) | ||||
| 	if err != nil { | ||||
| 		ctx.ServerError("GetLabelsByRepoID", err) | ||||
| 		return nil | ||||
| 	} | ||||
| 	ctx.Data["Labels"] = labels | ||||
| 	labelsData.RepoLabels = labels | ||||
| 
 | ||||
| 	if repo.Owner.IsOrganization() { | ||||
| 		orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}) | ||||
| 		if err != 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 | ||||
| 		labels = append(labels, orgLabels...) | ||||
| // retrieveRepoMetasForIssueWriter finds some the meta information of a repository for an issue/pr writer
 | ||||
| func retrieveRepoMetasForIssueWriter(ctx *context.Context, repo *repo_model.Repository, isPull bool) { | ||||
| 	if !ctx.Repo.CanWriteIssuesOrPulls(isPull) { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	RetrieveRepoMilestonesAndAssignees(ctx, repo) | ||||
| 	if ctx.Written() { | ||||
| 		return nil | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	retrieveProjects(ctx, repo) | ||||
| 	if ctx.Written() { | ||||
| 		return nil | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	PrepareBranchList(ctx) | ||||
| 	if ctx.Written() { | ||||
| 		return nil | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// Contains true if the user can create issue dependencies
 | ||||
| 	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.
 | ||||
| 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) | ||||
| 	if err != nil { | ||||
| 		return false, nil | ||||
| @ -951,26 +1012,9 @@ func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles | ||||
| 			ctx.Data["Fields"] = template.Fields | ||||
| 			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 { | ||||
| 				for _, repoLabel := range repoLabels { | ||||
| 					if strings.EqualFold(repoLabel.Name, metaLabel) { | ||||
| 						repoLabel.IsChecked = true | ||||
| 						labelIDs = append(labelIDs, strconv.FormatInt(repoLabel.ID, 10)) | ||||
| 						break | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		labelsData.SetSelectedLabelNames(template.Labels) | ||||
| 
 | ||||
| 		selectedAssigneeIDs := make([]int64, 0, len(template.Assignees)) | ||||
| 		selectedAssigneeIDStrings := make([]string, 0, len(template.Assignees)) | ||||
| 		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>
 | ||||
| 			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["assignee_ids"] = strings.Join(selectedAssigneeIDStrings, ",") | ||||
| 		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) | ||||
| 	if err != nil { | ||||
| 		ctx.ServerError("GetTagNamesByRepoID", err) | ||||
| @ -1052,7 +1101,7 @@ func NewIssue(ctx *context.Context) { | ||||
| 	ctx.Data["Tags"] = tags | ||||
| 
 | ||||
| 	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 { | ||||
| 		ret.TemplateErrors[k] = v | ||||
| 	} | ||||
| @ -1161,34 +1210,25 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull | ||||
| 		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() { | ||||
| 		return ret | ||||
| 	} | ||||
| 
 | ||||
| 	var labelIDs []int64 | ||||
| 	hasSelected := false | ||||
| 	// Check labels.
 | ||||
| 	if len(form.LabelIDs) > 0 { | ||||
| 		labelIDs, err = base.StringsToInt64s(strings.Split(form.LabelIDs, ",")) | ||||
| 		if err != nil { | ||||
| 			return ret | ||||
| 		} | ||||
| 		labelIDMark := make(container.Set[int64]) | ||||
| 		labelIDMark.AddMultiple(labelIDs...) | ||||
| 
 | ||||
| 		for i := range labels { | ||||
| 			if labelIDMark.Contains(labels[i].ID) { | ||||
| 				labels[i].IsChecked = true | ||||
| 				hasSelected = true | ||||
| 			} | ||||
| 		} | ||||
| 		labelsData.SetSelectedLabelIDs(labelIDs) | ||||
| 	} | ||||
| 
 | ||||
| 	ctx.Data["Labels"] = labels | ||||
| 	ctx.Data["HasSelectedLabel"] = hasSelected | ||||
| 	ctx.Data["label_ids"] = form.LabelIDs | ||||
| 
 | ||||
| 	// Check milestone.
 | ||||
| 	milestoneID := form.MilestoneID | ||||
| 	if milestoneID > 0 { | ||||
| @ -1579,38 +1619,15 @@ func ViewIssue(ctx *context.Context) { | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// Metas.
 | ||||
| 	// Check labels.
 | ||||
| 	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) | ||||
| 	retrieveRepoMetasForIssueWriter(ctx, repo, issue.IsPull) | ||||
| 	if ctx.Written() { | ||||
| 		return | ||||
| 	} | ||||
| 	ctx.Data["Labels"] = labels | ||||
| 
 | ||||
| 	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 | ||||
| 		} | ||||
| 		ctx.Data["OrgLabels"] = orgLabels | ||||
| 
 | ||||
| 		labels = append(labels, orgLabels...) | ||||
| 	labelsData := retrieveRepoLabels(ctx, repo, issue.ID, issue.IsPull) | ||||
| 	if ctx.Written() { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	hasSelected := false | ||||
| 	for i := range labels { | ||||
| 		if labelIDMark.Contains(labels[i].ID) { | ||||
| 			labels[i].IsChecked = true | ||||
| 			hasSelected = true | ||||
| 		} | ||||
| 	} | ||||
| 	ctx.Data["HasSelectedLabel"] = hasSelected | ||||
| 	labelsData.SetSelectedLabels(issue.Labels) | ||||
| 
 | ||||
| 	// Check milestone and assignee.
 | ||||
| 	if ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) { | ||||
|  | ||||
| @ -53,11 +53,11 @@ func InitializeLabels(ctx *context.Context) { | ||||
| 	ctx.Redirect(ctx.Repo.RepoLink + "/labels") | ||||
| } | ||||
| 
 | ||||
| // RetrieveLabels find all the labels of a repository and organization
 | ||||
| func RetrieveLabels(ctx *context.Context) { | ||||
| // RetrieveLabelsForList find all the labels of a repository and organization, it is only used by "/labels" page to list all labels
 | ||||
| func RetrieveLabelsForList(ctx *context.Context) { | ||||
| 	labels, err := issues_model.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, ctx.FormString("sort"), db.ListOptions{}) | ||||
| 	if err != nil { | ||||
| 		ctx.ServerError("RetrieveLabels.GetLabels", err) | ||||
| 		ctx.ServerError("RetrieveLabelsForList.GetLabels", err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
|  | ||||
| @ -62,7 +62,7 @@ func TestRetrieveLabels(t *testing.T) { | ||||
| 		contexttest.LoadUser(t, ctx, 2) | ||||
| 		contexttest.LoadRepo(t, ctx, testCase.RepoID) | ||||
| 		ctx.Req.Form.Set("sort", testCase.Sort) | ||||
| 		RetrieveLabels(ctx) | ||||
| 		RetrieveLabelsForList(ctx) | ||||
| 		assert.False(t, ctx.Written()) | ||||
| 		labels, ok := ctx.Data["Labels"].([]*issues_model.Label) | ||||
| 		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("/pulls/posters", repo.PullPosters) | ||||
| 		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("/milestone/{id}", context.RepoRef(), repo.MilestoneIssuesAndPulls) | ||||
| 		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}}"> | ||||
| 						{{range .Fields}} | ||||
| 							{{if eq .Type "input"}} | ||||
| 								{{template "repo/issue/fields/input" "item" .}} | ||||
| 								{{template "repo/issue/fields/input" dict "item" .}} | ||||
| 							{{else if eq .Type "markdown"}} | ||||
| 								{{template "repo/issue/fields/markdown" "item" .}} | ||||
| 								{{template "repo/issue/fields/markdown" dict "item" .}} | ||||
| 							{{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"}} | ||||
| 								{{template "repo/issue/fields/dropdown" "item" .}} | ||||
| 								{{template "repo/issue/fields/dropdown" dict "item" .}} | ||||
| 							{{else if eq .Type "checkboxes"}} | ||||
| 								{{template "repo/issue/fields/checkboxes" "item" .}} | ||||
| 								{{template "repo/issue/fields/checkboxes" dict "item" .}} | ||||
| 							{{end}} | ||||
| 						{{end}} | ||||
| 					{{else}} | ||||
| @ -49,13 +49,11 @@ | ||||
| 	<div class="issue-content-right ui segment"> | ||||
| 		{{template "repo/issue/branch_selector_field" $}} | ||||
| 		{{if .PageIsComparePull}} | ||||
| 			{{template "repo/issue/sidebar/reviewer_list" dict "IssueSidebarReviewersData" $.IssueSidebarReviewersData}} | ||||
| 			{{template "repo/issue/sidebar/reviewer_list" $.IssueSidebarReviewersData}} | ||||
| 			<div class="divider"></div> | ||||
| 		{{end}} | ||||
| 
 | ||||
| 		<input id="label_ids" name="label_ids" type="hidden" value="{{.label_ids}}"> | ||||
| 		{{template "repo/issue/labels/labels_selector_field" .}} | ||||
| 		{{template "repo/issue/labels/labels_sidebar" dict "root" $}} | ||||
| 		{{template "repo/issue/sidebar/label_list" $.IssueSidebarLabelsData}} | ||||
| 
 | ||||
| 		<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}} | ||||
| <div class="issue-sidebar-combo" data-sidebar-combo-for="reviewers" | ||||
| 		{{if $data.IssueID}}data-update-url="{{$data.RepoLink}}/issues/request_review?issue_ids={{$data.IssueID}}"{{end}} | ||||
| > | ||||
| <div class="issue-sidebar-combo" {{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 */}} | ||||
| 	<div class="ui dropdown custom {{if or (not $hasCandidates) (not $data.CanChooseReviewer)}}disabled{{end}}"> | ||||
| 		<a class="muted text"> | ||||
| 	<div class="ui dropdown {{if or (not $hasCandidates) (not $data.CanChooseReviewer)}}disabled{{end}}"> | ||||
| 		<a class="text muted"> | ||||
| 			<strong>{{ctx.Locale.Tr "repo.issues.review.reviewers"}}</strong> {{if and $data.CanChooseReviewer}}{{svg "octicon-gear"}}{{end}} | ||||
| 		</a> | ||||
| 		<div class="menu flex-items-menu"> | ||||
| @ -19,7 +17,8 @@ | ||||
| 				{{if .User}} | ||||
| 					<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}}> | ||||
| 						{{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> | ||||
| 				{{end}} | ||||
| 			{{end}} | ||||
| @ -29,7 +28,8 @@ | ||||
| 					{{if .Team}} | ||||
| 						<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}}> | ||||
| 							{{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> | ||||
| 					{{end}} | ||||
| 				{{end}} | ||||
|  | ||||
| @ -2,13 +2,12 @@ | ||||
| 	{{template "repo/issue/branch_selector_field" $}} | ||||
| 
 | ||||
| 	{{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" $}} | ||||
| 		<div class="divider"></div> | ||||
| 	{{end}} | ||||
| 
 | ||||
| 	{{template "repo/issue/labels/labels_selector_field" $}} | ||||
| 	{{template "repo/issue/labels/labels_sidebar" dict "root" $}} | ||||
| 	{{template "repo/issue/sidebar/label_list" $.IssueSidebarLabelsData}} | ||||
| 
 | ||||
| 	{{template "repo/issue/sidebar/milestone_list" $}} | ||||
| 	{{template "repo/issue/sidebar/project_list" $}} | ||||
|  | ||||
| @ -50,7 +50,7 @@ | ||||
|   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; | ||||
| } | ||||
| /* 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 { | ||||
|   max-width: 270px; | ||||
|   min-width: 0; | ||||
|   max-height: 500px; | ||||
|   overflow-x: auto; | ||||
| } | ||||
| 
 | ||||
| @media (max-width: 767.98px) { | ||||
| @ -110,10 +112,6 @@ | ||||
|   left: 0; | ||||
| } | ||||
| 
 | ||||
| .repository .select-label .desc { | ||||
|   padding-left: 23px; | ||||
| } | ||||
| 
 | ||||
| /* For the secondary pointing menu, respect its own border-bottom */ | ||||
| /* style reference: https://semantic-ui.com/collections/menu.html#pointing */ | ||||
| .repository .ui.tabs.container .ui.menu:not(.secondary.pointing) { | ||||
|  | ||||
| @ -47,6 +47,7 @@ | ||||
| } | ||||
| 
 | ||||
| .archived-label-hint { | ||||
|   float: right; | ||||
|   margin: -12px; | ||||
|   position: absolute; | ||||
|   top: 10px; | ||||
|   right: 5px; | ||||
| } | ||||
|  | ||||
| @ -32,13 +32,13 @@ export function initGlobalDropdown() { | ||||
|   const $uiDropdowns = fomanticQuery('.ui.dropdown'); | ||||
| 
 | ||||
|   // 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,
 | ||||
|   // clicking an item will jump to somewhere else or trigger an action/function.
 | ||||
|   // 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.
 | ||||
|   $uiDropdowns.filter('.jump').dropdown({ | ||||
|   $uiDropdowns.filter('.jump').dropdown('setting', { | ||||
|     action: 'hide', | ||||
|     onShow() { | ||||
|       // hide associated tooltip while dropdown is open
 | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| import {fomanticQuery} from '../modules/fomantic/base.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
 | ||||
| export function issueSidebarReloadConfirmDraftComment() { | ||||
| @ -27,20 +27,37 @@ function collectCheckedValues(elDropdown: HTMLElement) { | ||||
| } | ||||
| 
 | ||||
| export function initIssueSidebarComboList(container: HTMLElement) { | ||||
|   if (!container) return; | ||||
| 
 | ||||
|   const updateUrl = container.getAttribute('data-update-url'); | ||||
|   const elDropdown = container.querySelector<HTMLElement>(':scope > .ui.dropdown'); | ||||
|   const elList = container.querySelector<HTMLElement>(':scope > .ui.list'); | ||||
|   const elComboValue = container.querySelector<HTMLInputElement>(':scope > .combo-value'); | ||||
|   const initialValues = collectCheckedValues(elDropdown); | ||||
|   let initialValues = collectCheckedValues(elDropdown); | ||||
| 
 | ||||
|   elDropdown.addEventListener('click', (e) => { | ||||
|     const elItem = (e.target as HTMLElement).closest('.item'); | ||||
|     if (!elItem) return; | ||||
|     e.preventDefault(); | ||||
|     if (elItem.getAttribute('data-can-change') !== 'true') return; | ||||
|     elItem.classList.toggle('checked'); | ||||
|     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'); | ||||
|       } 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(','); | ||||
|   }); | ||||
| 
 | ||||
| @ -61,29 +78,28 @@ export function initIssueSidebarComboList(container: HTMLElement) { | ||||
|     if (changed) issueSidebarReloadConfirmDraftComment(); | ||||
|   }; | ||||
| 
 | ||||
|   const syncList = (changedValues) => { | ||||
|   const syncUiList = (changedValues) => { | ||||
|     const elEmptyTip = elList.querySelector('.item.empty-list'); | ||||
|     queryElemChildren(elList, '.item:not(.empty-list)', (el) => el.remove()); | ||||
|     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; | ||||
|       listItem.querySelector('svg.octicon-check')?.remove(); | ||||
|       queryElems(listItem, '.item-check-mark, .item-secondary-info', (el) => el.remove()); | ||||
|       elList.append(listItem); | ||||
|     } | ||||
|     const hasItems = Boolean(elList.querySelector('.item:not(.empty-list)')); | ||||
|     toggleElem(elEmptyTip, !hasItems); | ||||
|   }; | ||||
| 
 | ||||
|   fomanticQuery(elDropdown).dropdown({ | ||||
|   fomanticQuery(elDropdown).dropdown('setting', { | ||||
|     action: 'nothing', // do not hide the menu if user presses Enter
 | ||||
|     fullTextSearch: 'exact', | ||||
|     async onHide() { | ||||
|       // TODO: support "Esc" to cancel the selection. Use partial page loading to avoid losing inputs.
 | ||||
|       const changedValues = collectCheckedValues(elDropdown); | ||||
|       if (updateUrl) { | ||||
|         await updateToBackend(changedValues); // send requests to backend and reload the page
 | ||||
|       } else { | ||||
|         syncList(changedValues); // only update the list in the sidebar
 | ||||
|       } | ||||
|       syncUiList(changedValues); | ||||
|       if (updateUrl) await updateToBackend(changedValues); | ||||
|       initialValues = changedValues; | ||||
|     }, | ||||
|   }); | ||||
| } | ||||
|  | ||||
							
								
								
									
										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 {svg} from '../svg.ts'; | ||||
| 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'; | ||||
| 
 | ||||
| function initBranchSelector() { | ||||
| @ -28,7 +28,7 @@ function initBranchSelector() { | ||||
|     } else { | ||||
|       // for new issue, only update UI&form, do not send request/reload
 | ||||
|       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; | ||||
|     } | ||||
|   }); | ||||
| @ -53,7 +53,7 @@ function initListSubmits(selector, outerSelector) { | ||||
|         for (const [elementId, item] of itemEntries) { | ||||
|           await updateIssuesMeta( | ||||
|             item['update-url'], | ||||
|             item.action, | ||||
|             item['action'], | ||||
|             item['issue-id'], | ||||
|             elementId, | ||||
|           ); | ||||
| @ -80,14 +80,14 @@ function initListSubmits(selector, outerSelector) { | ||||
|       if (scope) { | ||||
|         // Enable only clicked item for scoped labels
 | ||||
|         if (this.getAttribute('data-scope') !== scope) { | ||||
|           return true; | ||||
|           return; | ||||
|         } | ||||
|         if (this !== clickedItem && !this.classList.contains('checked')) { | ||||
|           return true; | ||||
|           return; | ||||
|         } | ||||
|       } else if (this !== clickedItem) { | ||||
|         // Toggle for other labels
 | ||||
|         return true; | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       if (this.classList.contains('checked')) { | ||||
| @ -258,13 +258,13 @@ export function initRepoIssueSidebar() { | ||||
|   initRepoIssueDue(); | ||||
| 
 | ||||
|   // TODO: refactor the legacy initListSubmits&selectItem to initIssueSidebarComboList
 | ||||
|   initListSubmits('select-label', 'labels'); | ||||
|   initListSubmits('select-assignees', 'assignees'); | ||||
|   initListSubmits('select-assignees-modify', 'assignees'); | ||||
|   selectItem('.select-project', '#project_id'); | ||||
|   selectItem('.select-milestone', '#milestone_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
 | ||||
|   initIssueSidebarComboList(document.querySelector('.issue-sidebar-combo[data-sidebar-combo-for="reviewers"]')); | ||||
|   selectItem('.select-project', '#project_id'); | ||||
|   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) => { | ||||
|     if (e.altKey && e.key === 'Enter') { | ||||
|       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() { | ||||
| @ -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() { | ||||
|   const $commentForm = $('.comment.form'); | ||||
|   if (!$commentForm.length) return; | ||||
|  | ||||
| @ -30,7 +30,7 @@ import { | ||||
|   initRepoIssueWipTitle, | ||||
|   initRepoPullRequestMergeInstruction, | ||||
|   initRepoPullRequestAllowMaintainerEdit, | ||||
|   initRepoPullRequestReview, initRepoIssueSidebarList, initArchivedLabelHandler, | ||||
|   initRepoPullRequestReview, initRepoIssueSidebarList, | ||||
| } from './features/repo-issue.ts'; | ||||
| import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts'; | ||||
| import {initRepoTopicBar} from './features/repo-home.ts'; | ||||
| @ -182,7 +182,6 @@ onDomReady(() => { | ||||
|     initRepoIssueContentHistory, | ||||
|     initRepoIssueList, | ||||
|     initRepoIssueSidebarList, | ||||
|     initArchivedLabelHandler, | ||||
|     initRepoIssueReferenceRepositorySearch, | ||||
|     initRepoIssueTimeTracking, | ||||
|     initRepoIssueWipTitle, | ||||
|  | ||||
| @ -3,7 +3,7 @@ import type {Promisable} from 'type-fest'; | ||||
| import type $ from 'jquery'; | ||||
| 
 | ||||
| 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 ArrayLikeIterable<T> = ArrayLike<T> & Iterable<T>; // for NodeListOf and Array
 | ||||
| 
 | ||||
| @ -58,7 +58,7 @@ export function isElemHidden(el: ElementArg) { | ||||
|   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) { | ||||
|     for (const el of elems) { | ||||
|       fn(el); | ||||
| @ -67,7 +67,7 @@ function applyElemsCallback<T extends Element>(elems: ArrayLikeIterable<T>, fn?: | ||||
|   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[]; | ||||
|   return applyElemsCallback<T>(elems.filter((child: Element) => { | ||||
|     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
 | ||||
| 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); | ||||
| } | ||||
| 
 | ||||
| // it works like parent.querySelectorAll: all descendants are selected
 | ||||
| // 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); | ||||
| } | ||||
| 
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 wxiaoguang
						wxiaoguang