Skip to content

Commit

Permalink
Improve Search Experience
Browse files Browse the repository at this point in the history
* Removing un-documented tokenization of search to plain word search (Fixes issues #78);
* Change "Searching..." to be at the top and obvious (Fixes issue #79);
* Remove confusing empty message while searching;
* Increase search max items to 250;
* Add actual error found during query to UX;
  • Loading branch information
supermem613 authored Mar 21, 2022
1 parent c1d099a commit 9aea9d5
Show file tree
Hide file tree
Showing 7 changed files with 52 additions and 152 deletions.
10 changes: 9 additions & 1 deletion Source/Foundation/Windows/Controls/Data/ListViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -379,12 +379,20 @@ public void ToggleSort()
/// <summary>
/// Invalidates the no items text.
/// </summary>
private void InvalidateNoItemsText()
public void InvalidateNoItemsText()
{
string text = (SearchFilter != null) ? EmptyFilteredCollectionText : EmptyCollectionText;
NoItemsText = text;
}

/// <summary>
/// Clears the no items text.
/// </summary>
public void ClearNoItemsText()
{
NoItemsText = String.Empty;
}

/// <summary>
/// Invalidates the filters.
/// </summary>
Expand Down
30 changes: 15 additions & 15 deletions Source/TeamMate/Pages/SearchPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,34 +25,34 @@
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
Margin="22"
HorizontalAlignment="Center"
VerticalAlignment="Top"
FontSize="18"
Text="{Binding ProgressContext.Status}"
Visibility="{Binding ProgressContext.IsRunning,
Converter={x:Static fw:Converters.Visibility}}" />

<fwc:ProgressIndicator Grid.Row="0"

<fwc:ProgressIndicator Grid.Row="1"
Foreground="{StaticResource ApplicationColorBrush}"
Visibility="{Binding ProgressContext.IsRunning,
Converter={x:Static fw:Converters.VisibilityHidden}}" />

<fwcd:ListView Name="listView"
Grid.Row="1"
Grid.Row="2"
DataContext="{Binding ListViewModel}"
Visibility="{Binding NoResultsText,
Visibility="{Binding ProgressContext.IsRunning,
Converter={x:Static fw:Converters.InverseVisibility}}" />

<TextBlock Grid.Row="2"
Margin="22"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="11"
Text="{Binding ProgressContext.Status}"
Visibility="{Binding ProgressContext.IsRunning,
Converter={x:Static fw:Converters.Visibility}}" />

<Grid Grid.Row="2" Visibility="{Binding ProgressContext.IsFailed, Converter={x:Static fw:Converters.Visibility}}">
<Grid Grid.Row="2" Visibility="{Binding ProgressContext.IsFailed, Converter={x:Static fw:Converters.Visibility}}">
<StackPanel Margin="22"
HorizontalAlignment="Center"
VerticalAlignment="Center"
VerticalAlignment="Top"
Orientation="Horizontal">
<fwc:SymbolIcon Name="attentionIcon"
Margin="0,0,6,0"
Expand Down
14 changes: 7 additions & 7 deletions Source/TeamMate/Services/SearchService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace Microsoft.Tools.TeamMate.Services
{
public class SearchService
{
private const int MaxItemsFromVsts = 25;
private const int MaxItemsFromAdo = 250;

[Import]
public WindowService WindowService { get; set; }
Expand Down Expand Up @@ -66,7 +66,7 @@ private ICollection<QueryViewModelBase> GetLocalQueries()
return this.WindowService.MainWindow.ViewModel.HomePage.TileCollection.Tiles.Select(t => t.Query).ToArray();
}

public async Task<SearchResults> VstsSearch(SearchExpression searchExpression, CancellationToken cancellationToken)
public async Task<SearchResults> AdoSearch(SearchExpression searchExpression, CancellationToken cancellationToken)
{
var pc = this.SessionService.Session.ProjectContext;

Expand All @@ -75,14 +75,14 @@ public async Task<SearchResults> VstsSearch(SearchExpression searchExpression, C
ProjectName = pc.ProjectName,
Wiql = searchExpression.ToVstsWiql(),
RequiredFields = pc.RequiredWorkItemFieldNames,
MaxItemsToFetch = MaxItemsFromVsts
MaxItemsToFetch = MaxItemsFromAdo
};

await ChaosMonkey.ChaosAsync(ChaosScenarios.VstsSearch);

var result = await pc.WorkItemTrackingClient.QueryAsync(query);
var workItems = result.WorkItems.Select(wi => CreateWorkItemViewModel(wi));
var searchResults = workItems.Select(wi => new SearchResult(wi, SearchResultSource.Vsts)).ToArray();
var searchResults = workItems.Select(wi => new SearchResult(wi, SearchResultSource.Ado)).ToArray();
return new SearchResults(searchResults, result.QueryResult.WorkItems.Count());
}

Expand Down Expand Up @@ -132,7 +132,7 @@ public class SearchResultSource : IComparable
private QueryViewModelBase source;
private string sourceName;

public static readonly SearchResultSource Vsts = new SearchResultSource("VSTS");
public static readonly SearchResultSource Ado = new SearchResultSource("ADO");

public SearchResultSource(QueryViewModelBase source)
{
Expand All @@ -146,7 +146,7 @@ private SearchResultSource(string sourceName)

public bool IsLocal
{
get { return this != Vsts; }
get { return this != Ado; }
}

public int CompareTo(object obj)
Expand All @@ -164,7 +164,7 @@ public int CompareTo(object obj)

private int GetRanking()
{
if (this == Vsts)
if (this == Ado)
{
return 1;
}
Expand Down
2 changes: 1 addition & 1 deletion Source/TeamMate/Utilities/ChaosScenarios.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public static class ChaosScenarios
public static readonly ChaosScenario DownloadAttachment = new ChaosScenario("DownloadAttachment");
public static readonly ChaosScenario SaveWorkItem = new ChaosScenario("SaveWorkItem");
public static readonly ChaosScenario LocalSearch = new ChaosScenario("LocalSearch");
public static readonly ChaosScenario VstsSearch = new ChaosScenario("VstsSearch");
public static readonly ChaosScenario VstsSearch = new ChaosScenario("AdoSearch");
public static readonly ChaosScenario GetLinkedChangesetInfo = new ChaosScenario("GetLinkedChangesetInfo");
public static readonly ChaosScenario GetLinkedWorkItemsInfo = new ChaosScenario("GetLinkedWorkItemsInfo");
public static readonly ChaosScenario FileUpload = new ChaosScenario("FileUpload");
Expand Down
131 changes: 9 additions & 122 deletions Source/TeamMate/Utilities/SearchExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,9 @@ namespace Microsoft.Tools.TeamMate.Utilities
{
public class SearchExpression
{
private static readonly Regex SearchRegex = new Regex(@"(?<key>\w+):""(?<value>[^\""]*)""|(?<key>\w+):(?<value>\w*)|(?<value>\w+)", RegexOptions.Compiled);
private const string KeyGroup = "key";
private const string ValueGroup = "value";

private SearchExpression()
{
this.Tokens = new List<SearchExpressionToken>();
this.Tokens = new string[0];
}

public static SearchExpression Parse(string text)
Expand All @@ -29,30 +25,13 @@ public static SearchExpression Parse(string text)

if (!String.IsNullOrWhiteSpace(text))
{
var matches = SearchRegex.Matches(text);
foreach (Match item in matches)
{
var valueGroup = item.Groups[ValueGroup];

if (valueGroup != null)
{
string value = valueGroup.Value;
if (!String.IsNullOrWhiteSpace(value))
{
var keyGroup = item.Groups[KeyGroup];
string key = (keyGroup != null) ? keyGroup.Value : null;

var token = (!String.IsNullOrWhiteSpace(key)) ? new SearchExpressionToken(key, value) : new SearchExpressionToken(value);
expression.Tokens.Add(token);
}
}
}
expression.Tokens = text.Split(' ');
}

return expression;
}

public IList<SearchExpressionToken> Tokens { get; private set; }
public string[] Tokens { get; private set; }
public string Text { get; private set; }

public bool IsEmpty { get { return !Tokens.Any(); } }
Expand Down Expand Up @@ -93,25 +72,11 @@ public bool Matches(PullRequestRowViewModel item)

private Predicate<PullRequestRowViewModel> BuildCodeReviewPredicate()
{
List<string> plainWords = new List<string>();
List<Predicate<PullRequestRowViewModel>> predicates = new List<Predicate<PullRequestRowViewModel>>();
foreach (var token in Tokens)
{
string value = token.Value;
if (IsKey(token.Key, "c"))
{
predicates.Add((r) => Matches(r.Reference.CreatedBy.DisplayName, value));
}
else
{
// If we don't recognize the key token, treat the value as a plain word for matching
plainWords.Add(value);
}
}

if (plainWords.Any())
if (this.Tokens.Any())
{
var predicate = TextMatcher.MatchAllWordStartsMultiText(plainWords);
var predicate = TextMatcher.MatchAllWordStartsMultiText(this.Tokens);
var matcher = new MultiWordMatcher(predicate);
predicates.Add((wi) => wi.Matches(matcher));
}
Expand All @@ -133,37 +98,11 @@ public bool Matches(WorkItemRowViewModel item)
private Predicate<WorkItemRowViewModel> BuildWorkItemPredicate()
{
// IMPORTANT: Keep in sync with ToVstsWiql
List<string> plainWords = new List<string>();
List<Predicate<WorkItemRowViewModel>> predicates = new List<Predicate<WorkItemRowViewModel>>();
foreach (var token in Tokens)
{
string value = token.Value;
if (IsKey(token.Key, "a"))
{
predicates.Add((wi) => Matches(wi.AssignedTo, value));
}
else if (IsKey(token.Key, "s"))
{
predicates.Add((wi) => Matches(wi.State, value));
}
else if (IsKey(token.Key, "t"))
{
predicates.Add((wi) => Matches(wi.Type, value));
}
else if (IsKey(token.Key, "c"))
{
predicates.Add((wi) => Matches(wi.CreatedBy, value));
}
else
{
// If we don't recognize the key token, treat the value as a plain word for matching
plainWords.Add(value);
}
}

if (plainWords.Any())
if (this.Tokens.Any())
{
var predicate = TextMatcher.MatchAllWordStartsMultiText(plainWords);
var predicate = TextMatcher.MatchAllWordStartsMultiText(this.Tokens);
var matcher = new MultiWordMatcher(predicate);
predicates.Add((wi) => wi.Matches(matcher));
}
Expand Down Expand Up @@ -202,65 +141,13 @@ public string ToVstsWiql()
WorkItemQueryBuilder builder = new WorkItemQueryBuilder();
builder.Condition = FieldConditionInfo.CurrentProjectCondition;

List<string> plainWords = new List<string>();
List<Predicate<WorkItemRowViewModel>> predicates = new List<Predicate<WorkItemRowViewModel>>();
foreach (var token in Tokens)
if (this.Tokens.Any())
{
string value = token.Value;
if (IsKey(token.Key, "a"))
{
builder.Condition = builder.Condition.And(new FieldConditionInfo(WorkItemConstants.CoreFields.AssignedTo, Operators.Contains, value));
}
else if (IsKey(token.Key, "s"))
{
builder.Condition = builder.Condition.And(new FieldConditionInfo(WorkItemConstants.CoreFields.State, Operators.Contains, value));
}
else if (IsKey(token.Key, "t"))
{
builder.Condition = builder.Condition.And(new FieldConditionInfo(WorkItemConstants.CoreFields.WorkItemType, Operators.Contains, value));
}
else if (IsKey(token.Key, "c"))
{
builder.Condition = builder.Condition.And(new FieldConditionInfo(WorkItemConstants.CoreFields.CreatedBy, Operators.Contains, value));
}
else
{
// If we don't recognize the key token, treat the value as a plain word for matching
plainWords.Add(value);
}
}

if (plainWords.Any())
{
builder.Condition = builder.Condition.And(WorkItemQueryFactory.CreateWordSearchClause(plainWords));
builder.Condition = builder.Condition.And(WorkItemQueryFactory.CreateWordSearchClause(this.Tokens));
}

builder.AddOrderBy(WorkItemConstants.CoreFields.ChangedDate, false);
return builder.ToString();
}
}

public class SearchExpressionToken
{
public SearchExpressionToken(string value)
{
this.Value = value;
}

public SearchExpressionToken(string key, string value)
{
this.Key = key;
this.Value = value;
}

public string Key { get; private set; }
public string Value { get; private set; }
public bool HasKey { get { return Key != null; } }

public override string ToString()
{
string escapedValue = (Value.Contains(' ')) ? '\"' + Value + '\"' : Value;
return (Key != null) ? String.Format("{0}:{1}", Key, escapedValue) : escapedValue;
}
}
}
5 changes: 2 additions & 3 deletions Source/TeamMate/Utilities/SearchTextHighlighter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,9 @@ static SearchTextHighlighter()

public static void Highlight(ListView listView, SearchExpression searchExpression)
{
var words = searchExpression.Tokens.Select(t => t.Value).ToArray();
if (words.Any())
if (searchExpression.Tokens.Any())
{
Regex regex = TextMatcher.MatchAnyWordStartRegex(words);
Regex regex = TextMatcher.MatchAnyWordStartRegex(searchExpression.Tokens);

var listBoxItems = GetHighlightableListBoxItems(listView);
foreach (var listBoxItem in listBoxItems)
Expand Down
12 changes: 9 additions & 3 deletions Source/TeamMate/ViewModels/SearchPageViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ private static ListViewModel CreateListViewModel(ICollectionView collectionView)

listViewModel.Filters.Add(new ListViewFilter("All"));
listViewModel.Filters.Add(new ListViewFilter("Work Items", (o) => ((SearchResult)o).Item is WorkItemRowViewModel));
listViewModel.Filters.Add(new ListViewFilter("Code PullRequests", (o) => ((SearchResult)o).Item is PullRequestRowViewModel));
listViewModel.Filters.Add(new ListViewFilter("Pull Requests", (o) => ((SearchResult)o).Item is PullRequestRowViewModel));
listViewModel.Filters.Add(new ListViewFilter("Local Only", (o) => ((SearchResult)o).Source.IsLocal));

return listViewModel;
Expand Down Expand Up @@ -297,6 +297,9 @@ private async void InvalidateSearch()
return;
}

this.listViewModel.IsFilterByVisible = false;
this.listViewModel.ClearNoItemsText();

SearchStarted?.Invoke(this, EventArgs.Empty);

TaskContext taskContext = new TaskContext();
Expand All @@ -309,7 +312,7 @@ private async void InvalidateSearch()
var localSearchTask = searchService.LocalSearch(this.SearchExpression, CancellationToken.None);
searchTasks.Add(localSearchTask);

var vstsSearchTask = searchService.VstsSearch(this.SearchExpression, CancellationToken.None);
var vstsSearchTask = searchService.AdoSearch(this.SearchExpression, CancellationToken.None);
searchTasks.Add(vstsSearchTask);
taskContext.Status = "Searching...";

Expand All @@ -330,14 +333,17 @@ private async void InvalidateSearch()
{
if (!taskContext.IsFailed)
{
taskContext.Fail("An error occurred while performing the search", ex);
taskContext.Fail("An error occurred while performing the search: " + ex.InnerException.Message, ex);
}

Log.Warn(ex);
}
}
}

this.listViewModel.IsFilterByVisible = true;
this.listViewModel.InvalidateNoItemsText();

SearchCompleted?.Invoke(this, EventArgs.Empty);
}

Expand Down

0 comments on commit 9aea9d5

Please sign in to comment.