First cut of profile search migrated
This commit is contained in:
parent
9c9bb16cf2
commit
8f5ccd7338
|
@ -237,35 +237,46 @@ module Profile =
|
||||||
})
|
})
|
||||||
|
|
||||||
/// Search profiles (logged-on users)
|
/// Search profiles (logged-on users)
|
||||||
let search (srch : ProfileSearch) conn = task {
|
let search (srch : ProfileSearch) conn =
|
||||||
let results =
|
withReconn(conn).ExecuteAsync(fun () ->
|
||||||
seq {
|
(seq {
|
||||||
match srch.continentId with
|
match srch.continentId with
|
||||||
| Some conId ->
|
| Some conId ->
|
||||||
yield (fun (q : ReqlExpr) ->
|
yield (fun (q : ReqlExpr) ->
|
||||||
q.Filter(r.HashMap(nameof srch.continentId, ContinentId.ofString conId)) :> ReqlExpr)
|
q.Filter(r.HashMap(nameof srch.continentId, ContinentId.ofString conId)) :> ReqlExpr)
|
||||||
| None -> ()
|
| None -> ()
|
||||||
match srch.remoteWork with
|
match srch.remoteWork with
|
||||||
| "" -> ()
|
| "" -> ()
|
||||||
| _ -> yield (fun q -> q.Filter(r.HashMap(nameof srch.remoteWork, srch.remoteWork = "yes")) :> ReqlExpr)
|
| _ -> yield (fun q -> q.Filter(r.HashMap(nameof srch.remoteWork, srch.remoteWork = "yes")) :> ReqlExpr)
|
||||||
match srch.skill with
|
match srch.skill with
|
||||||
| Some skl ->
|
| Some skl ->
|
||||||
yield (fun q -> q.Filter(ReqlFunction1(fun it ->
|
yield (fun q -> q.Filter(ReqlFunction1(fun it ->
|
||||||
upcast it.G("skills.description").Downcase().Match(skl.ToLowerInvariant ()))) :> ReqlExpr)
|
upcast it.G("skills.description").Downcase().Match(skl.ToLowerInvariant ()))) :> ReqlExpr)
|
||||||
| None -> ()
|
| None -> ()
|
||||||
match srch.bioExperience with
|
match srch.bioExperience with
|
||||||
| Some text ->
|
| Some text ->
|
||||||
let txt = text.ToLowerInvariant ()
|
let txt = text.ToLowerInvariant ()
|
||||||
yield (fun q -> q.Filter(ReqlFunction1(fun it ->
|
yield (fun q -> q.Filter(ReqlFunction1(fun it ->
|
||||||
upcast it.G("biography" ).Downcase().Match(txt)
|
upcast it.G("biography" ).Downcase().Match(txt)
|
||||||
.Or(it.G("experience").Downcase().Match(txt)))) :> ReqlExpr)
|
.Or(it.G("experience").Downcase().Match(txt)))) :> ReqlExpr)
|
||||||
| None -> ()
|
| None -> ()
|
||||||
}
|
}
|
||||||
|> Seq.toList
|
|> Seq.toList
|
||||||
|> List.fold (fun q f -> f q) (r.Table(Table.Profile) :> ReqlExpr)
|
|> List.fold
|
||||||
// TODO: pluck fields, include display name
|
(fun q f -> f q)
|
||||||
return! results.RunResultAsync<ProfileSearchResult list> conn
|
(r.Table(Table.Profile)
|
||||||
}
|
.EqJoin("id", r.Table(Table.Citizen))
|
||||||
|
.Without(r.HashMap("right", "id"))
|
||||||
|
.Zip() :> ReqlExpr))
|
||||||
|
.Merge(ReqlFunction1(fun it ->
|
||||||
|
upcast r
|
||||||
|
.HashMap("displayName",
|
||||||
|
r.Branch(it.G("realName" ).Default_("").Ne(""), it.G("realName"),
|
||||||
|
it.G("displayName").Default_("").Ne(""), it.G("displayName"),
|
||||||
|
it.G("naUser")))
|
||||||
|
.With("citizenId", it.G("id"))))
|
||||||
|
.Pluck("citizenId", "displayName", "seekingEmployment", "remoteWork", "fullTime", "lastUpdatedOn")
|
||||||
|
.RunResultAsync<ProfileSearchResult list> conn)
|
||||||
|
|
||||||
// Search profiles (public)
|
// Search profiles (public)
|
||||||
let publicSearch (srch : PublicSearch) conn = task {
|
let publicSearch (srch : PublicSearch) conn = task {
|
||||||
|
@ -438,6 +449,11 @@ module Success =
|
||||||
.EqJoin("citizenId", r.Table(Table.Citizen))
|
.EqJoin("citizenId", r.Table(Table.Citizen))
|
||||||
.Without(r.HashMap("right", "id"))
|
.Without(r.HashMap("right", "id"))
|
||||||
.Zip()
|
.Zip()
|
||||||
.Merge(Javascript "function (s) { return { citizenName: s.realName || s.displayName || s.naUser } }")
|
.Merge(ReqlFunction1(fun it ->
|
||||||
|
upcast r
|
||||||
|
.HashMap("displayName",
|
||||||
|
r.Branch(it.G("realName" ).Default_("").Ne(""), it.G("realName"),
|
||||||
|
it.G("displayName").Default_("").Ne(""), it.G("displayName"),
|
||||||
|
it.G("naUser")))))
|
||||||
.Pluck("id", "citizenId", "citizenName", "recordedOn", "fromHere", "hasStory")
|
.Pluck("id", "citizenId", "citizenName", "recordedOn", "fromHere", "hasStory")
|
||||||
.RunResultAsync<StoryEntry list> conn)
|
.RunResultAsync<StoryEntry list> conn)
|
||||||
|
|
|
@ -1,5 +1,16 @@
|
||||||
import { MarkedOptions } from 'marked'
|
import { MarkedOptions } from 'marked'
|
||||||
import { Citizen, Continent, Count, LogOnSuccess, Profile, ProfileForView, StoryEntry, Success } from './types'
|
import {
|
||||||
|
Citizen,
|
||||||
|
Continent,
|
||||||
|
Count,
|
||||||
|
LogOnSuccess,
|
||||||
|
Profile,
|
||||||
|
ProfileForView,
|
||||||
|
ProfileSearch,
|
||||||
|
ProfileSearchResult,
|
||||||
|
StoryEntry,
|
||||||
|
Success
|
||||||
|
} from './types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a URL that will access the API
|
* Create a URL that will access the API
|
||||||
|
@ -126,6 +137,23 @@ export default {
|
||||||
retreiveForView: async (id : string, user : LogOnSuccess) : Promise<ProfileForView | string | undefined> =>
|
retreiveForView: async (id : string, user : LogOnSuccess) : Promise<ProfileForView | string | undefined> =>
|
||||||
apiResult<ProfileForView>(await fetch(apiUrl(`profile/view/${id}`), reqInit('GET', user)), 'retrieving profile'),
|
apiResult<ProfileForView>(await fetch(apiUrl(`profile/view/${id}`), reqInit('GET', user)), 'retrieving profile'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for profiles using the given parameters
|
||||||
|
*
|
||||||
|
* @param query The profile search parameters
|
||||||
|
* @param user The currently logged-on user
|
||||||
|
* @returns The matching profiles (if found), undefined (if API returns 404), or an error string
|
||||||
|
*/
|
||||||
|
search: async (query : ProfileSearch, user : LogOnSuccess) : Promise<ProfileSearchResult[] | string | undefined> => {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (query.continentId) params.append('continentId', query.continentId)
|
||||||
|
if (query.skill) params.append('skill', query.skill)
|
||||||
|
if (query.bioExperience) params.append('bioExperience', query.bioExperience)
|
||||||
|
params.append('remoteWork', query.remoteWork)
|
||||||
|
return apiResult<ProfileSearchResult[]>(await fetch(apiUrl(`profile/search?${params.toString()}`),
|
||||||
|
reqInit('GET', user)), 'searching profiles')
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Count profiles in the system
|
* Count profiles in the system
|
||||||
*
|
*
|
||||||
|
|
|
@ -81,6 +81,34 @@ export interface ProfileForView {
|
||||||
continent : Continent
|
continent : Continent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The various ways profiles can be searched */
|
||||||
|
export interface ProfileSearch {
|
||||||
|
/** Retrieve citizens from this continent */
|
||||||
|
continentId : string | undefined
|
||||||
|
/** Text for a search within a citizen's skills */
|
||||||
|
skill : string | undefined
|
||||||
|
/** Text for a search with a citizen's professional biography and experience fields */
|
||||||
|
bioExperience : string | undefined
|
||||||
|
/** Whether to retrieve citizens who do or do not want remote work */
|
||||||
|
remoteWork : string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A user matching the profile search */
|
||||||
|
export interface ProfileSearchResult {
|
||||||
|
/** The ID of the citizen */
|
||||||
|
citizenId : string
|
||||||
|
/** The citizen's display name */
|
||||||
|
displayName : string
|
||||||
|
/** Whether this citizen is currently seeking employment */
|
||||||
|
seekingEmployment : boolean
|
||||||
|
/** Whether this citizen is looking for remote work */
|
||||||
|
remoteWork : boolean
|
||||||
|
/** Whether this citizen is looking for full-time work */
|
||||||
|
fullTime : boolean
|
||||||
|
/** When this profile was last updated (date) */
|
||||||
|
lastUpdatedOn : string
|
||||||
|
}
|
||||||
|
|
||||||
/** A count */
|
/** A count */
|
||||||
export interface Count {
|
export interface Count {
|
||||||
/** The count being returned */
|
/** The count being returned */
|
||||||
|
|
55
src/JobsJobsJobs/App/src/components/CollapsePanel.vue
Normal file
55
src/JobsJobsJobs/App/src/components/CollapsePanel.vue
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
<template>
|
||||||
|
<v-card>
|
||||||
|
<v-card-header>
|
||||||
|
<v-card-header-text>
|
||||||
|
<v-card-title>
|
||||||
|
<a href="#" :class="{ 'cp-c': isCollapsed, 'cp-o': !isCollapsed }" @click.prevent="toggle">{{headerText}}</a>
|
||||||
|
</v-card-title>
|
||||||
|
</v-card-header-text>
|
||||||
|
</v-card-header>
|
||||||
|
<v-card-text v-if="!isCollapsed">
|
||||||
|
<slot></slot>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, ref } from 'vue'
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'CollapsePanel',
|
||||||
|
props: {
|
||||||
|
headerText: {
|
||||||
|
type: String,
|
||||||
|
default: 'Toggle'
|
||||||
|
},
|
||||||
|
collapsed: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup (props) {
|
||||||
|
/** Whether the panel is collapsed or not */
|
||||||
|
const isCollapsed = ref(props.collapsed)
|
||||||
|
|
||||||
|
return {
|
||||||
|
isCollapsed,
|
||||||
|
toggle: () => { isCollapsed.value = !isCollapsed.value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="sass" scoped>
|
||||||
|
a.cp-c,
|
||||||
|
a.cp-o
|
||||||
|
text-decoration: none
|
||||||
|
font-weight: bold
|
||||||
|
color: black
|
||||||
|
a.cp-c:hover,
|
||||||
|
a.cp-o:hover
|
||||||
|
cursor: pointer
|
||||||
|
.cp-c::before
|
||||||
|
content: '\2b9e \00a0'
|
||||||
|
.cp-o::before
|
||||||
|
content: '\2b9f \00a0'
|
||||||
|
</style>
|
23
src/JobsJobsJobs/App/src/components/ErrorList.vue
Normal file
23
src/JobsJobsJobs/App/src/components/ErrorList.vue
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<template>
|
||||||
|
<template v-if="errors.length > 0">
|
||||||
|
<p>The following error<template v-if="errors.length !== 1">s</template> occurred:</p>
|
||||||
|
<ul>
|
||||||
|
<li v-for="(error, idx) in errors" :key="idx"><pre>{{error}}</pre></li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
<slot v-else></slot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'ErrorList',
|
||||||
|
props: {
|
||||||
|
errors: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
|
@ -1,17 +1,17 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="loading">Loading…</div>
|
<div v-if="loading">Loading…</div>
|
||||||
<template v-else>
|
<error-list v-else :errors="errors">
|
||||||
<div v-if="errors.length > 0">
|
<slot></slot>
|
||||||
<p v-for="(error, idx) in errors" :key="idx">{{error}}</p>
|
</error-list>
|
||||||
</div>
|
|
||||||
<slot v-else></slot>
|
|
||||||
</template>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, onMounted, ref } from 'vue'
|
import { defineComponent, onMounted, ref } from 'vue'
|
||||||
|
import ErrorList from './ErrorList.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'LoadData',
|
name: 'LoadData',
|
||||||
|
components: { ErrorList },
|
||||||
props: {
|
props: {
|
||||||
load: {
|
load: {
|
||||||
type: Function,
|
type: Function,
|
||||||
|
|
82
src/JobsJobsJobs/App/src/components/profile/SearchForm.vue
Normal file
82
src/JobsJobsJobs/App/src/components/profile/SearchForm.vue
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
<template>
|
||||||
|
<form Model=@Criteria OnValidSubmit=@OnSearch>
|
||||||
|
<v-container>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" sm="6" md="4" lg="3">
|
||||||
|
<label for="continentId" class="jjj-label">Continent</label>
|
||||||
|
<select id="continentId" class="form-control form-control-sm"
|
||||||
|
:value="criteria.continentId" @change="updateValue('continentId', $event.target.value)">
|
||||||
|
<option value="">– Any –</option>
|
||||||
|
<option v-for="c in continents" :key="c.id" :value="c.id">{{c.name}}</option>
|
||||||
|
</select>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" offset-md="2" lg="3" offset-lg="0">
|
||||||
|
<label class="jjj-label">Seeking Remote Work?</label><br>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input type="radio" id="remoteNull" name="remoteWork" class="form-check-input"
|
||||||
|
:checked="criteria.remoteWork === ''" @click="updateValue('remoteWork', '')">
|
||||||
|
<label for="remoteNull" class="form-check-label">No Selection</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input type="radio" id="remoteYes" name="remoteWork" class="form-check-input"
|
||||||
|
:checked="criteria.remoteWork === 'yes'" @click="updateValue('remoteWork', 'yes')">
|
||||||
|
<label for="remoteYes" class="form-check-label">Yes</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input type="radio" id="remoteNo" name="remoteWork" class="form-check-input"
|
||||||
|
:checked="criteria.remoteWork === 'no'" @click="updateValue('remoteWork', 'no')">
|
||||||
|
<label for="remoteNo" class="form-check-label">No</label>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" lg="3">
|
||||||
|
<label for="skillSearch" class="jjj-label">Skill</label>
|
||||||
|
<input type="text" id="skillSearch" class="form-control form-control-sm" placeholder="(free-form text)"
|
||||||
|
:value="criteria.skill" @input="updateValue('skill', $event.target.value)">
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" lg="3">
|
||||||
|
<label for="bioSearch" class="jjj-label">Bio / Experience</label>
|
||||||
|
<input type="text" id="bioSearch" class="form-control form-control-sm" placeholder="(free-form text)"
|
||||||
|
:value="criteria.bioExperience" @input="updateValue('bioExperience', $event.target.value)">
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row class="form-row">
|
||||||
|
<v-col cols="12">
|
||||||
|
<br>
|
||||||
|
<v-btn type="submit" color="primary" variant="outlined" @click.prevent="$emit('search')">Search</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, onMounted } from 'vue'
|
||||||
|
import { ProfileSearch } from '@/api'
|
||||||
|
import { useStore } from '@/store'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'ProfileSearchForm',
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['search', 'update:modelValue'],
|
||||||
|
setup (props, { emit }) {
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
/** The initial search criteria passed; this is what we'll update and emit when data changes */
|
||||||
|
const criteria : ProfileSearch = { ...props.modelValue as ProfileSearch }
|
||||||
|
|
||||||
|
/** Make sure we have continents */
|
||||||
|
onMounted(async () => await store.dispatch('ensureContinents'))
|
||||||
|
|
||||||
|
return {
|
||||||
|
criteria,
|
||||||
|
continents: computed(() => store.state.continents),
|
||||||
|
updateValue: (key : string, value : string) => emit('update:modelValue', { ...criteria, [key]: value })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
|
@ -24,7 +24,7 @@ export default createStore({
|
||||||
state: () : State => {
|
state: () : State => {
|
||||||
return {
|
return {
|
||||||
user: undefined,
|
user: undefined,
|
||||||
logOnState: 'Logging you on with No Agenda Social...',
|
logOnState: '<em>Welcome back! Verifying your No Agenda Social account…</em>',
|
||||||
continents: []
|
continents: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<article>
|
<article>
|
||||||
<page-title title="Logging on..." />
|
<page-title title="Logging on..." />
|
||||||
<p>{{message}}</p>
|
<p v-html="message"></p>
|
||||||
</article>
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -32,7 +32,8 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
store.commit('setLogOnState', 'Did not receive a token from No Agenda Social (perhaps you clicked "Cancel"?)')
|
store.commit('setLogOnState',
|
||||||
|
'Did not receive a token from No Agenda Social (perhaps you clicked “Cancel”?)')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,144 @@
|
||||||
<template>
|
<template>
|
||||||
<p>TODO: convert this view</p>
|
<article>
|
||||||
|
<page-title title="Search Profiles" />
|
||||||
|
<h3>Search Profiles</h3>
|
||||||
|
|
||||||
|
<error-list :errors="errors">
|
||||||
|
<p v-if="searching">Searching profiles...</p>
|
||||||
|
<template v-else>
|
||||||
|
<p v-if="!searched">
|
||||||
|
Enter one or more criteria to filter results, or just click “Search” to list all profiles.
|
||||||
|
</p>
|
||||||
|
<collapse-panel headerText="Search Criteria" :collapsed="searched && results.length > 0">
|
||||||
|
<profile-search-form v-model="criteria" @search="doSearch" />
|
||||||
|
</collapse-panel>
|
||||||
|
<br>
|
||||||
|
<table v-if="results.length > 0" class="table table-sm table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Profile</th>
|
||||||
|
<th scope="col">Name</th>
|
||||||
|
<th scope="col" class="text-center">Seeking?</th>
|
||||||
|
<th scope="col" class="text-center">Remote?</th>
|
||||||
|
<th scope="col" class="text-center">Full-Time?</th>
|
||||||
|
<th scope="col">Last Updated</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="profile in results" :key="profile.citzenId">
|
||||||
|
<td><router-link :to="`/profile/view/${profile.citizenId}`">View</router-link></td>
|
||||||
|
<td :class="{ 'font-weight-bold' : profile.seekingEmployment }">{{profile.displayName}}</td>
|
||||||
|
<td class="text-center">{{yesOrNo(profile.seekingEmployment)}}</td>
|
||||||
|
<td class="text-center">{{yesOrNo(profile.remoteWork)}}</td>
|
||||||
|
<td class="text-center">{{yesOrNo(profile.fullTime)}}</td>
|
||||||
|
<td><full-date :date="profile.lastUpdatedOn" /></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p v-else-if="searched">No results found for the specified criteria</p>
|
||||||
|
</template>
|
||||||
|
</error-list>
|
||||||
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, Ref, ref, watch } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import api, { LogOnSuccess, ProfileSearch, ProfileSearchResult } from '@/api'
|
||||||
|
import { useStore } from '@/store'
|
||||||
|
|
||||||
|
import CollapsePanel from '@/components/CollapsePanel.vue'
|
||||||
|
import ErrorList from '@/components/ErrorList.vue'
|
||||||
|
import FullDate from '@/components/FullDate.vue'
|
||||||
|
import ProfileSearchForm from '@/components/profile/SearchForm.vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'ProfileSearch',
|
||||||
|
components: {
|
||||||
|
CollapsePanel,
|
||||||
|
ErrorList,
|
||||||
|
FullDate,
|
||||||
|
ProfileSearchForm
|
||||||
|
},
|
||||||
|
setup () {
|
||||||
|
const store = useStore()
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
/** Any errors encountered while retrieving data */
|
||||||
|
const errors : Ref<string[]> = ref([])
|
||||||
|
|
||||||
|
/** Whether we are currently searching (retrieving data) */
|
||||||
|
const searching = ref(false)
|
||||||
|
|
||||||
|
/** Whether a search has been performed on this page since it has been loaded */
|
||||||
|
const searched = ref(false)
|
||||||
|
|
||||||
|
/** An empty set of search criteria */
|
||||||
|
const emptyCriteria = {
|
||||||
|
continentId: undefined,
|
||||||
|
skill: undefined,
|
||||||
|
bioExperience: undefined,
|
||||||
|
remoteWork: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The search criteria being built from the page */
|
||||||
|
const criteria : Ref<ProfileSearch> = ref(emptyCriteria)
|
||||||
|
|
||||||
|
/** The current search results */
|
||||||
|
const results : Ref<ProfileSearchResult[]> = ref([])
|
||||||
|
|
||||||
|
/** Return "Yes" for true and "No" for false */
|
||||||
|
const yesOrNo = (cond : boolean) => cond ? 'Yes' : 'No'
|
||||||
|
|
||||||
|
/** Get a value from the query string */
|
||||||
|
const queryValue = (key : string) : string | undefined => {
|
||||||
|
const value = route.query[key]
|
||||||
|
if (value) return Array.isArray(value) && value.length > 0 ? value[0]?.toString() : value.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
const setUpPage = async () => {
|
||||||
|
if (queryValue('searched') === 'true') {
|
||||||
|
searched.value = true
|
||||||
|
try {
|
||||||
|
searching.value = true
|
||||||
|
const searchParams : ProfileSearch = { // eslint-disable-line
|
||||||
|
continentId: queryValue('continentId'),
|
||||||
|
skill: queryValue('skill'),
|
||||||
|
bioExperience: queryValue('bioExperience'),
|
||||||
|
remoteWork: queryValue('remoteWork') || ''
|
||||||
|
}
|
||||||
|
const searchResult = await api.profile.search(searchParams, store.state.user as LogOnSuccess)
|
||||||
|
if (typeof searchResult === 'string') {
|
||||||
|
errors.value.push(searchResult)
|
||||||
|
} else if (searchResult === undefined) {
|
||||||
|
errors.value.push('The server returned a "Not Found" response (this should not happen)')
|
||||||
|
} else {
|
||||||
|
results.value = searchResult
|
||||||
|
criteria.value = searchParams
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
searching.value = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
searched.value = false
|
||||||
|
criteria.value = emptyCriteria
|
||||||
|
errors.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => route.query, setUpPage, { immediate: true })
|
||||||
|
|
||||||
|
return {
|
||||||
|
errors,
|
||||||
|
criteria,
|
||||||
|
doSearch: () => router.push({ query: { searched: 'true', ...criteria.value } }),
|
||||||
|
searching,
|
||||||
|
searched,
|
||||||
|
yesOrNo,
|
||||||
|
results,
|
||||||
|
continents: computed(() => store.state.continents)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
|
@ -82,6 +82,7 @@ module ProfileForm =
|
||||||
|
|
||||||
|
|
||||||
/// The various ways profiles can be searched
|
/// The various ways profiles can be searched
|
||||||
|
[<CLIMutable>]
|
||||||
type ProfileSearch = {
|
type ProfileSearch = {
|
||||||
/// Retrieve citizens from this continent
|
/// Retrieve citizens from this continent
|
||||||
continentId : string option
|
continentId : string option
|
||||||
|
@ -107,18 +108,18 @@ module ProfileSearch =
|
||||||
|
|
||||||
/// A user matching the profile search
|
/// A user matching the profile search
|
||||||
type ProfileSearchResult = {
|
type ProfileSearchResult = {
|
||||||
// The ID of the citizen
|
/// The ID of the citizen
|
||||||
citizenId : CitizenId
|
citizenId : CitizenId
|
||||||
// The citizen's display name
|
/// The citizen's display name
|
||||||
displayName : string
|
displayName : string
|
||||||
// Whether this citizen is currently seeking employment
|
/// Whether this citizen is currently seeking employment
|
||||||
seekingEmployment : bool
|
seekingEmployment : bool
|
||||||
// Whether this citizen is looking for remote work
|
/// Whether this citizen is looking for remote work
|
||||||
remoteWork : bool
|
remoteWork : bool
|
||||||
// Whether this citizen is looking for full-time work
|
/// Whether this citizen is looking for full-time work
|
||||||
fullTime : bool
|
fullTime : bool
|
||||||
// When this profile was last updated
|
/// When this profile was last updated
|
||||||
lastUpdated : Instant
|
lastUpdatedOn : Instant
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user