-
Notifications
You must be signed in to change notification settings - Fork 322
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
variables and mentions #590
Changes from 2 commits
6c056f2
70804ef
4640b26
1b5dc99
3773313
f8c2b30
1e96b46
92dc063
4c19d66
5ffec25
e864d41
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,13 +17,21 @@ class SubmissionConfirmationMail extends OpenFormMail implements ShouldQueue | |
use Queueable; | ||
use SerializesModels; | ||
|
||
private $formattedData; | ||
|
||
/** | ||
* Create a new message instance. | ||
* | ||
* @return void | ||
*/ | ||
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; | ||
} | ||
Comment on lines
+76
to
+81
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Consider Refactoring Repetitive Parsing Logic The parsing logic using Here's how you might refactor the parsing logic: private function parseIntegrationDataField($field)
{
$value = $this->integrationData->$field ?? null;
if ($value) {
$parser = new MentionParser($value, $this->formattedData);
return $parser->parse();
}
return null;
} Update the methods accordingly: private function getReplyToEmail($default)
{
$parsedReplyTo = $this->parseIntegrationDataField('confirmation_reply_to');
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();
+ return $this->parseIntegrationDataField('notification_subject') ?? '';
}
private function getNotificationBody()
{
- $parser = new MentionParser($this->integrationData->notification_body, $this->formattedData);
- return $parser->parse();
+ return $this->parseIntegrationDataField('notification_body') ?? '';
} |
||
} | ||
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(); | ||
} | ||
Comment on lines
+92
to
+96
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Review the Usage of The |
||
} |
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,55 @@ | ||||||||||||
<?php | ||||||||||||
|
||||||||||||
namespace App\Open; | ||||||||||||
|
||||||||||||
class MentionParser | ||||||||||||
{ | ||||||||||||
private $content; | ||||||||||||
private $data; | ||||||||||||
|
||||||||||||
public function __construct($content, $data) | ||||||||||||
{ | ||||||||||||
$this->content = $content; | ||||||||||||
$this->data = $data; | ||||||||||||
} | ||||||||||||
Comment on lines
+13
to
+17
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add type hints and return types for improved code clarity Adding parameter and return type hints enhances readability and helps catch type-related bugs. Consider updating your methods with type declarations: -public function __construct($content, $data)
+public function __construct(string $content, array $data)
-public function parse()
+public function parse(): string
-private function replaceMentions()
+private function replaceMentions(): string
-private function getData($fieldId)
+private function getData(string $fieldId) Also applies to: 16-20, 21-41, 43-54 |
||||||||||||
|
||||||||||||
public function parse() | ||||||||||||
{ | ||||||||||||
return $this->replaceMentions(); | ||||||||||||
} | ||||||||||||
|
||||||||||||
private function replaceMentions() | ||||||||||||
{ | ||||||||||||
$pattern = '/<span[^>]*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); | ||||||||||||
} | ||||||||||||
Comment on lines
+65
to
+85
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clarify the purpose of the unused The
Keeping unused methods can lead to confusion and maintenance issues in the future. |
||||||||||||
|
||||||||||||
private function getData($fieldId) | ||||||||||||
{ | ||||||||||||
$value = collect($this->data)->filter(function ($item) use ($fieldId) { | ||||||||||||
return $item['id'] === $fieldId; | ||||||||||||
})->first()['value'] ?? null; | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ensure the The code uses the use Illuminate\Support\Arr; Alternatively, if you prefer to use the global helper function, ensure that it is accessible in your application context. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prevent potential null dereference when accessing The expression -$value = collect($this->data)->filter(function ($item) use ($fieldId) {
- return $item['id'] === $fieldId;
-})->first()['value'] ?? null;
+$firstItem = collect($this->data)->filter(function ($item) use ($fieldId) {
+ return $item['id'] === $fieldId;
+})->first();
+$value = $firstItem['value'] ?? null; 📝 Committable suggestion
Suggested change
|
||||||||||||
|
||||||||||||
if (is_object($value)) { | ||||||||||||
return (array) $value; | ||||||||||||
} | ||||||||||||
|
||||||||||||
return $value; | ||||||||||||
} | ||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
<?php | ||
|
||
namespace App\Service\HtmlPurifier; | ||
|
||
use HTMLPurifier_HTMLDefinition; | ||
use Stevebauman\Purify\Definitions\Definition; | ||
use Stevebauman\Purify\Definitions\Html5Definition; | ||
|
||
class OpenFormsHtmlDefinition implements Definition | ||
{ | ||
public static function apply(HTMLPurifier_HTMLDefinition $definition) | ||
{ | ||
Html5Definition::apply($definition); | ||
|
||
$definition->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'); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,189 @@ | ||
<template> | ||
<InputWrapper v-bind="inputWrapperProps"> | ||
<template #label> | ||
<slot name="label" /> | ||
</template> | ||
|
||
<MentionDropdown | ||
:state="mentionState" | ||
:mentions="mentions" | ||
/> | ||
|
||
<div class="relative"> | ||
<div | ||
ref="editableDiv" | ||
:contenteditable="!disabled" | ||
class="mention-input" | ||
:style="inputStyle" | ||
:class="[ | ||
theme.default.input, | ||
theme.default.borderRadius, | ||
theme.default.spacing.horizontal, | ||
theme.default.spacing.vertical, | ||
theme.default.fontSize, | ||
{ | ||
'!ring-red-500 !ring-2 !border-transparent': hasError, | ||
'!cursor-not-allowed dark:!bg-gray-600 !bg-gray-200': disabled, | ||
}, | ||
'pr-12' | ||
]" | ||
@input="onInput" | ||
/> | ||
<UButton | ||
type="button" | ||
color="white" | ||
class="absolute right-2 top-1/2 transform -translate-y-1/2 p-1 px-2" | ||
icon="i-heroicons-at-symbol-16-solid" | ||
@click="openMentionDropdown" | ||
/> | ||
</div> | ||
|
||
<template | ||
v-if="$slots.help" | ||
#help | ||
> | ||
<slot name="help" /> | ||
</template> | ||
|
||
<template | ||
v-if="$slots.error" | ||
#error | ||
> | ||
<slot name="error" /> | ||
</template> | ||
</InputWrapper> | ||
</template> | ||
|
||
<script setup> | ||
import { ref, onMounted, watch, computed } from 'vue' | ||
import { inputProps, useFormInput } from './useFormInput.js' | ||
import InputWrapper from './components/InputWrapper.vue' | ||
import MentionDropdown from './components/MentionDropdown.vue' | ||
const props = defineProps({ | ||
...inputProps, | ||
mentions: { type: Array, default: () => [] }, | ||
}) | ||
const emit = defineEmits(['update:modelValue']) | ||
const { compVal, inputStyle, hasValidation, hasError, inputWrapperProps } = useFormInput(props, { emit }) | ||
const editableDiv = ref(null) | ||
const savedRange = ref(null) | ||
const mentionState = ref({ | ||
open: false, | ||
onInsert: (mention) => { | ||
insertMention(mention) | ||
mentionState.value.open = false | ||
}, | ||
onCancel: () => { | ||
mentionState.value.open = false | ||
restoreSelection() | ||
}, | ||
}) | ||
const createMentionSpan = (mention) => { | ||
const mentionSpan = document.createElement('span') | ||
mentionSpan.setAttribute('mention-field-id', mention.field.id) | ||
mentionSpan.setAttribute('mention-field-name', mention.field.name) | ||
mentionSpan.setAttribute('mention-fallback', mention.fallback || '') | ||
mentionSpan.setAttribute('contenteditable', 'false') | ||
mentionSpan.setAttribute('mention', 'true') | ||
mentionSpan.textContent = mention.field.name.length > 25 ? `${mention.field.name.slice(0, 25)}...` : mention.field.name | ||
return mentionSpan | ||
} | ||
const insertMention = (mention) => { | ||
const mentionSpan = createMentionSpan(mention) | ||
restoreSelection() | ||
const range = window.getSelection().getRangeAt(0) | ||
// Insert the mention span | ||
range.insertNode(mentionSpan) | ||
Comment on lines
+95
to
+97
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add null checks for selection and range objects In functions like Apply this diff to add null checks: const insertMention = (mention) => {
const mentionSpan = createMentionSpan(mention)
restoreSelection()
+ const selection = window.getSelection()
+ if (!selection || selection.rangeCount === 0) {
+ return
+ }
- const range = window.getSelection().getRangeAt(0)
+ const range = selection.getRangeAt(0)
// Insert the mention span
range.insertNode(mentionSpan)
// ...
}
const restoreSelection = () => {
if (savedRange.value) {
+ const selection = window.getSelection()
+ if (!selection) {
+ return
+ }
selection.removeAllRanges()
selection.addRange(savedRange.value)
editableDiv.value.focus()
}
} Also applies to: 131-135 |
||
|
||
// Move the cursor after the inserted mention | ||
range.setStartAfter(mentionSpan) | ||
range.collapse(true) | ||
// Apply the new selection | ||
const selection = window.getSelection() | ||
selection.removeAllRanges() | ||
selection.addRange(range) | ||
// Ensure the editableDiv is focused | ||
editableDiv.value.focus() | ||
updateCompVal() | ||
} | ||
const openMentionDropdown = () => { | ||
saveSelection() | ||
if (!savedRange.value) { | ||
// If no previous selection, move cursor to the end | ||
const range = document.createRange() | ||
range.selectNodeContents(editableDiv.value) | ||
range.collapse(false) | ||
const selection = window.getSelection() | ||
selection.removeAllRanges() | ||
selection.addRange(range) | ||
savedRange.value = range | ||
} | ||
mentionState.value.open = true | ||
} | ||
const saveSelection = () => { | ||
const selection = window.getSelection() | ||
if (selection.rangeCount > 0) { | ||
savedRange.value = selection.getRangeAt(0) | ||
} | ||
} | ||
const restoreSelection = () => { | ||
if (savedRange.value) { | ||
const selection = window.getSelection() | ||
selection.removeAllRanges() | ||
selection.addRange(savedRange.value) | ||
editableDiv.value.focus() | ||
} | ||
} | ||
const updateCompVal = () => { | ||
compVal.value = editableDiv.value.innerHTML | ||
} | ||
Comment on lines
+138
to
+140
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sanitize user input to prevent XSS vulnerabilities In Apply this diff to implement sanitization: +import DOMPurify from 'dompurify'
...
const updateCompVal = () => {
- compVal.value = editableDiv.value.innerHTML
+ compVal.value = DOMPurify.sanitize(editableDiv.value.innerHTML)
}
...
onMounted(() => {
if (compVal.value) {
- editableDiv.value.innerHTML = compVal.value
+ editableDiv.value.innerHTML = DOMPurify.sanitize(compVal.value)
}
})
...
watch(compVal, (newVal) => {
if (editableDiv.value && editableDiv.value.innerHTML !== newVal) {
- editableDiv.value.innerHTML = newVal
+ editableDiv.value.innerHTML = DOMPurify.sanitize(newVal)
}
}) Also applies to: 144-147, 148-152 |
||
const onInput = () => { | ||
updateCompVal() | ||
} | ||
onMounted(() => { | ||
if (compVal.value) { | ||
editableDiv.value.innerHTML = compVal.value | ||
} | ||
}) | ||
watch(compVal, (newVal) => { | ||
if (editableDiv.value && editableDiv.value.innerHTML !== newVal) { | ||
editableDiv.value.innerHTML = newVal | ||
} | ||
}) | ||
defineExpose({ | ||
editableDiv, | ||
compVal, | ||
mentionState, | ||
openMentionDropdown, | ||
onInput, | ||
}) | ||
</script> | ||
Comment on lines
+58
to
+161
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Comprehensive mention handling, but with room for improvement The script section demonstrates good use of the Composition API and composables. However, there are a few areas that need attention:
To address these issues, consider implementing the following changes:
import DOMPurify from 'dompurify'
const updateCompVal = () => {
compVal.value = DOMPurify.sanitize(editableDiv.value.innerHTML)
}
onMounted(() => {
if (compVal.value) {
editableDiv.value.innerHTML = DOMPurify.sanitize(compVal.value)
}
})
watch(compVal, (newVal) => {
if (editableDiv.value && editableDiv.value.innerHTML !== newVal) {
editableDiv.value.innerHTML = DOMPurify.sanitize(newVal)
}
})
const insertMention = (mention) => {
const mentionSpan = createMentionSpan(mention)
restoreSelection()
const selection = window.getSelection()
if (!selection || selection.rangeCount === 0) return
const range = selection.getRangeAt(0)
// ... rest of the method
}
const restoreSelection = () => {
if (savedRange.value) {
const selection = window.getSelection()
if (!selection) return
selection.removeAllRanges()
selection.addRange(savedRange.value)
editableDiv.value.focus()
}
} These changes will improve the security and robustness of the component. |
||
|
||
<style scoped> | ||
.mention-input { | ||
min-height: 1.5rem; | ||
white-space: pre-wrap; | ||
word-break: break-word; | ||
} | ||
.mention-input:empty::before { | ||
content: attr(placeholder); | ||
color: #9ca3af; | ||
} | ||
Comment on lines
+165
to
+172
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ensure placeholder text is accessible Using the Apply this diff to enhance accessibility: -.mention-input:empty::before {
- content: attr(placeholder);
- color: #9ca3af;
-}
+/* Remove the pseudo-element and use a data attribute */
+<div
+ ref="editableDiv"
+ :contenteditable="!disabled"
+ class="mention-input"
+ :style="inputStyle"
+ :class="[ /* existing classes */ ]"
+ @input="onInput"
+ :data-placeholder="placeholder"
+ role="textbox"
+ aria-multiline="true"
+/> Update the CSS: .mention-input:empty::before {
content: attr(data-placeholder);
color: #9ca3af;
} |
||
.mention-input span[mention] { | ||
max-width: 150px; | ||
overflow: hidden; | ||
text-overflow: ellipsis; | ||
white-space: nowrap; | ||
display: inline-flex; | ||
align-items: center; | ||
background-color: #dbeafe; | ||
color: #1e40af; | ||
border: 1px solid #bfdbfe; | ||
border-radius: 0.25rem; | ||
padding: 0 0.25rem; | ||
font-size: 0.875rem; | ||
line-height: 1.25rem; | ||
position: relative; | ||
vertical-align: baseline; | ||
} | ||
</style> | ||
Comment on lines
+163
to
+190
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Consider improving placeholder accessibility The styles are well-structured and scoped to the component, which is good practice. However, the use of a pseudo-element for the placeholder text might not be accessible to screen readers. Consider using the
<div
ref="editableDiv"
:contenteditable="!disabled"
class="mention-input"
:style="inputStyle"
:aria-placeholder="placeholder"
@input="onInput"
<!-- ... other attributes ... -->
/>
.mention-input:empty::before {
content: attr(aria-placeholder);
color: #9ca3af;
} This change will make the placeholder text accessible to screen readers while maintaining the visual appearance. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handle Potential Exceptions from
FormSubmissionFormatter
While constructing the
FormSubmissionFormatter
, exceptions could be thrown if the formatting fails. Consider adding exception handling to manage potential errors gracefully and provide fallback mechanisms if necessary.Example: