UNPKG

@digital-blueprint/formalize-app

Version:

[GitHub Repository](https://github.com/digital-blueprint/formalize-app) | [npmjs package](https://www.npmjs.com/package/@digital-blueprint/formalize-app) | [Unpkg CDN](https://unpkg.com/browse/@digital-blueprint/formalize-app/) | [Formalize Bundle](https:

541 lines (482 loc) • 23.1 kB
import {BaseFormElement, BaseObject} from '../form/base-object.js'; import {html, css} from 'lit'; import {classMap} from 'lit/directives/class-map.js'; import * as commonStyles from '@dbp-toolkit/common/styles.js'; import { DbpStringElement, DbpDateElement, DbpTimeElement, DbpBooleanElement, } from '@dbp-toolkit/form-elements'; import {sendNotification} from '@dbp-toolkit/common'; import {DbpPersonSelectElement} from '../form/elements/personselect.js'; import {DbpCourseSelectElement} from '../form/elements/courseselect.js'; import {DbpRoomSelectElement} from '../form/elements/roomselect.js'; import {createRef, ref} from 'lit/directives/ref.js'; import {getFormManageFormsUrl, getFormRenderUrl} from '../utils.js'; import {validateRequiredFields} from '@dbp-toolkit/form-elements/src/utils.js'; /** @typedef {import('lit/directives/ref.js').Ref<DbpTimeElement>} TimeElementRef */ /** @typedef {import('lit/directives/ref.js').Ref<DbpStringElement>} StringElementRef */ export default class extends BaseObject { getUrlSlug() { return 'accessible-exams'; } /** * @returns {typeof BaseFormElement} */ getFormComponent() { return FormalizeFormElement; } getFormIdentifier() { return '0193cfbd-9b68-703a-81f9-c10d0e2375b7'; } } class FormalizeFormElement extends BaseFormElement { constructor() { super(); /** @type {TimeElementRef} */ this.beginTimeRef = createRef(); /** @type {StringElementRef} */ this.examinerTextRef = createRef(); this.examinerTextDisabled = false; this.hasAttemptedValidation = false; this.wasSubmissionSuccessful = false; this.handleValidationOnFocusOut = this.handleValidationOnFocusOut.bind(this); } static get properties() { return { ...super.properties, examinerTextDisabled: {type: Boolean}, hasAttemptedValidation: {type: Boolean, attribute: false}, }; } /** * Records that a submission was attempted (pass or fail), enabling live * validation on subsequent focusout/change events. Accessible exams forms * have no DRAFT state, so we avoid showing errors before the first submit. * @param {Event} event */ async validateAndSendSubmission(event) { this.hasAttemptedValidation = true; return super.validateAndSendSubmission(event); } /** * Re-run validation on focus out of form elements once the user has * attempted a submission. Keeps co-dependent fields (examiner/examinerText) * in sync and clears stale warnings. * @param {Event} event */ async handleValidationOnFocusOut(event) { if (!this.hasAttemptedValidation) return; const formElement = this.formElement; this.isFormValid = await validateRequiredFields(formElement); } connectedCallback() { super.connectedCallback(); void this.updateComplete.then(() => { // Event listener for form submission this.addEventListener( 'DbpFormalizeFormSubmission', async (/** @type {CustomEvent} */ event) => { const i18n = this._i18n; // Get the form data from the event detail const formData = event.detail.formData; // Include unique identifier for the person who is submitting formData.identifier = this.formData.identifier; // Create UUID for each submission this.createUUID(); formData.uuid = this.formData.uuid; // Create a human-readable exam id for each submission this.createExamID(); formData.examid = this.formData.examid; if (formData.examinerText) { // Set examinerText as examiner formData.examiner = formData.examinerText; formData.email_examiner = ''; } else { // Extract name and email from examiner data let examinerData = this.getExaminerMail(formData.examiner); formData.examiner = examinerData[0] ?? ''; formData.email_examiner = examinerData[1] ?? ''; } // We always need to delete the examinerText field, because it's not in the schema delete formData.examinerText; // Extract name and email from additional examiner data let additionalExaminerData = this.getExaminerMail(formData.additionalExaminer); formData.additionalExaminer = additionalExaminerData[0]; formData.email_additionalExaminer = additionalExaminerData[1]; // Handle the event console.log('Form submission data:', formData); try { this.isPostingSubmission = true; if (this.wasSubmissionSuccessful) { return; } let body = { form: '/formalize/forms/' + '0193cfbd-9b68-703a-81f9-c10d0e2375b7', dataFeedElement: JSON.stringify(formData), }; const response = await fetch( this.entryPointUrl + '/formalize/submissions', { method: 'POST', headers: { 'Content-Type': 'application/ld+json', Authorization: 'Bearer ' + this.getToken(), }, body: JSON.stringify(body), }, ); if (!response.ok) { this.saveButtonEnabled = true; await this.displayErrors(response); } else { this.wasSubmissionSuccessful = true; sendNotification({ summary: i18n.t('success.success-title'), body: i18n.t('success.form-saved-successfully'), type: 'success', timeout: 5, }); // Hide form after successful submission this._('.form-title').style.display = 'none'; this._('.description').style.display = 'none'; this._('#accessible-exams-form').style.display = 'none'; } this.submitted = this.wasSubmissionSuccessful; console.log(this.wasSubmissionSuccessful, response); return response; } catch (error) { console.error(error.message); } finally { this.isPostingSubmission = false; } }, ); // Handle field validation on focus out this.renderRoot.addEventListener('focusout', this.handleValidationOnFocusOut, { capture: true, }); }); } disconnectedCallback() { super.disconnectedCallback(); this.renderRoot.removeEventListener('focusout', this.handleValidationOnFocusOut, { capture: true, }); } static get scopedElements() { return { ...super.scopedElements, 'dbp-form-string-element': DbpStringElement, 'dbp-form-date-element': DbpDateElement, 'dbp-form-time-element': DbpTimeElement, 'dbp-form-boolean-element': DbpBooleanElement, 'dbp-course-select-element': DbpCourseSelectElement, 'dbp-room-select-element': DbpRoomSelectElement, 'dbp-person-select-element': DbpPersonSelectElement, }; } async fetchUser(id) { return await fetch( this.entryPointUrl + '/base/people/' + id + '?includeLocal=email,matriculationNumber', { headers: { 'Content-Type': 'application/ld+json', Authorization: 'Bearer ' + this.getToken(), }, }, ); } async fetchUserData() { console.log('Fetching user data ...'); try { let response = await this.fetchUser(this.getUserId()); if (!response.ok) { throw new Error( `Request failed with status ${response.status}: ${response.statusText}`, {cause: response}, ); } this.formData = await response.json(); this.formData.identifier = `${this.formData['identifier']}`; this.formData.givenName = `${this.formData['givenName']}`; this.formData.familyName = `${this.formData['familyName']}`; this.formData.matriculationNumber = `${this.formData['localData']['matriculationNumber']}`; this.formData.email_student = `${this.formData['localData']['email']}`; } catch (error) { console.error(error.message); } } createUUID() { let uuid = self.crypto.randomUUID(); console.log('Created UUID: ' + uuid); this.formData.uuid = uuid; } createExamID() { // create a random five-digit ID let min = 10000; let max = 99999; let examid = Math.floor(Math.random() * (max - min + 1)) + min; console.log('Created ExamID: ' + examid); this.formData.examid = examid; } getExaminerMail(examinerdata) { const nameAndMail = examinerdata.split(' '); const name = nameAndMail.slice(0, -1).join(' '); const mail = nameAndMail[nameAndMail.length - 1]; return [name, mail]; } static get styles() { return [ super.styles, commonStyles.getButtonCSS(), css` .field-note { margin-top: 0; font-style: italic; } fieldset { margin-bottom: 20px; } `, ]; } render() { const i18n = this._i18n; console.log('-- Render FormalizeFormElement --'); if (!this.formData.givenName && !this.formData.familyName) { void this.fetchUserData(); } console.log('this.formData', this.formData); const data = this.formData || {}; return html` <form id="accessible-exams-form" class=${classMap({ hidden: this.hideForm, 'readonly-mode': this.readOnly, 'edit-mode': !this.readOnly, 'formalize-form': true, })}> <div class="form-header">${this.getButtonRowHtml()}</div> <h2 class="form-title"> ${i18n.t('render-form.forms.accessible-exams-form.title')} </h2> <p class="description"> ${i18n.t('render-form.forms.accessible-exams-form.mandatory-fields')} <br /> ${i18n.t('render-form.forms.accessible-exams-form.exam-date')} </p> <fieldset> <legend>${i18n.t('render-form.forms.accessible-exams-form.exam-data')}</legend> <dbp-course-select-element subscribe="lang,auth,entry-point-url" name="courseName" label="${i18n.t('render-form.forms.accessible-exams-form.course-name')}" value=${data.courseName || ''} required></dbp-course-select-element> <dbp-form-string-element subscribe="lang" name="differentTerm" label=${i18n.t('render-form.forms.accessible-exams-form.different-term')} value=${data.differentTerm || ''} hidden></dbp-form-string-element> <dbp-form-date-element subscribe="lang" name="date" label=${i18n.t('render-form.forms.accessible-exams-form.date')} .min=${new Date(Date.now() + 1209600000)} .customValidator=${(value) => { const date = new Date(value); // The minimum date has to be two weeks ahead const minDate = new Date(Date.now() + 1209600000); minDate.setHours(0, 0, 0); return date < minDate ? [ i18n.t( 'render-form.forms.accessible-exams-form.date-validation-error', ), ] : []; }} value=${data.date || ''} required></dbp-form-date-element> <dbp-form-time-element ${ref(this.beginTimeRef)} subscribe="lang" name="beginTime" label=${i18n.t('render-form.forms.accessible-exams-form.begin-time')} value=${data.beginTime || ''} required></dbp-form-time-element> <dbp-form-time-element subscribe="lang" name="endTime" label=${i18n.t('render-form.forms.accessible-exams-form.end-time')} .customValidator=${(value) => { const beginTime = this.beginTimeRef.value?.value; return beginTime !== undefined && beginTime > value ? [ i18n.t( 'render-form.forms.accessible-exams-form.time-validation-error', ), ] : []; }} value=${data.endTime || ''} required></dbp-form-time-element> <dbp-person-select-element id="examiner-picker-element" subscribe="lang" name="examiner" label=${i18n.t('render-form.forms.accessible-exams-form.examiner')} value=${data.examiner || ''} @change="${async (e) => { const hasValue = !!e.detail.value; this.examinerTextDisabled = hasValue; if (hasValue && this.examinerTextRef.value) { this.examinerTextRef.value.value = ''; } // Selecting from the picker may not emit a focusout, // so revalidate here to clear stale warnings. if (this.hasAttemptedValidation) { const formElement = this.formElement; this.isFormValid = await validateRequiredFields(formElement); } }}" .customValidator=${(value, evaluationData) => { // We can't use this.examinerCustomValidation directly, or this._i18n will not find the message key return this.examinerCustomValidation(evaluationData); }}></dbp-person-select-element> <dbp-form-string-element subscribe="lang" ${ref(this.examinerTextRef)} name="examinerText" label=${i18n.t('render-form.forms.accessible-exams-form.examiner-text')} value=${data.examinerText || ''} ?disabled=${this.examinerTextDisabled} .customValidator=${(value, evaluationData) => { // We can't use this.examinerCustomValidation directly, or this._i18n will not find the message key return this.examinerCustomValidation(evaluationData); }}></dbp-form-string-element> <dbp-person-select-element id="additional-examiner-picker-element" subscribe="lang" name="additionalExaminer" label=${i18n.t( 'render-form.forms.accessible-exams-form.additional-examiner', )} value=${data.additionalExaminer || ''}></dbp-person-select-element> <dbp-room-select-element subscribe="lang" name="room" label="${i18n.t('render-form.forms.accessible-exams-form.room')}" value=${data.room || ''} hidden></dbp-room-select-element> <dbp-form-string-element subscribe="lang" name="comment" label=${i18n.t('render-form.forms.accessible-exams-form.comment')} value=${data.comment || ''}></dbp-form-string-element> <dbp-form-boolean-element subscribe="lang" name="group" label=${i18n.t('render-form.forms.accessible-exams-form.group')} hidden .state=${data.group || false}></dbp-form-boolean-element> <dbp-form-boolean-element subscribe="lang" name="online" label=${i18n.t('render-form.forms.accessible-exams-form.online')} .state=${data.online || false}></dbp-form-boolean-element> </fieldset> <fieldset> <legend> ${i18n.t('render-form.forms.accessible-exams-form.personal-data')} </legend> <dbp-form-string-element subscribe="lang" name="matriculationNumber" label=${i18n.t( 'render-form.forms.accessible-exams-form.matriculation-number', )} value=${data.matriculationNumber || ''} disabled></dbp-form-string-element> <dbp-form-string-element subscribe="lang" name="givenName" label=${i18n.t('render-form.forms.accessible-exams-form.given-name')} value=${data.givenName || ''} disabled></dbp-form-string-element> <dbp-form-string-element subscribe="lang" name="familyName" label=${i18n.t('render-form.forms.accessible-exams-form.family-name')} value=${data.familyName || ''} disabled></dbp-form-string-element> <dbp-form-string-element subscribe="lang" name="email_student" label=${i18n.t('render-form.forms.accessible-exams-form.email')} value=${data.email_student || ''} disabled></dbp-form-string-element> </fieldset> </form> ${this.renderResult(this.submitted)} `; } examinerCustomValidation(evaluationData) { return evaluationData.examinerText === '' && evaluationData.examiner === '' ? [this._i18n.t('render-form.forms.accessible-exams-form.examiner-validation-error')] : []; } renderResult(submitted) { const i18n = this._i18n; if (submitted) { return html` <div class="container"> <h2> ${i18n.t( 'render-form.forms.accessible-exams-form.submission-result-thanks', )} </h2> <p> ${i18n.t( 'render-form.forms.accessible-exams-form.submission-result-notification', )} </p> <div class="after-submission-button-container"> ${ this.canUserSubmitSubmission() ? html` <a href="${getFormRenderUrl(this.formUrlSlug, this.lang)}" class="button button--new-submission"> <dbp-icon name="plus" aria-hidden="true"></dbp-icon> ${i18n.t('success.create-new-submission-button-label')} </a> ` : '' } ${ this.areSubmissionReadableAfterSubmission() ? html` <a href="${getFormManageFormsUrl( this.formIdentifier, this.lang, )}" class="button button--back-to-submissions-list"> <dbp-icon name="list" aria-hidden="true"></dbp-icon> ${i18n.t('success.back-to-submissions-list-button-label')} </a> ` : '' } </div> </div> `; } return html``; } }