From 641e6969b7d81fe39df388cb44a5fed456333de3 Mon Sep 17 00:00:00 2001 From: Jason Smith Date: Fri, 3 May 2024 18:22:03 +1000 Subject: [PATCH 1/7] feat: :sparkles: spin off new layer pkg --- examples/nuxt-app/nuxt.config.ts | 3 +- examples/nuxt-app/package.json | 1 + examples/nuxt-app/pages/_form.vue | 87 ++++ .../fixtures/case/landing-page-webform.json | 169 ++++++++ .../nuxt-app/test/fixtures/case/webform.json | 231 ++++++++++ .../global/TideLandingPage/WebForm.vue | 6 +- .../components/webforms/webforms-mapping.ts | 395 ++---------------- .../ripple-tide-landing-page/package.json | 1 + packages/ripple-tide-webform/CHANGELOG.md | 0 packages/ripple-tide-webform/LICENSE | 201 +++++++++ packages/ripple-tide-webform/README.md | 24 ++ .../composables/use-web-form.ts | 107 +++++ packages/ripple-tide-webform/mapping/index.ts | 1 + .../webform-conditional-logic.test.ts | 0 .../mapping}/webform-conditional-logic.ts | 0 .../mapping}/webform-utils.ts | 0 .../mapping}/webform-validation.ts | 0 .../mapping}/webforms-address.ts | 0 .../mapping/webforms-mapping.ts | 330 +++++++++++++++ packages/ripple-tide-webform/nuxt.config.ts | 3 + packages/ripple-tide-webform/package.json | 20 + .../server/api/tide/webform.ts | 99 +++++ packages/ripple-tide-webform/tsconfig.json | 3 + packages/ripple-tide-webform/types.ts | 49 +++ pnpm-lock.yaml | 213 +++++----- tsconfig.json | 6 +- 26 files changed, 1479 insertions(+), 470 deletions(-) create mode 100644 examples/nuxt-app/pages/_form.vue create mode 100644 examples/nuxt-app/test/fixtures/case/landing-page-webform.json create mode 100644 examples/nuxt-app/test/fixtures/case/webform.json create mode 100644 packages/ripple-tide-webform/CHANGELOG.md create mode 100644 packages/ripple-tide-webform/LICENSE create mode 100644 packages/ripple-tide-webform/README.md create mode 100644 packages/ripple-tide-webform/composables/use-web-form.ts create mode 100644 packages/ripple-tide-webform/mapping/index.ts rename packages/{ripple-tide-landing-page/mapping/components/webforms => ripple-tide-webform/mapping}/webform-conditional-logic.test.ts (100%) rename packages/{ripple-tide-landing-page/mapping/components/webforms => ripple-tide-webform/mapping}/webform-conditional-logic.ts (100%) rename packages/{ripple-tide-landing-page/mapping/components/webforms => ripple-tide-webform/mapping}/webform-utils.ts (100%) rename packages/{ripple-tide-landing-page/mapping/components/webforms => ripple-tide-webform/mapping}/webform-validation.ts (100%) rename packages/{ripple-tide-landing-page/mapping/components/webforms => ripple-tide-webform/mapping}/webforms-address.ts (100%) create mode 100644 packages/ripple-tide-webform/mapping/webforms-mapping.ts create mode 100644 packages/ripple-tide-webform/nuxt.config.ts create mode 100644 packages/ripple-tide-webform/package.json create mode 100644 packages/ripple-tide-webform/server/api/tide/webform.ts create mode 100644 packages/ripple-tide-webform/tsconfig.json create mode 100644 packages/ripple-tide-webform/types.ts diff --git a/examples/nuxt-app/nuxt.config.ts b/examples/nuxt-app/nuxt.config.ts index a7a86b814d..16c703c7e2 100644 --- a/examples/nuxt-app/nuxt.config.ts +++ b/examples/nuxt-app/nuxt.config.ts @@ -26,7 +26,8 @@ export default defineNuxtConfig({ '@dpc-sdp/ripple-tide-publication', '@dpc-sdp/ripple-tide-media', '@dpc-sdp/ripple-tide-news', - '@dpc-sdp/ripple-tide-search' + '@dpc-sdp/ripple-tide-search', + '@dpc-sdp/ripple-tide-webform' ], // Nuxt devtools sourcemap: true, diff --git a/examples/nuxt-app/package.json b/examples/nuxt-app/package.json index 55a3ae67bb..b8ee81ec4c 100644 --- a/examples/nuxt-app/package.json +++ b/examples/nuxt-app/package.json @@ -34,6 +34,7 @@ "@dpc-sdp/ripple-tide-publication": "workspace:*", "@dpc-sdp/ripple-tide-search": "workspace:*", "@dpc-sdp/ripple-tide-topic": "workspace:*", + "@dpc-sdp/ripple-tide-webform": "workspace:*", "@dpc-sdp/ripple-ui-maps": "workspace:*" }, "devDependencies": { diff --git a/examples/nuxt-app/pages/_form.vue b/examples/nuxt-app/pages/_form.vue new file mode 100644 index 0000000000..bef3f4cb97 --- /dev/null +++ b/examples/nuxt-app/pages/_form.vue @@ -0,0 +1,87 @@ + + + diff --git a/examples/nuxt-app/test/fixtures/case/landing-page-webform.json b/examples/nuxt-app/test/fixtures/case/landing-page-webform.json new file mode 100644 index 0000000000..e3fc259a29 --- /dev/null +++ b/examples/nuxt-app/test/fixtures/case/landing-page-webform.json @@ -0,0 +1,169 @@ +{ + "title": "Webform", + "changed": "2024-05-02T16:22:08+10:00", + "created": "2024-05-02T14:42:13+10:00", + "type": "landing_page", + "nid": "45fa8751-74af-43ee-9cb9-8a8fdee7e593", + "_sectionId": "8888", + "sidebar": { + "contacts": [], + "relatedLinks": [], + "whatsNext": [], + "socialShareNetworks": ["Facebook", "X", "LinkedIn"], + "siteSectionNav": null + }, + "status": "published", + "topicTags": [ + { + "text": "Demo Topic", + "url": "/topic/demo-topic" + } + ], + "siteSection": { + "id": 8888, + "name": "Demo Site" + }, + "meta": { + "url": "/webform", + "langcode": "en", + "description": "Testing the landing page webform mapping", + "additional": [ + { + "tag": "meta", + "attributes": { + "name": "title", + "content": "Webform | Single Digital Presence Content Management System" + } + }, + { + "tag": "link", + "attributes": { + "rel": "canonical", + "href": "http://content-sdp.docker.internal/webform" + } + }, + { + "tag": "meta", + "attributes": { + "property": "og:locale", + "content": "en-AU" + } + } + ], + "keywords": "", + "image": null + }, + "showContentRating": true, + "summary": "Testing the landing page webform mapping", + "showHeroAcknowledgement": false, + "showInPageNav": false, + "showHeroImageCaption": false, + "showTopicTags": true, + "inPageNavHeadingLevel": "h2", + "background": "default", + "header": { + "title": "Webform", + "summary": "", + "links": { + "title": "", + "items": [], + "more": null + }, + "backgroundImageCaption": "", + "theme": "default", + "logoImage": null, + "backgroundImage": null, + "cornerTop": null, + "cornerBottom": null, + "primaryAction": null, + "secondaryAction": null, + "secondaryActionLabel": "" + }, + "primaryCampaign": null, + "secondaryCampaign": null, + "headerComponents": [], + "bodyComponents": [ + { + "uuid": "2947c380-ab74-4b11-ac53-64ff91c760ff", + "component": "TideLandingPageWebForm", + "id": 1793, + "title": "Use the form", + "props": { + "title": "Use the form", + "formId": "contact", + "hideFormOnSubmit": false, + "successMessageTitle": "Form submitted", + "successMessageHTML": "Your message has been sent.", + "errorMessageTitle": "Form not submitted", + "errorMessageHTML": "We are experiencing a server error. Please try again, otherwise contact us.", + "schema": [ + { + "$formkit": "RplFormText", + "key": "name", + "name": "name", + "label": "Your Name", + "id": "contact_name", + "validation": [["length", 0, 255], ["required"]], + "validationMessages": { + "required": "Your Name is required", + "accepted": "Your Name is required", + "length": "You can enter a maximum of 255 characters" + } + }, + { + "$formkit": "RplFormEmail", + "key": "email", + "name": "email", + "label": "Your Email", + "id": "contact_email", + "validation": [["email"], ["length", 0, 255], ["required"]], + "validationMessages": { + "required": "Your Email is required", + "accepted": "Your Email is required", + "email": "Your Email must be a valid email address", + "length": "You can enter a maximum of 255 characters" + } + }, + { + "$formkit": "RplFormText", + "key": "subject", + "name": "subject", + "label": "Subject", + "id": "contact_subject", + "validation": [["required"]], + "validationMessages": { + "required": "Subject is required", + "accepted": "Subject is required" + } + }, + { + "$formkit": "RplFormTextarea", + "key": "message", + "id": "contact_message", + "name": "message", + "label": "Message", + "validation": [["required"]], + "validationMessages": { + "required": "Message is required", + "accepted": "Message is required" + } + }, + { + "$formkit": "RplFormActions", + "key": "actions", + "name": "submit", + "variant": "filled", + "label": "Send message", + "id": "contact_actions", + "displayResetButton": false, + "validation": [], + "validationMessages": { + "required": "Submit button(s) is required", + "accepted": "Submit button(s) is required" + } + } + ] + } + } + ] +} diff --git a/examples/nuxt-app/test/fixtures/case/webform.json b/examples/nuxt-app/test/fixtures/case/webform.json new file mode 100644 index 0000000000..b1c8d9c135 --- /dev/null +++ b/examples/nuxt-app/test/fixtures/case/webform.json @@ -0,0 +1,231 @@ +{ + "field_landing_page_component": [ + { + "links": { + "self": { + "href": "http://content-sdp.docker.internal/api/v1/paragraph/embedded_webform/2947c380-ab74-4b11-ac53-64ff91c760ff?resourceVersion=id%3A2214" + } + }, + "meta": { + "target_revision_id": 2214, + "drupal_internal__target_id": 1793 + }, + "drupal_internal__id": 1793, + "drupal_internal__revision_id": 2214, + "langcode": "en", + "status": true, + "created": "2024-05-02T04:41:54+00:00", + "parent_id": "201", + "parent_type": "node", + "parent_field_name": "field_landing_page_component", + "behavior_settings": [], + "default_langcode": true, + "revision_translation_affected": true, + "field_paragraph_title": "Use the form", + "field_paragraph_webform": { + "links": { + "self": { + "href": "http://content-sdp.docker.internal/api/v1/webform/webform/ca775dd1-64d7-4e15-aef0-6d15665a241f" + } + }, + "meta": { + "default_data": "", + "status": "open", + "open": null, + "close": null, + "drupal_internal__target_id": "contact" + }, + "langcode": "en", + "status": "open", + "dependencies": { + "enforced": { + "module": ["webform"] + } + }, + "open": null, + "close": null, + "weight": 0, + "uid": 1, + "template": false, + "archive": false, + "drupal_internal__id": "contact", + "title": "Contact", + "description": "

Basic email contact webform.

", + "categories": [], + "elements": { + "name": { + "#type": "textfield", + "#title": "Your Name", + "#maxlength": 255, + "#required": true + }, + "email": { + "#type": "email", + "#title": "Your Email", + "#maxlength": 255, + "#required": true + }, + "subject": { + "#title": "Subject", + "#type": "textfield", + "#required": true, + "#test": "Testing contact webform from [site:name]" + }, + "message": { + "#title": "Message", + "#type": "textarea", + "#required": true, + "#test": "Please ignore this email." + }, + "actions": { + "#type": "webform_actions", + "#title": "Submit button(s)", + "#submit__label": "Send message" + } + }, + "css": "", + "javascript": "", + "settings": { + "ajax": false, + "ajax_scroll_top": "form", + "ajax_progress_type": "", + "ajax_effect": "", + "ajax_speed": null, + "page": true, + "page_submit_path": "", + "page_confirm_path": "", + "page_theme_name": "", + "form_title": "source_entity_webform", + "form_submit_once": false, + "form_open_message": "", + "form_close_message": "", + "form_exception_message": "", + "form_previous_submissions": true, + "form_confidential": false, + "form_confidential_message": "", + "form_disable_remote_addr": false, + "form_convert_anonymous": false, + "form_prepopulate": false, + "form_prepopulate_source_entity": false, + "form_prepopulate_source_entity_required": false, + "form_prepopulate_source_entity_type": "", + "form_unsaved": false, + "form_disable_back": false, + "form_submit_back": false, + "form_disable_autocomplete": false, + "form_novalidate": false, + "form_disable_inline_errors": false, + "form_required": false, + "form_autofocus": false, + "form_details_toggle": false, + "form_reset": false, + "form_access_denied": "default", + "form_access_denied_title": "", + "form_access_denied_message": "", + "form_access_denied_attributes": [], + "form_file_limit": "", + "form_attributes": [], + "form_method": "", + "form_action": "", + "share": false, + "share_node": false, + "share_theme_name": "", + "share_title": true, + "share_page_body_attributes": [], + "submission_label": "", + "submission_exception_message": "", + "submission_locked_message": "", + "submission_log": false, + "submission_excluded_elements": [], + "submission_exclude_empty": false, + "submission_exclude_empty_checkbox": false, + "submission_views": [], + "submission_views_replace": [], + "submission_user_columns": [], + "submission_user_duplicate": false, + "submission_access_denied": "default", + "submission_access_denied_title": "", + "submission_access_denied_message": "", + "submission_access_denied_attributes": [], + "previous_submission_message": "", + "previous_submissions_message": "", + "autofill": false, + "autofill_message": "", + "autofill_excluded_elements": [], + "wizard_progress_bar": true, + "wizard_progress_pages": false, + "wizard_progress_percentage": false, + "wizard_progress_link": false, + "wizard_progress_states": false, + "wizard_start_label": "", + "wizard_preview_link": false, + "wizard_confirmation": true, + "wizard_confirmation_label": "", + "wizard_auto_forward": true, + "wizard_auto_forward_hide_next_button": false, + "wizard_keyboard": true, + "wizard_track": "", + "wizard_prev_button_label": "", + "wizard_next_button_label": "", + "wizard_toggle": false, + "wizard_toggle_show_label": "", + "wizard_toggle_hide_label": "", + "wizard_page_type": "container", + "wizard_page_title_tag": "h2", + "preview": 0, + "preview_label": "", + "preview_title": "", + "preview_message": "", + "preview_attributes": [], + "preview_excluded_elements": [], + "preview_exclude_empty": true, + "preview_exclude_empty_checkbox": false, + "draft": "none", + "draft_multiple": false, + "draft_auto_save": false, + "draft_saved_message": "", + "draft_loaded_message": "", + "draft_pending_single_message": "", + "draft_pending_multiple_message": "", + "confirmation_type": "url_message", + "confirmation_url": "", + "confirmation_title": "", + "confirmation_message": "Your message has been sent.", + "confirmation_attributes": [], + "confirmation_back": true, + "confirmation_back_label": "", + "confirmation_back_attributes": [], + "confirmation_exclude_query": false, + "confirmation_exclude_token": false, + "confirmation_update": false, + "limit_total": null, + "limit_total_interval": null, + "limit_total_message": "", + "limit_total_unique": false, + "limit_user": null, + "limit_user_interval": null, + "limit_user_message": "", + "limit_user_unique": false, + "entity_limit_total": null, + "entity_limit_total_interval": null, + "entity_limit_user": null, + "entity_limit_user_interval": null, + "purge": "none", + "purge_days": null, + "results_disabled": false, + "results_disabled_ignore": false, + "results_customize": false, + "token_view": false, + "token_update": false, + "token_delete": false, + "serial_disabled": false + }, + "variants": [], + "id": "ca775dd1-64d7-4e15-aef0-6d15665a241f", + "type": "webform--webform" + }, + "id": "2947c380-ab74-4b11-ac53-64ff91c760ff", + "type": "paragraph--embedded_webform" + } + ] +} diff --git a/packages/ripple-tide-landing-page/components/global/TideLandingPage/WebForm.vue b/packages/ripple-tide-landing-page/components/global/TideLandingPage/WebForm.vue index d9670b0987..1cec279422 100644 --- a/packages/ripple-tide-landing-page/components/global/TideLandingPage/WebForm.vue +++ b/packages/ripple-tide-landing-page/components/global/TideLandingPage/WebForm.vue @@ -26,7 +26,7 @@ const props = withDefaults(defineProps(), { const honeypotId = `${props.formId}-important-email` const isHoneypotTriggered = () => { - const honeypotElement: HTMLInputElement = document.querySelector( + const honeypotElement: HTMLInputElement | null = document.querySelector( `#${honeypotId}` ) @@ -57,7 +57,7 @@ const postForm = async (formId: string, formData = {}) => { const url = `/api/tide/${formResource}/${formId}` const { data, error } = await $fetch(url, { method: 'POST', - baseURL: config.apiUrl || '', + baseURL: (config.apiUrl as string) || '', body, params: { site: config.tide.site @@ -84,7 +84,7 @@ const submissionState = ref({ message: '' }) -const serverSuccessRef = ref(null) +const serverSuccessRef = ref(null) const submitHandler = async ({ data }) => { submissionState.value = { diff --git a/packages/ripple-tide-landing-page/mapping/components/webforms/webforms-mapping.ts b/packages/ripple-tide-landing-page/mapping/components/webforms/webforms-mapping.ts index ea0c1086ab..cfc21cfa80 100644 --- a/packages/ripple-tide-landing-page/mapping/components/webforms/webforms-mapping.ts +++ b/packages/ripple-tide-landing-page/mapping/components/webforms/webforms-mapping.ts @@ -1,378 +1,41 @@ -import { TideDynamicPageComponent, getBody } from '@dpc-sdp/ripple-tide-api' -import { FormKitSchemaNode } from '@formkit/core' -import { getInputIcons, getValidationAndConditionals } from './webform-utils.js' -import { getAdvancedAddressMapping } from './webforms-address' +import type { TideDynamicPageComponent } from '@dpc-sdp/ripple-tide-api/types' +import { TidePageApi } from '@dpc-sdp/ripple-tide-api' +import type { TideWebform, ApiField } from '@dpc-sdp/ripple-tide-webform/types' +import { getFormSchemaFromMapping } from '@dpc-sdp/ripple-tide-webform/mapping' -export interface ITideWebform { - formId: string - successMessageHTML: string - errorMessageHTML: string - schema: FormKitSchemaNode -} - -export interface ITideFormElement { - '#type': string - '#title': string - '#required'?: boolean - '#required_error'?: string - '#description'?: string - '#help_title'?: string -} - -const getFormSchemaFromMapping = async ( - webform, - tidePageApi -): Promise => { - const elements: ITideFormElement[] = webform?.elements || [] - const fields: any[] = [] - const formId = webform?.drupal_internal__id - - for (const [fieldKey, fieldData] of Object.entries(elements)) { - let mappedField - const field = { ...fieldData, formId } - const fieldID = `${formId}_${fieldKey}` - - switch (field['#type']) { - case 'hidden': - mappedField = { - $formkit: 'hidden', - key: fieldKey, - name: fieldKey, - id: fieldID, - value: field['#default_value'] - } - break - case 'textfield': - mappedField = { - $formkit: 'RplFormText', - key: fieldKey, - name: fieldKey, - label: field['#title'], - disabled: field['#disabled'], - placeholder: field['#placeholder'], - id: fieldID, - help: field['#description'] || field['#help_title'], - value: field['#default_value'], - ...getValidationAndConditionals(field), - ...getInputIcons(field) - } - break - case 'email': - mappedField = { - $formkit: 'RplFormEmail', - key: fieldKey, - name: fieldKey, - label: field['#title'], - disabled: field['#disabled'], - placeholder: field['#placeholder'], - id: fieldID, - help: field['#description'] || field['#help_title'], - value: field['#default_value'], - ...getValidationAndConditionals(field), - ...getInputIcons(field) - } - break - case 'number': - mappedField = { - $formkit: 'RplFormNumber', - key: fieldKey, - name: fieldKey, - label: field['#title'], - disabled: field['#disabled'], - placeholder: field['#placeholder'], - id: fieldID, - help: field['#description'] || field['#help_title'], - value: field['#default_value'], - min: field['#min'], - max: field['#max'], - step: field['#step'], - ...getValidationAndConditionals(field), - ...getInputIcons(field) - } - break - case 'tel': - mappedField = { - $formkit: 'RplFormTel', - key: fieldKey, - name: fieldKey, - label: field['#title'], - disabled: field['#disabled'], - placeholder: field['#placeholder'], - id: fieldID, - help: field['#description'] || field['#help_title'], - value: field['#default_value'], - ...getValidationAndConditionals(field), - ...getInputIcons(field) - } - break - case 'url': - mappedField = { - $formkit: 'RplFormUrl', - key: fieldKey, - name: fieldKey, - label: field['#title'], - disabled: field['#disabled'], - placeholder: field['#placeholder'], - id: fieldID, - help: field['#description'] || field['#help_title'], - value: field['#default_value'], - ...getValidationAndConditionals(field), - ...getInputIcons(field) - } - break - case 'textarea': - mappedField = { - $formkit: 'RplFormTextarea', - key: fieldKey, - id: fieldID, - name: fieldKey, - label: field['#title'], - disabled: field['#disabled'], - placeholder: field['#placeholder'], - rows: field['#rows'], - help: field['#description'] || field['#help_title'], - value: field['#default_value'], - ...getValidationAndConditionals(field) - } - break - case 'date': - mappedField = { - $formkit: 'RplFormDate', - key: fieldKey, - id: fieldID, - name: fieldKey, - label: field['#title'], - disabled: field['#disabled'], - help: field['#description'] || field['#help_title'], - value: field['#default_value'], - dateFormat: 'yyyy-MM-dd', - ...getValidationAndConditionals(field) - } - break - case 'checkbox': - mappedField = { - $formkit: 'RplFormCheckbox', - key: fieldKey, - id: fieldID, - name: fieldKey, - disabled: field['#disabled'], - label: field['#help_title'], - help: field['#description'], - checkboxLabel: field['#title'], - value: field['#default_value'], - pii: false, - ...getValidationAndConditionals(field) - } - break - case 'webform_privacy_statement': - mappedField = { - $formkit: 'RplFormCheckbox', - key: fieldKey, - id: fieldID, - name: fieldKey, - disabled: field['#disabled'], - // TODO: It's not clear what field we should be using for the 'label' here because it's a new requirement, setting as 'help title' for now - label: field['#privacy_statement_heading'], - help: field['#privacy_statement_content'], - checkboxLabel: field['#title'], - value: field['#default_value'], - pii: false, - ...getValidationAndConditionals(field) - } - break - case 'select': - mappedField = { - $formkit: 'RplFormDropdown', - key: fieldKey, - id: fieldID, - name: fieldKey, - disabled: field['#disabled'], - placeholder: field['#empty_option'], - label: field['#title'], - help: field['#description'], - multiple: !!field['#multiple'], - options: Object.entries(field['#options'] || {}).map( - ([value, label]) => { - return { - id: value, - value, - label - } - } - ), - value: field['#default_value'], - pii: false, - ...getValidationAndConditionals(field) - } - break - case 'radios': - mappedField = { - $formkit: 'RplFormRadioGroup', - key: fieldKey, - id: fieldID, - name: fieldKey, - disabled: field['#disabled'], - label: field['#title'], - help: field['#description'], - options: Object.entries(field['#options'] || {}).map( - ([value, label]) => { - return { - id: value, - value, - label - } - } - ), - value: field['#default_value'], - pii: false, - ...getValidationAndConditionals(field) - } - break - case 'checkboxes': - mappedField = { - $formkit: 'RplFormCheckboxGroup', - key: fieldKey, - id: fieldID, - name: fieldKey, - disabled: field['#disabled'], - label: field['#title'], - help: field['#description'], - options: Object.entries(field['#options'] || {}).map( - ([value, label]) => { - return { - id: value, - value, - label - } - } - ), - value: field['#default_value'], - pii: false, - ...getValidationAndConditionals(field) - } - break - - case 'webform_term_select': { - const options = await tidePageApi.getTaxonomy(field['#vocabulary']) - - mappedField = { - $formkit: 'RplFormDropdown', - key: fieldKey, - id: fieldID, - name: fieldKey, - disabled: field['#disabled'], - placeholder: field['#placeholder'], - label: field['#title'], - help: field['#description'], - multiple: !!field['#multiple'], - options: (options || []).map((term) => { - return { - id: `${term.drupal_internal__tid}`, - value: `${term.drupal_internal__tid}`, - label: term.name - } - }), - value: field['#default_value'], - pii: false, - ...getValidationAndConditionals(field) - } - break - } - case 'address': - mappedField = getAdvancedAddressMapping(fieldKey, field, formId) - break - case 'webform_markup': - mappedField = { - $formkit: 'RplFormContent', - key: fieldKey, - html: getBody(field['#markup']), - ...getValidationAndConditionals(field) - } - break - case 'processed_text': - mappedField = { - $formkit: 'RplFormContent', - key: fieldKey, - html: getBody(field['#text']), - ...getValidationAndConditionals(field) - } - break - case 'webform_horizontal_rule': - mappedField = { - $formkit: 'RplFormDivider', - key: fieldKey, - ...getValidationAndConditionals(field) - } - break - case 'label': - mappedField = { - $formkit: 'RplFormLabel', - key: fieldKey, - label: field['#title'], - required: field['#required'], - ...getValidationAndConditionals(field) - } - break - case 'webform_actions': - mappedField = { - $formkit: 'RplFormActions', - key: fieldKey, - name: 'submit', - variant: 'filled', - label: field['#submit__label'], - id: fieldID, - displayResetButton: !!webform?.settings?.form_reset, - ...getValidationAndConditionals(field), - ...getInputIcons(field) - } - break - default: - mappedField = { - $el: 'div', - attrs: { - class: 'rpl-form__outer rpl-form__input--unsupported' - }, - children: [`"${field['#type']}" is not yet supported`] - } - } - - fields.push(mappedField) +const componentMapping = async (field: ApiField, tidePageApi: TidePageApi) => { + return { + title: field.field_paragraph_title, + formId: field.field_paragraph_webform.meta.drupal_internal__target_id, + hideFormOnSubmit: + field.field_paragraph_webform.settings?.confirmation_type === 'inline', + successMessageTitle: + field.field_paragraph_webform.settings?.confirmation_title || + 'Form submitted', + successMessageHTML: + field.field_paragraph_webform.settings?.confirmation_message || + 'Thank you! Your response has been submitted.', + errorMessageTitle: 'Form not submitted', + errorMessageHTML: + field.field_paragraph_webform.settings?.submission_exception_message || + 'We are experiencing a server error. Please try again, otherwise contact us.', + schema: await getFormSchemaFromMapping( + field.field_paragraph_webform, + tidePageApi + ) } - - return fields } - export const webformMapping = async ( - field, + field: ApiField, + // @ts-expect-error unused page, - tidePageApi -): TideDynamicPageComponent => { + tidePageApi: TidePageApi +): Promise> => { return { component: 'TideLandingPageWebForm', id: field.drupal_internal__id, title: field.field_paragraph_title, - props: { - title: field.field_paragraph_title, - formId: field?.field_paragraph_webform?.drupal_internal__id, - hideFormOnSubmit: - field?.field_paragraph_webform?.settings?.confirmation_type === - 'inline', - successMessageTitle: - field?.field_paragraph_webform?.settings?.confirmation_title || - 'Form submitted', - successMessageHTML: - field?.field_paragraph_webform?.settings?.confirmation_message || - 'Thank you! Your response has been submitted.', - errorMessageTitle: 'Form not submitted', - errorMessageHTML: - field?.field_paragraph_webform?.settings - ?.submission_exception_message || - 'We are experiencing a server error. Please try again, otherwise contact us.', - schema: await getFormSchemaFromMapping( - field?.field_paragraph_webform, - tidePageApi - ) - } + props: await componentMapping(field, tidePageApi) } } diff --git a/packages/ripple-tide-landing-page/package.json b/packages/ripple-tide-landing-page/package.json index 17c2e5ccf1..efe123fe5a 100644 --- a/packages/ripple-tide-landing-page/package.json +++ b/packages/ripple-tide-landing-page/package.json @@ -13,6 +13,7 @@ "dependencies": { "@dpc-sdp/nuxt-ripple": "workspace:*", "@dpc-sdp/ripple-tide-api": "workspace:*", + "@dpc-sdp/ripple-tide-webform": "workspace:*", "@dpc-sdp/ripple-ui-core": "workspace:*", "@dpc-sdp/ripple-ui-forms": "workspace:*", "ohmyfetch": "^0.4.21" diff --git a/packages/ripple-tide-webform/CHANGELOG.md b/packages/ripple-tide-webform/CHANGELOG.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/ripple-tide-webform/LICENSE b/packages/ripple-tide-webform/LICENSE new file mode 100644 index 0000000000..6b97259524 --- /dev/null +++ b/packages/ripple-tide-webform/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2018 Software Freedom Conservancy (SFC) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/packages/ripple-tide-webform/README.md b/packages/ripple-tide-webform/README.md new file mode 100644 index 0000000000..f425e8cacf --- /dev/null +++ b/packages/ripple-tide-webform/README.md @@ -0,0 +1,24 @@ +# Ripple Tide Webform + +> Ripple mappings and templates for the Tide webforms. + +## Installation + +To use this package in your Nuxt project first install it with `npm`. + +```bash +npm install @dpc-sdp/ripple-tide-webform +``` + +## Usage + +Add the installed package in your sites `nuxt.config.js` file under the extends property, this includes the package as a [Nuxt Layer](https://nuxt.com/docs/getting-started/layers). +This will add an API request endpoint for back end webform schemas, and front end support for form submissions. + +```js +export default defineNuxtConfig({ + extends: [ + '@dpc-sdp/ripple-tide-webform' + ] +}) +``` diff --git a/packages/ripple-tide-webform/composables/use-web-form.ts b/packages/ripple-tide-webform/composables/use-web-form.ts new file mode 100644 index 0000000000..52e6527a4b --- /dev/null +++ b/packages/ripple-tide-webform/composables/use-web-form.ts @@ -0,0 +1,107 @@ +import { $fetch } from 'ohmyfetch' +import { ref, useRuntimeConfig } from '#imports' + +export function useWebForm() { + const postForm = async (formId: string, formData = {}) => { + const { public: config } = useRuntimeConfig() + + const formResource = 'webform_submission' + + const body = { + data: { + type: formResource, + attributes: { + remote_addr: '0.0.0.0', // IP placeholder for Tide validation, incase the IP is required. + data: JSON.stringify(formData) + } + } + } + + // TODO: Add better error handling/log for form API error. + // It's blocked by Tide webform response issue SDPA-477. + // Currently the Tide webform has no right response. + const url = `/api/tide/${formResource}/${formId}` + const { data, error } = await $fetch(url, { + method: 'POST', + baseURL: config.apiUrl || '', + body, + params: { + site: config.tide.site + }, + headers: { + 'Content-Type': 'application/vnd.api+json;charset=UTF-8' + } + }) + + if (error) { + throw error + } + + if (!data) { + throw new Error('Form submission failed') + } + + return data + } + + const submissionState = ref({ + status: 'idle', + title: '', + message: '', + receipt: '' + }) + + interface FormConfig { + formId: string + successMessageTitle: string + successMessageHTML: string + errorMessageTitle: string + errorMessageHTML: string + } + + const submitHandler = async (props: FormConfig, data: any) => { + submissionState.value = { + status: 'submitting', + title: '', + message: '', + receipt: '' + } + + try { + const resData = await postForm(props.formId, data) + + const [code, note] = resData.attributes?.notes?.split('|') || [] + + // Upstream error + if (code && Number.isInteger(+code) && (+code <= 199 || +code >= 300)) { + submissionState.value = { + status: 'error', + title: props.errorMessageTitle, + message: note || props.errorMessageHTML, + receipt: '' + } + } else { + submissionState.value = { + status: 'success', + title: props.successMessageTitle, + message: note || props.successMessageHTML, + receipt: resData.attributes?.serial + } + } + } catch (error) { + console.error(error) + + submissionState.value = { + status: 'error', + title: props.errorMessageTitle, + message: props.errorMessageHTML, + receipt: '' + } + } + } + + return { + submissionState, + submitHandler + } +} diff --git a/packages/ripple-tide-webform/mapping/index.ts b/packages/ripple-tide-webform/mapping/index.ts new file mode 100644 index 0000000000..5da3acaf11 --- /dev/null +++ b/packages/ripple-tide-webform/mapping/index.ts @@ -0,0 +1 @@ +export { getFormSchemaFromMapping } from './webforms-mapping' diff --git a/packages/ripple-tide-landing-page/mapping/components/webforms/webform-conditional-logic.test.ts b/packages/ripple-tide-webform/mapping/webform-conditional-logic.test.ts similarity index 100% rename from packages/ripple-tide-landing-page/mapping/components/webforms/webform-conditional-logic.test.ts rename to packages/ripple-tide-webform/mapping/webform-conditional-logic.test.ts diff --git a/packages/ripple-tide-landing-page/mapping/components/webforms/webform-conditional-logic.ts b/packages/ripple-tide-webform/mapping/webform-conditional-logic.ts similarity index 100% rename from packages/ripple-tide-landing-page/mapping/components/webforms/webform-conditional-logic.ts rename to packages/ripple-tide-webform/mapping/webform-conditional-logic.ts diff --git a/packages/ripple-tide-landing-page/mapping/components/webforms/webform-utils.ts b/packages/ripple-tide-webform/mapping/webform-utils.ts similarity index 100% rename from packages/ripple-tide-landing-page/mapping/components/webforms/webform-utils.ts rename to packages/ripple-tide-webform/mapping/webform-utils.ts diff --git a/packages/ripple-tide-landing-page/mapping/components/webforms/webform-validation.ts b/packages/ripple-tide-webform/mapping/webform-validation.ts similarity index 100% rename from packages/ripple-tide-landing-page/mapping/components/webforms/webform-validation.ts rename to packages/ripple-tide-webform/mapping/webform-validation.ts diff --git a/packages/ripple-tide-landing-page/mapping/components/webforms/webforms-address.ts b/packages/ripple-tide-webform/mapping/webforms-address.ts similarity index 100% rename from packages/ripple-tide-landing-page/mapping/components/webforms/webforms-address.ts rename to packages/ripple-tide-webform/mapping/webforms-address.ts diff --git a/packages/ripple-tide-webform/mapping/webforms-mapping.ts b/packages/ripple-tide-webform/mapping/webforms-mapping.ts new file mode 100644 index 0000000000..65af660d71 --- /dev/null +++ b/packages/ripple-tide-webform/mapping/webforms-mapping.ts @@ -0,0 +1,330 @@ +import { getBody, TidePageApi } from '@dpc-sdp/ripple-tide-api' +import { FormKitSchemaNode } from '@formkit/core' +import { getInputIcons, getValidationAndConditionals } from './webform-utils.js' +import { getAdvancedAddressMapping } from './webforms-address' +import type { TideWebformElement, ApiWebForm } from './../types.js' + +export const getFormSchemaFromMapping = async ( + webform: ApiWebForm, + tidePageApi: TidePageApi +): Promise => { + const elements: TideWebformElement[] = webform.elements || [] + const fields = [] + const formId = webform.drupal_internal__id + + for (const [fieldKey, fieldData] of Object.entries(elements)) { + let mappedField + const field: TideWebformElement = { ...fieldData, formId } + const fieldID = `${formId}_${fieldKey}` + + switch (field['#type']) { + case 'hidden': + mappedField = { + $formkit: 'hidden', + key: fieldKey, + name: fieldKey, + id: fieldID, + value: field['#default_value'] + } + break + case 'textfield': + mappedField = { + $formkit: 'RplFormText', + key: fieldKey, + name: fieldKey, + label: field['#title'], + disabled: field['#disabled'], + placeholder: field['#placeholder'], + id: fieldID, + help: field['#description'] || field['#help_title'], + value: field['#default_value'], + ...getValidationAndConditionals(field), + ...getInputIcons(field) + } + break + case 'email': + mappedField = { + $formkit: 'RplFormEmail', + key: fieldKey, + name: fieldKey, + label: field['#title'], + disabled: field['#disabled'], + placeholder: field['#placeholder'], + id: fieldID, + help: field['#description'] || field['#help_title'], + value: field['#default_value'], + ...getValidationAndConditionals(field), + ...getInputIcons(field) + } + break + case 'number': + mappedField = { + $formkit: 'RplFormNumber', + key: fieldKey, + name: fieldKey, + label: field['#title'], + disabled: field['#disabled'], + placeholder: field['#placeholder'], + id: fieldID, + help: field['#description'] || field['#help_title'], + value: field['#default_value'], + min: field['#min'], + max: field['#max'], + step: field['#step'], + ...getValidationAndConditionals(field), + ...getInputIcons(field) + } + break + case 'tel': + mappedField = { + $formkit: 'RplFormTel', + key: fieldKey, + name: fieldKey, + label: field['#title'], + disabled: field['#disabled'], + placeholder: field['#placeholder'], + id: fieldID, + help: field['#description'] || field['#help_title'], + value: field['#default_value'], + ...getValidationAndConditionals(field), + ...getInputIcons(field) + } + break + case 'url': + mappedField = { + $formkit: 'RplFormUrl', + key: fieldKey, + name: fieldKey, + label: field['#title'], + disabled: field['#disabled'], + placeholder: field['#placeholder'], + id: fieldID, + help: field['#description'] || field['#help_title'], + value: field['#default_value'], + ...getValidationAndConditionals(field), + ...getInputIcons(field) + } + break + case 'textarea': + mappedField = { + $formkit: 'RplFormTextarea', + key: fieldKey, + id: fieldID, + name: fieldKey, + label: field['#title'], + disabled: field['#disabled'], + placeholder: field['#placeholder'], + rows: field['#rows'], + help: field['#description'] || field['#help_title'], + value: field['#default_value'], + ...getValidationAndConditionals(field) + } + break + case 'date': + mappedField = { + $formkit: 'RplFormDate', + key: fieldKey, + id: fieldID, + name: fieldKey, + label: field['#title'], + disabled: field['#disabled'], + help: field['#description'] || field['#help_title'], + value: field['#default_value'], + dateFormat: 'yyyy-MM-dd', + ...getValidationAndConditionals(field) + } + break + case 'checkbox': + mappedField = { + $formkit: 'RplFormCheckbox', + key: fieldKey, + id: fieldID, + name: fieldKey, + disabled: field['#disabled'], + label: field['#help_title'], + help: field['#description'], + checkboxLabel: field['#title'], + value: field['#default_value'], + pii: false, + ...getValidationAndConditionals(field) + } + break + case 'webform_privacy_statement': + mappedField = { + $formkit: 'RplFormCheckbox', + key: fieldKey, + id: fieldID, + name: fieldKey, + disabled: field['#disabled'], + // TODO: It's not clear what field we should be using for the 'label' here because it's a new requirement, setting as 'help title' for now + label: field['#privacy_statement_heading'], + help: field['#privacy_statement_content'], + checkboxLabel: field['#title'], + value: field['#default_value'], + pii: false, + ...getValidationAndConditionals(field) + } + break + case 'select': + mappedField = { + $formkit: 'RplFormDropdown', + key: fieldKey, + id: fieldID, + name: fieldKey, + disabled: field['#disabled'], + placeholder: field['#empty_option'], + label: field['#title'], + help: field['#description'], + multiple: !!field['#multiple'], + options: Object.entries(field['#options'] || {}).map( + ([value, label]) => { + return { + id: value, + value, + label + } + } + ), + value: field['#default_value'], + pii: false, + ...getValidationAndConditionals(field) + } + break + case 'radios': + mappedField = { + $formkit: 'RplFormRadioGroup', + key: fieldKey, + id: fieldID, + name: fieldKey, + disabled: field['#disabled'], + label: field['#title'], + help: field['#description'], + options: Object.entries(field['#options'] || {}).map( + ([value, label]) => { + return { + id: value, + value, + label + } + } + ), + value: field['#default_value'], + pii: false, + ...getValidationAndConditionals(field) + } + break + case 'checkboxes': + mappedField = { + $formkit: 'RplFormCheckboxGroup', + key: fieldKey, + id: fieldID, + name: fieldKey, + disabled: field['#disabled'], + label: field['#title'], + help: field['#description'], + options: Object.entries(field['#options'] || {}).map( + ([value, label]) => { + return { + id: value, + value, + label + } + } + ), + value: field['#default_value'], + pii: false, + ...getValidationAndConditionals(field) + } + break + + case 'webform_term_select': { + const options = await tidePageApi.getTaxonomy(field['#vocabulary']) + + mappedField = { + $formkit: 'RplFormDropdown', + key: fieldKey, + id: fieldID, + name: fieldKey, + disabled: field['#disabled'], + placeholder: field['#placeholder'], + label: field['#title'], + help: field['#description'], + multiple: !!field['#multiple'], + options: (options || []).map( + (term: { drupal_internal__tid: string; name: string }) => { + return { + id: `${term.drupal_internal__tid}`, + value: `${term.drupal_internal__tid}`, + label: term.name + } + } + ), + value: field['#default_value'], + pii: false, + ...getValidationAndConditionals(field) + } + break + } + case 'address': + mappedField = getAdvancedAddressMapping(fieldKey, field, formId) + break + case 'webform_markup': + mappedField = { + $formkit: 'RplFormContent', + key: fieldKey, + html: getBody(field['#markup']), + ...getValidationAndConditionals(field) + } + break + case 'processed_text': + mappedField = { + $formkit: 'RplFormContent', + key: fieldKey, + html: getBody(field['#text']), + ...getValidationAndConditionals(field) + } + break + case 'webform_horizontal_rule': + mappedField = { + $formkit: 'RplFormDivider', + key: fieldKey, + ...getValidationAndConditionals(field) + } + break + case 'label': + mappedField = { + $formkit: 'RplFormLabel', + key: fieldKey, + label: field['#title'], + required: field['#required'], + ...getValidationAndConditionals(field) + } + break + case 'webform_actions': + mappedField = { + $formkit: 'RplFormActions', + key: fieldKey, + name: 'submit', + variant: 'filled', + label: field['#submit__label'], + id: fieldID, + displayResetButton: !!webform?.settings?.form_reset, + ...getValidationAndConditionals(field), + ...getInputIcons(field) + } + break + default: + mappedField = { + $el: 'div', + attrs: { + class: 'rpl-form__outer rpl-form__input--unsupported' + }, + children: [`"${field['#type']}" is not yet supported`] + } + } + + fields.push(mappedField) + } + + return fields +} diff --git a/packages/ripple-tide-webform/nuxt.config.ts b/packages/ripple-tide-webform/nuxt.config.ts new file mode 100644 index 0000000000..f716d9f9da --- /dev/null +++ b/packages/ripple-tide-webform/nuxt.config.ts @@ -0,0 +1,3 @@ +import { defineNuxtConfig } from 'nuxt/config' + +export default defineNuxtConfig({}) diff --git a/packages/ripple-tide-webform/package.json b/packages/ripple-tide-webform/package.json new file mode 100644 index 0000000000..c6b7c722ed --- /dev/null +++ b/packages/ripple-tide-webform/package.json @@ -0,0 +1,20 @@ +{ + "name": "@dpc-sdp/ripple-tide-webform", + "description": "Ripple mappings and components for Tide webforms", + "version": "2.8.2", + "license": "Apache-2.0", + "repository": "https://github.com/dpc-sdp/ripple-framework", + "main": "./nuxt.config.ts", + "exports": { + ".": "./nuxt.config.ts", + "./mapping": "./mapping/index.ts", + "./types": "./types.ts" + }, + "dependencies": { + "@dpc-sdp/nuxt-ripple": "workspace:*", + "@dpc-sdp/ripple-tide-api": "workspace:*", + "@dpc-sdp/nuxt-ripple-preview": "workspace:*", + "h3": "^1.9.0", + "ohmyfetch": "^0.4.21" + } +} diff --git a/packages/ripple-tide-webform/server/api/tide/webform.ts b/packages/ripple-tide-webform/server/api/tide/webform.ts new file mode 100644 index 0000000000..77fb29e00d --- /dev/null +++ b/packages/ripple-tide-webform/server/api/tide/webform.ts @@ -0,0 +1,99 @@ +import jsonapiParse from 'jsonapi-parse' +import { defineEventHandler, getCookie, getQuery, H3Event } from 'h3' +import { + createHandler, + TideApiBase, + logger, + TidePageApi +} from '@dpc-sdp/ripple-tide-api' +import { BadRequestError } from '@dpc-sdp/ripple-tide-api/errors' +import type { + RplTideModuleConfig, + IRplTideModuleMapping, + ILogger +} from '@dpc-sdp/ripple-tide-api/types' +import { useRuntimeConfig } from '#imports' +import { AuthCookieNames } from '@dpc-sdp/nuxt-ripple-preview/utils' +import { getFormSchemaFromMapping } from '../../../mapping' + +/** + * @description Custom API call methods and response mapping for webform + */ +class TideWebformApi extends TideApiBase { + webformMapping: IRplTideModuleMapping + declare logLabel: string + + constructor(tide: RplTideModuleConfig, logger: ILogger) { + super(tide, logger) + this.webformMapping = { + mapping: { + _src: (field: any) => + process.env.NODE_ENV === 'development' ? field : undefined, + schema: async (field: any, page, tidePageApi: TidePageApi) => + await getFormSchemaFromMapping(field, tidePageApi) + }, + includes: [] + } + this.logLabel = 'TideWebform' + } + + async getWebform(id: string, headers = {}) { + try { + const { data: response } = await this.get(`/webform/webform/${id}`, { + headers + }) + const resource = jsonapiParse.parse(response).data + const siteData = await this.getMappedData( + this.webformMapping.mapping, + resource + ) + return siteData + } catch (error: any) { + // Could be 404? webform could be in share or preview so need to ignore this error and render page anyway + logger.error(`Error fetching webform`, error) + } + } +} + +export const createWebformHandler = async ( + event: H3Event, + webformApi: TideWebformApi +) => { + return createHandler(event, 'WebformHandler', async () => { + const query: any = await getQuery(event) + + if (!query.id) { + throw new BadRequestError('Webform ID is required') + } + + const tokenCookie = getCookie(event, AuthCookieNames.ACCESS_TOKEN) + const accessTokenExpiry = parseFloat( + getCookie(event, AuthCookieNames.ACCESS_TOKEN_EXPIRY) || '' + ) + const isTokenExpired = accessTokenExpiry + ? accessTokenExpiry < Date.now() + : true + + const headers: any = {} + + if (tokenCookie && !isTokenExpired) { + headers['X-OAuth2-Authorization'] = `Bearer ${tokenCookie}` + } + + return await webformApi.getWebform(query.id, headers) + }) +} + +export default defineEventHandler(async (event: H3Event) => { + const config = useRuntimeConfig() + const webformApi = new TideWebformApi( + { ...(config.public.tide as any), ...(config.tide as any) }, + logger + ) + + event.context.tide = { + webformApi + } + + return createWebformHandler(event, event.context.tide.webformApi) +}) diff --git a/packages/ripple-tide-webform/tsconfig.json b/packages/ripple-tide-webform/tsconfig.json new file mode 100644 index 0000000000..2403fca1a1 --- /dev/null +++ b/packages/ripple-tide-webform/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./../../examples/nuxt-app/.nuxt/tsconfig.json" +} diff --git a/packages/ripple-tide-webform/types.ts b/packages/ripple-tide-webform/types.ts new file mode 100644 index 0000000000..7c0d2fcb7a --- /dev/null +++ b/packages/ripple-tide-webform/types.ts @@ -0,0 +1,49 @@ +import type { FormKitSchemaNode } from '@formkit/core' + +export interface TideWebform { + title: string + formId: string + hideFormOnSubmit?: boolean + successMessageTitle: string + successMessageHTML: string + errorMessageTitle: string + errorMessageHTML: string + schema: FormKitSchemaNode[] +} + +export interface TideWebformElement { + '#type': string + '#title': string + '#required'?: boolean + '#required_error'?: string + '#description'?: string + '#help_title'?: string + [key: string]: any +} + +/** + * @description Raw API field response + */ +export interface ApiWebForm { + drupal_internal__id: string + elements: TideWebformElement[] + meta: { + drupal_internal__target_id: string + } + settings?: { + confirmation_type?: string + confirmation_title?: string + confirmation_message?: string + submission_exception_message?: string + form_reset?: boolean + } +} + +/** + * @description Raw API field response + */ +export interface ApiField { + drupal_internal__id: string + field_paragraph_title: string + field_paragraph_webform: ApiWebForm +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 201398ba65..1422658cd2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -223,6 +223,9 @@ importers: '@dpc-sdp/ripple-tide-topic': specifier: workspace:* version: link:../../packages/ripple-tide-topic + '@dpc-sdp/ripple-tide-webform': + specifier: workspace:* + version: link:../../packages/ripple-tide-webform '@dpc-sdp/ripple-ui-maps': specifier: workspace:* version: link:../../packages/ripple-ui-maps @@ -652,6 +655,9 @@ importers: '@dpc-sdp/ripple-tide-api': specifier: workspace:* version: link:../ripple-tide-api + '@dpc-sdp/ripple-tide-webform': + specifier: workspace:* + version: link:../ripple-tide-webform '@dpc-sdp/ripple-ui-core': specifier: workspace:* version: link:../ripple-ui-core @@ -740,6 +746,24 @@ importers: specifier: workspace:* version: link:../ripple-ui-core + packages/ripple-tide-webform: + dependencies: + '@dpc-sdp/nuxt-ripple': + specifier: workspace:* + version: link:../nuxt-ripple + '@dpc-sdp/nuxt-ripple-preview': + specifier: workspace:* + version: link:../nuxt-ripple-preview + '@dpc-sdp/ripple-tide-api': + specifier: workspace:* + version: link:../ripple-tide-api + h3: + specifier: ^1.9.0 + version: 1.10.1 + ohmyfetch: + specifier: ^0.4.21 + version: 0.4.21 + packages/ripple-ui-core: dependencies: '@nuxt/kit': @@ -4595,7 +4619,7 @@ packages: ms: 2.1.3 secure-json-parse: 2.7.0 tslib: 2.5.0 - undici: 5.19.1 + undici: 5.28.2 transitivePeerDependencies: - supports-color dev: false @@ -7260,6 +7284,7 @@ packages: - nuxt - rollup - supports-color + - uWebSockets.js - utf-8-validate - vue dev: false @@ -7428,7 +7453,7 @@ packages: '@nuxt/kit': 3.11.2 birpc: 0.2.14 consola: 3.2.3 - destr: 2.0.2 + destr: 2.0.3 error-stack-parser-es: 0.1.1 execa: 7.2.0 fast-glob: 3.3.2 @@ -7452,7 +7477,7 @@ packages: semver: 7.6.0 simple-git: 3.22.0 sirv: 2.0.4 - unimport: 3.7.1(rollup@4.9.1) + unimport: 3.7.1(rollup@4.14.3) vite: 4.1.5(@types/node@18.15.10) vite-plugin-inspect: 0.8.1(@nuxt/kit@3.11.2)(vite@4.1.5) vite-plugin-vue-inspector: 4.0.2(vite@4.1.5) @@ -7507,7 +7532,7 @@ packages: semver: 7.6.0 simple-git: 3.24.0 sirv: 2.0.4 - unimport: 3.7.1(rollup@4.9.1) + unimport: 3.7.1(rollup@4.14.3) vite: 4.1.5(@types/node@18.15.10) vite-plugin-inspect: 0.8.3(@nuxt/kit@3.11.2)(vite@4.1.5) vite-plugin-vue-inspector: 4.0.2(vite@4.1.5) @@ -7596,9 +7621,9 @@ packages: pkg-types: 1.0.3 scule: 1.3.0 semver: 7.6.0 - ufo: 1.4.0 + ufo: 1.5.3 unctx: 2.3.1 - unimport: 3.7.1(rollup@4.9.1) + unimport: 3.7.1(rollup@4.14.3) untyped: 1.4.2 transitivePeerDependencies: - rollup @@ -7625,7 +7650,7 @@ packages: semver: 7.6.0 ufo: 1.5.3 unctx: 2.3.1 - unimport: 3.7.1(rollup@4.9.1) + unimport: 3.7.1(rollup@4.14.3) untyped: 1.4.2 transitivePeerDependencies: - rollup @@ -7676,9 +7701,9 @@ packages: pkg-types: 1.0.3 scule: 1.3.0 semver: 7.6.0 - ufo: 1.4.0 + ufo: 1.5.3 unctx: 2.3.1 - unimport: 3.7.1(rollup@4.9.1) + unimport: 3.7.1(rollup@4.14.3) untyped: 1.4.2 transitivePeerDependencies: - rollup @@ -7718,8 +7743,8 @@ packages: pkg-types: 1.0.3 scule: 1.3.0 std-env: 3.7.0 - ufo: 1.4.0 - unimport: 3.7.1(rollup@4.9.1) + ufo: 1.5.3 + unimport: 3.7.1(rollup@4.14.3) untyped: 1.4.2 transitivePeerDependencies: - rollup @@ -7739,7 +7764,7 @@ packages: scule: 1.3.0 std-env: 3.7.0 ufo: 1.5.3 - unimport: 3.7.1(rollup@4.9.1) + unimport: 3.7.1(rollup@4.14.3) untyped: 1.4.2 transitivePeerDependencies: - rollup @@ -7760,7 +7785,7 @@ packages: scule: 1.3.0 std-env: 3.7.0 ufo: 1.5.3 - unimport: 3.7.1(rollup@4.9.1) + unimport: 3.7.1(rollup@4.14.3) untyped: 1.4.2 transitivePeerDependencies: - rollup @@ -7780,7 +7805,7 @@ packages: scule: 1.3.0 std-env: 3.7.0 ufo: 1.5.3 - unimport: 3.7.1(rollup@4.9.1) + unimport: 3.7.1(rollup@4.14.3) untyped: 1.4.2 transitivePeerDependencies: - rollup @@ -7795,7 +7820,7 @@ packages: consola: 3.2.3 create-require: 1.1.1 defu: 6.1.4 - destr: 2.0.2 + destr: 2.0.3 dotenv: 16.4.4 git-url-parse: 13.1.1 is-docker: 3.0.0 @@ -7825,7 +7850,7 @@ packages: vue: ^3.3.4 dependencies: '@nuxt/kit': 3.10.2 - '@rollup/plugin-replace': 5.0.5(rollup@4.9.1) + '@rollup/plugin-replace': 5.0.5(rollup@4.14.3) '@vitejs/plugin-vue': 5.0.4(vite@5.1.1)(vue@3.4.23) '@vitejs/plugin-vue-jsx': 3.1.0(vite@5.1.1)(vue@3.4.23) autoprefixer: 10.4.17(postcss@8.4.38) @@ -7839,7 +7864,7 @@ packages: externality: 1.0.2 fs-extra: 11.2.0 get-port-please: 3.1.2 - h3: 1.10.1 + h3: 1.11.1 knitwork: 1.0.0 magic-string: 0.30.10 mlly: 1.5.0 @@ -7848,10 +7873,10 @@ packages: perfect-debounce: 1.0.0 pkg-types: 1.0.3 postcss: 8.4.38 - rollup-plugin-visualizer: 5.12.0(rollup@4.9.1) + rollup-plugin-visualizer: 5.12.0(rollup@4.14.3) std-env: 3.7.0 strip-literal: 2.1.0 - ufo: 1.4.0 + ufo: 1.5.3 unenv: 1.9.0 unplugin: 1.10.1 vite: 5.1.1(@types/node@18.15.10) @@ -7874,6 +7899,7 @@ packages: - supports-color - terser - typescript + - uWebSockets.js - vls - vti - vue-tsc @@ -7886,7 +7912,7 @@ packages: vue: ^3.3.4 dependencies: '@nuxt/kit': 3.11.2 - '@rollup/plugin-replace': 5.0.5(rollup@4.9.1) + '@rollup/plugin-replace': 5.0.5(rollup@4.14.3) '@vitejs/plugin-vue': 5.0.4(vite@5.2.9)(vue@3.4.23) '@vitejs/plugin-vue-jsx': 3.1.0(vite@5.2.9)(vue@3.4.23) autoprefixer: 10.4.19(postcss@8.4.38) @@ -7909,7 +7935,7 @@ packages: perfect-debounce: 1.0.0 pkg-types: 1.0.3 postcss: 8.4.38 - rollup-plugin-visualizer: 5.12.0(rollup@4.9.1) + rollup-plugin-visualizer: 5.12.0(rollup@4.14.3) std-env: 3.7.0 strip-literal: 2.1.0 ufo: 1.5.3 @@ -7968,7 +7994,7 @@ packages: consola: 3.2.3 debug: 4.3.4(supports-color@8.1.1) defu: 6.1.4 - destr: 2.0.2 + destr: 2.0.3 detab: 3.0.2 github-slugger: 2.0.0 hast-util-to-string: 3.0.0 @@ -7990,7 +8016,7 @@ packages: remark-rehype: 11.1.0 scule: 1.3.0 shiki: 1.1.5 - ufo: 1.4.0 + ufo: 1.5.3 unified: 11.0.4 unist-builder: 4.0.0 unist-util-visit: 5.0.0 @@ -9154,7 +9180,6 @@ packages: '@rollup/pluginutils': 5.1.0(rollup@4.14.3) magic-string: 0.30.10 rollup: 4.14.3 - dev: true /@rollup/plugin-replace@5.0.5(rollup@4.9.1): resolution: {integrity: sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ==} @@ -9168,6 +9193,7 @@ packages: '@rollup/pluginutils': 5.1.0(rollup@4.9.1) magic-string: 0.30.10 rollup: 4.9.1 + dev: false /@rollup/plugin-terser@0.4.4(rollup@4.14.3): resolution: {integrity: sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==} @@ -9245,7 +9271,6 @@ packages: estree-walker: 2.0.2 picomatch: 2.3.1 rollup: 4.14.3 - dev: true /@rollup/pluginutils@5.1.0(rollup@4.9.1): resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} @@ -9260,13 +9285,13 @@ packages: estree-walker: 2.0.2 picomatch: 2.3.1 rollup: 4.9.1 + dev: false /@rollup/rollup-android-arm-eabi@4.14.3: resolution: {integrity: sha512-X9alQ3XM6I9IlSlmC8ddAvMSyG1WuHk5oUnXGw+yUBs3BFoTizmG1La/Gr8fVJvDWAq+zlYTZ9DBgrlKRVY06g==} cpu: [arm] os: [android] requiresBuild: true - dev: true optional: true /@rollup/rollup-android-arm-eabi@4.9.1: @@ -9281,7 +9306,6 @@ packages: cpu: [arm64] os: [android] requiresBuild: true - dev: true optional: true /@rollup/rollup-android-arm64@4.9.1: @@ -9296,7 +9320,6 @@ packages: cpu: [arm64] os: [darwin] requiresBuild: true - dev: true optional: true /@rollup/rollup-darwin-arm64@4.9.1: @@ -9311,7 +9334,6 @@ packages: cpu: [x64] os: [darwin] requiresBuild: true - dev: true optional: true /@rollup/rollup-darwin-x64@4.9.1: @@ -9326,7 +9348,6 @@ packages: cpu: [arm] os: [linux] requiresBuild: true - dev: true optional: true /@rollup/rollup-linux-arm-gnueabihf@4.9.1: @@ -9341,7 +9362,6 @@ packages: cpu: [arm] os: [linux] requiresBuild: true - dev: true optional: true /@rollup/rollup-linux-arm64-gnu@4.14.3: @@ -9349,7 +9369,6 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true - dev: true optional: true /@rollup/rollup-linux-arm64-gnu@4.9.1: @@ -9364,7 +9383,6 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true - dev: true optional: true /@rollup/rollup-linux-arm64-musl@4.9.1: @@ -9379,7 +9397,6 @@ packages: cpu: [ppc64] os: [linux] requiresBuild: true - dev: true optional: true /@rollup/rollup-linux-riscv64-gnu@4.14.3: @@ -9387,7 +9404,6 @@ packages: cpu: [riscv64] os: [linux] requiresBuild: true - dev: true optional: true /@rollup/rollup-linux-riscv64-gnu@4.9.1: @@ -9402,7 +9418,6 @@ packages: cpu: [s390x] os: [linux] requiresBuild: true - dev: true optional: true /@rollup/rollup-linux-x64-gnu@4.14.3: @@ -9410,7 +9425,6 @@ packages: cpu: [x64] os: [linux] requiresBuild: true - dev: true optional: true /@rollup/rollup-linux-x64-gnu@4.9.1: @@ -9425,7 +9439,6 @@ packages: cpu: [x64] os: [linux] requiresBuild: true - dev: true optional: true /@rollup/rollup-linux-x64-musl@4.9.1: @@ -9440,7 +9453,6 @@ packages: cpu: [arm64] os: [win32] requiresBuild: true - dev: true optional: true /@rollup/rollup-win32-arm64-msvc@4.9.1: @@ -9455,7 +9467,6 @@ packages: cpu: [ia32] os: [win32] requiresBuild: true - dev: true optional: true /@rollup/rollup-win32-ia32-msvc@4.9.1: @@ -9470,7 +9481,6 @@ packages: cpu: [x64] os: [win32] requiresBuild: true - dev: true optional: true /@rollup/rollup-win32-x64-msvc@4.9.1: @@ -10589,7 +10599,7 @@ packages: ts-dedent: 2.2.0 type-fest: 2.19.0 vue: 3.4.23(typescript@5.0.2) - vue-component-type-helpers: 2.0.14 + vue-component-type-helpers: 2.0.16 transitivePeerDependencies: - encoding - supports-color @@ -11836,7 +11846,7 @@ packages: hasBin: true dependencies: '@ampproject/remapping': 2.3.0 - '@rollup/pluginutils': 5.1.0(rollup@4.9.1) + '@rollup/pluginutils': 5.1.0(rollup@4.14.3) '@unocss/config': 0.59.3 '@unocss/core': 0.59.3 '@unocss/preset-uno': 0.59.3 @@ -12014,7 +12024,7 @@ packages: vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 dependencies: '@ampproject/remapping': 2.3.0 - '@rollup/pluginutils': 5.1.0(rollup@4.9.1) + '@rollup/pluginutils': 5.1.0(rollup@4.14.3) '@unocss/config': 0.59.3 '@unocss/core': 0.59.3 '@unocss/inspector': 0.59.3 @@ -12183,12 +12193,12 @@ packages: optional: true dependencies: '@babel/types': 7.23.9 - '@rollup/pluginutils': 5.1.0(rollup@4.9.1) + '@rollup/pluginutils': 5.1.0(rollup@4.14.3) '@vue/compiler-sfc': 3.4.23 ast-kit: 0.11.3 local-pkg: 0.5.0 magic-string-ast: 0.3.0 - vue: 3.4.23(typescript@5.0.2) + vue: 3.4.23(typescript@5.1.3) transitivePeerDependencies: - rollup @@ -13448,7 +13458,7 @@ packages: engines: {node: '>=16.14.0'} dependencies: '@babel/parser': 7.24.4 - '@rollup/pluginutils': 5.1.0(rollup@4.9.1) + '@rollup/pluginutils': 5.1.0(rollup@4.14.3) pathe: 1.1.2 transitivePeerDependencies: - rollup @@ -13458,7 +13468,7 @@ packages: engines: {node: '>=16.14.0'} dependencies: '@babel/parser': 7.24.4 - '@rollup/pluginutils': 5.1.0(rollup@4.9.1) + '@rollup/pluginutils': 5.1.0(rollup@4.14.3) pathe: 1.1.2 transitivePeerDependencies: - rollup @@ -15523,10 +15533,10 @@ packages: /cookie-es@1.0.0: resolution: {integrity: sha512-mWYvfOLrfEc996hlKcdABeIiPHUPC6DM2QYZdGGOvhOTbA3tjm2eBwqlJpoFdjC89NI4Qt6h0Pu06Mp+1Pj5OQ==} + dev: false /cookie-es@1.1.0: resolution: {integrity: sha512-L2rLOcK0wzWSfSDA33YR+PUHDG10a8px7rUHKWbGLP4YfbsMed2KFUw5fczvDPbT98DDe3LEzviswl810apTEw==} - dev: true /cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} @@ -15717,7 +15727,6 @@ packages: peerDependenciesMeta: uWebSockets.js: optional: true - dev: true /crypto-random-string@2.0.0: resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} @@ -16507,10 +16516,10 @@ packages: /destr@2.0.2: resolution: {integrity: sha512-65AlobnZMiCET00KaFFjUefxDX0khFA/E4myqZ7a6Sq1yZtR8+FVIvilVX66vF2uobSumxooYZChiRPCKNqhmg==} + dev: false /destr@2.0.3: resolution: {integrity: sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==} - dev: true /destroy@1.2.0: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} @@ -19198,13 +19207,13 @@ packages: /h3@1.10.1: resolution: {integrity: sha512-UBAUp47hmm4BB5/njB4LrEa9gpuvZj4/Qf/ynSMzO6Ku2RXaouxEfiG2E2IFnv6fxbhAkzjasDxmo6DFdEeXRg==} dependencies: - cookie-es: 1.0.0 + cookie-es: 1.1.0 defu: 6.1.4 - destr: 2.0.2 + destr: 2.0.3 iron-webcrypto: 1.0.0 ohash: 1.1.3 - radix3: 1.1.0 - ufo: 1.4.0 + radix3: 1.1.2 + ufo: 1.5.3 uncrypto: 0.1.3 unenv: 1.9.0 dev: false @@ -19224,27 +19233,26 @@ packages: unenv: 1.9.0 transitivePeerDependencies: - uWebSockets.js - dev: true /h3@1.2.1: resolution: {integrity: sha512-uYRENV7pNd4kl4nGFDkVOZRZgDVmH4Ur4h64QAsqPsHj1jxI6MAU+7xT0JX9Tg3iKbmgMPpRf8QI6e4CJZ12MQ==} dependencies: cookie-es: 0.5.0 destr: 1.2.2 - radix3: 1.1.0 - ufo: 1.4.0 + radix3: 1.1.2 + ufo: 1.5.3 uncrypto: 0.1.3 dev: false /h3@1.7.1: resolution: {integrity: sha512-A9V2NEDNHet7v1gCg7CMwerSigLi0SRbhTy7C3lGb0N4YKIpPmLDjedTUopqp4dnn7COHfqUjjaz3zbtz4QduA==} dependencies: - cookie-es: 1.0.0 + cookie-es: 1.1.0 defu: 6.1.4 - destr: 2.0.2 + destr: 2.0.3 iron-webcrypto: 0.7.0 - radix3: 1.1.0 - ufo: 1.4.0 + radix3: 1.1.2 + ufo: 1.5.3 uncrypto: 0.1.3 dev: true @@ -21971,16 +21979,18 @@ packages: crossws: 0.1.1 defu: 6.1.4 get-port-please: 3.1.2 - h3: 1.10.1 + h3: 1.11.1 http-shutdown: 1.2.2 jiti: 1.21.0 mlly: 1.5.0 node-forge: 1.3.1 pathe: 1.1.2 std-env: 3.7.0 - ufo: 1.4.0 + ufo: 1.5.3 untun: 0.1.3 uqr: 0.1.2 + transitivePeerDependencies: + - uWebSockets.js dev: false /listhen@1.7.2: @@ -23380,7 +23390,7 @@ packages: acorn: 8.11.3 pathe: 1.1.2 pkg-types: 1.0.3 - ufo: 1.4.0 + ufo: 1.5.3 /mlly@1.6.1: resolution: {integrity: sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==} @@ -23622,9 +23632,9 @@ packages: chokidar: 3.6.0 citty: 0.1.5 consola: 3.2.3 - cookie-es: 1.0.0 + cookie-es: 1.1.0 defu: 6.1.4 - destr: 2.0.2 + destr: 2.0.3 dot-prop: 8.0.2 esbuild: 0.19.12 escape-string-regexp: 5.0.0 @@ -23633,7 +23643,7 @@ packages: fs-extra: 11.2.0 globby: 14.0.1 gzip-size: 7.0.0 - h3: 1.10.1 + h3: 1.11.1 hookable: 5.5.3 httpxy: 0.1.5 is-primitive: 3.0.1 @@ -23645,7 +23655,7 @@ packages: mime: 3.0.0 mlly: 1.5.0 mri: 1.2.0 - node-fetch-native: 1.6.1 + node-fetch-native: 1.6.4 ofetch: 1.3.3 ohash: 1.1.3 openapi-typescript: 6.7.3 @@ -23653,7 +23663,7 @@ packages: perfect-debounce: 1.0.0 pkg-types: 1.0.3 pretty-bytes: 6.1.1 - radix3: 1.1.0 + radix3: 1.1.2 rollup: 4.9.1 rollup-plugin-visualizer: 5.12.0(rollup@4.9.1) scule: 1.3.0 @@ -23661,7 +23671,7 @@ packages: serve-placeholder: 2.0.1 serve-static: 1.15.0 std-env: 3.7.0 - ufo: 1.4.0 + ufo: 1.5.3 uncrypto: 0.1.3 unctx: 2.3.1 unenv: 1.9.0 @@ -23682,6 +23692,7 @@ packages: - encoding - idb-keyval - supports-color + - uWebSockets.js dev: false /nitropack@2.9.6: @@ -23846,6 +23857,7 @@ packages: /node-fetch-native@1.6.1: resolution: {integrity: sha512-bW9T/uJDPAJB2YNYEpWzE54U5O3MQidXsOyTfnbKYtTtFexRvGzb1waphBN4ZwP6EcIvYYEOwW0b72BpAqydTw==} + dev: false /node-fetch-native@1.6.4: resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==} @@ -24342,7 +24354,7 @@ packages: uncrypto: 0.1.3 unctx: 2.3.1 unenv: 1.9.0 - unimport: 3.7.1(rollup@4.9.1) + unimport: 3.7.1(rollup@4.14.3) unplugin: 1.7.1 unplugin-vue-router: 0.7.0(vue-router@4.2.5)(vue@3.4.23) untyped: 1.4.2 @@ -24379,6 +24391,7 @@ packages: - supports-color - terser - typescript + - uWebSockets.js - utf-8-validate - vite - vls @@ -24448,7 +24461,7 @@ packages: uncrypto: 0.1.3 unctx: 2.3.1 unenv: 1.9.0 - unimport: 3.7.1(rollup@4.9.1) + unimport: 3.7.1(rollup@4.14.3) unplugin: 1.10.1 unplugin-vue-router: 0.7.0(vue-router@4.3.1)(vue@3.4.23) unstorage: 1.10.2(ioredis@5.3.2) @@ -24568,7 +24581,7 @@ packages: citty: 0.1.5 execa: 8.0.1 pathe: 1.1.2 - ufo: 1.4.0 + ufo: 1.5.3 dev: false /nypm@0.3.8: @@ -24669,16 +24682,16 @@ packages: resolution: {integrity: sha512-icBz2JYfEpt+wZz1FRoGcrMigjNKjzvufE26m9+yUiacRQRHwnNlGRPiDnW4op7WX/MR6aniwS8xw8jyVelF2g==} dependencies: destr: 1.2.2 - node-fetch-native: 1.6.1 - ufo: 1.4.0 + node-fetch-native: 1.6.4 + ufo: 1.5.3 dev: true /ofetch@1.3.3: resolution: {integrity: sha512-s1ZCMmQWXy4b5K/TW9i/DtiN8Ku+xCiHcjQ6/J/nDdssirrQNOoB165Zu8EqLMA2lln1JUth9a0aW9Ap2ctrUg==} dependencies: - destr: 2.0.2 - node-fetch-native: 1.6.1 - ufo: 1.4.0 + destr: 2.0.3 + node-fetch-native: 1.6.4 + ufo: 1.5.3 /ofetch@1.3.4: resolution: {integrity: sha512-KLIET85ik3vhEfS+3fDlc/BAZiAp+43QEC/yCo5zkNoY2YaKvNkOaFr/6wCFgFH1kuYQM5pMNi0Tg8koiIemtw==} @@ -24697,6 +24710,7 @@ packages: /ohmyfetch@0.4.21: resolution: {integrity: sha512-VG7f/JRvqvBOYvL0tHyEIEG7XHWm7OqIfAs6/HqwWwDfjiJ1g0huIpe5sFEmyb+7hpFa1EGNH2aERWR72tlClw==} + deprecated: Package renamed to https://github.com/unjs/ofetch dependencies: destr: 1.2.2 node-fetch-native: 0.1.8 @@ -27159,10 +27173,10 @@ packages: /radix3@1.1.0: resolution: {integrity: sha512-pNsHDxbGORSvuSScqNJ+3Km6QAVqk8CfsCBIEoDgpqLrkD2f3QM4I7d1ozJJ172OmIcoUcerZaNWqtLkRXTV3A==} + dev: false /radix3@1.1.2: resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} - dev: true /raf@3.4.1: resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} @@ -27214,7 +27228,7 @@ packages: resolution: {integrity: sha512-lNeOl38Ws0eNxpO3+wD1I9rkHGQyj1NU1jlzv4go2CtEnEQEUfqnIvZG7W+bC/aXdJ27n5x/yUjb6RoT9tko+Q==} dependencies: defu: 6.1.4 - destr: 2.0.2 + destr: 2.0.3 flat: 5.0.2 /rc@1.2.8: @@ -27958,7 +27972,6 @@ packages: rollup: 4.14.3 source-map: 0.7.4 yargs: 17.7.2 - dev: true /rollup-plugin-visualizer@5.12.0(rollup@4.9.1): resolution: {integrity: sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ==} @@ -27975,6 +27988,7 @@ packages: rollup: 4.9.1 source-map: 0.7.4 yargs: 17.7.2 + dev: false /rollup@3.28.0: resolution: {integrity: sha512-d7zhvo1OUY2SXSM6pfNjgD5+d0Nz87CUp4mt8l/GgVP3oBsPwzNvSzyu1me6BSG9JIgWNTVcafIXBIyM8yQ3yw==} @@ -28007,7 +28021,6 @@ packages: '@rollup/rollup-win32-ia32-msvc': 4.14.3 '@rollup/rollup-win32-x64-msvc': 4.14.3 fsevents: 2.3.3 - dev: true /rollup@4.9.1: resolution: {integrity: sha512-pgPO9DWzLoW/vIhlSoDByCzcpX92bKEorbgXuZrqxByte3JFk2xSW2JEeAcyLc9Ru9pqcNNW+Ob7ntsk2oT/Xw==} @@ -30263,6 +30276,7 @@ packages: /ufo@1.4.0: resolution: {integrity: sha512-Hhy+BhRBleFjpJ2vchUNN40qgkh0366FWJGqVLYBHev0vpHTrXSA0ryT+74UiW6KWsldNurQMKGqCm1M2zBciQ==} + dev: false /ufo@1.5.3: resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==} @@ -30341,7 +30355,7 @@ packages: consola: 3.2.3 defu: 6.1.4 mime: 3.0.0 - node-fetch-native: 1.6.1 + node-fetch-native: 1.6.4 pathe: 1.1.2 /unhead@1.8.10: @@ -30409,7 +30423,7 @@ packages: /unimport@2.2.4: resolution: {integrity: sha512-qMgmeEGqqrrmEtm0dqxMG37J6xBtrriqxq9hILvDb+e6l2F0yTnJomLoCCp0eghLR7bYGeBsUU5Y0oyiUYhViw==} dependencies: - '@rollup/pluginutils': 5.1.0(rollup@4.9.1) + '@rollup/pluginutils': 5.1.0(rollup@4.14.3) escape-string-regexp: 5.0.0 fast-glob: 3.3.2 local-pkg: 0.4.3 @@ -30427,7 +30441,7 @@ packages: /unimport@3.1.0: resolution: {integrity: sha512-ybK3NVWh30MdiqSyqakrrQOeiXyu5507tDA0tUf7VJHrsq4DM6S43gR7oAsZaFojM32hzX982Lqw02D3yf2aiA==} dependencies: - '@rollup/pluginutils': 5.1.0(rollup@4.9.1) + '@rollup/pluginutils': 5.1.0(rollup@4.14.3) escape-string-regexp: 5.0.0 fast-glob: 3.3.2 local-pkg: 0.4.3 @@ -30445,7 +30459,7 @@ packages: /unimport@3.6.1: resolution: {integrity: sha512-zKzbp8AQ+l8QK3XrONtUBdgBbMI8TkGh8hBYF77ZkVqMLLIAHwGSwJRFolPQMBx/5pezeRKvmu2gzlqnxRZeqQ==} dependencies: - '@rollup/pluginutils': 5.1.0(rollup@4.9.1) + '@rollup/pluginutils': 5.1.0(rollup@4.14.3) escape-string-regexp: 5.0.0 fast-glob: 3.3.2 local-pkg: 0.5.0 @@ -30478,7 +30492,6 @@ packages: unplugin: 1.10.1 transitivePeerDependencies: - rollup - dev: true /unimport@3.7.1(rollup@4.9.1): resolution: {integrity: sha512-V9HpXYfsZye5bPPYUgs0Otn3ODS1mDUciaBlXljI4C2fTwfFpvFZRywmlOu943puN9sncxROMZhsZCjNXEpzEQ==} @@ -30498,6 +30511,7 @@ packages: unplugin: 1.10.1 transitivePeerDependencies: - rollup + dev: false /union-value@1.0.1: resolution: {integrity: sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==} @@ -30704,7 +30718,7 @@ packages: optional: true dependencies: '@babel/types': 7.23.9 - '@rollup/pluginutils': 5.1.0(rollup@4.9.1) + '@rollup/pluginutils': 5.1.0(rollup@4.14.3) '@vue-macros/common': 1.10.0(vue@3.4.23) ast-walker-scope: 0.5.0 chokidar: 3.6.0 @@ -30731,7 +30745,7 @@ packages: optional: true dependencies: '@babel/types': 7.23.9 - '@rollup/pluginutils': 5.1.0(rollup@4.9.1) + '@rollup/pluginutils': 5.1.0(rollup@4.14.3) '@vue-macros/common': 1.10.0(vue@3.4.23) ast-walker-scope: 0.5.0 chokidar: 3.6.0 @@ -30822,17 +30836,18 @@ packages: dependencies: anymatch: 3.1.3 chokidar: 3.6.0 - destr: 2.0.2 - h3: 1.10.1 + destr: 2.0.3 + h3: 1.11.1 ioredis: 5.3.2 listhen: 1.6.0 lru-cache: 10.1.0 mri: 1.2.0 node-fetch-native: 1.6.1 ofetch: 1.3.3 - ufo: 1.4.0 + ufo: 1.5.3 transitivePeerDependencies: - supports-color + - uWebSockets.js dev: false /unstorage@1.10.2(ioredis@5.3.2): @@ -31389,7 +31404,7 @@ packages: vite: ^3.1.0 || ^4.0.0 dependencies: '@antfu/utils': 0.7.7 - '@rollup/pluginutils': 5.1.0(rollup@4.9.1) + '@rollup/pluginutils': 5.1.0(rollup@4.14.3) debug: 4.3.4(supports-color@8.1.1) fs-extra: 11.2.0 open: 9.1.0 @@ -31413,7 +31428,7 @@ packages: dependencies: '@antfu/utils': 0.7.7 '@nuxt/kit': 3.11.2 - '@rollup/pluginutils': 5.1.0(rollup@4.9.1) + '@rollup/pluginutils': 5.1.0(rollup@4.14.3) debug: 4.3.4(supports-color@8.1.1) error-stack-parser-es: 0.1.1 fs-extra: 11.2.0 @@ -31438,7 +31453,7 @@ packages: dependencies: '@antfu/utils': 0.7.7 '@nuxt/kit': 3.11.2 - '@rollup/pluginutils': 5.1.0(rollup@4.9.1) + '@rollup/pluginutils': 5.1.0(rollup@4.14.3) debug: 4.3.4(supports-color@8.1.1) error-stack-parser-es: 0.1.1 fs-extra: 11.2.0 @@ -31683,7 +31698,7 @@ packages: /vue-bundle-renderer@2.0.0: resolution: {integrity: sha512-oYATTQyh8XVkUWe2kaKxhxKVuuzK2Qcehe+yr3bGiaQAhK3ry2kYE4FWOfL+KO3hVFwCdLmzDQTzYhTi9C+R2A==} dependencies: - ufo: 1.4.0 + ufo: 1.5.3 /vue-component-meta@1.2.0(typescript@5.1.3): resolution: {integrity: sha512-z+/pL4txu5qCULbGHFn6vOlSR1V5gFDGWkD64Z2yLlKtYr0Wlb9oOfWTaXxpSl7R+EiX7JusbTlek0szSYeH1g==} @@ -31696,8 +31711,8 @@ packages: typescript: 5.1.3 dev: true - /vue-component-type-helpers@2.0.14: - resolution: {integrity: sha512-DInfgOyXlMyliyqAAD9frK28tTfch0+tMi4qoWJcZlRxUf+NFAtraJBnAsKLep+FOyLMiajkhfyEb3xLK08i7w==} + /vue-component-type-helpers@2.0.16: + resolution: {integrity: sha512-qisL/iAfdO++7w+SsfYQJVPj6QKvxp4i1MMxvsNO41z/8zu3KuAw9LkhKUfP/kcOWGDxESp+pQObWppXusejCA==} dev: true /vue-demi@0.14.6(vue@3.4.23): diff --git a/tsconfig.json b/tsconfig.json index 13a0b278d7..aa3da2cf6a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,8 +21,10 @@ "@types/react" ], "paths": { + "@dpc-sdp/nuxt-ripple-preview/utils": ["nuxt-ripple-preview/utils"], "@dpc-sdp/ripple-tide-api/errors": ["ripple-tide-api/src/errors/errors"], "@dpc-sdp/ripple-tide-api/helpers": ["ripple-tide-api/src/helpers"], + "@dpc-sdp/ripple-tide-api/types": ["ripple-tide-api/types"], "@dpc-sdp/ripple-tide-api": ["ripple-tide-api/src"], "@dpc-sdp/ripple-ui-core": ["ripple-ui-core/src"], "@dpc-sdp/ripple-tide-event": ["ripple-tide-event/src"], @@ -30,7 +32,9 @@ "@dpc-sdp/ripple-tide-landing-page": ["ripple-tide-landing-page/src"], "@dpc-sdp/ripple-tide-media": ["ripple-tide-media/src"], "@dpc-sdp/ripple-tide-publication": ["ripple-tide-publication/src"], - "@dpc-sdp/ripple-tide-topic": ["ripple-tide-topic/src"] + "@dpc-sdp/ripple-tide-topic": ["ripple-tide-topic/src"], + "@dpc-sdp/ripple-tide-webform": ["ripple-tide-webform/src"], + "#imports": ["../examples/nuxt-app/.nuxt/imports"] } }, "exclude": ["**/*.stories.ts", "**/*.stories.mdx"] From 94b564ffb9e7c46d9d62c5d4639591a018eae455 Mon Sep 17 00:00:00 2001 From: Jason Smith Date: Mon, 6 May 2024 12:48:29 +1000 Subject: [PATCH 2/7] refactor(@dpc-sdp/ripple-tide-webform): :recycle: use internal id --- .../ripple-tide-webform/server/api/tide/webform.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/ripple-tide-webform/server/api/tide/webform.ts b/packages/ripple-tide-webform/server/api/tide/webform.ts index 77fb29e00d..27d07e273d 100644 --- a/packages/ripple-tide-webform/server/api/tide/webform.ts +++ b/packages/ripple-tide-webform/server/api/tide/webform.ts @@ -39,10 +39,15 @@ class TideWebformApi extends TideApiBase { async getWebform(id: string, headers = {}) { try { - const { data: response } = await this.get(`/webform/webform/${id}`, { - headers - }) - const resource = jsonapiParse.parse(response).data + // /webform/webform/${id} + // /webform/webform?filter[drupal_internal__id]=${id} + const { data: response } = await this.get( + `/webform/webform?filter[drupal_internal__id]=${id}`, + { + headers + } + ) + const resource = jsonapiParse.parse(response).data[0] const siteData = await this.getMappedData( this.webformMapping.mapping, resource From 05356487b5124dc69ed92c0e1666d6938a309646 Mon Sep 17 00:00:00 2001 From: Jason Smith Date: Mon, 6 May 2024 12:49:23 +1000 Subject: [PATCH 3/7] refactor(@dpc-sdp/ripple-tide-landing-page): :recycle: use composable for webform submit --- .vscode/settings.json | 3 +- .../global/TideLandingPage/WebForm.vue | 114 +----------------- ...{use-web-form.ts => use-webform-submit.ts} | 22 +++- 3 files changed, 27 insertions(+), 112 deletions(-) rename packages/ripple-tide-webform/composables/{use-web-form.ts => use-webform-submit.ts} (80%) diff --git a/.vscode/settings.json b/.vscode/settings.json index 755da58cbc..0c7d783297 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -29,7 +29,8 @@ "@dpc-sdp/ripple-test-utils", "@dpc-sdp/nuxt-ripple-analytics", "eslint-config-ripple", - "@dpc-sdp/ripple-ui-maps" + "@dpc-sdp/ripple-ui-maps", + "@dpc-sdp/ripple-tide-webform" ], "cSpell.words": [ "colour", diff --git a/packages/ripple-tide-landing-page/components/global/TideLandingPage/WebForm.vue b/packages/ripple-tide-landing-page/components/global/TideLandingPage/WebForm.vue index 1cec279422..d0016975e3 100644 --- a/packages/ripple-tide-landing-page/components/global/TideLandingPage/WebForm.vue +++ b/packages/ripple-tide-landing-page/components/global/TideLandingPage/WebForm.vue @@ -1,7 +1,5 @@