Skip to content

Commit b71ae6f

Browse files
committed
Merge remote-tracking branch 'giteaofficial/main'
* giteaofficial/main: Refactor issue label selection (go-gitea#31497) Refactor dropzone (go-gitea#31482) [skip ci] Updated translations via Crowdin Optimization of labels handling in issue_search (go-gitea#26460) use correct l10n string (go-gitea#31487) Fix overflow menu flickering on mobile (go-gitea#31484)
2 parents 77bab03 + 00fc29a commit b71ae6f

File tree

14 files changed

+252
-202
lines changed

14 files changed

+252
-202
lines changed

models/issues/issue_search.go

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ package issues
66
import (
77
"context"
88
"fmt"
9+
"strconv"
910
"strings"
1011

1112
"code.gitea.io/gitea/models/db"
1213
"code.gitea.io/gitea/models/organization"
1314
repo_model "code.gitea.io/gitea/models/repo"
1415
"code.gitea.io/gitea/models/unit"
1516
user_model "code.gitea.io/gitea/models/user"
17+
"code.gitea.io/gitea/modules/container"
1618
"code.gitea.io/gitea/modules/optional"
1719

1820
"xorm.io/builder"
@@ -116,14 +118,30 @@ func applyLabelsCondition(sess *xorm.Session, opts *IssuesOptions) {
116118
if opts.LabelIDs[0] == 0 {
117119
sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_label)")
118120
} else {
119-
for i, labelID := range opts.LabelIDs {
121+
// deduplicate the label IDs for inclusion and exclusion
122+
includedLabelIDs := make(container.Set[int64])
123+
excludedLabelIDs := make(container.Set[int64])
124+
for _, labelID := range opts.LabelIDs {
120125
if labelID > 0 {
121-
sess.Join("INNER", fmt.Sprintf("issue_label il%d", i),
122-
fmt.Sprintf("issue.id = il%[1]d.issue_id AND il%[1]d.label_id = %[2]d", i, labelID))
126+
includedLabelIDs.Add(labelID)
123127
} else if labelID < 0 { // 0 is not supported here, so just ignore it
124-
sess.Where("issue.id not in (select issue_id from issue_label where label_id = ?)", -labelID)
128+
excludedLabelIDs.Add(-labelID)
125129
}
126130
}
131+
// ... and use them in a subquery of the form :
132+
// where (select count(*) from issue_label where issue_id=issue.id and label_id in (2, 4, 6)) = 3
133+
// This equality is guaranteed thanks to unique index (issue_id,label_id) on table issue_label.
134+
if len(includedLabelIDs) > 0 {
135+
subQuery := builder.Select("count(*)").From("issue_label").Where(builder.Expr("issue_id = issue.id")).
136+
And(builder.In("label_id", includedLabelIDs.Values()))
137+
sess.Where(builder.Eq{strconv.Itoa(len(includedLabelIDs)): subQuery})
138+
}
139+
// or (select count(*)...) = 0 for excluded labels
140+
if len(excludedLabelIDs) > 0 {
141+
subQuery := builder.Select("count(*)").From("issue_label").Where(builder.Expr("issue_id = issue.id")).
142+
And(builder.In("label_id", excludedLabelIDs.Values()))
143+
sess.Where(builder.Eq{"0": subQuery})
144+
}
127145
}
128146
}
129147

models/issues/label.go

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ package issues
77
import (
88
"context"
99
"fmt"
10+
"slices"
1011
"strconv"
1112
"strings"
1213

1314
"code.gitea.io/gitea/models/db"
15+
"code.gitea.io/gitea/modules/container"
1416
"code.gitea.io/gitea/modules/label"
1517
"code.gitea.io/gitea/modules/optional"
1618
"code.gitea.io/gitea/modules/timeutil"
@@ -142,28 +144,33 @@ func (l *Label) CalOpenOrgIssues(ctx context.Context, repoID, labelID int64) {
142144

143145
// LoadSelectedLabelsAfterClick calculates the set of selected labels when a label is clicked
144146
func (l *Label) LoadSelectedLabelsAfterClick(currentSelectedLabels []int64, currentSelectedExclusiveScopes []string) {
145-
var labelQuerySlice []string
147+
labelQueryParams := container.Set[string]{}
146148
labelSelected := false
147-
labelID := strconv.FormatInt(l.ID, 10)
148-
labelScope := l.ExclusiveScope()
149-
for i, s := range currentSelectedLabels {
150-
if s == l.ID {
149+
exclusiveScope := l.ExclusiveScope()
150+
for i, curSel := range currentSelectedLabels {
151+
if curSel == l.ID {
151152
labelSelected = true
152-
} else if -s == l.ID {
153+
} else if -curSel == l.ID {
153154
labelSelected = true
154155
l.IsExcluded = true
155-
} else if s != 0 {
156+
} else if curSel != 0 {
156157
// Exclude other labels in the same scope from selection
157-
if s < 0 || labelScope == "" || labelScope != currentSelectedExclusiveScopes[i] {
158-
labelQuerySlice = append(labelQuerySlice, strconv.FormatInt(s, 10))
158+
if curSel < 0 || exclusiveScope == "" || exclusiveScope != currentSelectedExclusiveScopes[i] {
159+
labelQueryParams.Add(strconv.FormatInt(curSel, 10))
159160
}
160161
}
161162
}
163+
162164
if !labelSelected {
163-
labelQuerySlice = append(labelQuerySlice, labelID)
165+
labelQueryParams.Add(strconv.FormatInt(l.ID, 10))
164166
}
165167
l.IsSelected = labelSelected
166-
l.QueryString = strings.Join(labelQuerySlice, ",")
168+
169+
// Sort and deduplicate the ids to avoid the crawlers asking for the
170+
// same thing with simply a different order of parameters
171+
labelQuerySliceStrings := labelQueryParams.Values()
172+
slices.Sort(labelQuerySliceStrings) // the sort is still needed because the underlying map of Set doesn't guarantee order
173+
l.QueryString = strings.Join(labelQuerySliceStrings, ",")
167174
}
168175

169176
// BelongsToOrg returns true if label is an organization label
@@ -176,7 +183,7 @@ func (l *Label) BelongsToRepo() bool {
176183
return l.RepoID > 0
177184
}
178185

179-
// Return scope substring of label name, or empty string if none exists
186+
// ExclusiveScope returns scope substring of label name, or empty string if none exists
180187
func (l *Label) ExclusiveScope() string {
181188
if !l.Exclusive {
182189
return ""

models/issues/label_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,27 @@ func TestLabel_CalOpenIssues(t *testing.T) {
2323
assert.EqualValues(t, 2, label.NumOpenIssues)
2424
}
2525

26+
func TestLabel_LoadSelectedLabelsAfterClick(t *testing.T) {
27+
assert.NoError(t, unittest.PrepareTestDatabase())
28+
// Loading the label id:8 which have a scope and an exclusivity
29+
label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 8})
30+
31+
// First test : with negative and scope
32+
label.LoadSelectedLabelsAfterClick([]int64{1, -8}, []string{"", "scope"})
33+
assert.Equal(t, "1", label.QueryString)
34+
assert.Equal(t, true, label.IsSelected)
35+
36+
// Second test : with duplicates
37+
label.LoadSelectedLabelsAfterClick([]int64{1, 7, 1, 7, 7}, []string{"", "scope", "", "scope", "scope"})
38+
assert.Equal(t, "1,8", label.QueryString)
39+
assert.Equal(t, false, label.IsSelected)
40+
41+
// Third test : empty set
42+
label.LoadSelectedLabelsAfterClick([]int64{}, []string{})
43+
assert.False(t, label.IsSelected)
44+
assert.Equal(t, "8", label.QueryString)
45+
}
46+
2647
func TestLabel_ExclusiveScope(t *testing.T) {
2748
assert.NoError(t, unittest.PrepareTestDatabase())
2849
label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 7})

options/locale/locale_fr-FR.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2562,7 +2562,7 @@ release.stable=Stable
25622562
release.compare=Comparer
25632563
release.edit=Éditer
25642564
release.ahead.commits=<strong>%d</strong> révisions
2565-
release.ahead.target=à %s depuis cette publication
2565+
release.ahead.target=sur %s depuis cette publication
25662566
tag.ahead.target=à %s depuis cette étiquette
25672567
release.source_code=Code source
25682568
release.new_subheader=Les publications vous aide à organiser les versions marquantes de votre projet.

templates/repo/settings/collaboration.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
<form class="ui form" id="repo-collab-form" action="{{.Link}}" method="post">
4343
{{.CsrfTokenHtml}}
4444
<div id="search-user-box" class="ui search input tw-align-middle">
45-
<input class="prompt" name="collaborator" placeholder="{{ctx.Locale.Tr "search.team_kind"}}" autocomplete="off" autofocus required>
45+
<input class="prompt" name="collaborator" placeholder="{{ctx.Locale.Tr "search.user_kind"}}" autocomplete="off" autofocus required>
4646
</div>
4747
<button class="ui primary button">{{ctx.Locale.Tr "repo.settings.add_collaborator"}}</button>
4848
</form>

web_src/js/features/comp/ComboMarkdownEditor.js

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {initTextExpander} from './TextExpander.js';
1111
import {showErrorToast} from '../../modules/toast.js';
1212
import {POST} from '../../modules/fetch.js';
1313
import {initTextareaMarkdown} from './EditorMarkdown.js';
14+
import {initDropzone} from '../dropzone.js';
1415

1516
let elementIdCounter = 0;
1617

@@ -47,7 +48,7 @@ class ComboMarkdownEditor {
4748
this.prepareEasyMDEToolbarActions();
4849
this.setupContainer();
4950
this.setupTab();
50-
this.setupDropzone();
51+
await this.setupDropzone(); // textarea depends on dropzone
5152
this.setupTextarea();
5253

5354
await this.switchToUserPreference();
@@ -114,13 +115,30 @@ class ComboMarkdownEditor {
114115
}
115116
}
116117

117-
setupDropzone() {
118+
async setupDropzone() {
118119
const dropzoneParentContainer = this.container.getAttribute('data-dropzone-parent-container');
119120
if (dropzoneParentContainer) {
120121
this.dropzone = this.container.closest(this.container.getAttribute('data-dropzone-parent-container'))?.querySelector('.dropzone');
122+
if (this.dropzone) this.attachedDropzoneInst = await initDropzone(this.dropzone);
121123
}
122124
}
123125

126+
dropzoneGetFiles() {
127+
if (!this.dropzone) return null;
128+
return Array.from(this.dropzone.querySelectorAll('.files [name=files]'), (el) => el.value);
129+
}
130+
131+
dropzoneReloadFiles() {
132+
if (!this.dropzone) return;
133+
this.attachedDropzoneInst.emit('reload');
134+
}
135+
136+
dropzoneSubmitReload() {
137+
if (!this.dropzone) return;
138+
this.attachedDropzoneInst.emit('submit');
139+
this.attachedDropzoneInst.emit('reload');
140+
}
141+
124142
setupTab() {
125143
const tabs = this.container.querySelectorAll('.tabular.menu > .item');
126144

0 commit comments

Comments
 (0)