Apply Mentions everywhere (#595)

* variables and mentions

* fix lint

* add missing changes

* fix tests

* update quilly, fix bugs

* fix lint

* apply fixes

* apply fixes

* Fix MentionParser

* Apply Mentions everywhere

* Fix MentionParserTest

* Small refactoring

* Fixing quill import issues

* Polished email integration, added customer sender mail

* Add missing changes

* improve migration command

---------

Co-authored-by: Frank <csskfaves@gmail.com>
Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
Chirag Chhatrala
2024-10-22 14:04:29 +05:30
committed by GitHub
parent 2fdf2a439b
commit dad5c825b1
50 changed files with 1903 additions and 874 deletions

View File

@@ -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 ? `<a href="${value}">${value}</a>` : value
case 'email':
return this.createLinks ? `<a href="mailto:${value}">${value}</a>` : 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)
}
}

View File

@@ -0,0 +1,116 @@
<template>
<UPopover
ref="popover"
v-model:open="open"
class="h-0"
@close="cancel"
>
<span class="hidden" />
<template #panel>
<div class="p-2 max-h-[300px] flex flex-col">
<div class="flex items-center border-b -mx-2 px-2">
<div class="font-semibold w-1/2 mb-2 flex-grow">
Insert Mention
</div>
<input
v-model="fallbackValue"
class="p-1 mb-2 text-sm w-1/2 border rounded-md hover:bg-gray-50"
placeholder="Fallback value"
>
</div>
<div class="overflow-scroll pt-2">
<div class="w-full max-w-xs mb-2">
<div class="text-sm text-gray-500 mb-1">
Select a field
</div>
<div class="space-y-1">
<div
v-for="field in filteredMentions"
:key="field.id"
class="flex items-center p-2 rounded-md cursor-pointer hover:bg-gray-100"
:class="{ 'bg-blue-50 border border-blue-100 inset-0': selectedField?.id === field.id, 'border border-transparent': selectedField?.id !== field.id }"
@click="selectField(field)"
@dblclick="selectField(field, true)"
>
<BlockTypeIcon
:type="field.type"
class="mr-2"
/>
<p class="text-sm text-gray-700 truncate">
{{ field.name }}
</p>
</div>
</div>
</div>
</div>
<div class="flex border-t pt-2 -mx-2 px-2 justify-end space-x-2">
<UButton
size="sm"
color="primary"
class="px-6"
:disabled="!selectedField"
@click="insertMention"
>
Insert
</UButton>
<UButton
size="sm"
color="gray"
@click="cancel"
>
Cancel
</UButton>
</div>
</div>
</template>
</UPopover>
</template>
<script setup>
import { ref, toRefs } from 'vue'
import BlockTypeIcon from '~/components/open/forms/components/BlockTypeIcon.vue'
import blocksTypes from '~/data/blocks_types.json'
const props = defineProps({
state: Object,
mentions: Array
})
defineShortcuts({
escape: () => {
open.value = false
}
})
const { open, onInsert, onCancel } = toRefs(props.state)
const selectedField = ref(null)
const fallbackValue = ref('')
const filteredMentions = computed(() => {
return props.mentions.filter(mention => blocksTypes[mention.type]?.is_input ?? false)
})
function selectField(field, insert = false) {
selectedField.value = {...field}
if (insert) {
insertMention()
}
}
watch(open, (newValue) => {
if (newValue) {
selectedField.value = null
fallbackValue.value = ''
}
})
const insertMention = () => {
if (selectedField.value && onInsert.value) {
onInsert.value({
field: selectedField.value,
fallback: fallbackValue.value
})
open.value = false
}
}
const cancel = () => {
if (onCancel.value) {
onCancel.value()
}
open.value = false
}
</script>

View File

@@ -0,0 +1,98 @@
<template>
<div
ref="container"
class="quilly-editor"
/>
</template>
<script setup>
import Quill from 'quill'
import 'quill/dist/quill.snow.css'
import { onMounted, onBeforeUnmount, ref, watch } from 'vue'
const props = defineProps({
modelValue: {
type: String,
default: null
},
options: {
type: Object,
default: () => ({})
}
})
const emit = defineEmits([
'update:modelValue',
'text-change',
'selection-change',
'editor-change',
'blur',
'focus',
'ready'
])
let quillInstance = null
const container = ref(null)
let isInternalChange = false
const setContents = (content) => {
if (!quillInstance) return
isInternalChange = true
quillInstance.root.innerHTML = content
quillInstance.update()
isInternalChange = false
}
const initializeQuill = () => {
if (container.value) {
quillInstance = new Quill(container.value, props.options)
quillInstance.on('selection-change', (range, oldRange, source) => {
if (!range) {
emit('blur', quillInstance)
} else {
emit('focus', quillInstance)
}
emit('selection-change', { range, oldRange, source })
})
quillInstance.on('text-change', (delta, oldContents, source) => {
if (!isInternalChange) {
const html = quillInstance.root.innerHTML
emit('text-change', { delta, oldContents, source })
emit('update:modelValue', html)
}
})
quillInstance.on('editor-change', (eventName, ...args) => {
emit('editor-change', eventName, ...args)
})
if (props.modelValue) {
setContents(props.modelValue)
}
emit('ready', quillInstance)
}
}
onMounted(() => {
initializeQuill()
})
watch(() => props.modelValue, (newValue) => {
if (quillInstance && newValue !== quillInstance.root.innerHTML) {
setContents(newValue || '')
}
}, { immediate: true })
onBeforeUnmount(() => {
if (quillInstance) {
quillInstance.off('selection-change')
quillInstance.off('text-change')
quillInstance.off('editor-change')
}
quillInstance = null
})
</script>