WIP on comp API (#37)

This commit is contained in:
Daniel J. Summers 2019-11-27 11:09:54 -06:00
parent 23c6bc1f1f
commit 4e97acb600
9 changed files with 258 additions and 151 deletions

View File

@ -30,7 +30,7 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue' import Vue from 'vue'
import { computed, ref, onMounted, provide } from '@vue/composition-api' import { computed, ref, onMounted, provide, inject } from '@vue/composition-api'
import Navigation from '@/components/common/Navigation.vue' import Navigation from '@/components/common/Navigation.vue'
@ -38,13 +38,14 @@ import auth from './auth'
import router from './router' import router from './router'
import store from './store' import store from './store'
import { Actions } from './store/types' import { Actions } from './store/types'
import { ISnackbar, IProgress } from './types' // eslint-disable-line no-unused-vars
import { provideAuth } from './plugins/auth' import { provideAuth } from './plugins/auth'
import { provideRouter } from './plugins/router' import { provideRouter } from './plugins/router'
import { provideStore } from './plugins/store' import { provideStore } from './plugins/store'
// import { version } = require('../package.json') // import { version } = require('../package.json')
function useSnackbar () { function setupSnackbar (): ISnackbar {
const events = new Vue() const events = new Vue()
const visible = ref(false) const visible = ref(false)
const message = ref('') const message = ref('')
@ -81,7 +82,7 @@ function useSnackbar () {
} }
} }
function useProgress () { function setupProgress (): IProgress {
const events = new Vue() const events = new Vue()
const visible = ref(false) const visible = ref(false)
const mode = ref('query') const mode = ref('query')
@ -107,6 +108,9 @@ function useProgress () {
} }
} }
const SnackbarSymbol = Symbol('Snackbar events')
const ProgressSymbol = Symbol('Progress events')
export default { export default {
name: 'app', name: 'app',
components: { components: {
@ -126,15 +130,12 @@ export default {
: pkg.version.substr(0, pkg.version.length - 2) : pkg.version.substr(0, pkg.version.length - 2)
: pkg.version) : pkg.version)
const progress = useProgress() const progress = setupProgress()
const snackbar = useSnackbar() const snackbar = setupSnackbar()
onMounted(async () => store.dispatch(Actions.CheckAuthentication)) onMounted(async () => store.dispatch(Actions.CheckAuthentication))
const SnackbarSymbol = Symbol('Snackbar events')
provide(SnackbarSymbol, snackbar.events) provide(SnackbarSymbol, snackbar.events)
const ProgressSymbol = Symbol('Progress events')
provide(ProgressSymbol, progress.events) provide(ProgressSymbol, progress.events)
return { return {
@ -144,6 +145,22 @@ export default {
} }
} }
} }
export function useSnackbar () {
const snackbar = inject(SnackbarSymbol)
if (!snackbar) {
throw new Error('Snackbar not configured')
}
return snackbar as ISnackbar
}
export function useProgress () {
const progress = inject(ProgressSymbol)
if (!progress) {
throw new Error('Progress not configured')
}
return progress as IProgress
}
</script> </script>
<style lang="sass"> <style lang="sass">

View File

@ -21,53 +21,66 @@ md-content(role='main').mpj-main-content-wide
snooze-request snooze-request
</template> </template>
<script> <script lang="ts">
'use strict'
import Vue from 'vue' import Vue from 'vue'
import { mapState } from 'vuex' import { computed, inject, onBeforeMount, provide } from '@vue/composition-api'
import { Store } from 'vuex' // eslint-disable-line no-unused-vars
import NotesEdit from './request/NotesEdit' import NotesEdit from './request/NotesEdit.vue'
import RequestCard from './request/RequestCard' import RequestCard from './request/RequestCard.vue'
import SnoozeRequest from './request/SnoozeRequest' import SnoozeRequest from './request/SnoozeRequest.vue'
import { Actions } from '@/store/types' import { Actions, AppState } from '../store/types' // eslint-disable-line no-unused-vars
import { useStore } from '../plugins/store'
import { useSnackbar, useProgress } from '../App.vue'
const EventSymbol = Symbol('Journal events')
export default { export default {
name: 'journal',
inject: [
'messages',
'progress'
],
components: { components: {
NotesEdit, NotesEdit,
RequestCard, RequestCard,
SnoozeRequest SnoozeRequest
}, },
data () { setup () {
/** The Vuex store */
const store = useStore() as Store<AppState>
/** The title of the page */
const title = computed(() => `${store.state.user.given_name}&rsquo;s Prayer Journal`)
/** Events to which the journal will respond */
const eventBus = new Vue()
/** Reference to the application's snackbar component */
const snackbar = useSnackbar()
/** Reference to the application's progress bar component */
const progress = useProgress()
/** Provide the event bus for child components */
provide(EventSymbol, eventBus)
onBeforeMount(async () => {
await store.dispatch(Actions.LoadJournal, progress)
snackbar.events.$emit('info', `Loaded ${store.state.journal.length} prayer requests`)
})
return { return {
eventBus: new Vue() title,
} journal: store.state.journal,
}, isLoadingJournal: store.state.isLoadingJournal
computed: {
title () {
return `${this.user.given_name}&rsquo;s Prayer Journal`
},
snackbar () {
return this.$parent.$refs.snackbar
},
...mapState(['user', 'journal', 'isLoadingJournal'])
},
async created () {
await this.$store.dispatch(Actions.LoadJournal, this.progress)
this.messages.$emit('info', `Loaded ${this.journal.length} prayer requests`)
},
provide () {
return {
journalEvents: this.eventBus
} }
} }
} }
export function useEvents () {
const events = inject(EventSymbol)
if (!events) {
throw new Error('Event bus not configured')
}
return events as Vue
}
</script> </script>
<style lang="sass"> <style lang="sass">

View File

@ -44,6 +44,9 @@ export default {
/** The auth service */ /** The auth service */
const auth = useAuth() as AuthService const auth = useAuth() as AuthService
/** The router for myPrayerJournal */
const router = useRouter()
/** Whether the user has any snoozed requests */ /** Whether the user has any snoozed requests */
const hasSnoozed = computed(() => const hasSnoozed = computed(() =>
store.state.isAuthenticated && store.state.isAuthenticated &&
@ -56,7 +59,6 @@ export default {
/** Log a user off using Auth0 */ /** Log a user off using Auth0 */
const logOff = () => { const logOff = () => {
auth.logout(store) auth.logout(store)
const router = useRouter()
router.push('/') router.push('/')
} }

View File

@ -2,7 +2,7 @@
md-content(role='main').mpj-main-content md-content(role='main').mpj-main-content
page-title(title='Active Requests' page-title(title='Active Requests'
hide-on-page=true) hide-on-page=true)
template(v-if='loaded') template(v-if='isLoaded.value')
md-empty-state(v-if='requests.length === 0' md-empty-state(v-if='requests.length === 0'
md-icon='sentiment_dissatisfied' md-icon='sentiment_dissatisfied'
md-label='No Active Requests' md-label='No Active Requests'
@ -14,47 +14,53 @@ md-content(role='main').mpj-main-content
p(v-else) Loading journal... p(v-else) Loading journal...
</template> </template>
<script> <script lang="ts">
'use strict' import { onBeforeMount, ref } from '@vue/composition-api'
import { Store } from 'vuex'
import { mapState } from 'vuex' import RequestList from '@/components/request/RequestList.vue'
import { useProgress } from '../../App.vue'
import RequestList from '@/components/request/RequestList' import { Actions, AppState, JournalRequest } from '../../store/types'
import { useStore } from '../../plugins/store'
import { Actions } from '@/store/types'
export default { export default {
name: 'active-requests',
inject: ['progress'], inject: ['progress'],
components: { components: {
RequestList RequestList
}, },
data () { setup () {
/** The Vuex store */
const store = useStore() as Store<AppState>
/** The progress bar component instance */
const progress = useProgress()
/** The requests, sorted by the date they will be next shown */
let requests: JournalRequest[] = []
/** Whether all requests have been loaded */
const isLoaded = ref(false)
const ensureJournal = async () => {
if (!Array.isArray(store.state.journal)) {
isLoaded.value = false
await store.dispatch(Actions.LoadJournal, progress)
}
requests = store.state.journal.sort((a, b) => a.showAfter - b.showAfter)
isLoaded.value = true
}
onBeforeMount(async () => { await ensureJournal() })
// TODO: is "this" what we think it is here?
this.$on('requestUnsnoozed', ensureJournal)
this.$on('requestNowShown', ensureJournal)
return { return {
requests: [], requests,
loaded: false isLoaded
} }
},
computed: {
...mapState(['journal', 'isLoadingJournal'])
},
created () {
this.$on('requestUnsnoozed', this.ensureJournal)
this.$on('requestNowShown', this.ensureJournal)
},
methods: {
async ensureJournal () {
if (!Array.isArray(this.journal)) {
this.loaded = false
await this.$store.dispatch(Actions.LoadJournal, this.progress)
}
this.requests = this.journal
.sort((a, b) => a.showAfter - b.showAfter)
this.loaded = true
}
},
async mounted () {
await this.ensureJournal()
} }
} }
</script> </script>

View File

@ -8,16 +8,12 @@ md-table(md-card)
request-list-item(v-for='req in requests' request-list-item(v-for='req in requests'
:key='req.requestId' :key='req.requestId'
:request='req') :request='req')
</template> </template>
<script> <script lang="ts">
'use strict' import RequestListItem from './RequestListItem.vue'
import RequestListItem from '@/components/request/RequestListItem'
export default { export default {
name: 'request-list',
components: { RequestListItem }, components: { RequestListItem },
props: { props: {
title: { title: {
@ -29,12 +25,13 @@ export default {
required: true required: true
} }
}, },
data () { setup (props, { parent }) {
return { } this.$on('requestUnsnoozed', parent.$emit('requestUnsnoozed'))
}, this.$on('requestNowShown', parent.$emit('requestNowShown'))
created () { return {
this.$on('requestUnsnoozed', this.$parent.$emit('requestUnsnoozed')) title: props.title,
this.$on('requestNowShown', this.$parent.$emit('requestNowShown')) requests: props.requests
} }
},
} }
</script> </script>

View File

@ -5,84 +5,104 @@ md-table-row
md-icon description md-icon description
md-tooltip(md-direction='top' md-tooltip(md-direction='top'
md-delay=250) View Full Request md-delay=250) View Full Request
template(v-if='!isAnswered') template(v-if='!isAnswered.value')
md-button(@click='editRequest').md-icon-button.md-raised md-button(@click='editRequest').md-icon-button.md-raised
md-icon edit md-icon edit
md-tooltip(md-direction='top' md-tooltip(md-direction='top'
md-delay=250) Edit Request md-delay=250) Edit Request
template(v-if='isSnoozed') template(v-if='isSnoozed.value')
md-button(@click='cancelSnooze()').md-icon-button.md-raised md-button(@click='cancelSnooze()').md-icon-button.md-raised
md-icon restore md-icon restore
md-tooltip(md-direction='top' md-tooltip(md-direction='top'
md-delay=250) Cancel Snooze md-delay=250) Cancel Snooze
template(v-if='isPending') template(v-if='isPending.value')
md-button(@click='showNow()').md-icon-button.md-raised md-button(@click='showNow()').md-icon-button.md-raised
md-icon restore md-icon restore
md-tooltip(md-direction='top' md-tooltip(md-direction='top'
md-delay=250) Show Now md-delay=250) Show Now
md-table-cell.mpj-valign-top md-table-cell.mpj-valign-top
p.mpj-request-text {{ request.text }} p.mpj-request-text {{ request.text }}
br(v-if='isSnoozed || isPending || isAnswered') br(v-if='isSnoozed.value || isPending.value || isAnswered.value')
small(v-if='isSnoozed').mpj-muted-text: em Snooze expires #[date-from-now(:value='request.snoozedUntil')] small(v-if='isSnoozed.value').mpj-muted-text: em Snooze expires #[date-from-now(:value='request.snoozedUntil')]
small(v-if='isPending').mpj-muted-text: em Request appears next #[date-from-now(:value='request.showAfter')] small(v-if='isPending.value').mpj-muted-text: em Request appears next #[date-from-now(:value='request.showAfter')]
small(v-if='isAnswered').mpj-muted-text: em Answered #[date-from-now(:value='request.asOf')] small(v-if='isAnswered.value').mpj-muted-text: em Answered #[date-from-now(:value='request.asOf')]
</template> </template>
<script> <script lang="ts">
'use strict' import { computed } from '@vue/composition-api'
import { Actions } from '@/store/types' import { Actions, JournalRequest, ISnoozeRequestAction, IShowRequestAction } from '../../store/types'
import { useStore } from '../../plugins/store'
import { useRouter } from '../../plugins/router'
import { useProgress, useSnackbar } from '../../App.vue'
export default { export default {
name: 'request-list-item',
inject: [
'messages',
'progress'
],
props: { props: {
request: { required: true } request: { required: true }
}, },
data () { setup (props, { parent }) {
return {} /** The request to be rendered */
}, const request = props.request as JournalRequest
computed: {
answered () { /** The Vuex store */
return this.request.history.find(hist => hist.status === 'Answered').asOf const store = useStore()
},
isAnswered () { /** The application router */
return this.request.lastStatus === 'Answered' const router = useRouter()
},
isPending () { /** The snackbar instance */
return !this.isSnoozed && this.request.showAfter > Date.now() const snackbar = useSnackbar()
},
isSnoozed () { /** The progress bar component instance */
return this.request.snoozedUntil > Date.now() const progress = useProgress()
}
}, /** Whether the request has been answered */
methods: { const isAnswered = computed(() => request.lastStatus === 'Answered')
async cancelSnooze () {
await this.$store.dispatch(Actions.SnoozeRequest, { /** Whether the request is snoozed */
progress: this.progress, const isSnoozed = computed(() => request.snoozedUntil > Date.now())
requestId: this.request.requestId,
/** Whether the request is not shown because of an interval */
const isPending = computed(() => !isSnoozed.value && request.showAfter > Date.now())
/** Cancel the snooze period for this request */
const cancelSnooze = async () => {
const opts: ISnoozeRequestAction = {
progress: progress,
requestId: request.requestId,
until: 0 until: 0
}) }
this.messages.$emit('info', 'Request un-snoozed') await store.dispatch(Actions.SnoozeRequest, opts)
this.$parent.$emit('requestUnsnoozed') snackbar.events.$emit('info', 'Request un-snoozed')
}, parent.$emit('requestUnsnoozed')
editRequest () { }
this.$router.push({ name: 'EditRequest', params: { id: this.request.requestId } })
}, /** Edit the given request */
async showNow () { const editRequest = () => { router.push({ name: 'EditRequest', params: { id: request.requestId } }) }
await this.$store.dispatch(Actions.ShowRequestNow, {
/** Show the request now */
const showNow = async () => {
const opts: IShowRequestAction = {
progress: this.progress, progress: this.progress,
requestId: this.request.requestId, requestId: this.request.requestId,
showAfter: 0 showAfter: 0
}) }
this.messages.$emit('info', 'Recurrence skipped; request now shows in journal') await store.dispatch(Actions.ShowRequestNow, opts)
this.$parent.$emit('requestNowShown') snackbar.events.$emit('info', 'Recurrence skipped; request now shows in journal')
}, parent.$emit('requestNowShown')
viewFull () { }
this.$router.push({ name: 'FullRequest', params: { id: this.request.requestId } })
/** View the full request */
const viewFull = () => { router.push({ name: 'FullRequest', params: { id: request.requestId } }) }
return {
cancelSnooze,
editRequest,
isAnswered,
isPending,
isSnoozed,
showNow,
viewFull
} }
} }
} }

View File

@ -4,7 +4,15 @@ import Vuex, { StoreOptions } from 'vuex'
import api from '@/api' import api from '@/api'
import auth from '@/auth' import auth from '@/auth'
import { AppState, Actions, JournalRequest, Mutations } from './types' import {
AppState,
Actions,
JournalRequest,
Mutations,
ISnoozeRequestAction,
IShowRequestAction
} from './types'
import { IProgress } from '@/types'
Vue.use(Vuex) Vue.use(Vuex)
@ -113,18 +121,18 @@ const store : StoreOptions<AppState> = {
commit(Mutations.SetAuthentication, false) commit(Mutations.SetAuthentication, false)
} }
}, },
async [Actions.LoadJournal] ({ commit }, progress) { async [Actions.LoadJournal] ({ commit }, progress: IProgress) {
commit(Mutations.LoadedJournal, []) commit(Mutations.LoadedJournal, [])
progress.$emit('show', 'query') progress.events.$emit('show', 'query')
commit(Mutations.LoadingJournal, true) commit(Mutations.LoadingJournal, true)
await setBearer() await setBearer()
try { try {
const jrnl = await api.journal() const jrnl = await api.journal()
commit(Mutations.LoadedJournal, jrnl.data) commit(Mutations.LoadedJournal, jrnl.data)
progress.$emit('done') progress.events.$emit('done')
} catch (err) { } catch (err) {
logError(err) logError(err)
progress.$emit('done') progress.events.$emit('done')
} finally { } finally {
commit(Mutations.LoadingJournal, false) commit(Mutations.LoadingJournal, false)
} }
@ -150,30 +158,32 @@ const store : StoreOptions<AppState> = {
progress.$emit('done') progress.$emit('done')
} }
}, },
async [Actions.ShowRequestNow] ({ commit }, { progress, requestId, showAfter }) { async [Actions.ShowRequestNow] ({ commit }, p: IShowRequestAction) {
progress.$emit('show', 'indeterminate') const { progress, requestId, showAfter } = p
progress.events.$emit('show', 'indeterminate')
try { try {
await setBearer() await setBearer()
await api.showRequest(requestId, showAfter) await api.showRequest(requestId, showAfter)
const request = await api.getRequest(requestId) const request = await api.getRequest(requestId)
commit(Mutations.RequestUpdated, request.data) commit(Mutations.RequestUpdated, request.data)
progress.$emit('done') progress.events.$emit('done')
} catch (err) { } catch (err) {
logError(err) logError(err)
progress.$emit('done') progress.events.$emit('done')
} }
}, },
async [Actions.SnoozeRequest] ({ commit }, { progress, requestId, until }) { async [Actions.SnoozeRequest] ({ commit }, p: ISnoozeRequestAction) {
progress.$emit('show', 'indeterminate') const { progress, requestId, until } = p
progress.events.$emit('show', 'indeterminate')
try { try {
await setBearer() await setBearer()
await api.snoozeRequest(requestId, until) await api.snoozeRequest(requestId, until)
const request = await api.getRequest(requestId) const request = await api.getRequest(requestId)
commit(Mutations.RequestUpdated, request.data) commit(Mutations.RequestUpdated, request.data)
progress.$emit('done') progress.events.$emit('done')
} catch (err) { } catch (err) {
logError(err) logError(err)
progress.$emit('done') progress.events.$emit('done')
} }
} }
}, },

View File

@ -1,3 +1,5 @@
import { IProgress } from '@/types'
/** A prayer request that is part of the user's journal */ /** A prayer request that is part of the user's journal */
export interface JournalRequest { export interface JournalRequest {
/** The ID of the request (just the CUID part) */ /** The ID of the request (just the CUID part) */
@ -93,3 +95,23 @@ const mutations = {
UserLoggedOn: 'user-logged-on' UserLoggedOn: 'user-logged-on'
} }
export { mutations as Mutations } export { mutations as Mutations }
/** The shape of the parameter to the show request action */
export interface IShowRequestAction {
/** The progress bar component instance */
progress: IProgress
/** The ID of the prayer request being shown */
requestId: string
/** The date/time after which the request will be once again shown */
showAfter: number
}
/** The shape of the parameter to the snooze request action */
export interface ISnoozeRequestAction {
/** The progress bar component instance */
progress: IProgress
/** The ID of the prayer request being snoozed/unsnoozed */
requestId: string
/** The date/time after which the request will be once again shown */
until: number
}

20
src/app/src/types.ts Normal file
View File

@ -0,0 +1,20 @@
import Vue from 'vue'
import { Ref } from '@vue/composition-api';
export interface ISnackbar {
events: Vue
visible: Ref<boolean>
message: Ref<string>
interval: Ref<number>
showSnackbar: (msg: string) => void
showInfo: (msg: string) => void
showError: (msg: string) => void
}
export interface IProgress {
events: Vue
visible: Ref<boolean>
mode: Ref<string>
showProgress: (mod: string) => void
hideProgress: () => void
}