From 69e386d91bd79a052a310dd2d5b1184199811501 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 14 Aug 2021 23:02:54 -0400 Subject: [PATCH] WIP on job listing search (#17) --- src/JobsJobsJobs/Api/Data.fs | 59 ++++--- src/JobsJobsJobs/Api/Handlers.fs | 15 +- src/JobsJobsJobs/App/src/api/index.ts | 18 +++ src/JobsJobsJobs/App/src/api/types.ts | 12 ++ .../App/src/components/ListingSearchForm.vue | 80 ++++++++++ .../App/src/components/layout/AppNav.vue | 6 +- src/JobsJobsJobs/App/src/router/index.ts | 15 +- .../App/src/views/listing/HelpWanted.vue | 145 ++++++++++++++++++ .../App/src/views/listing/ListingSearch.vue | 3 - .../App/src/views/listing/ListingView.vue | 3 + src/JobsJobsJobs/Domain/SharedTypes.fs | 25 +-- 11 files changed, 338 insertions(+), 43 deletions(-) create mode 100644 src/JobsJobsJobs/App/src/components/ListingSearchForm.vue create mode 100644 src/JobsJobsJobs/App/src/views/listing/HelpWanted.vue delete mode 100644 src/JobsJobsJobs/App/src/views/listing/ListingSearch.vue create mode 100644 src/JobsJobsJobs/App/src/views/listing/ListingView.vue diff --git a/src/JobsJobsJobs/Api/Data.fs b/src/JobsJobsJobs/Api/Data.fs index 88ff47b..15f4109 100644 --- a/src/JobsJobsJobs/Api/Data.fs +++ b/src/JobsJobsJobs/Api/Data.fs @@ -193,6 +193,7 @@ let regexContains (it : string) = System.Text.RegularExpressions.Regex.Escape it |> sprintf "(?i).*%s.*" +open JobsJobsJobs.Domain open JobsJobsJobs.Domain.SharedTypes open RethinkDb.Driver.Ast @@ -200,8 +201,6 @@ open RethinkDb.Driver.Ast [] module Profile = - open JobsJobsJobs.Domain - let count conn = withReconn(conn).ExecuteAsync(fun () -> r.Table(Table.Profile) @@ -430,25 +429,15 @@ module Continent = [] module Listing = - /// This is how RethinkDB is going to return our listing/continent combo - // fsharplint:disable RecordFieldNames - [] - type ListingAndContinent = { - left : Listing - right : Continent - } - // fsharplint:enable - /// Find all job listings posted by the given citizen let findByCitizen (citizenId : CitizenId) conn = - withReconn(conn).ExecuteAsync(fun () -> task { - let! both = - r.Table(Table.Listing) - .GetAll(citizenId).OptArg("index", nameof citizenId) - .EqJoin("continentId", r.Table(Table.Continent)) - .RunResultAsync conn - return both |> List.map (fun b -> { listing = b.left; continent = b.right}) - }) + withReconn(conn).ExecuteAsync(fun () -> + r.Table(Table.Listing) + .GetAll(citizenId).OptArg("index", nameof citizenId) + .EqJoin("continentId", r.Table(Table.Continent)) + .Merge(ReqlFunction1(fun it -> upcast r.HashMap("listing", it.G("left")).With("continent", it.G("right")))) + .Pluck("listing", "continent") + .RunResultAsync conn) /// Find a listing by its ID let findById (listingId : ListingId) conn = @@ -481,6 +470,38 @@ module Listing = () }) + /// Search job listings + let search (srch : ListingSearch) conn = + withReconn(conn).ExecuteAsync(fun () -> + (seq { + match srch.continentId with + | Some conId -> + yield (fun (q : ReqlExpr) -> + q.Filter(r.HashMap(nameof srch.continentId, ContinentId.ofString conId)) :> ReqlExpr) + | None -> () + match srch.region with + | Some rgn -> + yield (fun q -> + q.Filter(ReqlFunction1(fun s -> upcast s.G("region").Match(regexContains rgn))) :> ReqlExpr) + | None -> () + match srch.remoteWork with + | "" -> () + | _ -> yield (fun q -> q.Filter(r.HashMap(nameof srch.remoteWork, srch.remoteWork = "yes")) :> ReqlExpr) + match srch.text with + | Some text -> + yield (fun q -> + q.Filter(ReqlFunction1(fun it -> upcast it.G("text").Match(regexContains text))) :> ReqlExpr) + | None -> () + } + |> Seq.toList + |> List.fold + (fun q f -> f q) + (r.Table(Table.Listing) + .EqJoin("continentId", r.Table(Table.Continent)) :> ReqlExpr)) + .Merge(ReqlFunction1(fun it -> upcast r.HashMap("listing", it.G("left")).With("continent", it.G("right")))) + .Pluck("listing", "continent") + .RunResultAsync conn) + /// Success story data access functions [] diff --git a/src/JobsJobsJobs/Api/Handlers.fs b/src/JobsJobsJobs/Api/Handlers.fs index b37ba66..18ad5cd 100644 --- a/src/JobsJobsJobs/Api/Handlers.fs +++ b/src/JobsJobsJobs/Api/Handlers.fs @@ -239,6 +239,16 @@ module Listing = | None -> return! Error.notFound next ctx } + // GET: /api/listing/search + let search : HttpHandler = + authorize + >=> fun next ctx -> task { + let search = ctx.BindQueryString () + let! results = Data.Listing.search search (conn ctx) + return! json results next ctx + } + + /// Handlers for /api/profile routes [] module Profile = @@ -441,8 +451,9 @@ let allEndpoints = [ GET_HEAD [ route "/continent/all" Continent.all ] subRoute "/listing" [ GET_HEAD [ - routef "/%O" Listing.get - route "s/mine" Listing.mine + routef "/%O" Listing.get + route "/search" Listing.search + route "s/mine" Listing.mine ] POST [ route "s" Listing.add diff --git a/src/JobsJobsJobs/App/src/api/index.ts b/src/JobsJobsJobs/App/src/api/index.ts index 0cd8667..e86c7bd 100644 --- a/src/JobsJobsJobs/App/src/api/index.ts +++ b/src/JobsJobsJobs/App/src/api/index.ts @@ -6,6 +6,7 @@ import { Listing, ListingForm, ListingForView, + ListingSearch, LogOnSuccess, Profile, ProfileForm, @@ -171,6 +172,23 @@ export default { retreive: async (id : string, user : LogOnSuccess) : Promise => apiResult(await fetch(apiUrl(`listing/${id}`), reqInit('GET', user)), 'retrieving job listing'), + /** + * Search for job listings using the given parameters + * + * @param query The listing search parameters + * @param user The currently logged-on user + * @returns The matching job listings (if found), undefined (if API returns 404), or an error string + */ + search: async (query : ListingSearch, user : LogOnSuccess) : Promise => { + const params = new URLSearchParams() + if (query.continentId) params.append('continentId', query.continentId) + if (query.region) params.append('skill', query.region) + params.append('remoteWork', query.remoteWork) + if (query.text) params.append('text', query.text) + return apiResult(await fetch(apiUrl(`listing/search?${params.toString()}`), + reqInit('GET', user)), 'searching job listings') + }, + /** * Update an existing job listing * diff --git a/src/JobsJobsJobs/App/src/api/types.ts b/src/JobsJobsJobs/App/src/api/types.ts index f822b64..2c2d312 100644 --- a/src/JobsJobsJobs/App/src/api/types.ts +++ b/src/JobsJobsJobs/App/src/api/types.ts @@ -85,6 +85,18 @@ export interface ListingForView { continent : Continent } +/** The various ways job listings can be searched */ +export interface ListingSearch { + /** Retrieve opportunities from this continent */ + continentId : string | undefined + /** Text for a search for a specific region */ + region : string | undefined + /** Whether to retrieve job listings for remote work */ + remoteWork : string + /** Text to search with a job's full description */ + text : string | undefined +} + /** A successful logon */ export interface LogOnSuccess { /** The JSON Web Token (JWT) to use for API access */ diff --git a/src/JobsJobsJobs/App/src/components/ListingSearchForm.vue b/src/JobsJobsJobs/App/src/components/ListingSearchForm.vue new file mode 100644 index 0000000..764e44b --- /dev/null +++ b/src/JobsJobsJobs/App/src/components/ListingSearchForm.vue @@ -0,0 +1,80 @@ + + + diff --git a/src/JobsJobsJobs/App/src/components/layout/AppNav.vue b/src/JobsJobsJobs/App/src/components/layout/AppNav.vue index 4b1d0ea..45a4449 100644 --- a/src/JobsJobsJobs/App/src/components/layout/AppNav.vue +++ b/src/JobsJobsJobs/App/src/components/layout/AppNav.vue @@ -9,10 +9,10 @@   View Profiles -   My Job Listings - -   View Listings + +   Help Wanted! +   My Job Listings   Success Stories   Log Off diff --git a/src/JobsJobsJobs/App/src/router/index.ts b/src/JobsJobsJobs/App/src/router/index.ts index 1236c09..d412594 100644 --- a/src/JobsJobsJobs/App/src/router/index.ts +++ b/src/JobsJobsJobs/App/src/router/index.ts @@ -73,21 +73,26 @@ const routes: Array = [ component: () => import(/* webpackChunkName: "logoff" */ '../views/citizen/LogOff.vue') }, // Job Listing URLs + { + path: '/help-wanted', + name: 'HelpWanted', + component: () => import(/* webpackChunkName: "joblist" */ '../views/listing/HelpWanted.vue') + }, { path: '/listing/:id/edit', name: 'EditListing', component: () => import(/* webpackChunkName: "jobedit" */ '../views/listing/ListingEdit.vue') }, + { + path: '/listing/:id/view', + name: 'ViewListing', + component: () => import(/* webpackChunkName: "joblist" */ '../views/listing/ListingView.vue') + }, { path: '/listings/mine', name: 'MyListings', component: () => import(/* webpackChunkName: "joblist" */ '../views/listing/MyListings.vue') }, - { - path: '/listings/search', - name: 'SearchListings', - component: () => import(/* webpackChunkName: "joblist" */ '../views/listing/ListingSearch.vue') - }, // Profile URLs { path: '/profile/:id/view', diff --git a/src/JobsJobsJobs/App/src/views/listing/HelpWanted.vue b/src/JobsJobsJobs/App/src/views/listing/HelpWanted.vue new file mode 100644 index 0000000..a787c57 --- /dev/null +++ b/src/JobsJobsJobs/App/src/views/listing/HelpWanted.vue @@ -0,0 +1,145 @@ + + + diff --git a/src/JobsJobsJobs/App/src/views/listing/ListingSearch.vue b/src/JobsJobsJobs/App/src/views/listing/ListingSearch.vue deleted file mode 100644 index 005f4b6..0000000 --- a/src/JobsJobsJobs/App/src/views/listing/ListingSearch.vue +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/src/JobsJobsJobs/App/src/views/listing/ListingView.vue b/src/JobsJobsJobs/App/src/views/listing/ListingView.vue new file mode 100644 index 0000000..2cd404f --- /dev/null +++ b/src/JobsJobsJobs/App/src/views/listing/ListingView.vue @@ -0,0 +1,3 @@ + diff --git a/src/JobsJobsJobs/Domain/SharedTypes.fs b/src/JobsJobsJobs/Domain/SharedTypes.fs index 6274d8c..e609d77 100644 --- a/src/JobsJobsJobs/Domain/SharedTypes.fs +++ b/src/JobsJobsJobs/Domain/SharedTypes.fs @@ -34,6 +34,20 @@ type ListingForView = { } +/// The various ways job listings can be searched +[] +type ListingSearch = { + /// Retrieve job listings for this continent + continentId : string option + /// Text for a search within a region + region : string option + /// Whether to retrieve job listings for remote work + remoteWork : string + /// Text for a search with the job listing description + text : string option + } + + /// A successful logon type LogOnSuccess = { /// The JSON Web Token (JWT) to use for API access @@ -122,17 +136,6 @@ type ProfileSearch = { remoteWork : string } -/// Support functions for profile searches -module ProfileSearch = - /// Is the search empty? - let isEmptySearch search = - [ search.continentId - search.skill - search.bioExperience - match search.remoteWork with "" -> Some search.remoteWork | _ -> None - ] - |> List.exists Option.isSome - /// A user matching the profile search type ProfileSearchResult = {