diff --git a/src/components/FeathersErrorManager.vue b/src/components/FeathersErrorManager.vue index 875ab978..e2298e54 100644 --- a/src/components/FeathersErrorManager.vue +++ b/src/components/FeathersErrorManager.vue @@ -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 } @@ -42,8 +42,6 @@ const errorMessages = computed(() => { 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 [] }) diff --git a/src/components/FeedbackForm.vue b/src/components/FeedbackForm.vue new file mode 100644 index 00000000..80970c7d --- /dev/null +++ b/src/components/FeedbackForm.vue @@ -0,0 +1,126 @@ + + + +{ + "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." + } +} + diff --git a/src/components/FeedbackModal.vue b/src/components/FeedbackModal.vue new file mode 100644 index 00000000..497e8080 --- /dev/null +++ b/src/components/FeedbackModal.vue @@ -0,0 +1,115 @@ + + diff --git a/src/components/Modals.vue b/src/components/Modals.vue index 19e93412..68460b64 100644 --- a/src/components/Modals.vue +++ b/src/components/Modals.vue @@ -141,6 +141,14 @@ fetchCorpusOverviewResponse.status === 'idle' " /> + @@ -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' @@ -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 @@ -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) @@ -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' @@ -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 @@ -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", } } diff --git a/src/components/UserArea.vue b/src/components/UserArea.vue index 7556d7a0..8566d13e 100644 --- a/src/components/UserArea.vue +++ b/src/components/UserArea.vue @@ -55,6 +55,9 @@ {{ $t('send_update_bitmap') }} + + {{ $t('label_feedback') }} + { "label_verbose_info": "Settings", "label_plans": "Plans", "label_corpus_overview": "Corpus Overview", + "label_feedback": "[staff only] Feedback", "logout": "Logout", "join_slack_channel": "Join Slack Channel", "current_version": "Current version: {version}", diff --git a/src/components/stories/FeedbackModal.stories.ts b/src/components/stories/FeedbackModal.stories.ts new file mode 100644 index 00000000..a1a3e6f9 --- /dev/null +++ b/src/components/stories/FeedbackModal.stories.ts @@ -0,0 +1,63 @@ +import type { Meta, StoryObj } from '@storybook/vue3' +import FeedbackModal from '@/components/FeedbackModal.vue' +import { vueRouter } from 'storybook-vue3-router' +import { ref } from 'vue' +import { action } from '@storybook/addon-actions' + +const meta: Meta = { + title: 'Components/FeedbackModal', + component: FeedbackModal, + tags: ['autodocs'], + render: args => { + return { + setup() { + const handleSubmit = action('submit') + return { args, handleSubmit } + }, + components: { FeedbackModal }, + template: ` +
+ + +
+ ` + } + } +} + +export default meta + +type Story = StoryObj + +export const Default: Story = { + args: { + isVisible: true, + isLoading: false, + title: 'Help us improve Impresso' + } +} +Default.decorators = [ + /* this is the basic setup with no params passed to the decorator */ + vueRouter() +] + +export const WithErrors: Story = { + args: { + isVisible: true, + title: 'Help us improve Impresso', + errorMessages: [ + { + id: '019029302019201', + message: 'Something went wrong', + code: 400, + name: 'BadRequest', + route: ['feedback.create'] + } + ] + } +} + +WithErrors.decorators = [ + /* this is the basic setup with no params passed to the decorator */ + vueRouter() +] diff --git a/src/constants.ts b/src/constants.ts index a812c68b..502947f6 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -32,11 +32,28 @@ export const ViewInfoModal = 'info-modal' export const ViewUserRequests = 'user-requests' export const ViewPlans = 'plans' export const ViewCorpusOverview = 'corpus-overview' +export const ViewFeedback = 'feedback' export const Views: string[] = [ ViewTermsOfUse, ViewChangePlanRequest, ViewConfirmChangePlanRequest, ViewUserRequests, ViewPlans, - ViewCorpusOverview + ViewCorpusOverview, + ViewFeedback +] + +const FeedbackOptionContentItemMetadataIssue = 'ContentItemMetadataIssue' +const FeedbackOptionContentItemFacsimileIssue = 'ContentItemFacsimileIssue' +const FeedbackOptionContentItemTranscriptionIssue = 'ContentItemTranscriptionIssue' +const FeedbackOptionLayoutSegmentationIssue = 'LayoutSegmentationIssue' +const FeedbackOptionDocumentLoadingIssue = 'DocumentLoadingIssue' +const FeedbackOptionOtherIssue = 'OtherIssue' +export const AvailableFeedbackOptions = [ + FeedbackOptionContentItemMetadataIssue, + FeedbackOptionContentItemFacsimileIssue, + FeedbackOptionContentItemTranscriptionIssue, + FeedbackOptionLayoutSegmentationIssue, + FeedbackOptionDocumentLoadingIssue, + FeedbackOptionOtherIssue ] diff --git a/src/services/index.ts b/src/services/index.ts index 245489ec..6fcea99c 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -190,6 +190,7 @@ export const termsOfUse = app.service('terms-of-use') export const userChangePlanRequest = app.service('user-change-plan-request') export const userRequests = app.service('user-requests') export const subscriptionDatasets = app.service('subscriptions') +export const feedback = app.service('feedback-collector') export const MIDDLELAYER_API = import.meta.env.VITE_MIDDLELAYER_API export const MIDDLELAYER_MEDIA_PATH = import.meta.env.VITE_MIDDLELAYER_MEDIA_PATH