Search works (#3)
Still need to clean it up a bit, and make the UI have a collapsed section once the search is completed
This commit is contained in:
parent
15c1a3ff2c
commit
7839b8eb57
|
@ -12,6 +12,59 @@
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
if (!Searched)
|
||||||
|
{
|
||||||
|
<p>Instructions go here</p>
|
||||||
|
}
|
||||||
|
<section>
|
||||||
|
<EditForm Model=@Criteria OnValidSubmit=@RetrieveProfiles>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="col col-12 col-sm-6 col-md-4 col-lg-3">
|
||||||
|
<label for="continentId" class="jjj-label">Continent</label>
|
||||||
|
<InputSelect id="continentId" @bind-Value=@Criteria.ContinentId class="form-control form-control-sm">
|
||||||
|
<option value="">– Any –</option>
|
||||||
|
@foreach (var (id, name) in Continents)
|
||||||
|
{
|
||||||
|
<option value="@id">@name</option>
|
||||||
|
}
|
||||||
|
</InputSelect>
|
||||||
|
</div>
|
||||||
|
<div class="col col-12 col-sm-6 offset-md-2 col-lg-3 offset-lg-0">
|
||||||
|
<label class="jjj-label">Seeking Remote Work?</label><br>
|
||||||
|
<InputRadioGroup @bind-Value=@Criteria.RemoteWork>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<InputRadio id="remoteNull" Value=@("") class="form-check-input" />
|
||||||
|
<label for="remoteNull" class="form-check-label">No Selection</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<InputRadio id="remoteYes" Value=@("yes") class="form-check-input" />
|
||||||
|
<label for="remoteYes" class="form-check-label">Yes</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<InputRadio id="remoteNo" Value=@("no") class="form-check-input" />
|
||||||
|
<label for="remoteNo" class="form-check-label">No</label>
|
||||||
|
</div>
|
||||||
|
</InputRadioGroup>
|
||||||
|
</div>
|
||||||
|
<div class="col col-12 col-sm-6 col-lg-3">
|
||||||
|
<label for="skillSearch" class="jjj-label">Skill</label>
|
||||||
|
<InputText id="skillSearch" @bind-Value=@Criteria.Skill class="form-control form-control-sm"
|
||||||
|
placeholder="(free-form text)" />
|
||||||
|
</div>
|
||||||
|
<div class="col col-12 col-sm-6 col-lg-3">
|
||||||
|
<label for="bioSearch" class="jjj-label">Bio / Experience</label>
|
||||||
|
<InputText id="bioSearch" @bind-Value=@Criteria.BioExperience class="form-control form-control-sm"
|
||||||
|
placeholder="(free-form text)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="col">
|
||||||
|
<br>
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-primary">Search</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</EditForm>
|
||||||
|
</section>
|
||||||
@if (SearchResults.Any())
|
@if (SearchResults.Any())
|
||||||
{
|
{
|
||||||
<table class="table table-sm table-hover">
|
<table class="table table-sm table-hover">
|
||||||
|
|
|
@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Components;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace JobsJobsJobs.Client.Pages.Profile
|
namespace JobsJobsJobs.Client.Pages.Profile
|
||||||
|
@ -20,6 +21,11 @@ namespace JobsJobsJobs.Client.Pages.Profile
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private bool Searching { get; set; } = false;
|
private bool Searching { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The search criteria
|
||||||
|
/// </summary>
|
||||||
|
private ProfileSearch Criteria { get; set; } = new ProfileSearch();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Error messages encountered while searching for profiles
|
/// Error messages encountered while searching for profiles
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -60,8 +66,8 @@ namespace JobsJobsJobs.Client.Pages.Profile
|
||||||
{
|
{
|
||||||
Searching = true;
|
Searching = true;
|
||||||
|
|
||||||
// TODO: send a filter with this request
|
var searchResult = await ServerApi.RetrieveMany<ProfileSearchResult>(http,
|
||||||
var searchResult = await ServerApi.RetrieveMany<ProfileSearchResult>(http, "profile/search");
|
$"profile/search{SearchQuery()}");
|
||||||
|
|
||||||
if (searchResult.IsOk)
|
if (searchResult.IsOk)
|
||||||
{
|
{
|
||||||
|
@ -85,5 +91,27 @@ namespace JobsJobsJobs.Client.Pages.Profile
|
||||||
/// <param name="condition">The condition in question</param>
|
/// <param name="condition">The condition in question</param>
|
||||||
/// <returns>"Yes" for true, "No" for false</returns>
|
/// <returns>"Yes" for true, "No" for false</returns>
|
||||||
private static string YesOrNo(bool condition) => condition ? "Yes" : "No";
|
private static string YesOrNo(bool condition) => condition ? "Yes" : "No";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a search query string from the currently-entered criteria
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The query string for the currently-entered criteria</returns>
|
||||||
|
private string SearchQuery()
|
||||||
|
{
|
||||||
|
if (Criteria.IsEmptySearch) return "";
|
||||||
|
|
||||||
|
string part(string name, Func<ProfileSearch, string?> func) =>
|
||||||
|
string.IsNullOrEmpty(func(Criteria)) ? "" : $"{name}={WebUtility.UrlEncode(func(Criteria))}";
|
||||||
|
|
||||||
|
IEnumerable<string> parts()
|
||||||
|
{
|
||||||
|
yield return part("ContinentId", it => it.ContinentId);
|
||||||
|
yield return part("Skill", it => it.Skill);
|
||||||
|
yield return part("BioExperience", it => it.BioExperience);
|
||||||
|
yield return part("RemoteWork", it => it.RemoteWork);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"?{string.Join("&", parts().Where(it => !string.IsNullOrEmpty(it)).ToArray())}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,3 +65,7 @@ label.jjj-required::after {
|
||||||
color: red;
|
color: red;
|
||||||
content: ' *';
|
content: ' *';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
label.jjj-label {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<AssemblyVersion>0.7.1.0</AssemblyVersion>
|
<AssemblyVersion>0.7.2.0</AssemblyVersion>
|
||||||
<FileVersion>0.7.1.0</FileVersion>
|
<FileVersion>0.7.2.0</FileVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -112,9 +112,7 @@ namespace JobsJobsJobs.Server.Areas.Api.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("search")]
|
[HttpGet("search")]
|
||||||
public async Task<IActionResult> Search()
|
public async Task<IActionResult> Search([FromQuery] ProfileSearch search) =>
|
||||||
{
|
Ok(await _db.SearchProfiles(search));
|
||||||
return Ok(await _db.SearchProfiles());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -115,12 +115,51 @@ namespace JobsJobsJobs.Server.Data
|
||||||
/// </summary>
|
/// </summary>
|
||||||
// TODO: A criteria parameter!
|
// TODO: A criteria parameter!
|
||||||
/// <returns>The information for profiles matching the criteria</returns>
|
/// <returns>The information for profiles matching the criteria</returns>
|
||||||
public static async Task<IEnumerable<ProfileSearchResult>> SearchProfiles(this JobsDbContext db)
|
public static async Task<IEnumerable<ProfileSearchResult>> SearchProfiles(this JobsDbContext db,
|
||||||
|
ProfileSearch search)
|
||||||
{
|
{
|
||||||
return await db.Profiles
|
var query = db.Profiles
|
||||||
.Join(db.Citizens, p => p.Id, c => c.Id, (p, c) => new { Profile = p, Citizen = c })
|
.Join(db.Citizens, p => p.Id, c => c.Id, (p, c) => new { Profile = p, Citizen = c });
|
||||||
.Select(x => new ProfileSearchResult(x.Citizen.Id, x.Citizen.DisplayName, x.Profile.SeekingEmployment,
|
|
||||||
x.Profile.RemoteWork, x.Profile.FullTime, x.Profile.LastUpdatedOn))
|
var useIds = false;
|
||||||
|
var citizenIds = new List<CitizenId>();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(search.ContinentId))
|
||||||
|
{
|
||||||
|
query = query.Where(it => it.Profile.ContinentId == ContinentId.Parse(search.ContinentId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(search.RemoteWork))
|
||||||
|
{
|
||||||
|
query = query.Where(it => it.Profile.RemoteWork == (search.RemoteWork == "yes"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(search.Skill))
|
||||||
|
{
|
||||||
|
useIds = true;
|
||||||
|
citizenIds.AddRange(await db.Skills
|
||||||
|
.Where(s => s.Description.ToLower().Contains(search.Skill.ToLower()))
|
||||||
|
.Select(s => s.CitizenId)
|
||||||
|
.ToListAsync().ConfigureAwait(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(search.BioExperience))
|
||||||
|
{
|
||||||
|
useIds = true;
|
||||||
|
citizenIds.AddRange(await db.Profiles
|
||||||
|
.FromSqlRaw("SELECT citizen_id FROM profile WHERE biography ILIKE {0} OR experience ILIKE {0}",
|
||||||
|
$"%{search.BioExperience}%")
|
||||||
|
.Select(p => p.Id)
|
||||||
|
.ToListAsync().ConfigureAwait(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useIds)
|
||||||
|
{
|
||||||
|
query = query.Where(it => citizenIds.Contains(it.Citizen.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
return await query.Select(x => new ProfileSearchResult(x.Citizen.Id, x.Citizen.DisplayName,
|
||||||
|
x.Profile.SeekingEmployment, x.Profile.RemoteWork, x.Profile.FullTime, x.Profile.LastUpdatedOn))
|
||||||
.ToListAsync().ConfigureAwait(false);
|
.ToListAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
37
src/JobsJobsJobs/Shared/Api/ProfileSearch.cs
Normal file
37
src/JobsJobsJobs/Shared/Api/ProfileSearch.cs
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
namespace JobsJobsJobs.Shared.Api
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The various ways profiles can be searched
|
||||||
|
/// </summary>
|
||||||
|
public class ProfileSearch
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieve citizens from this continent
|
||||||
|
/// </summary>
|
||||||
|
public string? ContinentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Text for a search within a citizen's skills
|
||||||
|
/// </summary>
|
||||||
|
public string? Skill { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Text for a search with a citizen's professional biography and experience fields
|
||||||
|
/// </summary>
|
||||||
|
public string? BioExperience { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to retrieve citizens who do or do not want remote work
|
||||||
|
/// </summary>
|
||||||
|
public string RemoteWork { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is the search empty?
|
||||||
|
/// </summary>
|
||||||
|
public bool IsEmptySearch =>
|
||||||
|
string.IsNullOrEmpty(ContinentId)
|
||||||
|
&& string.IsNullOrEmpty(Skill)
|
||||||
|
&& string.IsNullOrEmpty(BioExperience)
|
||||||
|
&& string.IsNullOrEmpty(RemoteWork);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,6 @@
|
||||||
namespace JobsJobsJobs.Shared
|
using System;
|
||||||
|
|
||||||
|
namespace JobsJobsJobs.Shared
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A result with two different possibilities
|
/// A result with two different possibilities
|
||||||
|
@ -60,5 +62,24 @@
|
||||||
/// <param name="error">The error message</param>
|
/// <param name="error">The error message</param>
|
||||||
/// <returns>The Error result</returns>
|
/// <returns>The Error result</returns>
|
||||||
public static Result<TOk> AsError(string error) => new Result<TOk>(false) { Error = error };
|
public static Result<TOk> AsError(string error) => new Result<TOk>(false) { Error = error };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Transform a result if it is OK, passing the error along if it is an error
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="f">The transforming function</param>
|
||||||
|
/// <param name="result">The existing result</param>
|
||||||
|
/// <returns>The resultant result</returns>
|
||||||
|
public static Result<TOk> Bind(Func<TOk, Result<TOk>> f, Result<TOk> result) =>
|
||||||
|
result.IsOk ? f(result.Ok) : result;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Transform a result to a different type if it is OK, passing the error along if it is an error
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TOther">The type to which the result is transformed</typeparam>
|
||||||
|
/// <param name="f">The transforming function</param>
|
||||||
|
/// <param name="result">The existing result</param>
|
||||||
|
/// <returns>The resultant result</returns>
|
||||||
|
public static Result<TOther> Map<TOther>(Func<TOk, Result<TOther>> f, Result<TOk> result) =>
|
||||||
|
result.IsOk ? f(result.Ok) : Result<TOther>.AsError(result.Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user