Migration now at feature parity (plus)
This commit is contained in:
parent
2922ced971
commit
f8ea4f4e8d
|
@ -54,7 +54,7 @@ html
|
||||||
a:link,
|
a:link,
|
||||||
a:visited
|
a:visited
|
||||||
text-decoration: none
|
text-decoration: none
|
||||||
a:hover
|
a:not(.btn):hover
|
||||||
text-decoration: underline
|
text-decoration: underline
|
||||||
label.jjj-required::after
|
label.jjj-required::after
|
||||||
color: red
|
color: red
|
||||||
|
|
|
@ -3,6 +3,7 @@ import {
|
||||||
Citizen,
|
Citizen,
|
||||||
Continent,
|
Continent,
|
||||||
Count,
|
Count,
|
||||||
|
Listing,
|
||||||
LogOnSuccess,
|
LogOnSuccess,
|
||||||
Profile,
|
Profile,
|
||||||
ProfileForm,
|
ProfileForm,
|
||||||
|
@ -135,6 +136,19 @@ export default {
|
||||||
apiResult<Continent[]>(await fetch(apiUrl('continent/all'), { method: 'GET' }), 'retrieving continents')
|
apiResult<Continent[]>(await fetch(apiUrl('continent/all'), { method: 'GET' }), 'retrieving continents')
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** API functions for job listings */
|
||||||
|
listings: {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the job listings posted by the current citizen
|
||||||
|
*
|
||||||
|
* @param user The currently logged-on user
|
||||||
|
* @returns The job listings the user has posted, or an error string
|
||||||
|
*/
|
||||||
|
mine: async (user : LogOnSuccess) : Promise<Listing[] | string | undefined> =>
|
||||||
|
apiResult<Listing[]>(await fetch(apiUrl('listings/mine'), reqInit('GET', user)), 'retrieving your job listings')
|
||||||
|
},
|
||||||
|
|
||||||
/** API functions for profiles */
|
/** API functions for profiles */
|
||||||
profile: {
|
profile: {
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,34 @@ export interface Count {
|
||||||
count : number
|
count : number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A job listing */
|
||||||
|
export interface Listing {
|
||||||
|
/** The ID of the job listing */
|
||||||
|
id : string
|
||||||
|
/** The ID of the citizen who posted the job listing */
|
||||||
|
citizenId : string
|
||||||
|
/** When this job listing was created (date) */
|
||||||
|
createdOn : string
|
||||||
|
/** The short title of the job listing */
|
||||||
|
title : string
|
||||||
|
/** The ID of the continent on which the job is located */
|
||||||
|
continentId : string
|
||||||
|
/** The region in which the job is located */
|
||||||
|
region : string
|
||||||
|
/** Whether this listing is for remote work */
|
||||||
|
remoteWork : boolean
|
||||||
|
/** Whether this listing has expired */
|
||||||
|
isExpired : boolean
|
||||||
|
/** When this listing was last updated (date) */
|
||||||
|
updatedOn : string
|
||||||
|
/** The details of this job */
|
||||||
|
text : string
|
||||||
|
/** When this job needs to be filled (date) */
|
||||||
|
neededBy : string | undefined
|
||||||
|
/** Was this job filled as part of its appearance on Jobs, Jobs, Jobs? */
|
||||||
|
wasFilledHere : boolean | undefined
|
||||||
|
}
|
||||||
|
|
||||||
/** A successful logon */
|
/** A successful logon */
|
||||||
export interface LogOnSuccess {
|
export interface LogOnSuccess {
|
||||||
/** The JSON Web Token (JWT) to use for API access */
|
/** The JSON Web Token (JWT) to use for API access */
|
||||||
|
|
92
src/JobsJobsJobs/App/src/components/MaybeSave.vue
Normal file
92
src/JobsJobsJobs/App/src/components/MaybeSave.vue
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
<template>
|
||||||
|
<div class="modal fade" id="maybeSaveModal" tabindex="-1" aria-labelledby="maybeSaveLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="maybeSaveLabel">Unsaved Changes</h5>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
You have modified the data on this page since it was last saved. What would you like to do?
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" @click.prevent="onStay">Stay on This Page</button>
|
||||||
|
<button type="button" class="btn btn-primary" @click.prevent="onSave">Save Changes</button>
|
||||||
|
<button type="button" class="btn btn-danger" @click.prevent="onDiscard">Discard Changes</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, onMounted, ref, Ref, watch } from 'vue'
|
||||||
|
import { RouteLocationNormalized, useRouter } from 'vue-router'
|
||||||
|
import { Validation } from '@vuelidate/core'
|
||||||
|
import { Modal } from 'bootstrap'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'MaybeSave',
|
||||||
|
props: {
|
||||||
|
isShown: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
toRoute: {
|
||||||
|
// Can't type this because it's not filled until just before the modal is shown
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
saveAction: {
|
||||||
|
type: Function
|
||||||
|
},
|
||||||
|
validator: {
|
||||||
|
type: Object
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['close', 'discard', 'cancel'],
|
||||||
|
setup (props, { emit }) {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
/** The route where we tried to go */
|
||||||
|
const newRoute = computed(() => props.toRoute as RouteLocationNormalized)
|
||||||
|
|
||||||
|
/** Reference to the modal dialog (we can't get it until the component is rendered) */
|
||||||
|
const modal : Ref<Modal | undefined> = ref(undefined)
|
||||||
|
|
||||||
|
/** Save changes (if required) and go to the next route */
|
||||||
|
const onSave = async () => {
|
||||||
|
if (props.saveAction) await Promise.resolve(props.saveAction())
|
||||||
|
emit('close')
|
||||||
|
router.push(newRoute.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Discard changes (if required) and go to the next route */
|
||||||
|
const onDiscard = () => {
|
||||||
|
if (props.validator) (props.validator as Validation).$reset()
|
||||||
|
emit('close')
|
||||||
|
router.push(newRoute.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
modal.value = new Modal(document.getElementById('maybeSaveModal') as HTMLElement,
|
||||||
|
{ backdrop: 'static', keyboard: false })
|
||||||
|
})
|
||||||
|
|
||||||
|
/** Show or hide the modal based on the property value changing */
|
||||||
|
watch(() => props.isShown, (toShow) => {
|
||||||
|
if (modal.value) {
|
||||||
|
if (toShow) {
|
||||||
|
modal.value.show()
|
||||||
|
} else {
|
||||||
|
modal.value.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
onStay: () => emit('close'),
|
||||||
|
onSave,
|
||||||
|
onDiscard
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
|
@ -72,9 +72,20 @@ const routes: Array<RouteRecordRaw> = [
|
||||||
name: 'LogOff',
|
name: 'LogOff',
|
||||||
component: () => import(/* webpackChunkName: "logoff" */ '../views/citizen/LogOff.vue')
|
component: () => import(/* webpackChunkName: "logoff" */ '../views/citizen/LogOff.vue')
|
||||||
},
|
},
|
||||||
|
// Job Listing URLs
|
||||||
|
{
|
||||||
|
path: '/listing/:id/edit',
|
||||||
|
name: 'EditListing',
|
||||||
|
component: () => import(/* webpackChunkName: "jobedit" */ '../views/listing/ListingEdit.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/listings/mine',
|
||||||
|
name: 'MyListings',
|
||||||
|
component: () => import(/* webpackChunkName: "joblist" */ '../views/listing/MyListings.vue')
|
||||||
|
},
|
||||||
// Profile URLs
|
// Profile URLs
|
||||||
{
|
{
|
||||||
path: '/profile/view/:id',
|
path: '/profile/:id/view',
|
||||||
name: 'ViewProfile',
|
name: 'ViewProfile',
|
||||||
component: () => import(/* webpackChunkName: "profview" */ '../views/profile/ProfileView.vue')
|
component: () => import(/* webpackChunkName: "profview" */ '../views/profile/ProfileView.vue')
|
||||||
},
|
},
|
||||||
|
@ -106,12 +117,12 @@ const routes: Array<RouteRecordRaw> = [
|
||||||
component: () => import(/* webpackChunkName: "success" */ '../views/success-story/StoryList.vue')
|
component: () => import(/* webpackChunkName: "success" */ '../views/success-story/StoryList.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/success-story/edit/:id',
|
path: '/success-story/:id/edit',
|
||||||
name: 'EditStory',
|
name: 'EditStory',
|
||||||
component: () => import(/* webpackChunkName: "succedit" */ '../views/success-story/StoryEdit.vue')
|
component: () => import(/* webpackChunkName: "succedit" */ '../views/success-story/StoryEdit.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/success-story/view/:id',
|
path: '/success-story/:id/view',
|
||||||
name: 'ViewStory',
|
name: 'ViewStory',
|
||||||
component: () => import(/* webpackChunkName: "success" */ '../views/success-story/StoryView.vue')
|
component: () => import(/* webpackChunkName: "success" */ '../views/success-story/StoryView.vue')
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,10 +27,11 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
<template v-if="profile">
|
<template v-if="profile">
|
||||||
<button class="btn btn-outline-secondary" @click="viewProfile">View Profile</button>
|
<router-link class="btn btn-outline-secondary"
|
||||||
<button class="btn btn-outline-secondary" @click="editProfile">Edit Profile</button>
|
:to="`/profile/${user.citizenId}/view`">View Profile</router-link>
|
||||||
|
<router-link class="btn btn-outline-secondary" to="/citizen/profile">Edit Profile</router-link>
|
||||||
</template>
|
</template>
|
||||||
<button v-else class="btn btn-primary" @click="editProfile">Create Profile</button>
|
<router-link v-else class="btn btn-primary" to="/citizen/profile">Create Profile</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -53,7 +54,7 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
<button class="btn btn-outline-secondary" @click="searchProfiles">Search Profiles</button>
|
<router-link class="btn btn-outline-secondary" to="/profile/search">Search Profiles</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -69,7 +70,6 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, Ref, ref } from 'vue'
|
import { defineComponent, Ref, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import api, { LogOnSuccess, Profile } from '@/api'
|
import api, { LogOnSuccess, Profile } from '@/api'
|
||||||
import { useStore } from '@/store'
|
import { useStore } from '@/store'
|
||||||
|
|
||||||
|
@ -84,7 +84,6 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
setup () {
|
setup () {
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
/** The currently logged-in user */
|
/** The currently logged-in user */
|
||||||
const user = store.state.user as LogOnSuccess
|
const user = store.state.user as LogOnSuccess
|
||||||
|
@ -114,10 +113,7 @@ export default defineComponent({
|
||||||
retrieveData,
|
retrieveData,
|
||||||
user,
|
user,
|
||||||
profile,
|
profile,
|
||||||
profileCount,
|
profileCount
|
||||||
viewProfile: () => router.push(`/profile/view/${user.citizenId}`),
|
|
||||||
editProfile: () => router.push('/citizen/profile'),
|
|
||||||
searchProfiles: () => router.push('/profile/search')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
<label for="isSeeking" 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.isSeekingEmployment">
|
<p v-if="profile.isSeekingEmployment">
|
||||||
<em>If you have found employment, consider <router-link to="/success-story/edit/new">telling your fellow
|
<em>If you have found employment, consider <router-link to="/success-story/new/edit">telling your fellow
|
||||||
citizens about it!</router-link></em>
|
citizens about it!</router-link></em>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -84,12 +84,14 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<p v-if="v$.$error" class="text-danger">Please correct the errors above</p>
|
<p v-if="v$.$error" class="text-danger">Please correct the errors above</p>
|
||||||
<button class="btn btn-primary" @click.prevent="saveProfile">Save</button>
|
<button class="btn btn-primary" @click.prevent="saveProfile">
|
||||||
|
<icon icon="content-save-outline" /> Save
|
||||||
|
</button>
|
||||||
<template v-if="!isNew">
|
<template v-if="!isNew">
|
||||||
|
|
||||||
<button class="btn btn-outline-secondary" @click.prevent="viewProfile">
|
<router-link class="btn btn-outline-secondary" :to="`/profile/${user.citizenId}/view`">
|
||||||
<icon icon="file-account-outline" /> View Your User Profile
|
<icon icon="file-account-outline" /> View Your User Profile
|
||||||
</button>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -99,20 +101,24 @@
|
||||||
(If you want to delete your profile, or your entire account, <router-link to="/so-long/options">see your deletion
|
(If you want to delete your profile, or your entire account, <router-link to="/so-long/options">see your deletion
|
||||||
options here</router-link>.)
|
options here</router-link>.)
|
||||||
</p>
|
</p>
|
||||||
|
<maybe-save :isShown="confirmNavShown" :toRoute="nextRoute" :saveAction="saveProfile" :validator="v$"
|
||||||
|
@close="confirmClose" />
|
||||||
</article>
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, ref, reactive } from 'vue'
|
import { computed, defineComponent, ref, reactive, Ref } from 'vue'
|
||||||
import { onBeforeRouteLeave, useRouter } from 'vue-router'
|
import { onBeforeRouteLeave, RouteLocationNormalized } 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'
|
||||||
|
import { toastError, toastSuccess } from '@/components/layout/AppToaster.vue'
|
||||||
import { useStore } from '@/store'
|
import { useStore } from '@/store'
|
||||||
|
|
||||||
import LoadData from '@/components/LoadData.vue'
|
import LoadData from '@/components/LoadData.vue'
|
||||||
import MarkdownEditor from '@/components/MarkdownEditor.vue'
|
import MarkdownEditor from '@/components/MarkdownEditor.vue'
|
||||||
import { toastError, toastSuccess } from '@/components/layout/AppToaster.vue'
|
import MaybeSave from '@/components/MaybeSave.vue'
|
||||||
import ProfileSkillEdit from '@/components/profile/SkillEdit.vue'
|
import ProfileSkillEdit from '@/components/profile/SkillEdit.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
@ -120,11 +126,11 @@ export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
LoadData,
|
LoadData,
|
||||||
MarkdownEditor,
|
MarkdownEditor,
|
||||||
|
MaybeSave,
|
||||||
ProfileSkillEdit
|
ProfileSkillEdit
|
||||||
},
|
},
|
||||||
setup () {
|
setup () {
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
/** The currently logged-on user */
|
/** The currently logged-on user */
|
||||||
const user = store.state.user as LogOnSuccess
|
const user = store.state.user as LogOnSuccess
|
||||||
|
@ -240,13 +246,17 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether the navigation confirmation is shown */
|
||||||
|
const confirmNavShown = ref(false)
|
||||||
|
|
||||||
|
/** The "next" route (will be navigated or cleared) */
|
||||||
|
const nextRoute : Ref<RouteLocationNormalized | undefined> = ref(undefined)
|
||||||
|
|
||||||
/** If the user has unsaved changes, give them an opportunity to save before moving on */
|
/** If the user has unsaved changes, give them an opportunity to save before moving on */
|
||||||
onBeforeRouteLeave(async (to, from) => { // eslint-disable-line
|
onBeforeRouteLeave(async (to, from) => { // eslint-disable-line
|
||||||
if (!v$.value.$anyDirty) return true
|
if (!v$.value.$anyDirty) return true
|
||||||
if (confirm('There are unsaved changes; save before viewing?')) {
|
nextRoute.value = to
|
||||||
await saveProfile()
|
confirmNavShown.value = true
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -261,7 +271,9 @@ export default defineComponent({
|
||||||
addSkill,
|
addSkill,
|
||||||
removeSkill,
|
removeSkill,
|
||||||
saveProfile,
|
saveProfile,
|
||||||
viewProfile: () => router.push(`/profile/view/${user.citizenId}`)
|
confirmNavShown,
|
||||||
|
nextRoute,
|
||||||
|
confirmClose: () => { confirmNavShown.value = false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
3
src/JobsJobsJobs/App/src/views/listing/ListingEdit.vue
Normal file
3
src/JobsJobsJobs/App/src/views/listing/ListingEdit.vue
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<p>TODO: placeholder</p>
|
||||||
|
</template>
|
70
src/JobsJobsJobs/App/src/views/listing/MyListings.vue
Normal file
70
src/JobsJobsJobs/App/src/views/listing/MyListings.vue
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
<template>
|
||||||
|
<article>
|
||||||
|
<page-title title="My Job Listings" />
|
||||||
|
<h3 class="pb-3">My Job Listings</h3>
|
||||||
|
<p>
|
||||||
|
<router-link class="btn btn-primary-outline" to="/listing/new/edit">Add a New Job Listing</router-link>
|
||||||
|
</p>
|
||||||
|
<load-data :load="getListings">
|
||||||
|
<table v-if="listings.length > 0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Action</th>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Created</th>
|
||||||
|
<th>Updated</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="listing in listings" :key="listing.id">
|
||||||
|
<td><router-link :to="`/listing/${listing.Id}/edit`">Edit</router-link></td>
|
||||||
|
<td>{{listing.Title}}</td>
|
||||||
|
<td><full-date-time :date="listing.createdOn" /></td>
|
||||||
|
<td><full-date-time :date="listing.updatedOn" /></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p v-else class="fst-italic">No job listings found</p>
|
||||||
|
</load-data>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, Ref, ref } from 'vue'
|
||||||
|
import api, { Listing, LogOnSuccess } from '@/api'
|
||||||
|
import { useStore } from '@/store'
|
||||||
|
|
||||||
|
import FullDateTime from '@/components/FullDateTime.vue'
|
||||||
|
import LoadData from '@/components/LoadData.vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'MyListings',
|
||||||
|
components: {
|
||||||
|
FullDateTime,
|
||||||
|
LoadData
|
||||||
|
},
|
||||||
|
setup () {
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
/** The listings for the user */
|
||||||
|
const listings : Ref<Listing[]> = ref([])
|
||||||
|
|
||||||
|
/** Retrieve the job listing posted by the current citizen */
|
||||||
|
const getListings = async (errors : string[]) => {
|
||||||
|
const listResult = await api.listings.mine(store.state.user as LogOnSuccess)
|
||||||
|
if (typeof listResult === 'string') {
|
||||||
|
errors.push(listResult)
|
||||||
|
} else if (typeof listResult === 'undefined') {
|
||||||
|
errors.push('API call returned 404 (this should not happen)')
|
||||||
|
} else {
|
||||||
|
listings.value = listResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getListings,
|
||||||
|
listings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
|
@ -25,7 +25,7 @@
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="profile in results" :key="profile.citzenId">
|
<tr v-for="profile in results" :key="profile.citzenId">
|
||||||
<td><router-link :to="`/profile/view/${profile.citizenId}`">View</router-link></td>
|
<td><router-link :to="`/profile/${profile.citizenId}/view`">View</router-link></td>
|
||||||
<td :class="{ 'font-weight-bold' : profile.seekingEmployment }">{{profile.displayName}}</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.seekingEmployment)}}</td>
|
||||||
<td class="text-center">{{yesOrNo(profile.remoteWork)}}</td>
|
<td class="text-center">{{yesOrNo(profile.remoteWork)}}</td>
|
||||||
|
|
|
@ -26,7 +26,9 @@
|
||||||
|
|
||||||
<template v-if="user.citizenId === it.citizen.id">
|
<template v-if="user.citizenId === it.citizen.id">
|
||||||
<br><br>
|
<br><br>
|
||||||
<button class="btn btn-primary" @click="editProfile"><icon icon="pencil" /> Edit Your Profile</button>
|
<router-link class="btn btn-primary" to="/citizen/profile">
|
||||||
|
<icon icon="pencil" /> Edit Your Profile
|
||||||
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
</load-data>
|
</load-data>
|
||||||
</article>
|
</article>
|
||||||
|
@ -34,7 +36,7 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, ref, Ref } from 'vue'
|
import { computed, defineComponent, ref, Ref } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import marked from 'marked'
|
import marked from 'marked'
|
||||||
import api, { LogOnSuccess, markedOptions, ProfileForView } from '@/api'
|
import api, { LogOnSuccess, markedOptions, ProfileForView } from '@/api'
|
||||||
import { useStore } from '@/store'
|
import { useStore } from '@/store'
|
||||||
|
@ -46,7 +48,6 @@ export default defineComponent({
|
||||||
setup () {
|
setup () {
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
/** The currently logged-on user */
|
/** The currently logged-on user */
|
||||||
const user = store.state.user as LogOnSuccess
|
const user = store.state.user as LogOnSuccess
|
||||||
|
@ -96,8 +97,7 @@ export default defineComponent({
|
||||||
workTypes,
|
workTypes,
|
||||||
citizenName,
|
citizenName,
|
||||||
bioHtml: computed(() => marked(it.value?.profile.biography || '', markedOptions)),
|
bioHtml: computed(() => marked(it.value?.profile.biography || '', markedOptions)),
|
||||||
expHtml: computed(() => marked(it.value?.profile.experience || '', markedOptions)),
|
expHtml: computed(() => marked(it.value?.profile.experience || '', markedOptions))
|
||||||
editProfile: () => router.push('/citizen/profile')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -17,19 +17,23 @@
|
||||||
</div>
|
</div>
|
||||||
<markdown-editor id="story" label="The Success Story" v-model:text="v$.story.$model" />
|
<markdown-editor id="story" label="The Success Story" v-model:text="v$.story.$model" />
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<button type="submit" class="btn btn-primary" @click.prevent="saveStory(true)"><icon icon="" /> Save</button>
|
<button type="submit" class="btn btn-primary" @click.prevent="saveStory(true)">
|
||||||
|
<icon icon="content-save-outline" /> Save
|
||||||
|
</button>
|
||||||
<p v-if="isNew">
|
<p v-if="isNew">
|
||||||
<em>(Saving this will set “Seeking Employment” to “No” on your profile.)</em>
|
<em>(Saving this will set “Seeking Employment” to “No” on your profile.)</em>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</load-data>
|
</load-data>
|
||||||
|
<maybe-save :isShown="confirmNavShown" :toRoute="nextRoute" :saveAction="doSave" :validator="v$"
|
||||||
|
@close="confirmClose" />
|
||||||
</article>
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, reactive } from 'vue'
|
import { computed, defineComponent, reactive, ref, Ref } from 'vue'
|
||||||
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
|
import { onBeforeRouteLeave, RouteLocationNormalized, useRoute, useRouter } from 'vue-router'
|
||||||
import useVuelidate from '@vuelidate/core'
|
import useVuelidate from '@vuelidate/core'
|
||||||
import api, { LogOnSuccess, StoryForm } from '@/api'
|
import api, { LogOnSuccess, StoryForm } from '@/api'
|
||||||
import { toastError, toastSuccess } from '@/components/layout/AppToaster.vue'
|
import { toastError, toastSuccess } from '@/components/layout/AppToaster.vue'
|
||||||
|
@ -37,12 +41,14 @@ import { useStore } from '@/store'
|
||||||
|
|
||||||
import LoadData from '@/components/LoadData.vue'
|
import LoadData from '@/components/LoadData.vue'
|
||||||
import MarkdownEditor from '@/components/MarkdownEditor.vue'
|
import MarkdownEditor from '@/components/MarkdownEditor.vue'
|
||||||
|
import MaybeSave from '@/components/MaybeSave.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'StoryEdit',
|
name: 'StoryEdit',
|
||||||
components: {
|
components: {
|
||||||
LoadData,
|
LoadData,
|
||||||
MarkdownEditor
|
MarkdownEditor,
|
||||||
|
MaybeSave
|
||||||
},
|
},
|
||||||
setup () {
|
setup () {
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
@ -105,28 +111,32 @@ export default defineComponent({
|
||||||
toastError(foundResult, 'clearing employment flag')
|
toastError(foundResult, 'clearing employment flag')
|
||||||
} else {
|
} else {
|
||||||
toastSuccess('Success Story saved and Seeking Employment flag cleared successfully')
|
toastSuccess('Success Story saved and Seeking Employment flag cleared successfully')
|
||||||
|
v$.value.$reset()
|
||||||
if (navigate) {
|
if (navigate) {
|
||||||
router.push('/success-story/list')
|
router.push('/success-story/list')
|
||||||
v$.value.$reset()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toastSuccess('Success Story saved successfully')
|
toastSuccess('Success Story saved successfully')
|
||||||
|
v$.value.$reset()
|
||||||
if (navigate) {
|
if (navigate) {
|
||||||
router.push('/success-story/list')
|
router.push('/success-story/list')
|
||||||
v$.value.$reset()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether the navigation confirmation is shown */
|
||||||
|
const confirmNavShown = ref(false)
|
||||||
|
|
||||||
|
/** The "next" route (will be navigated or cleared) */
|
||||||
|
const nextRoute : Ref<RouteLocationNormalized | undefined> = ref(undefined)
|
||||||
|
|
||||||
/** Prompt for save if the user navigates away with unsaved changes */
|
/** Prompt for save if the user navigates away with unsaved changes */
|
||||||
onBeforeRouteLeave(async (to, from) => { // eslint-disable-line
|
onBeforeRouteLeave(async (to, from) => { // eslint-disable-line
|
||||||
if (!v$.value.$anyDirty) return true
|
if (!v$.value.$anyDirty) return true
|
||||||
if (confirm('There are unsaved changes; save before leaving?')) {
|
nextRoute.value = to
|
||||||
await saveStory(false)
|
confirmNavShown.value = true
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -135,7 +145,11 @@ export default defineComponent({
|
||||||
isNew,
|
isNew,
|
||||||
retrieveStory,
|
retrieveStory,
|
||||||
v$,
|
v$,
|
||||||
saveStory
|
saveStory,
|
||||||
|
confirmNavShown,
|
||||||
|
nextRoute,
|
||||||
|
doSave: async () => await saveStory(false),
|
||||||
|
confirmClose: () => { confirmNavShown.value = false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -15,10 +15,10 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="story in stories" :key="story.id">
|
<tr v-for="story in stories" :key="story.id">
|
||||||
<td>
|
<td>
|
||||||
<router-link v-if="story.hasStory" :to="`/success-story/view/${story.id}`">View</router-link>
|
<router-link v-if="story.hasStory" :to="`/success-story/${story.id}/view`">View</router-link>
|
||||||
<em v-else>None</em>
|
<em v-else>None</em>
|
||||||
<template v-if="story.citizenId === user.citizenId">
|
<template v-if="story.citizenId === user.citizenId">
|
||||||
~ <router-link :to="`/success-story/edit/${story.id}`">Edit</router-link>
|
~ <router-link :to="`/success-story/${story.id}/edit`">Edit</router-link>
|
||||||
</template>
|
</template>
|
||||||
</td>
|
</td>
|
||||||
<td>{{story.citizenName}}</td>
|
<td>{{story.citizenName}}</td>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user