mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[log] IDEA-116242 Allow multiple users selection in the log filter
* Let FilterPopupComponent return several filters instead of one. Filter results will be united with OR. Clarify this fact in javadocs. * UserFilterPopupComponent: open a multi-line editor popup to enter user names. * Names can be separated with comma, | and \n. * Since there can be several user names, persist not single users, but user groups. Use a custom class, because List<List<String>> seems to be not possible in PersistentStateComponent by default. * Rename the State property to avoid conflicts when upgrading. * Display user names like this: "John|Bob". If the name is too long, shorten with ellipsis; still display full names in tooltips.
This commit is contained in:
@@ -74,7 +74,11 @@ public interface VcsLogProvider {
|
||||
void subscribeToRootRefreshEvents(@NotNull Collection<VirtualFile> roots, @NotNull VcsLogRefresher refresher);
|
||||
|
||||
/**
|
||||
* Return commits with full details, which correspond to the given filters.
|
||||
* <p>Return commits with full details, which correspond to the given filters.</p>
|
||||
*
|
||||
* <p>There can be several filters of a single type (e.g. several filters by users).<br/>
|
||||
* Filters of different types are concatenated with {@code AND}, while filters of a single type are concatenated with {@code OR}.
|
||||
* E.g.: (branch1 OR branch2) AND (user1 OR user2).</p>
|
||||
*
|
||||
* @param maxCount maximum number of commits to request from the VCS, or -1 for unlimited.
|
||||
*/
|
||||
|
||||
@@ -19,14 +19,13 @@ import com.intellij.openapi.components.PersistentStateComponent;
|
||||
import com.intellij.openapi.components.State;
|
||||
import com.intellij.openapi.components.Storage;
|
||||
import com.intellij.openapi.components.StoragePathMacros;
|
||||
import com.intellij.util.Function;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import com.intellij.vcs.log.VcsLogSettings;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Deque;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Stores UI configuration based on user activity and preferences.
|
||||
@@ -42,7 +41,7 @@ public class VcsLogUiProperties implements PersistentStateComponent<VcsLogUiProp
|
||||
|
||||
public static class State {
|
||||
public boolean SHOW_DETAILS = true;
|
||||
public Deque<String> RECENTLY_FILTERED_USERS = new ArrayDeque<String>();
|
||||
public Deque<UserGroup> RECENTLY_FILTERED_USER_GROUPS = new ArrayDeque<UserGroup>();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -68,19 +67,44 @@ public class VcsLogUiProperties implements PersistentStateComponent<VcsLogUiProp
|
||||
myState.SHOW_DETAILS = showDetails;
|
||||
}
|
||||
|
||||
public void addRecentlyFilteredUser(@NotNull String username) {
|
||||
if (myState.RECENTLY_FILTERED_USERS.contains(username)) {
|
||||
public void addRecentlyFilteredUserGroup(@NotNull List<String> usersInGroup) {
|
||||
UserGroup group = new UserGroup();
|
||||
group.users = usersInGroup;
|
||||
if (myState.RECENTLY_FILTERED_USER_GROUPS.contains(group)) {
|
||||
return;
|
||||
}
|
||||
myState.RECENTLY_FILTERED_USERS.addFirst(username);
|
||||
if (myState.RECENTLY_FILTERED_USERS.size() > RECENTLY_FILTERED_USERS_AMOUNT) {
|
||||
myState.RECENTLY_FILTERED_USERS.removeLast();
|
||||
myState.RECENTLY_FILTERED_USER_GROUPS.addFirst(group);
|
||||
if (myState.RECENTLY_FILTERED_USER_GROUPS.size() > RECENTLY_FILTERED_USERS_AMOUNT) {
|
||||
myState.RECENTLY_FILTERED_USER_GROUPS.removeLast();
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public List<String> getRecentlyFilteredUsers() {
|
||||
return new ArrayList<String>(myState.RECENTLY_FILTERED_USERS);
|
||||
public List<List<String>> getRecentlyFilteredUserGroups() {
|
||||
return ContainerUtil.map2List(myState.RECENTLY_FILTERED_USER_GROUPS, new Function<UserGroup, List<String>>() {
|
||||
@Override
|
||||
public List<String> fun(UserGroup group) {
|
||||
return group.users;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static class UserGroup {
|
||||
public List<String> users = new ArrayList<String>();
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
UserGroup group = (UserGroup)o;
|
||||
if (!users.equals(group.users)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return users.hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+3
-4
@@ -124,11 +124,10 @@ class BranchFilterPopupComponent extends FilterPopupComponent {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected VcsLogFilter getFilter() {
|
||||
protected Collection<VcsLogFilter> getFilters() {
|
||||
String value = getValue();
|
||||
return value == ALL
|
||||
? null
|
||||
: new VcsLogBranchFilter(myUi.getLogDataHolder().getDataPack().getRefsModel().getBranches(), value);
|
||||
Collection<VcsRef> allBranches = myUi.getLogDataHolder().getDataPack().getRefsModel().getBranches();
|
||||
return value == ALL ? null : Collections.<VcsLogFilter>singleton(new VcsLogBranchFilter(allBranches, value));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+4
-2
@@ -29,6 +29,8 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
|
||||
class DateFilterPopupComponent extends FilterPopupComponent {
|
||||
@@ -66,8 +68,8 @@ class DateFilterPopupComponent extends FilterPopupComponent {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected VcsLogFilter getFilter() {
|
||||
return myAfter == null && myBefore == null ? null : new VcsLogDateFilter(myAfter, myBefore);
|
||||
protected Collection<VcsLogFilter> getFilters() {
|
||||
return myAfter == null && myBefore == null ? null : Collections.<VcsLogFilter>singleton(new VcsLogDateFilter(myAfter, myBefore));
|
||||
}
|
||||
|
||||
private void setOnlyAfter(Date after) {
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.jetbrains.annotations.Nullable;
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.Border;
|
||||
import java.awt.event.*;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* Base class for components which allow to set up filter for the VCS Log, by displaying a popup with available choices.
|
||||
@@ -90,13 +91,19 @@ abstract class FilterPopupComponent extends JPanel {
|
||||
protected abstract ActionGroup createActionGroup();
|
||||
|
||||
/**
|
||||
* Return the filter currently selected by this component.
|
||||
* Return the filters (which would be concatenated with OR) currently selected via this component.</br>
|
||||
* Or return null if no filters are selected via this component.
|
||||
*/
|
||||
@Nullable
|
||||
protected abstract VcsLogFilter getFilter();
|
||||
protected abstract Collection<VcsLogFilter> getFilters();
|
||||
|
||||
protected void setValue(@NotNull String newValue) {
|
||||
myFilterValueLabel.setText(newValue);
|
||||
setValue(newValue, newValue);
|
||||
}
|
||||
|
||||
protected void setValue(@NotNull String value, @NotNull String tooltip) {
|
||||
myFilterValueLabel.setText(value);
|
||||
setToolTipText(tooltip);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
|
||||
+2
-2
@@ -52,8 +52,8 @@ class StructureFilterPopupComponent extends FilterPopupComponent {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected VcsLogFilter getFilter() {
|
||||
return getValue() == ALL || myFiles.isEmpty() ? null : new VcsLogStructureFilter(myFiles, myRoots);
|
||||
protected Collection<VcsLogFilter> getFilters() {
|
||||
return getValue() == ALL || myFiles.isEmpty() ? null : Collections.<VcsLogFilter>singleton(new VcsLogStructureFilter(myFiles, myRoots));
|
||||
}
|
||||
|
||||
private void setValue(@NotNull Collection<VirtualFile> files) {
|
||||
|
||||
+173
-27
@@ -15,24 +15,36 @@
|
||||
*/
|
||||
package com.intellij.vcs.log.ui.filter;
|
||||
|
||||
import com.intellij.openapi.actionSystem.ActionGroup;
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent;
|
||||
import com.intellij.openapi.actionSystem.DefaultActionGroup;
|
||||
import com.intellij.codeInsight.completion.CompletionResultSet;
|
||||
import com.intellij.codeInsight.lookup.LookupElementBuilder;
|
||||
import com.intellij.openapi.actionSystem.*;
|
||||
import com.intellij.openapi.components.ServiceManager;
|
||||
import com.intellij.openapi.fileTypes.FileTypes;
|
||||
import com.intellij.openapi.keymap.KeymapUtil;
|
||||
import com.intellij.openapi.project.DumbAwareAction;
|
||||
import com.intellij.openapi.ui.popup.JBPopup;
|
||||
import com.intellij.openapi.ui.popup.JBPopupAdapter;
|
||||
import com.intellij.openapi.ui.popup.LightweightWindowEvent;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.ui.popup.*;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.spellchecker.ui.SpellCheckingEditorCustomization;
|
||||
import com.intellij.ui.EditorCustomization;
|
||||
import com.intellij.ui.EditorTextField;
|
||||
import com.intellij.ui.EditorTextFieldProvider;
|
||||
import com.intellij.ui.SoftWrapsEditorCustomization;
|
||||
import com.intellij.util.Function;
|
||||
import com.intellij.util.TextFieldCompletionProviderDumbAware;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import com.intellij.vcs.log.VcsLogFilter;
|
||||
import com.intellij.vcs.log.VcsUser;
|
||||
import com.intellij.vcs.log.data.VcsLogDataHolder;
|
||||
import com.intellij.vcs.log.data.VcsLogUiProperties;
|
||||
import com.intellij.vcs.log.data.VcsLogUserFilter;
|
||||
import com.intellij.vcs.log.ui.PopupWithTextFieldWithAutoCompletion;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Collection;
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.CompoundBorder;
|
||||
import java.awt.*;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -41,9 +53,13 @@ import java.util.List;
|
||||
class UserFilterPopupComponent extends FilterPopupComponent {
|
||||
|
||||
private static final String ME = "me";
|
||||
private static final char[] USERS_SEPARATORS = { ',', '|', '\n' };
|
||||
|
||||
private final VcsLogDataHolder myDataHolder;
|
||||
private final VcsLogUiProperties myUiProperties;
|
||||
|
||||
@Nullable private Collection<String> mySelectedUsers;
|
||||
|
||||
UserFilterPopupComponent(VcsLogClassicFilterUi filterUi, VcsLogDataHolder dataHolder, VcsLogUiProperties uiProperties) {
|
||||
super(filterUi, "User");
|
||||
myDataHolder = dataHolder;
|
||||
@@ -52,15 +68,22 @@ class UserFilterPopupComponent extends FilterPopupComponent {
|
||||
|
||||
@Override
|
||||
protected ActionGroup createActionGroup() {
|
||||
DefaultActionGroup group = new DefaultActionGroup();
|
||||
group.add(createAllAction());
|
||||
group.add(new SetValueAction(ME, this));
|
||||
AnAction allAction = new DumbAwareAction(ALL) {
|
||||
@Override
|
||||
public void actionPerformed(AnActionEvent e) {
|
||||
apply(null, ALL, ALL);
|
||||
}
|
||||
};
|
||||
|
||||
List<String> recentlyFilteredUsers = myUiProperties.getRecentlyFilteredUsers();
|
||||
DefaultActionGroup group = new DefaultActionGroup();
|
||||
group.add(allAction);
|
||||
group.add(new UserAction(Collections.singleton(ME)));
|
||||
|
||||
List<List<String>> recentlyFilteredUsers = myUiProperties.getRecentlyFilteredUserGroups();
|
||||
if (!recentlyFilteredUsers.isEmpty()) {
|
||||
group.addSeparator("Recently searched");
|
||||
for (String recentUser : recentlyFilteredUsers) {
|
||||
group.add(new SetValueAction(recentUser, this));
|
||||
for (List<String> recentGroup : recentlyFilteredUsers) {
|
||||
group.add(new UserAction(recentGroup));
|
||||
}
|
||||
}
|
||||
group.addSeparator();
|
||||
@@ -70,16 +93,50 @@ class UserFilterPopupComponent extends FilterPopupComponent {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected VcsLogFilter getFilter() {
|
||||
String value = getValue();
|
||||
if (value == ALL) {
|
||||
protected Collection<VcsLogFilter> getFilters() {
|
||||
if (mySelectedUsers == null) {
|
||||
return null;
|
||||
}
|
||||
if (value == ME) {
|
||||
return new VcsLogUserFilter.Me(myDataHolder.getCurrentUser());
|
||||
myUiProperties.addRecentlyFilteredUserGroup(new ArrayList<String>(mySelectedUsers));
|
||||
return ContainerUtil.map(mySelectedUsers, new Function<String, VcsLogFilter>() {
|
||||
@Override
|
||||
public VcsLogFilter fun(String name) {
|
||||
return name == ME ? new VcsLogUserFilter.Me(myDataHolder.getCurrentUser()) : new VcsLogUserFilter.ByName(name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void apply(Collection<String> users, String text, String tooltip) {
|
||||
mySelectedUsers = users;
|
||||
applyFilters();
|
||||
setValue(text, tooltip);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static String displayableText(@NotNull Collection<String> users) {
|
||||
if (users.size() == 1) {
|
||||
return users.iterator().next();
|
||||
}
|
||||
return StringUtil.shortenTextWithEllipsis(StringUtil.join(users, "|"), 30, 0, true);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static String tooltip(@NotNull Collection<String> users) {
|
||||
return StringUtil.join(users, ", ");
|
||||
}
|
||||
|
||||
private class UserAction extends DumbAwareAction {
|
||||
@NotNull private final Collection<String> myUsers;
|
||||
|
||||
UserAction(@NotNull Collection<String> users) {
|
||||
super(displayableText(users), tooltip(users), null);
|
||||
myUsers = users;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void actionPerformed(AnActionEvent e) {
|
||||
apply(myUsers, displayableText(myUsers), tooltip(myUsers));
|
||||
}
|
||||
myUiProperties.addRecentlyFilteredUser(value);
|
||||
return new VcsLogUserFilter.ByName(value);
|
||||
}
|
||||
|
||||
private class SelectUserAction extends DumbAwareAction {
|
||||
@@ -90,6 +147,11 @@ class UserFilterPopupComponent extends FilterPopupComponent {
|
||||
|
||||
@Override
|
||||
public void actionPerformed(AnActionEvent e) {
|
||||
Project project = e.getProject();
|
||||
if (project == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Collection<String> users = ContainerUtil.map(myDataHolder.getAllUsers(), new Function<VcsUser, String>() {
|
||||
@Override
|
||||
public String fun(VcsUser user) {
|
||||
@@ -97,21 +159,105 @@ class UserFilterPopupComponent extends FilterPopupComponent {
|
||||
}
|
||||
});
|
||||
|
||||
final PopupWithTextFieldWithAutoCompletion textField = new PopupWithTextFieldWithAutoCompletion(e.getProject(), users);
|
||||
JBPopup popup = textField.createPopup();
|
||||
|
||||
final MultilinePopupBuilder popupBuilder = new MultilinePopupBuilder(project, users);
|
||||
JBPopup popup = popupBuilder.createPopup();
|
||||
popup.addListener(new JBPopupAdapter() {
|
||||
@Override
|
||||
public void onClosed(LightweightWindowEvent event) {
|
||||
if (event.isOk()) {
|
||||
String user = textField.getText();
|
||||
setValue(user);
|
||||
applyFilters();
|
||||
final String userText = popupBuilder.getText().trim();
|
||||
Collection<String> selectedUsers = ContainerUtil.toCollection(StringUtil.tokenize(userText, new String(USERS_SEPARATORS)));
|
||||
apply(selectedUsers, displayableText(selectedUsers), tooltip(selectedUsers));
|
||||
}
|
||||
}
|
||||
});
|
||||
popup.showUnderneathOf(UserFilterPopupComponent.this);
|
||||
}
|
||||
}
|
||||
|
||||
private static class MultilinePopupBuilder {
|
||||
private final EditorTextField myTextField;
|
||||
|
||||
MultilinePopupBuilder(@NotNull Project project, @NotNull final Collection<String> users) {
|
||||
myTextField = createTextField(project);
|
||||
new UsersCompletionProvider(users).apply(myTextField);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static EditorTextField createTextField(@NotNull Project project) {
|
||||
final EditorTextFieldProvider service = ServiceManager.getService(project, EditorTextFieldProvider.class);
|
||||
List<EditorCustomization> features = Arrays.<EditorCustomization>asList(SoftWrapsEditorCustomization.ENABLED,
|
||||
SpellCheckingEditorCustomization.DISABLED);
|
||||
EditorTextField textField = service.getEditorField(FileTypes.PLAIN_TEXT.getLanguage(), project, features);
|
||||
textField.setBorder(new CompoundBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2), textField.getBorder()));
|
||||
textField.setOneLineMode(false);
|
||||
return textField;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
JBPopup createPopup() {
|
||||
JPanel panel = new JPanel(new BorderLayout());
|
||||
panel.add(myTextField, BorderLayout.CENTER);
|
||||
ComponentPopupBuilder builder = JBPopupFactory.getInstance().createComponentPopupBuilder(panel, myTextField)
|
||||
.setCancelOnClickOutside(true)
|
||||
.setAdText(KeymapUtil.getShortcutsText(CommonShortcuts.CTRL_ENTER.getShortcuts()) + " to finish")
|
||||
.setMovable(true)
|
||||
.setRequestFocus(true)
|
||||
.setResizable(true)
|
||||
.setMayBeParent(true);
|
||||
|
||||
final JBPopup popup = builder.createPopup();
|
||||
popup.setMinimumSize(new Dimension(200, 90));
|
||||
AnAction okAction = new DumbAwareAction() {
|
||||
@Override
|
||||
public void actionPerformed(AnActionEvent e) {
|
||||
unregisterCustomShortcutSet(popup.getContent());
|
||||
popup.closeOk(e.getInputEvent());
|
||||
}
|
||||
};
|
||||
okAction.registerCustomShortcutSet(CommonShortcuts.CTRL_ENTER, popup.getContent());
|
||||
return popup;
|
||||
}
|
||||
|
||||
String getText() {
|
||||
return myTextField.getText();
|
||||
}
|
||||
|
||||
private static class UsersCompletionProvider extends TextFieldCompletionProviderDumbAware {
|
||||
@NotNull private final Collection<String> myUsers;
|
||||
|
||||
UsersCompletionProvider(@NotNull Collection<String> users) {
|
||||
super(true);
|
||||
myUsers = users;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
protected String getPrefix(@NotNull String currentTextPrefix) {
|
||||
final int separatorPosition = lastSeparatorPosition(currentTextPrefix);
|
||||
return separatorPosition == -1 ? currentTextPrefix : currentTextPrefix.substring(separatorPosition + 1).trim();
|
||||
}
|
||||
|
||||
private static int lastSeparatorPosition(@NotNull String text) {
|
||||
int lastPosition = -1;
|
||||
for (char separator : USERS_SEPARATORS) {
|
||||
int lio = text.lastIndexOf(separator);
|
||||
if (lio > lastPosition) {
|
||||
lastPosition = lio;
|
||||
}
|
||||
}
|
||||
return lastPosition;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addCompletionVariants(@NotNull String text, int offset, @NotNull String prefix,
|
||||
@NotNull CompletionResultSet result) {
|
||||
result.addLookupAdvertisement("Select one or more users separated with comma, | or new lines");
|
||||
for (String completionVariant : myUsers) {
|
||||
final LookupElementBuilder element = LookupElementBuilder.create(completionVariant);
|
||||
result.addElement(element.withLookupString(completionVariant.toLowerCase()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-9
@@ -15,12 +15,14 @@
|
||||
*/
|
||||
package com.intellij.vcs.log.ui.filter;
|
||||
|
||||
import com.intellij.openapi.actionSystem.*;
|
||||
import com.intellij.openapi.actionSystem.ActionGroup;
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent;
|
||||
import com.intellij.openapi.actionSystem.DefaultActionGroup;
|
||||
import com.intellij.openapi.actionSystem.Presentation;
|
||||
import com.intellij.openapi.actionSystem.ex.CustomComponentAction;
|
||||
import com.intellij.openapi.project.DumbAwareAction;
|
||||
import com.intellij.ui.SearchTextField;
|
||||
import com.intellij.ui.SearchTextFieldWithStoredHistory;
|
||||
import com.intellij.util.Function;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import com.intellij.util.ui.UIUtil;
|
||||
import com.intellij.vcs.log.VcsLogFilter;
|
||||
@@ -91,13 +93,14 @@ public class VcsLogClassicFilterUi implements VcsLogFilterUi {
|
||||
|
||||
@NotNull
|
||||
private List<VcsLogFilter> getPopupFilters() {
|
||||
return new ArrayList<VcsLogFilter>(ContainerUtil.mapNotNull(myFilterPopupComponents,
|
||||
new Function<FilterPopupComponent, VcsLogFilter>() {
|
||||
@Override
|
||||
public VcsLogFilter fun(FilterPopupComponent filterComponent) {
|
||||
return filterComponent.getFilter();
|
||||
}
|
||||
}));
|
||||
List<VcsLogFilter> filters = new ArrayList<VcsLogFilter>();
|
||||
for (FilterPopupComponent popupComponent : myFilterPopupComponents) {
|
||||
Collection<VcsLogFilter> popupFilters = popupComponent.getFilters();
|
||||
if (popupFilters != null) {
|
||||
filters.addAll(popupFilters);
|
||||
}
|
||||
}
|
||||
return filters;
|
||||
}
|
||||
|
||||
void applyFilters() {
|
||||
|
||||
Reference in New Issue
Block a user