Profile edit complete

This commit is contained in:
Daniel J. Summers 2021-08-08 19:14:02 -04:00
parent bd97a3828f
commit 33c0e5ff5b
5 changed files with 68 additions and 30 deletions

View File

@ -258,7 +258,9 @@ module Profile =
experience = noneIfBlank form.experience |> Option.map Text experience = noneIfBlank form.experience |> Option.map Text
skills = form.skills skills = form.skills
|> List.map (fun s -> |> List.map (fun s ->
{ id = SkillId.ofString s.id { id = match s.id.StartsWith "new" with
| true -> SkillId.create ()
| false -> SkillId.ofString s.id
description = s.description description = s.description
notes = noneIfBlank s.notes notes = noneIfBlank s.notes
}) })

View File

@ -1,5 +1,5 @@
<template> <template>
<div aria-live="polite" aria-atomic="true"> <div aria-live="polite" aria-atomic="true" id="toastHost">
<div class="toast-container position-absolute p-3 bottom-0 start-50 translate-middle-x" id="toasts"></div> <div class="toast-container position-absolute p-3 bottom-0 start-50 translate-middle-x" id="toasts"></div>
</div> </div>
</template> </template>
@ -81,3 +81,9 @@ export default defineComponent({
name: 'AppToaster' name: 'AppToaster'
}) })
</script> </script>
<style lang="sass" scoped>
#toastHost
position: sticky
bottom: 0
</style>

View File

@ -20,8 +20,7 @@ export default defineComponent({
</script> </script>
<style lang="sass" scoped> <style lang="sass" scoped>
span .navbar-text
font-style: italic font-style: italic
padding-right: 1rem padding-right: 1rem
// padding: 0 .5rem 0 0
</style> </style>

View File

@ -38,7 +38,7 @@ export default defineComponent({
required: true required: true
} }
}, },
emits: ['remove', 'update:modelValue'], emits: ['input', 'remove', 'update:modelValue'],
setup (props, { emit }) { setup (props, { emit }) {
/** The skill being edited */ /** The skill being edited */
const skill : Ref<Skill> = ref({ ...props.modelValue as Skill }) const skill : Ref<Skill> = ref({ ...props.modelValue as Skill })
@ -48,6 +48,7 @@ export default defineComponent({
updateValue: (key : string, value : string) => { updateValue: (key : string, value : string) => {
skill.value = { ...skill.value, [key]: value } skill.value = { ...skill.value, [key]: value }
emit('update:modelValue', skill.value) emit('update:modelValue', skill.value)
emit('input')
} }
} }
} }

View File

@ -1,12 +1,12 @@
<template> <template>
<article> <article>
<page-title title="Edit Profile" /> <page-title title="Edit Profile" />
<h3>Employment Profile</h3> <h3 class="pb-3">Employment Profile</h3>
<load-data :load="retrieveData"> <load-data :load="retrieveData">
<form class="row g-3"> <form class="row g-3">
<div class="col-12 col-sm-10 col-md-8 col-lg-6"> <div class="col-12 col-sm-10 col-md-8 col-lg-6">
<div class="form-floating"> <div class="form-floating">
<input type="text" id="realName" class="form-control" v-model="profile.realName" maxlength="255" <input type="text" id="realName" class="form-control" v-model="v$.realName.$model" maxlength="255"
placeholder="Leave blank to use your NAS display name"> placeholder="Leave blank to use your NAS display name">
<label for="realName">Real Name</label> <label for="realName">Real Name</label>
</div> </div>
@ -14,8 +14,8 @@
</div> </div>
<div class="col-12"> <div class="col-12">
<div class="form-check"> <div class="form-check">
<input type="checkbox" class="form-check-input" v-model="profile.seekingEmployment"> <input type="checkbox" id="isSeeking" class="form-check-input" v-model="v$.isSeekingEmployment.$model">
<label class="form-check-label">I am currently seeking employment</label> <label for="isSeeking" class="form-check-label">I am currently seeking employment</label>
</div> </div>
<p v-if="profile?.seekingEmployment"> <p v-if="profile?.seekingEmployment">
<em>If you have found employment, consider <router-link to="/success-story/add">telling your fellow <em>If you have found employment, consider <router-link to="/success-story/add">telling your fellow
@ -25,7 +25,7 @@
<div class="col-12 col-sm-6 col-md-4"> <div class="col-12 col-sm-6 col-md-4">
<div class="form-floating"> <div class="form-floating">
<select id="continentId" :class="{ 'form-select': true, 'is-invalid': v$.continentId.$error }" <select id="continentId" :class="{ 'form-select': true, 'is-invalid': v$.continentId.$error }"
:value="v$.continentId.$model"> :value="v$.continentId.$model" @change="continentChanged">
<option v-for="c in continents" :key="c.id" :value="c.id">{{c.name}}</option> <option v-for="c in continents" :key="c.id" :value="c.id">{{c.name}}</option>
</select> </select>
<label for="continentId" class="jjj-required">Continent</label> <label for="continentId" class="jjj-required">Continent</label>
@ -41,17 +41,17 @@
</div> </div>
<div class="form-text">Country, state, geographic area, etc.</div> <div class="form-text">Country, state, geographic area, etc.</div>
</div> </div>
<markdown-editor id="bio" label="Professional Biography" v-model:text="profile.biography" <markdown-editor id="bio" label="Professional Biography" v-model:text="v$.biography.$model"
:isInvalid="v$.biography.$error" /> :isInvalid="v$.biography.$error" />
<div class="col-12 col-offset-md-2 col-md-4"> <div class="col-12 col-offset-md-2 col-md-4">
<div class="form-check"> <div class="form-check">
<input type="checkbox" id="isRemote" class="form-check-input" v-model="profile.remoteWork"> <input type="checkbox" id="isRemote" class="form-check-input" v-model="v$.remoteWork.$model">
<label class="form-check-label" for="isRemote">I am looking for remote work</label> <label class="form-check-label" for="isRemote">I am looking for remote work</label>
</div> </div>
</div> </div>
<div class="col-12 col-md-4"> <div class="col-12 col-md-4">
<div class="form-check"> <div class="form-check">
<input type="checkbox" id="isFullTime" class="form-check-input" v-model="profile.fullTime"> <input type="checkbox" id="isFullTime" class="form-check-input" v-model="v$.fullTime.$model">
<label class="form-check-label" for="isFullTime">I am looking for full-time work</label> <label class="form-check-label" for="isFullTime">I am looking for full-time work</label>
</div> </div>
</div> </div>
@ -63,7 +63,7 @@
</h4> </h4>
</div> </div>
<profile-skill-edit v-for="(skill, idx) in profile.skills" :key="skill.id" v-model="profile.skills[idx]" <profile-skill-edit v-for="(skill, idx) in profile.skills" :key="skill.id" v-model="profile.skills[idx]"
@remove="removeSkill(skill.id)" /> @remove="removeSkill(skill.id)" @input="v$.skills.$touch" />
<div class="col-12"> <div class="col-12">
<hr> <hr>
<h4>Experience</h4> <h4>Experience</h4>
@ -73,10 +73,10 @@
already a part of your Professional Biography above. already a part of your Professional Biography above.
</p> </p>
</div> </div>
<markdown-editor id="experience" label="Experience" v-model:text="profile.experience" /> <markdown-editor id="experience" label="Experience" v-model:text="v$.experience.$model" />
<div class="col-12"> <div class="col-12">
<div class="form-check"> <div class="form-check">
<input type="checkbox" id="isPublic" class="form-check-input" v-model="profile.isPublic"> <input type="checkbox" id="isPublic" class="form-check-input" v-model="v$.isPublic.$model">
<label class="form-check-label" for="isPublic"> <label class="form-check-label" for="isPublic">
Allow my profile to be searched publicly (outside NA Social) Allow my profile to be searched publicly (outside NA Social)
</label> </label>
@ -104,7 +104,7 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, ref, reactive } from 'vue' import { computed, defineComponent, ref, reactive } from 'vue'
import { useRouter } from 'vue-router' import { onBeforeRouteLeave, useRouter } from 'vue-router'
import useVuelidate from '@vuelidate/core' import useVuelidate from '@vuelidate/core'
import { required } from '@vuelidate/validators' import { required } from '@vuelidate/validators'
import api, { Citizen, LogOnSuccess, Profile, ProfileForm } from '@/api' import api, { Citizen, LogOnSuccess, Profile, ProfileForm } from '@/api'
@ -151,14 +151,21 @@ export default defineComponent({
const profile = reactive(new ProfileForm()) const profile = reactive(new ProfileForm())
/** The validation rules for the form */ /** The validation rules for the form */
const rules = { const rules = computed(() => ({
realName: { },
isSeekingEmployment: { },
isPublic: { },
continentId: { required }, continentId: { required },
region: { required }, region: { required },
biography: { required } remoteWork: { },
} fullTime: { },
biography: { required },
experience: { },
skills: { }
}))
/** Initialize form validation */ /** Initialize form validation */
const v$ = useVuelidate(rules, profile /*, { $lazy: true } */) const v$ = useVuelidate(rules, profile, { $lazy: true })
/** Retrieve the user's profile and their real name */ /** Retrieve the user's profile and their real name */
const retrieveData = async (errors : string[]) => { const retrieveData = async (errors : string[]) => {
@ -188,14 +195,35 @@ export default defineComponent({
profile.realName = typeof nameResult !== 'undefined' ? (nameResult as Citizen).realName || '' : '' profile.realName = typeof nameResult !== 'undefined' ? (nameResult as Citizen).realName || '' : ''
} }
/**
* Mark the continent field as changed
*
* (This works around a really strange sequence where, if the "touch" call is directly wired up to the onChange
* event, the first time a value is selected, it doesn't stick (although the field is marked as touched). On second
* and subsequent times, it worked. The solution here is to grab the value and update the reactive source for the
* form, then manually set the field to touched; this restores the expected behavior. This is probably why the
* library doesn't hook into the onChange event to begin with...)
*/
const continentChanged = (e : Event) : boolean => {
profile.continentId = (e.target as HTMLSelectElement).value
v$.value.continentId.$touch()
return true
}
/** The ID for new skills */ /** The ID for new skills */
let newSkillId = 0 let newSkillId = 0
/** Add a skill to the profile */ /** Add a skill to the profile */
const addSkill = () => { profile.skills.push({ id: `new${newSkillId++}`, description: '', notes: undefined }) } const addSkill = () => {
profile.skills.push({ id: `new${newSkillId++}`, description: '', notes: undefined })
v$.value.skills.$touch()
}
/** Remove the given skill from the profile */ /** Remove the given skill from the profile */
const removeSkill = (skillId : string) => { profile.skills = profile.skills.filter(s => s.id !== skillId) } const removeSkill = (skillId : string) => {
profile.skills = profile.skills.filter(s => s.id !== skillId)
v$.value.skills.$touch()
}
/** Save the current profile values */ /** Save the current profile values */
const saveProfile = async () => { const saveProfile = async () => {
@ -212,14 +240,15 @@ export default defineComponent({
} }
} }
/** View the profile, prompting for save if data has changed */ /** If the user has unsaved changes, give them an opportunity to save before moving on */
const viewProfile = async () => { onBeforeRouteLeave(async (to, from) => { // eslint-disable-line
alert(v$.value.$dirty) if (!v$.value.$anyDirty) return true
if (v$.value.$dirty && confirm('There are unsaved changes; save before viewing?')) { if (confirm('There are unsaved changes; save before viewing?')) {
await saveProfile() await saveProfile()
router.push(`/profile/view/${user.citizenId}`) return true
} }
} return false
})
return { return {
v$, v$,
@ -228,10 +257,11 @@ export default defineComponent({
isNew, isNew,
profile, profile,
continents: computed(() => store.state.continents), continents: computed(() => store.state.continents),
continentChanged,
addSkill, addSkill,
removeSkill, removeSkill,
saveProfile, saveProfile,
viewProfile viewProfile: () => router.push(`/profile/view/${user.citizenId}`)
} }
} }
}) })