Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/issue 1443 feedback #1488

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from
4 changes: 1 addition & 3 deletions src/components/FeathersErrorManager.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { BadRequest, NotAuthenticated, type FeathersError } from '@feathersjs/er
*/
export type BadRequestData = { key?: string; message: string; label?: string }

interface BadRequestWithData extends BadRequest {
export interface BadRequestWithData extends BadRequest {
data: any
}

Expand All @@ -42,8 +42,6 @@ const errorMessages = computed<BadRequestData[]>(() => {
return [{ key: 'Error', message: props.error.message }]
} else if (props.error instanceof Error) {
return [{ key: 'Error', message: props.error.message }]
} else {
console.warn('FeathersErrorManager@errorMessages: No error data found', props.error)
}
return []
})
Expand Down
126 changes: 126 additions & 0 deletions src/components/FeedbackForm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<template>
<form :class="['ChangePlanRequestForm', props.className]" @submit="handleOnSubmit">
<FeathersErrorManager :error="props.error" v-if="error" />
<h3 class="form-label font-size-inherit font-weight-bold mb-3">
{{ $t('label_type_of_issue') }}
</h3>
<section class="mb-2 d-flex flex-wrap gap-2 align-items-center">
<label
v-for="(availableIssue, i) in availableFeedbackOptions"
:key="availableIssue"
:class="[
'border rounded-md shadow-sm d-block py-2 pr-3 pl-2 d-flex ',
{ active: form.issue === availableIssue }
]"
>
<input
type="radio"
:name="'plan'"
:id="`feedback-form-${uniqueId}-${i}`"
:checked="form.issue === availableIssue"
@change="form.issue = availableIssue"
/>
<div class="ml-2">
{{ $t('label_' + availableIssue) }}
</div>
</label>
</section>
<label :for="uniqueId" class="form-label font-size-inherit font-weight-bold">{{
$t('label_additional_details')
}}</label>
<p class="text-muted small">{{ $t('label_additional_details_hint') }}</p>
<textarea
class="form-control shadow-sm rounded-sm border"
:id="uniqueId"
v-model="form.content"
rows="3"
:class="{
'is-invalid': v$.content.$error,
'border-danger': v$.content.$error,
'border-success': !v$.content.$error
}"
>
</textarea>
<span v-if="v$.content.$error" class="text-danger">
{{ $t('label_max_length_exceeded') }}
</span>
<button
type="submit"
:disabled="!form.issue"
class="btn btn-outline-secondary btn-md px-4 rounded-sm border border-dark btn-block my-3"
>
<Icon name="sendMail" />
<span class="ml-2">Send feedback</span>
</button>
</form>
</template>
<script setup lang="ts">
import type { FeathersError } from '@feathersjs/errors'
import { computed, ref } from 'vue'
import Icon from './base/Icon.vue'
import FeathersErrorManager from './FeathersErrorManager.vue'
import { AvailableFeedbackOptions } from '@/constants'
import useVuelidate from '@vuelidate/core'
import { maxLength, minLength } from '@vuelidate/validators'

const uniqueId = ref<string>('feedbackform-' + Math.random().toString(36).substring(7))

export interface FeedbackFormProps {
className?: string
availableFeedbackOptions?: typeof AvailableFeedbackOptions
error?: FeathersError | null
}

export interface FeedbackFormPayload {
issue: string
content: string
}

const form = ref<FeedbackFormPayload>({
issue: '',
content: ''
})
// Validation rules
const rules = computed(() => ({
issue: { required: true, minLength: minLength(1) },
content: { maxLength: maxLength(500) }
}))
// Use Vuelidate
const v$ = useVuelidate(rules, form)

const props = withDefaults(defineProps<FeedbackFormProps>(), {
className: '',
availableFeedbackOptions: () => [...AvailableFeedbackOptions],
error: null
})

const emits = defineEmits(['submit'])

const handleOnSubmit = (event: Event) => {
event.preventDefault()
v$.value.$validate() // Trigger validation
if (v$.value.$error) {
return
}
emits('submit', {
issue: form.value.issue,
content: form.value.content
} as FeedbackFormPayload)
}
</script>
<i18n lang="json">
{
"en": {
"label_ContentItemMetadataIssue": "Faulty or missing metadata",
"label_ContentItemFacsimileIssue": "Facsimile issue",
"label_ContentItemTranscriptionIssue": "Transcription issue",
"label_LayoutSegmentationIssue": "Wrong Facsimile Layout segmentation",
"label_DocumentLoadingIssue": "User interface issue",
"label_OtherIssue": "Other issue",
"label_type_of_issue": "What type of issue are you experiencing?",
"label_additional_details": "Additional details (optional)",
"label_additional_details_hint": "(Provide any extra information that might help us understand the issue better.)",
"label_max_length_exceeded": "The content exceeds the maximum length of 500 characters."
}
}
</i18n>
115 changes: 115 additions & 0 deletions src/components/FeedbackModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<template>
<Modal
:show="isVisible"
:title="title"
modalClass="ChangePlanModal"
:dialogClass="props.dialogClass"
@close="dismiss"
@confirm="confirm"
hideBackdrop
>
<LoadingBlock :height="100" v-if="isLoading" label="please wait ...."> </LoadingBlock>
<div v-if="!feedbackFormError && errorMessages.length">
<h3 class="form-label font-size-inherit font-weight-bold mb-3">
Errors that will be reported
</h3>
<ul>
<li v-for="(d, index) in errorMessages" :key="index">
{{ d }}
<b>{{ d.message }}</b
><br />id: {{ d.id }} <br />code: {{ d.code }}
{{ d.route }}
</li>
</ul>
</div>
<FeedbackForm
v-if="!isLoading"
class="mt-2"
@submit="handleSubmit"
:error="feedbackFormError"
/>
<template v-slot:modal-footer>
<button type="button" class="btn btn-sm btn-outline-secondary" @click="dismiss">close</button>
</template>
</Modal>
</template>
<script setup lang="ts">
import Modal from './base/Modal.vue'
import LoadingBlock from './LoadingBlock.vue'
import FeedbackForm from './FeedbackForm.vue'
import type { FeedbackFormPayload } from './FeedbackForm.vue'
import { computed } from 'vue'
import { BadRequestWithData } from './FeathersErrorManager.vue'
import { useRoute } from 'vue-router'

export interface FeedbackFormPayloadWithRoute extends FeedbackFormPayload {
route: {
fullPath: string
params: Record<string, any>
query: Record<string, any>
name: string | null
}
errorMessages: {
id: string
code: number
name: string
message: string
route: string[]
}[]
}

const props = withDefaults(
defineProps<{
dialogClass?: string
title?: string
isVisible?: boolean
errorCode?: string
isLoading?: boolean
errorMessages?: {
id: string
code: number
name: string
message: string
route: string[]
}[]
}>(),
{
dialogClass: 'modal-dialog-scrollable modal-md',
isLoading: false,
errorMessages: () => []
}
)
const emit = defineEmits(['dismiss', 'confirm', 'submit'])
const route = useRoute()
const feedbackFormError = computed(() => {
if (!props.errorMessages.length) return null
// only forward 'feedback' service related error
if ((props.errorMessages[0] as unknown as BadRequestWithData)?.data) {
return props.errorMessages[0] as unknown as BadRequestWithData
}
return null
})
const dismiss = () => {
console.debug('[FeedbackModal] dismiss')
emit('dismiss')
}
const confirm = () => {
console.debug('[FeedbackModal] confirm')
emit('confirm')
}

const handleSubmit = (payload: FeedbackFormPayload) => {
const payloadWithRoute: FeedbackFormPayloadWithRoute = {
...payload,
route: {
fullPath: route.fullPath,
params: route.params,
query: route.query,
name: route.name as string | null
},
errorMessages: props.errorMessages
}
console.debug('[FeedbackModal] handleSubmit', payloadWithRoute)
emit('submit', payloadWithRoute)
}
</script>
53 changes: 50 additions & 3 deletions src/components/Modals.vue
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,14 @@
fetchCorpusOverviewResponse.status === 'idle'
"
/>
<FeedbackModal
:title="$t('label_feedback_modal')"
:isVisible="view === ViewFeedback"
@dismiss="() => resetView()"
@submit="createFeedback"
:errorMessages="errorMessages"
:is-loading="feedbackCollectorResponse.status === 'loading'"
></FeedbackModal>
</div>
</template>

Expand All @@ -162,16 +170,18 @@ import {
ViewUserRequests,
ViewConfirmChangePlanRequest,
ViewInfoModal,
ViewCorpusOverview
ViewCorpusOverview,
ViewFeedback
} from '@/constants'
import { useViewsStore } from '@/stores/views'
import {
userChangePlanRequest as userChangePlanRequestService,
termsOfUse as termsOfUseService,
userRequests as userRequestsService,
subscriptionDatasets as subscriptionDatasetsService
subscriptionDatasets as subscriptionDatasetsService,
feedback as feedbackService
} from '@/services'
import type { FeathersError } from '@feathersjs/errors'
import { BadRequest, type FeathersError } from '@feathersjs/errors'
import { useUserStore } from '@/stores/user'
import { AvailablePlans, PlanLabels } from '@/constants'
import TermsOfUseStatus from './TermsOfUseStatus.vue'
Expand All @@ -183,9 +193,13 @@ import CorpusOverviewModal from './CorpusOverviewModal.vue'
import type { Dataset } from './CorpusOverviewModal.vue'
import PlansModal from './PlansModal.vue'
import axios from 'axios'
import FeedbackModal from './FeedbackModal.vue'
import { FeedbackFormPayload } from './FeedbackForm.vue'
import { useNotificationsStore } from '@/stores/notifications'

const store = useViewsStore()
const userStore = useUserStore()
const notificationsStore = useNotificationsStore()
const userPlan = computed(() => userStore.userPlan)
const bitmap = computed(() => {
const base64String = userStore.bitmap
Expand All @@ -201,6 +215,12 @@ const bitmap = computed(() => {
const view = ref<(typeof Views)[number] | null>(store.view)
const isLoading = ref(false)
const isLoggedIn = computed(() => !!userStore.userData)
const errorMessages = computed(() => {
if (feedbackCollectorResponse.value.status === 'error') {
return [new BadRequest('Error', feedbackCollectorResponse.value.data)]
}
return notificationsStore.errorMessages
})
// date of accepting the ToU on localStorage
const acceptTermsDateOnLocalStorage = computed(() => userStore.acceptTermsDateOnLocalStorage)
// date of accepting the ToU on current store (sort of cached value)
Expand All @@ -214,6 +234,14 @@ const userChangePlanRequestResponse = ref<{
data: null
})

const feedbackCollectorResponse = ref<{
data: any
status: 'idle' | 'loading' | 'success' | 'error'
}>({
status: 'idle',
data: null
})

const termsOfUseResponse = ref<{
data: TermsOfUse
status: 'idle' | 'loading' | 'success' | 'error'
Expand Down Expand Up @@ -461,6 +489,24 @@ const patchCurrentPlanChangeRequest = async ({ plan }) => {
})
}

const createFeedback = async (payload: FeedbackFormPayload) => {
console.debug('[FeedbackModal] @createFeedback', payload)
feedbackCollectorResponse.value = { data: null, status: 'loading' }
await feedbackService
.create(payload, {
ignoreErrors: true
})
.then(data => {
console.info('[FeedbackModal] Feedback sent successfully. data:', data)
store.view = null
feedbackCollectorResponse.value = { data, status: 'success' }
})
.catch((err: FeathersError) => {
console.error('[FeedbackModal] create', err.message, err.data)
feedbackCollectorResponse.value = { data: err.data, status: 'error' }
})
}

const fetchUserRequest = async () => {
console.debug('[Modals] fetchUserRequest')
// load current status
Expand Down Expand Up @@ -522,6 +568,7 @@ onMounted(() => {
"verbose_info_label": "[staff only] Verbose Info",
"not_accepted_local_label": "Not accepted on this device",
"not_accepted_on_db_label": "Not accepted on the server",
"label_feedback_modal": "Help us improve Impresso",
}
}
</i18n>
Loading