Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 117 additions & 2 deletions components/Block/Form.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { onMounted, onBeforeUnmount, ref } from 'vue';
import type { BlockProps } from './types';
import useVisualEditing from '~/composables/useVisualEditing';

Expand All @@ -15,13 +16,67 @@ const { data: block, refresh } = useAsyncData(props.uuid, () =>
'alignment',
'show_labels',
'inline',
{ form: ['hubspot_form_id', 'typeform_form_id', 'route_to_meeting_link_on_success'] },
{
form: [
'hubspot_form_id',
'typeform_form_id',
'internal_form_url',
'route_to_meeting_link_on_success',
'form_config',
],
},
],
}),
),
);

const { data: formConfigSlug } = useAsyncData(`form-config-slug-${block.value?.form?.form_config}`, async () => {
if (block.value?.form?.form_config && typeof block.value.form.form_config === 'string') {
const config = await $directus.request(
$readItem('internal_form_config', block.value.form.form_config, {
fields: ['slug'],
}),
);

return config.slug;
}

return null;
});

autoApply(`[data-block-id="${props.uuid}"]`, refresh);

const iframeRef = ref<HTMLIFrameElement | null>(null);

const handleMessage = (event: MessageEvent) => {
if (!iframeRef.value || !block.value?.form.internal_form_url) return;

const allowedOrigin = new URL(block.value.form.internal_form_url).origin;
Comment on lines +51 to +54
Copy link

Copilot AI Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The message handler lacks proper error handling for malformed URLs. If block.value.form.internal_form_url contains an invalid URL, the new URL() constructor will throw an exception, potentially causing the component to crash. Wrap the URL construction in a try-catch block to handle invalid URLs gracefully.

Suggested change
const handleMessage = (event: MessageEvent) => {
if (!iframeRef.value || !block.value?.form.internal_form_url) return;
const allowedOrigin = new URL(block.value.form.internal_form_url).origin;
let allowedOrigin: string;
try {
allowedOrigin = new URL(block.value.form.internal_form_url).origin;
} catch (e) {
// Malformed URL, ignore the message
return;
}

Copilot uses AI. Check for mistakes.

if (event.origin !== allowedOrigin) return;

const { type, height, scrollToTop } = event.data || {};

if (type === 'set-height' && typeof height === 'number') {
iframeRef.value.style.height = `${height}px`;

if (scrollToTop) {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
}
};

onMounted(() => {
if (block.value?.form.internal_form_url) {
window.addEventListener('message', handleMessage);
}
});

onBeforeUnmount(() => {
if (block.value?.form.internal_form_url) {
window.removeEventListener('message', handleMessage);
}
});
Comment on lines +69 to +79
Copy link

Copilot AI Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The event listeners are added/removed based on the initial value of block.value?.form.internal_form_url but this reactive value could change during the component's lifecycle. If the URL changes from null to a valid URL after mounting, no event listener will be added. Consider using a watcher to handle dynamic changes to the internal form URL.

Copilot uses AI. Check for mistakes.
</script>

<template>
Expand All @@ -44,8 +99,9 @@ autoApply(`[data-block-id="${props.uuid}"]`, refresh);
: undefined
"
/>

<BaseTypeForm
v-if="block && block.form.typeform_form_id"
v-else-if="block && block.form.typeform_form_id"
:data-block-id="props.uuid"
:form-id="block.form.typeform_form_id"
:labels="block.show_labels"
Expand All @@ -62,4 +118,63 @@ autoApply(`[data-block-id="${props.uuid}"]`, refresh);
: undefined
"
/>

<div
v-else-if="block && block.form.internal_form_url"
class="form"
:class="[{ 'hide-labels': !block.show_labels, inline: block.inline }, `align-${block.alignment ?? 'center'}`]"
:data-block-id="props.uuid"
:data-directus="
isVisualEditingEnabled
? setAttr({
collection: 'block_form',
item: block.id,
fields: ['alignment', 'show_labels', 'inline', 'form'],
mode: 'modal',
})
: undefined
"
>
<iframe
:src="formConfigSlug ? `${block.form.internal_form_url}?config=${formConfigSlug}` : block.form.internal_form_url"
ref="iframeRef"
class="w-full border-none"
scrolling="no"
frameborder="0"
/>
</div>
</template>

<style scoped lang="scss">
.form {
&.align-left {
text-align: left;
}

&.align-center {
text-align: center;
}

&.inline {
display: inline-block;
}

:deep(iframe) {
width: 100%;
max-width: 100%;
margin: 0 auto;
transition: var(--duration-150) var(--ease-out);

&:hover {
border-color: var(--gray-400);
transition: none;
}

&:focus-within {
border-color: var(--primary);
outline: none;
box-shadow: 0px 0px var(--space-1) 0px var(--primary-100);
}
}
}
</style>
14 changes: 9 additions & 5 deletions types/schema/content/form.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import type { User } from '../system/index.js';
import type { InternalFormConfig } from './internal-form-config.js';

export interface Form {
/** @primaryKey */
id: string;
sort: number | null;
user_created: string | User | null;
date_created: string | null;
user_updated: string | User | null;
date_updated: string | null;
type: string | null;
title: string | null;
hubspot_form_id: string | null;
typeform_form_id: string | null;
route_to_meeting_link_on_success: boolean | null;
type?: 'hubspot' | 'typeform' | 'internal' | null;
title?: string | null;
hubspot_form_id?: string | null;
route_to_meeting_link_on_success?: boolean | null;
typeform_form_id?: string | null;
internal_form_url?: string | null;
form_config?: InternalFormConfig | string | null;
}
1 change: 1 addition & 0 deletions types/schema/content/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export type * from './agency-partner.js';
export type * from './feature.js';
export type * from './marketplace.js';
export type * from './testimonials.js';
export type * from './internal-form-config.js';
24 changes: 24 additions & 0 deletions types/schema/content/internal-form-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { User } from '../system/index.js';

export interface InternalFormConfig {
/** @primaryKey */
id: string;
status?: 'published' | 'draft' | 'archived';
sort?: number | null;
user_created?: User | string | null;
date_created?: string | null;
user_updated?: User | string | null;
date_updated?: string | null;
title?: string | null;
slug?: string | null;
description?: string | null;
allow_multiple?: boolean | null;
max_quantity?: number | null;
}

export interface InternalFormConfigProduct {
/** @primaryKey */
id: number;
internal_form_config_id?: InternalFormConfig | string | null;
products_id?: string | null;
}
4 changes: 4 additions & 0 deletions types/schema/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ import type {
Template,
Extension,
Testimonial,
InternalFormConfig,
InternalFormConfigProduct,
} from './content/index.js';
import type { Globals, Navigation, Redirect, Seo } from './meta/index.js';
import type { ContentType, Page, PageBlock } from './routes/index.js';
Expand All @@ -86,6 +88,8 @@ export interface Schema {
events: Event[];
features: Feature[];
testimonials: Testimonial[];
internal_form_config: InternalFormConfig[];
internal_form_config_products: InternalFormConfigProduct[];

// Partner Program
agency_partners: AgencyPartner[];
Expand Down