chore: Improve link mutiple issues UI

This commit is contained in:
Robin Shen 2025-09-08 20:55:48 +08:00
parent f87ceb0d13
commit 53e91f8520
7 changed files with 212 additions and 50 deletions

View File

@ -0,0 +1,41 @@
package io.onedev.server.web.component.issue.choice;
import static io.onedev.server.web.translation.Translation._T;
import java.util.Collection;
import org.apache.wicket.markup.head.IHeaderResponse;
import org.apache.wicket.markup.head.JavaScriptHeaderItem;
import org.apache.wicket.model.IModel;
import io.onedev.server.model.Issue;
import io.onedev.server.web.component.select2.Select2MultiChoice;
public class IssueMultiChoice extends Select2MultiChoice<Issue> {
public IssueMultiChoice(String id, IModel<Collection<Issue>> model, IssueChoiceProvider choiceProvider) {
super(id, model, choiceProvider);
}
@Override
protected void onInitialize() {
super.onInitialize();
getSettings().setAllowClear(!isRequired());
if (isRequired())
getSettings().setPlaceholder(_T("Choose issues..."));
else
getSettings().setPlaceholder(_T("Not specified"));
getSettings().setFormatResult("onedev.server.issueChoiceFormatter.formatResult");
getSettings().setFormatSelection("onedev.server.issueChoiceFormatter.formatSelection");
getSettings().setEscapeMarkup("onedev.server.issueChoiceFormatter.escapeMarkup");
setConvertEmptyInputStringToNull(true);
}
@Override
public void renderHead(IHeaderResponse response) {
super.renderHead(response);
response.render(JavaScriptHeaderItem.forReference(new IssueChoiceResourceReference()));
}
}

View File

@ -9,9 +9,9 @@ import org.apache.wicket.model.IModel;
import io.onedev.server.model.Issue;
import io.onedev.server.web.component.select2.Select2Choice;
public class IssueChoice extends Select2Choice<Issue> {
public class IssueSingleChoice extends Select2Choice<Issue> {
public IssueChoice(String id, IModel<Issue> model, IssueChoiceProvider choiceProvider) {
public IssueSingleChoice(String id, IModel<Issue> model, IssueChoiceProvider choiceProvider) {
super(id, model, choiceProvider);
}

View File

@ -416,32 +416,46 @@ public abstract class IssuePrimaryPanel extends Panel {
return editor;
}
private FormComponent<Issue> newLinkExistingPanel(String componentId) {
return new SelectIssuePanel(componentId) {
private IssueChoiceProvider getIssueChoiceProvider() {
return new IssueChoiceProvider() {
@Override
protected IssueChoiceProvider getChoiceProvider() {
return new IssueChoiceProvider() {
@Override
protected Project getProject() {
return getIssue().getProject();
}
@Override
protected IssueQuery getBaseQuery() {
LinkSpec spec = getLinkSpecManager().load(specId);
if (opposite)
return spec.getOpposite().getParsedIssueQuery(getProject());
else
return spec.getParsedIssueQuery(getProject());
}
};
protected Project getProject() {
return getIssue().getProject();
}
};
@Override
protected IssueQuery getBaseQuery() {
LinkSpec spec = getLinkSpecManager().load(specId);
if (opposite)
return spec.getOpposite().getParsedIssueQuery(getProject());
else
return spec.getParsedIssueQuery(getProject());
}
};
}
private FormComponent<Issue> issuePopulator;
private FormComponent<?> newLinkExistingPanel(String componentId) {
LinkSpec spec = getLinkSpecManager().load(specId);
if (!opposite && spec.isMultiple() || opposite && spec.getOpposite().isMultiple()) {
return new SelectIssuesPanel(componentId) {
@Override
protected IssueChoiceProvider getChoiceProvider() {
return getIssueChoiceProvider();
}
};
} else {
return new SelectIssuePanel(componentId) {
@Override
protected IssueChoiceProvider getChoiceProvider() {
return getIssueChoiceProvider();
}
};
}
}
private FormComponent<?> issuePopulator;
@Override
protected Component newContent(String id) {
@ -511,32 +525,52 @@ public abstract class IssuePrimaryPanel extends Panel {
form.add(new Label("title", MessageFormat.format(_T("Add {0}"), linkName)));
form.add(new AjaxButton("save") {
@SuppressWarnings("unchecked")
@Override
protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
var linkIssue = issuePopulator.getConvertedInput();
if (linkIssue.isNew()) {
getIssueManager().open(linkIssue);
notifyIssueChange(target, linkIssue);
var spec = getLinkSpecManager().load(specId);
getIssueChangeManager().addLink(spec, getIssue(), linkIssue, opposite);
notifyIssueChange(target, getIssue());
close();
} else {
LinkSpec spec = getLinkSpecManager().load(specId);
if (getIssue().getId().equals(linkIssue.getId())) {
form.error(_T("Can not link to self"));
target.add(form);
} else if (getIssue().findLinkedIssues(spec, opposite).contains(linkIssue)) {
form.error(_T("Issue already linked"));
target.add(form);
} else {
getIssueChangeManager().addLink(spec, getIssue(), linkIssue, opposite);
notifyIssueChange(target, getIssue());
var convertedInput = issuePopulator.getConvertedInput();
if (convertedInput instanceof Issue) {
var linkIssue = (Issue) convertedInput;
if (linkIssue.isNew()) {
getIssueManager().open(linkIssue);
notifyIssueChange(target, linkIssue);
addLink(target, linkIssue);
close();
}
} else if (checkLink(target, linkIssue)) {
addLink(target, linkIssue);
close();
}
} else {
var linkIssues = (List<Issue>) convertedInput;
if (linkIssues.stream().noneMatch(it->!checkLink(target, it))) {
for (var linkIssue: linkIssues)
addLink(target, linkIssue);
close();
}
}
}
private boolean checkLink(AjaxRequestTarget target, Issue linkIssue) {
LinkSpec spec = getLinkSpecManager().load(specId);
if (getIssue().getId().equals(linkIssue.getId())) {
form.error(_T("Can not link to self: " + linkIssue.getReference().toString(getProject())));
target.add(form);
return false;
} else if (getIssue().findLinkedIssues(spec, opposite).contains(linkIssue)) {
form.error(_T("Issue already linked: " + linkIssue.getReference().toString(getProject())));
target.add(form);
return false;
} else {
return true;
}
}
private void addLink(AjaxRequestTarget target, Issue linkIssue) {
LinkSpec spec = getLinkSpecManager().load(specId);
getIssueChangeManager().addLink(spec, getIssue(), linkIssue, opposite);
notifyIssueChange(target, getIssue());
}
@Override
protected void onError(AjaxRequestTarget target, Form<?> form) {
target.add(form);

View File

@ -8,14 +8,14 @@ import org.apache.wicket.model.Model;
import io.onedev.server.OneDev;
import io.onedev.server.entitymanager.IssueManager;
import io.onedev.server.model.Issue;
import io.onedev.server.web.component.issue.choice.IssueChoice;
import io.onedev.server.web.component.issue.choice.IssueChoiceProvider;
import io.onedev.server.web.component.issue.choice.IssueSingleChoice;
abstract class SelectIssuePanel extends FormComponentPanel<Issue> {
private Long issueId;
private IssueChoice choice;
private IssueSingleChoice choice;
public SelectIssuePanel(String id) {
super(id, Model.of((Issue)null));
@ -26,7 +26,7 @@ abstract class SelectIssuePanel extends FormComponentPanel<Issue> {
super.onInitialize();
add(new FencedFeedbackPanel("feedback"));
choice = new IssueChoice("choice", new IModel<Issue>() {
choice = new IssueSingleChoice("choice", new IModel<Issue>() {
@Override
public void detach() {

View File

@ -0,0 +1,4 @@
<wicket:panel>
<div wicket:id="feedback"></div>
<input wicket:id="choice" type="hidden" class="form-control">
</wicket:panel>

View File

@ -0,0 +1,83 @@
package io.onedev.server.web.component.issue.primary;
import java.util.ArrayList;
import java.util.Collection;
import java.util.stream.Collectors;
import org.apache.wicket.feedback.FencedFeedbackPanel;
import org.apache.wicket.markup.html.form.FormComponentPanel;
import org.apache.wicket.model.IModel;
import io.onedev.server.OneDev;
import io.onedev.server.entitymanager.IssueManager;
import io.onedev.server.model.Issue;
import io.onedev.server.web.component.issue.choice.IssueChoiceProvider;
import io.onedev.server.web.component.issue.choice.IssueMultiChoice;
abstract class SelectIssuesPanel extends FormComponentPanel<Collection<Issue>> {
private Collection<Long> issueIds = new ArrayList<>();
private IssueMultiChoice choice;
public SelectIssuesPanel(String id) {
super(id, new IModel<Collection<Issue>>() {
private Collection<Issue> collection = new ArrayList<Issue>();
@Override
public void detach() {
}
@Override
public Collection<Issue> getObject() {
return collection;
}
@Override
public void setObject(Collection<Issue> object) {
collection = object;
}
});
}
@Override
protected void onInitialize() {
super.onInitialize();
add(new FencedFeedbackPanel("feedback"));
choice = new IssueMultiChoice("choice", new IModel<Collection<Issue>>() {
@Override
public void detach() {
}
@Override
public Collection<Issue> getObject() {
return issueIds.stream().map(it->getIssueManager().load(it)).collect(Collectors.toList());
}
@Override
public void setObject(Collection<Issue> object) {
issueIds = object.stream().map(it->it.getId()).collect(Collectors.toList());
}
}, getChoiceProvider());
choice.setRequired(true);
add(choice);
setOutputMarkupId(true);
}
@Override
public void convertInput() {
super.convertInput();
setConvertedInput(choice.getConvertedInput());
}
protected abstract IssueChoiceProvider getChoiceProvider();
private IssueManager getIssueManager() {
return OneDev.getInstance(IssueManager.class);
}
}

View File

@ -13,7 +13,7 @@ import io.onedev.server.entitymanager.IssueManager;
import io.onedev.server.model.Issue;
import io.onedev.server.model.Project;
import io.onedev.server.search.entity.issue.IssueQuery;
import io.onedev.server.web.component.issue.choice.IssueChoice;
import io.onedev.server.web.component.issue.choice.IssueSingleChoice;
import io.onedev.server.web.component.issue.choice.IssueChoiceProvider;
import io.onedev.server.web.editable.PropertyDescriptor;
import io.onedev.server.web.editable.PropertyEditor;
@ -22,7 +22,7 @@ import io.onedev.server.web.util.ProjectAware;
public class IssueChoiceEditor extends PropertyEditor<Long> {
private IssueChoice input;
private IssueSingleChoice input;
public IssueChoiceEditor(String id, PropertyDescriptor propertyDescriptor,
IModel<Long> propertyModel) {
@ -64,7 +64,7 @@ public class IssueChoiceEditor extends PropertyEditor<Long> {
}
};
input = new IssueChoice("input", Model.of(issue), choiceProvider) {
input = new IssueSingleChoice("input", Model.of(issue), choiceProvider) {
@Override
protected void onInitialize() {