From 6c056f26925678b826b28d3c1c0de9dead3fca08 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 9 Oct 2024 14:26:13 +0100 Subject: [PATCH 1/9] variables and mentions --- .../SubmissionConfirmationIntegration.php | 3 +- .../Mail/Forms/SubmissionConfirmationMail.php | 39 +++- api/app/Open/MentionParser.php | 55 +++++ .../HtmlPurifier/OpenFormsHtmlDefinition.php | 21 ++ api/config/purify.php | 6 +- client/components/forms/MentionInput.vue | 189 ++++++++++++++++++ .../forms/RichTextAreaInput.client.vue | 96 ++++++--- client/components/forms/TextBlock.vue | 49 +++++ .../components/FormSubmissionFormatter.js | 105 ++++++++++ .../forms/components/MentionDropdown.vue | 116 +++++++++++ .../open/forms/OpenCompleteForm.vue | 12 +- .../forms/components/FirstSubmissionModal.vue | 2 +- .../FormSubmissionSettings.vue | 2 + .../SubmissionConfirmationIntegration.vue | 18 +- client/data/blocks_types.json | 60 ++++-- client/lib/quill/quillMentionExtension.js | 177 ++++++++++++++++ 16 files changed, 880 insertions(+), 70 deletions(-) create mode 100644 api/app/Open/MentionParser.php create mode 100644 api/app/Service/HtmlPurifier/OpenFormsHtmlDefinition.php create mode 100644 client/components/forms/MentionInput.vue create mode 100644 client/components/forms/TextBlock.vue create mode 100644 client/components/forms/components/FormSubmissionFormatter.js create mode 100644 client/components/forms/components/MentionDropdown.vue create mode 100644 client/lib/quill/quillMentionExtension.js diff --git a/api/app/Integrations/Handlers/SubmissionConfirmationIntegration.php b/api/app/Integrations/Handlers/SubmissionConfirmationIntegration.php index 8b40fe4a6..a556e13c5 100644 --- a/api/app/Integrations/Handlers/SubmissionConfirmationIntegration.php +++ b/api/app/Integrations/Handlers/SubmissionConfirmationIntegration.php @@ -26,7 +26,7 @@ function ($attribute, $value, $fail) { } }, ], - 'confirmation_reply_to' => 'email|nullable', + 'confirmation_reply_to' => 'nullable', 'notification_sender' => 'required', 'notification_subject' => 'required', 'notification_body' => 'required', @@ -107,6 +107,7 @@ public static function validateEmail($email): bool public static function formatData(array $data): array { return array_merge(parent::formatData($data), [ + 'notification_subject' => Purify::clean($data['notification_subject'] ?? ''), 'notification_body' => Purify::clean($data['notification_body'] ?? ''), ]); } diff --git a/api/app/Mail/Forms/SubmissionConfirmationMail.php b/api/app/Mail/Forms/SubmissionConfirmationMail.php index b50bb9c1e..2b0d249ee 100644 --- a/api/app/Mail/Forms/SubmissionConfirmationMail.php +++ b/api/app/Mail/Forms/SubmissionConfirmationMail.php @@ -4,6 +4,7 @@ use App\Events\Forms\FormSubmitted; use App\Mail\OpenFormMail; +use App\Open\MentionParser; use App\Service\Forms\FormSubmissionFormatter; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; @@ -16,6 +17,8 @@ class SubmissionConfirmationMail extends OpenFormMail implements ShouldQueue use Queueable; use SerializesModels; + private $formattedData; + /** * Create a new message instance. * @@ -23,6 +26,12 @@ class SubmissionConfirmationMail extends OpenFormMail implements ShouldQueue */ public function __construct(private FormSubmitted $event, private $integrationData) { + $formatter = (new FormSubmissionFormatter($event->form, $event->data)) + ->createLinks() + ->outputStringsOnly() + ->useSignedUrlForFiles(); + + $this->formattedData = $formatter->getFieldsWithValue(); } /** @@ -34,17 +43,13 @@ public function build() { $form = $this->event->form; - $formatter = (new FormSubmissionFormatter($form, $this->event->data)) - ->createLinks() - ->outputStringsOnly() - ->useSignedUrlForFiles(); - + return $this ->replyTo($this->getReplyToEmail($form->creator->email)) ->from($this->getFromEmail(), $this->integrationData->notification_sender) - ->subject($this->integrationData->notification_subject) + ->subject($this->getSubject()) ->markdown('mail.form.confirmation-submission-notification', [ - 'fields' => $formatter->getFieldsWithValue(), + 'fields' => $this->formattedData, 'form' => $form, 'integrationData' => $this->integrationData, 'noBranding' => $form->no_branding, @@ -67,9 +72,25 @@ private function getReplyToEmail($default) { $replyTo = $this->integrationData->confirmation_reply_to ?? null; - if ($replyTo && filter_var($replyTo, FILTER_VALIDATE_EMAIL)) { - return $replyTo; + if ($replyTo) { + $parser = new MentionParser($replyTo, $this->formattedData); + $parsedReplyTo = $parser->parse(); + if ($parsedReplyTo && filter_var($parsedReplyTo, FILTER_VALIDATE_EMAIL)) { + return $parsedReplyTo; + } } return $default; } + + private function getSubject() + { + $parser = new MentionParser($this->integrationData->notification_subject, $this->formattedData); + return $parser->parse(); + } + + private function getNotificationBody() + { + $parser = new MentionParser($this->integrationData->notification_body, $this->formattedData); + return $parser->parse(); + } } diff --git a/api/app/Open/MentionParser.php b/api/app/Open/MentionParser.php new file mode 100644 index 000000000..f520763c5 --- /dev/null +++ b/api/app/Open/MentionParser.php @@ -0,0 +1,55 @@ +content = $content; + $this->data = $data; + } + + public function parse() + { + return $this->replaceMentions(); + } + + private function replaceMentions() + { + $pattern = '/]*mention-field-id="([^"]*)"[^>]*mention-fallback="([^"]*)"[^>]*>.*?<\/span>/'; + return preg_replace_callback($pattern, function ($matches) { + $fieldId = $matches[1]; + $fallback = $matches[2]; + $value = $this->getData($fieldId); + + if ($value !== null) { + if (is_array($value)) { + return implode(' ', array_map(function ($v) { + return $v; + }, $value)); + } + return $value; + } elseif ($fallback) { + return $fallback; + } + return ''; + }, $this->content); + } + + private function getData($fieldId) + { + $value = collect($this->data)->filter(function ($item) use ($fieldId) { + return $item['id'] === $fieldId; + })->first()['value'] ?? null; + + if (is_object($value)) { + return (array) $value; + } + + return $value; + } +} diff --git a/api/app/Service/HtmlPurifier/OpenFormsHtmlDefinition.php b/api/app/Service/HtmlPurifier/OpenFormsHtmlDefinition.php new file mode 100644 index 000000000..ba9fb807f --- /dev/null +++ b/api/app/Service/HtmlPurifier/OpenFormsHtmlDefinition.php @@ -0,0 +1,21 @@ +addAttribute('span', 'mention-field-id', 'Text'); + $definition->addAttribute('span', 'mention-field-name', 'Text'); + $definition->addAttribute('span', 'mention-fallback', 'Text'); + $definition->addAttribute('span', 'mention', 'Bool'); + $definition->addAttribute('span', 'contenteditable', 'Bool'); + } +} diff --git a/api/config/purify.php b/api/config/purify.php index 24c8d1ce4..a7c77e8a7 100644 --- a/api/config/purify.php +++ b/api/config/purify.php @@ -1,6 +1,6 @@ [ 'default' => [ - 'HTML.Allowed' => 'h1,h2,b,strong,i,em,a[href|title],ul,ol,li,p,br,span,*[style]', + 'HTML.Allowed' => 'h1,h2,b,strong,i,em,a[href|title],ul,ol,li,p,br,span[mention|mention-field-id|mention-field-name|mention-fallback|contenteditable],*[style]', 'HTML.ForbiddenElements' => '', 'CSS.AllowedProperties' => 'font,font-size,font-weight,font-style,text-decoration,color,text-align', @@ -86,7 +86,7 @@ | */ - 'definitions' => Html5Definition::class, + 'definitions' => OpenFormsHtmlDefinition::class, /* |-------------------------------------------------------------------------- diff --git a/client/components/forms/MentionInput.vue b/client/components/forms/MentionInput.vue new file mode 100644 index 000000000..fd70c464b --- /dev/null +++ b/client/components/forms/MentionInput.vue @@ -0,0 +1,189 @@ + + + + + \ No newline at end of file diff --git a/client/components/forms/RichTextAreaInput.client.vue b/client/components/forms/RichTextAreaInput.client.vue index 26143045c..48f0da479 100644 --- a/client/components/forms/RichTextAreaInput.client.vue +++ b/client/components/forms/RichTextAreaInput.client.vue @@ -24,12 +24,25 @@ :style="inputStyle" /> + + + + + @@ -37,49 +50,74 @@ import { Quill, VueEditor } from 'vue3-editor' import { inputProps, useFormInput } from './useFormInput.js' import InputWrapper from './components/InputWrapper.vue' +import MentionDropdown from './components/MentionDropdown.vue' +import registerMentionExtension from '~/lib/quill/quillMentionExtension.js' Quill.imports['formats/link'].PROTOCOL_WHITELIST.push('notion') export default { name: 'RichTextAreaInput', - components: { InputWrapper, VueEditor }, + components: { InputWrapper, VueEditor, MentionDropdown }, props: { ...inputProps, editorOptions: { type: Object, - default: () => { - return { - formats: [ - 'bold', - 'color', - 'font', - 'italic', - 'link', - 'underline', - 'header', - 'indent', - 'list' - ] + default: () => ({ + formats: [ + 'bold', + 'color', + 'font', + 'italic', + 'link', + 'underline', + 'header', + 'indent', + 'list', + 'mention' + ], + modules: { + mention: { + mentions: [] // This will be populated with form fields + } } - } + }) }, editorToolbar: { type: Array, - default: () => { - return [ - [{ header: 1 }, { header: 2 }], - ['bold', 'italic', 'underline', 'link'], - [{ list: 'ordered' }, { list: 'bullet' }], - [{ color: [] }] - ] - } + default: () => [ + [{ header: 1 }, { header: 2 }], + ['bold', 'italic', 'underline', 'link'], + [{ list: 'ordered' }, { list: 'bullet' }], + [{ color: [] }] + ] + }, + mentions: { + type: Array, + default: () => [] + }, + enableMentions: { + type: Boolean, + default: false } }, - setup (props, context) { + setup(props, context) { + const editorOptions = { + ...props.editorOptions, + modules: { + ...props.editorOptions.modules, + mention: props.enableMentions ? { mentions: props.mentions } : undefined + } + } + const editorToolbar = props.enableMentions + ? [...props.editorToolbar, ['mention']] + : props.editorToolbar return { - ...useFormInput(props, context) + ...useFormInput(props, context), + editorOptions, + editorToolbar, + mentionState: registerMentionExtension(Quill) } } } @@ -120,4 +158,12 @@ export default { @apply text-nt-blue; } } + +.ql-mention::after { + content: '@'; + font-size: 18px; +} +span[mention] { + @apply max-w-[150px] truncate overflow-hidden bg-blue-100 text-blue-800 border border-blue-200 rounded-md px-1 inline-flex items-center align-baseline leading-tight text-sm relative; +} diff --git a/client/components/forms/TextBlock.vue b/client/components/forms/TextBlock.vue new file mode 100644 index 000000000..71b3218dc --- /dev/null +++ b/client/components/forms/TextBlock.vue @@ -0,0 +1,49 @@ + + + \ No newline at end of file diff --git a/client/components/forms/components/FormSubmissionFormatter.js b/client/components/forms/components/FormSubmissionFormatter.js new file mode 100644 index 000000000..ea7cab15e --- /dev/null +++ b/client/components/forms/components/FormSubmissionFormatter.js @@ -0,0 +1,105 @@ +import { format, parseISO } from 'date-fns' + +export class FormSubmissionFormatter { + constructor(form, formData) { + this.form = form + this.formData = formData + this.createLinks = false + this.outputStringsOnly = false + this.showGeneratedIds = false + this.datesIsoFormat = false + } + + setCreateLinks() { + this.createLinks = true + return this + } + + setShowGeneratedIds() { + this.showGeneratedIds = true + return this + } + + setOutputStringsOnly() { + this.outputStringsOnly = true + return this + } + + setUseIsoFormatForDates() { + this.datesIsoFormat = true + return this + } + + getFormattedData() { + const formattedData = {} + + this.form.properties.forEach(field => { + if (!this.formData[field.id] && !this.fieldGeneratesId(field)) { + return + } + + const value = this.formatFieldValue(field, this.formData[field.id]) + formattedData[field.id] = value + }) + + return formattedData + } + + formatFieldValue(field, value) { + switch (field.type) { + case 'url': + return this.createLinks ? `${value}` : value + case 'email': + return this.createLinks ? `${value}` : value + case 'checkbox': + return value ? 'Yes' : 'No' + case 'date': + return this.formatDateValue(field, value) + case 'people': + return this.formatPeopleValue(value) + case 'multi_select': + return this.outputStringsOnly ? value.join(', ') : value + case 'relation': + return this.formatRelationValue(value) + default: + return Array.isArray(value) && this.outputStringsOnly ? value.join(', ') : value + } + } + + formatDateValue(field, value) { + if (this.datesIsoFormat) { + return Array.isArray(value) + ? { start_date: value[0], end_date: value[1] || null } + : value + } + + const dateFormat = (field.date_format || 'dd/MM/yyyy') === 'dd/MM/yyyy' ? 'dd/MM/yyyy' : 'MM/dd/yyyy' + const timeFormat = field.with_time ? (field.time_format === 24 ? 'HH:mm' : 'h:mm a') : '' + const fullFormat = `${dateFormat}${timeFormat ? ' ' + timeFormat : ''}` + + if (Array.isArray(value)) { + const start = format(parseISO(value[0]), fullFormat) + const end = value[1] ? format(parseISO(value[1]), fullFormat) : null + return end ? `${start} - ${end}` : start + } + + return format(parseISO(value), fullFormat) + } + + formatPeopleValue(value) { + if (!value) return [] + const people = Array.isArray(value) ? value : [value] + return this.outputStringsOnly ? people.map(p => p.name).join(', ') : people + } + + formatRelationValue(value) { + if (!value) return [] + const relations = Array.isArray(value) ? value : [value] + const formatted = relations.map(r => r.title || 'Untitled') + return this.outputStringsOnly ? formatted.join(', ') : formatted + } + + fieldGeneratesId(field) { + return this.showGeneratedIds && (field.generates_auto_increment_id || field.generates_uuid) + } +} diff --git a/client/components/forms/components/MentionDropdown.vue b/client/components/forms/components/MentionDropdown.vue new file mode 100644 index 000000000..49dcd0398 --- /dev/null +++ b/client/components/forms/components/MentionDropdown.vue @@ -0,0 +1,116 @@ + + + \ No newline at end of file diff --git a/client/components/open/forms/OpenCompleteForm.vue b/client/components/open/forms/OpenCompleteForm.vue index d0eb36516..12ec56747 100644 --- a/client/components/open/forms/OpenCompleteForm.vue +++ b/client/components/open/forms/OpenCompleteForm.vue @@ -140,9 +140,13 @@ key="submitted" class="px-2" > -

{ + this.submittedData = form.data() useAmplitude().logEvent('form_submission', { workspace_id: this.form.workspace_id, form_id: this.form.id diff --git a/client/components/open/forms/components/FirstSubmissionModal.vue b/client/components/open/forms/components/FirstSubmissionModal.vue index 25fbbf990..333990ee9 100644 --- a/client/components/open/forms/components/FirstSubmissionModal.vue +++ b/client/components/open/forms/components/FirstSubmissionModal.vue @@ -2,7 +2,7 @@ - - - + :style="{ + '--font-size': theme.default.fontSize + }" + > + + @@ -46,81 +47,63 @@ - + \ No newline at end of file diff --git a/client/components/forms/components/QuillyEditor.vue b/client/components/forms/components/QuillyEditor.vue new file mode 100644 index 000000000..854cb714c --- /dev/null +++ b/client/components/forms/components/QuillyEditor.vue @@ -0,0 +1,98 @@ + + + \ No newline at end of file diff --git a/client/composables/lib/vForm/Form.js b/client/composables/lib/vForm/Form.js index b8e3c384c..cab2744cc 100644 --- a/client/composables/lib/vForm/Form.js +++ b/client/composables/lib/vForm/Form.js @@ -89,7 +89,7 @@ class Form { Object.keys(this) .filter((key) => !Form.ignore.includes(key)) .forEach((key) => { - this[key] = JSON.parse(JSON.stringify(this.originalData[key])) + this[key] = cloneDeep(this.originalData[key]) }) } diff --git a/client/lib/quill/quillMentionExtension.js b/client/lib/quill/quillMentionExtension.js index 853fb10a8..ffb28d8d6 100644 --- a/client/lib/quill/quillMentionExtension.js +++ b/client/lib/quill/quillMentionExtension.js @@ -1,177 +1,130 @@ +import { reactive } from 'vue' import Quill from 'quill' +const Inline = Quill.import('blots/inline') -import { reactive, nextTick } from 'vue' - - - - -const Embed = Quill.import('blots/embed') - - - - -class MentionBlot extends Embed { - - static blotName = "mention" - - static tagName = "span" - - - - - static create(data) { - - const node = super.create() - - node.setAttribute('mention-field-id', data.field.id) - - node.setAttribute('mention-field-name', data.field.name) - - node.setAttribute('mention-fallback', data.fallback) - - node.setAttribute('contenteditable', 'false') - - node.setAttribute('mention', true) - - - - - node.textContent = data.field.name.length > 25 ? `${data.field.name.slice(0, 25)}...` : data.field.name - - return node - - } - - - - - static value(node) { - - return { - - field_id: node.getAttribute('mention-field-id'), - - field_name: node.getAttribute('mention-field-name'), - - fallback: node.getAttribute('mention-fallback'), - +export default function registerMentionExtension(Quill) { + class MentionBlot extends Inline { + static blotName = 'mention' + static tagName = 'SPAN' + + static create(data) { + let node = super.create() + MentionBlot.setAttributes(node, data) + return node } - } + static setAttributes(node, data) { + node.setAttribute('contenteditable', 'false') + node.setAttribute('mention', 'true') + + if (data && typeof data === 'object') { + node.setAttribute('mention-field-id', data.field?.nf_id || '') + node.setAttribute('mention-field-name', data.field?.name || '') + node.setAttribute('mention-fallback', data.fallback || '') + node.textContent = data.field?.name || '' + } else { + // Handle case where data is not an object (e.g., during undo) + node.textContent = data || '' + } + } + static formats(domNode) { + return { + 'mention-field-id': domNode.getAttribute('mention-field-id') || '', + 'mention-field-name': domNode.getAttribute('mention-field-name') || '', + 'mention-fallback': domNode.getAttribute('mention-fallback') || '' + } + } + format(name, value) { + if (name === 'mention' && value) { + MentionBlot.setAttributes(this.domNode, value) + } else { + super.format(name, value) + } + } + formats() { + let formats = super.formats() + formats['mention'] = MentionBlot.formats(this.domNode) + return formats + } - static formats() { + static value(domNode) { + return { + field: { + nf_id: domNode.getAttribute('mention-field-id') || '', + name: domNode.getAttribute('mention-field-name') || '' + }, + fallback: domNode.getAttribute('mention-fallback') || '' + } + } - return true + // Override attach to ensure contenteditable is always set + attach() { + super.attach() + this.domNode.setAttribute('contenteditable', 'false') + } + length() { + return 1 + } } -} - - - - -export default function registerMentionExtension(Quill) { + Quill.register(MentionBlot) const mentionState = reactive({ - open: false, - onInsert: null, - onCancel: null, - }) + class MentionModule { + constructor(quill, options) { + this.quill = quill + this.options = options + this.setupMentions() + } - - if (!Quill.imports['modules/mention']) { - - Quill.register(MentionBlot) - - - - - class MentionModule { - - constructor(quill, options) { - - this.quill = quill - - this.options = options - - this.setupMentions() - - } - - - - - setupMentions() { - - this.quill.getModule('toolbar').addHandler('mention', () => { - + setupMentions() { + const toolbar = this.quill.getModule('toolbar') + if (toolbar) { + toolbar.addHandler('mention', () => { const range = this.quill.getSelection() - if (range) { - mentionState.open = true - mentionState.onInsert = (mention) => { - this.insertMention(mention, range.index) - } - mentionState.onCancel = () => { - mentionState.open = false - } - } - }) - } - - - - - insertMention(mention, index) { - - this.quill.insertEmbed(index, 'mention', mention) - - this.quill.insertText(index + 1, ' ') - - this.quill.setSelection(index + 2) - - - - - nextTick(() => { - - mentionState.open = false - - }) - - } - } + insertMention(mention, index) { + mentionState.open = false + // Insert the mention + this.quill.insertEmbed(index, 'mention', mention, Quill.sources.USER) + // Calculate the length of the inserted mention + const mentionLength = this.quill.getLength() - index - Quill.register('modules/mention', MentionModule) + nextTick(() => { + // Focus the editor + this.quill.focus() + // Set the selection after the mention + this.quill.setSelection(index + mentionLength, 0, Quill.sources.SILENT) + }) + } } - - + Quill.register('modules/mention', MentionModule) return mentionState - } \ No newline at end of file diff --git a/client/package.json b/client/package.json index d5ca746c2..ba1c30512 100644 --- a/client/package.json +++ b/client/package.json @@ -59,6 +59,7 @@ "prismjs": "^1.24.1", "qrcode": "^1.5.1", "query-builder-vue-3": "^1.0.1", + "quill": "^2.0.2", "tailwind-merge": "^2.3.0", "tinymotion": "^0.2.0", "v-calendar": "^3.1.2", @@ -70,7 +71,6 @@ "vue-json-pretty": "^2.4.0", "vue-notion": "^3.0.0-beta.1", "vue-signature-pad": "^3.0.2", - "vue3-editor": "^0.1.1", "vuedraggable": "next", "webcam-easy": "^1.1.1" }, From f8c2b30a8e09cbd5fddee530ad1a44fe8e46ce71 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 10 Oct 2024 11:14:20 +0100 Subject: [PATCH 6/9] fix lint --- api/tests/Unit/Service/Forms/MentionParserTest.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/api/tests/Unit/Service/Forms/MentionParserTest.php b/api/tests/Unit/Service/Forms/MentionParserTest.php index 79ed8667c..908f0bc1b 100644 --- a/api/tests/Unit/Service/Forms/MentionParserTest.php +++ b/api/tests/Unit/Service/Forms/MentionParserTest.php @@ -5,9 +5,6 @@ use App\Open\MentionParser; - - - test('it replaces mention elements with their corresponding values', function () { $content = '

Hello Placeholder

'; @@ -193,4 +190,4 @@ expect($result)->toBe('some text replaced text dewde'); -}); \ No newline at end of file +}); From 1e96b468c70258ed077b0c76e64f573a11140f6e Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 10 Oct 2024 12:07:13 +0100 Subject: [PATCH 7/9] apply fixes --- client/components/forms/MentionInput.vue | 1 + .../forms/RichTextAreaInput.client.vue | 3 +- client/composables/useParseMention.js | 39 +++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 client/composables/useParseMention.js diff --git a/client/components/forms/MentionInput.vue b/client/components/forms/MentionInput.vue index fd70c464b..3dbb8369f 100644 --- a/client/components/forms/MentionInput.vue +++ b/client/components/forms/MentionInput.vue @@ -27,6 +27,7 @@ }, 'pr-12' ]" + :placeholder="placeholder" @input="onInput" /> { content: '@'; font-size: 16px; } -.rich-editor { +.rich-editor, .mention-input { span[mention] { @apply inline-flex items-center align-baseline leading-tight text-sm relative bg-blue-100 text-blue-800 border border-blue-200 rounded-md px-1 py-0.5 mx-0.5; max-width: 200px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - cursor: default; } } \ No newline at end of file diff --git a/client/composables/useParseMention.js b/client/composables/useParseMention.js new file mode 100644 index 000000000..59494d307 --- /dev/null +++ b/client/composables/useParseMention.js @@ -0,0 +1,39 @@ +import { FormSubmissionFormatter } from '~/components/forms/components/FormSubmissionFormatter' + +export function useParseMention(content, mentionsAllowed, form, formData) { + if (!mentionsAllowed || !form || !formData) { + return content + } + + const formatter = new FormSubmissionFormatter(form, formData).setOutputStringsOnly() + const formattedData = formatter.getFormattedData() + + // Create a new DOMParser + const parser = new DOMParser() + // Parse the content as HTML + const doc = parser.parseFromString(content, 'text/html') + + // Find all elements with mention attribute + const mentionElements = doc.querySelectorAll('[mention]') + + mentionElements.forEach(element => { + const fieldId = element.getAttribute('mention-field-id') + const fallback = element.getAttribute('mention-fallback') + const value = formattedData[fieldId] + + if (value) { + if (Array.isArray(value)) { + element.textContent = value.join(', ') + } else { + element.textContent = value + } + } else if (fallback) { + element.textContent = fallback + } else { + element.remove() + } + }) + + // Return the processed HTML content + return doc.body.innerHTML +} \ No newline at end of file From 92dc063b186f8edccce351f2da8eff0655095915 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 10 Oct 2024 15:25:06 +0100 Subject: [PATCH 8/9] apply fixes --- client/components/forms/TextBlock.vue | 22 +------------------ .../forms/components/MentionDropdown.vue | 2 +- 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/client/components/forms/TextBlock.vue b/client/components/forms/TextBlock.vue index 71b3218dc..8a2c7c641 100644 --- a/client/components/forms/TextBlock.vue +++ b/client/components/forms/TextBlock.vue @@ -5,7 +5,6 @@ \ No newline at end of file diff --git a/client/components/forms/components/MentionDropdown.vue b/client/components/forms/components/MentionDropdown.vue index 49dcd0398..1e7b116cc 100644 --- a/client/components/forms/components/MentionDropdown.vue +++ b/client/components/forms/components/MentionDropdown.vue @@ -84,7 +84,7 @@ const selectedField = ref(null) const fallbackValue = ref('') const filteredMentions = computed(() => { - return props.mentions.filter(mention => blocksTypes[mention.type].is_input) + return props.mentions.filter(mention => blocksTypes[mention.type]?.is_input ?? false) }) function selectField(field, insert = false) { selectedField.value = {...field} From 5ffec25000d2fe76b3b4b496857f7f1412502cb0 Mon Sep 17 00:00:00 2001 From: Chirag Chhatrala Date: Thu, 17 Oct 2024 13:51:44 +0530 Subject: [PATCH 9/9] Fix MentionParser --- api/app/Open/MentionParser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/app/Open/MentionParser.php b/api/app/Open/MentionParser.php index 4b9e4dd7d..01c8e3c80 100644 --- a/api/app/Open/MentionParser.php +++ b/api/app/Open/MentionParser.php @@ -86,7 +86,7 @@ private function replaceMentions() private function getData($fieldId) { - $value = collect($this->data)->firstWhere('nf_id', $fieldId)['value'] ?? null; + $value = collect($this->data)->firstWhere('id', $fieldId)['value'] ?? null; if (is_object($value)) { return (array) $value;