Notification & Integrations refactoring (#346)

* Integrations Refactoring - WIP

* integrations list & edit - WIP

* Fix integration store binding issue

* integrations refactor - WIP

* Form integration - WIP

* Form integration Edit - WIP

* Integration Refactor - Slack - WIP

* Integration Refactor - Discord - WIP

* Integration Refactor - Webhook - WIP

* Integration Refactor - Send Submission Confirmation - WIP

* Integration Refactor - Backend handler - WIP

* Form Integration Status field

* Integration Refactor - Backend SubmissionConfirmation - WIP

* IntegrationMigration Command

* skip confirmation email test case

* Small refactoring

* FormIntegration status active/inactive

* formIntegrationData to integrationData

* Rename file name with Integration suffix for integration realted files

* Loader on form integrations

* WIP

* form integration test case

* WIP

* Added Integration card - working on refactoring

* change location for IntegrationCard and update package file

* Form Integration Create/Edit in single Modal

* Remove integration extra pages

* crisp_help_page_slug for integration json

* integration logic as collapse

* UI improvements

* WIP

* Trying to debug vue devtools

* WIP for integrations

* getIntegrationHandler change namespace name

* useForm for integration fields + validation structure

* Integration Test case & apply validation rules

* Apply useform changes to integration other files

* validation rules for FormNotificationsMessageActions fields

* Zapier integration as coming soon

* Update FormCleaner

* set default settings for confirmation integration

* WIP

* Finish validation for all integrations

* Updated purify, added integration formatData

* Fix testcase

* Ran pint; working on integration errors

* Handle integration events

* command for Delete Old Integration Events

* Display Past Events in Modal

* on Integration event create with status error send email to form creator

* Polish styling

* Minor improvements

* Finish badge and integration status

* Fix tests and add an integration event test

* Lint

---------

Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
formsdev
2024-03-28 22:44:30 +05:30
committed by GitHub
parent d9996e0d9d
commit 6f61faa9ef
84 changed files with 6121 additions and 2205 deletions

View File

@@ -16,7 +16,14 @@
@change="onChange" @keydown.enter.prevent="onEnterPress"
>
<template v-if="maxCharLimit && showCharLimit" #bottom_after_help>
<template #help>
<slot name="help" />
</template>
<template
v-if="maxCharLimit && showCharLimit"
#bottom_after_help
>
<small :class="theme.default.help">
{{ charCount }}/{{ maxCharLimit }}
</small>

View File

@@ -96,7 +96,7 @@ export default {
this.cameraPermissionStatus = 'allowed';
})
.catch(err => {
console.log(err)
console.error(err)
if(err.toString() === 'NotAllowedError: Permission denied'){
this.cameraPermissionStatus = 'blocked';
return;
@@ -140,4 +140,4 @@ export default {
}
}
</script>
</script>

View File

@@ -1,21 +1,26 @@
<template>
<div class="flex mb-1 input-help">
<small :class="theme.default.help" class="grow flex">
<slot name="help"><span v-if="help" class="field-help" v-html="help" /></slot>
<small
:class="helpClasses"
class="grow flex"
>
<slot name="help">
<span
v-if="help"
class="field-help"
v-html="help"/>
</slot>
</small>
<slot name="after-help">
<small class="flex-grow" />
<small class="flex-grow"/>
</slot>
</div>
</template>
<script>
export default {
name: 'InputHelp',
<script setup>
props: {
theme: { type: Object, required: true },
help: { type: String, required: false }
}
}
defineProps({
helpClasses: {type: String, default: 'text-gray-400 dark:text-gray-500'},
help: {type: String, required: false}
})
</script>

View File

@@ -1,11 +1,18 @@
<template>
<label :for="nativeFor"
class="input-label"
:class="[theme.default.label,{'uppercase text-xs': uppercaseLabels, 'text-sm': !uppercaseLabels}]"
<label
:for="nativeFor"
class="input-label"
:class="[
theme.default.label,
{ 'uppercase text-xs': uppercaseLabels, 'text-sm': !uppercaseLabels },
]"
>
<slot>
{{ label }}
<span v-if="required" class="text-red-500 required-dot">*</span>
<span
v-if="required"
class="text-red-500 required-dot"
>*</span>
</slot>
</label>
</template>
@@ -19,7 +26,7 @@ export default {
theme: { type: Object, required: true },
uppercaseLabels: { type: Boolean, default: false },
required: { type: Boolean, default: false },
label: { type: String, required: true }
}
label: { type: String, required: true },
},
}
</script>

View File

@@ -1,54 +1,71 @@
<template>
<div :class="wrapperClass" :style="inputStyle">
<div
:class="wrapperClass"
:style="inputStyle"
>
<slot name="label">
<input-label v-if="label && !hideFieldName"
:label="label"
:theme="theme"
:required="required"
:native-for="id?id:name"
:uppercase-labels="uppercaseLabels"
<InputLabel
v-if="label && !hideFieldName"
:label="label"
:theme="theme"
:required="required"
:native-for="id ? id : name"
:uppercase-labels="uppercaseLabels"
/>
</slot>
<slot v-if="help && helpPosition==='above_input'" name="help">
<input-help :help="help" :theme="theme" />
<slot
v-if="helpPosition === 'above_input'"
name="help"
>
<InputHelp
v-if="help"
:help="help"
:help-classes="theme.default.help"
/>
</slot>
<slot />
<slot v-if="(help && helpPosition==='below_input') || $slots.bottom_after_help" name="help">
<input-help :help="help" :theme="theme">
<slot
v-if="helpPosition === 'below_input'"
name="help"
>
<InputHelp
v-if="help"
:help="help"
:help-classes="theme.default.help"
>
<template #after-help>
<slot name="bottom_after_help" />
</template>
</input-help>
</InputHelp>
</slot>
<slot name="error">
<has-error v-if="hasValidation && form" :form="form" :field="name" />
<has-error
v-if="hasValidation && form"
:form="form"
:field="name"
/>
</slot>
</div>
</template>
<script>
<script setup>
import InputLabel from './InputLabel.vue'
import InputHelp from './InputHelp.vue'
export default {
name: 'InputWrapper',
components: { InputLabel, InputHelp },
props: {
id: { type: String, required: false },
name: { type: String, required: false },
label: { type: String, required: false },
form: { type: Object, required: false },
theme: { type: Object, required: true },
wrapperClass: { type: String, required: false },
inputStyle: { type: Object, required: false },
help: { type: String, required: false },
helpPosition: { type: String, default: 'below_input' },
uppercaseLabels: { type: Boolean, default: true },
hideFieldName: { type: Boolean, default: true },
required: { type: Boolean, default: false },
hasValidation: { type: Boolean, default: true }
}
}
defineProps({
id: { type: String, required: false },
name: { type: String, required: false },
label: { type: String, required: false },
form: { type: Object, required: false },
theme: { type: Object, required: true },
wrapperClass: { type: String, required: false },
inputStyle: { type: Object, required: false },
help: { type: String, required: false },
helpPosition: { type: String, default: 'below_input' },
uppercaseLabels: { type: Boolean, default: true },
hideFieldName: { type: Boolean, default: true },
required: { type: Boolean, default: false },
hasValidation: { type: Boolean, default: true },
})
</script>

View File

@@ -2,24 +2,35 @@
<div class="flex items-center">
<input
:id="id || name"
:name="name"
v-model="internalValue"
:name="name"
type="checkbox"
:class="sizeClasses"
class="rounded border-gray-500 cursor-pointer"
:disabled="disabled?true:null"
:disabled="disabled ? true : null"
>
<label
:for="id || name"
class="text-gray-700 dark:text-gray-300 ml-2"
:class="{ '!cursor-not-allowed': disabled }"
>
<label :for="id || name" class="text-gray-700 dark:text-gray-300 ml-2" :class="{'!cursor-not-allowed':disabled}">
<slot />
</label>
</div>
</template>
<script setup>
import { ref, watch, onMounted, defineProps, defineEmits, defineOptions } from 'vue'
import {
defineEmits,
defineOptions,
defineProps,
onMounted,
ref,
watch,
} from 'vue'
defineOptions({
name: 'VCheckbox'
name: 'VCheckbox',
})
const props = defineProps({
@@ -27,33 +38,42 @@ const props = defineProps({
name: { type: String, default: 'checkbox' },
modelValue: { type: [Boolean, String], default: false },
disabled: { type: Boolean, default: false },
sizeClasses: { type: String, default: 'w-4 h-4' }
sizeClasses: { type: String, default: 'w-4 h-4' },
})
const emit = defineEmits(['update:modelValue', 'click'])
const internalValue = ref(props.modelValue)
watch(() => props.modelValue, val => {
internalValue.value = val
})
watch(
() => props.modelValue,
(val) => {
internalValue.value = val
},
)
watch(() => props.checked, val => {
internalValue.value = val
})
watch(
() => props.checked,
(val) => {
internalValue.value = val
},
)
watch(() => internalValue.value, (val, oldVal) => {
if (val === 0 || val === '0') val = false
if (val === 1 || val === '1') val = true
watch(
() => internalValue.value,
(val, oldVal) => {
if (val === 0 || val === '0')
val = false
if (val === 1 || val === '1')
val = true
if (val !== oldVal) {
emit('update:modelValue', val)
}
})
if (val !== oldVal)
emit('update:modelValue', val)
},
)
onMounted(() => {
if (internalValue.value === null) {
if (internalValue.value === null)
internalValue.value = false
}
})
</script>

View File

@@ -1,19 +1,48 @@
<template>
<div class="v-select relative" :class="[{ 'w-0': multiple, 'min-w-full': multiple }]" ref="select">
<div
ref="select"
class="v-select relative"
:class="[{ 'w-0': multiple, 'min-w-full': multiple }]"
>
<span class="inline-block w-full rounded-md">
<button type="button" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label"
class="cursor-pointer" :style="inputStyle"
<button
type="button"
aria-haspopup="listbox"
aria-expanded="true"
aria-labelledby="listbox-label"
class="cursor-pointer"
:style="inputStyle"
:class="[theme.SelectInput.input, { 'py-2': !multiple || loading, 'py-1': multiple, '!ring-red-500 !ring-2 !border-transparent': hasError, '!cursor-not-allowed !bg-gray-200': disabled }, inputClass]"
@click="toggleDropdown">
@click="toggleDropdown"
>
<div :class="{ 'h-6': !multiple, 'min-h-8': multiple && !loading }">
<transition name="fade" mode="out-in">
<Loader v-if="loading" key="loader" class="h-6 w-6 text-nt-blue mx-auto" />
<div v-else-if="modelValue" key="value" class="flex" :class="{ 'min-h-8': multiple }">
<slot name="selected" :option="modelValue" />
<transition
name="fade"
mode="out-in"
>
<Loader
v-if="loading"
key="loader"
class="h-6 w-6 text-nt-blue mx-auto"
/>
<div
v-else-if="modelValue"
key="value"
class="flex"
:class="{ 'min-h-8': multiple }"
>
<slot
name="selected"
:option="modelValue"
/>
</div>
<div v-else key="placeholder">
<div
v-else
key="placeholder"
>
<slot name="placeholder">
<div class="text-gray-400 dark:text-gray-500 w-full text-left truncate pr-3"
<div
class="text-gray-400 dark:text-gray-500 w-full text-left truncate pr-3"
:class="{ 'py-1': multiple && !loading }">
{{ placeholder }}
</div>
@@ -22,8 +51,18 @@
</transition>
</div>
<span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<svg class="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="none" stroke="currentColor">
<path d="M7 7l3-3 3 3m0 6l-3 3-3-3" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<svg
class="h-5 w-5 text-gray-400"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
>
<path
d="M7 7l3-3 3 3m0 6l-3 3-3-3"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</span>
</button>
@@ -36,23 +75,40 @@
<div v-if="isSearchable" class="px-2 pt-2 sticky top-0 bg-white dark-bg-notion-dark-light z-10">
<text-input v-model="searchTerm" name="search" :color="color" :theme="theme" placeholder="Search..." />
</div>
<div v-if="loading" class="w-full py-2 flex justify-center">
<div
v-if="loading"
class="w-full py-2 flex justify-center"
>
<Loader class="h-6 w-6 text-nt-blue mx-auto" />
</div>
<template v-if="filteredOptions.length > 0">
<li v-for="item in filteredOptions" :key="item[optionKey]" role="option" :style="optionStyle"
<li
v-for="item in filteredOptions"
:key="item[optionKey]"
role="option"
:style="optionStyle"
:class="{ 'px-3 pr-9': multiple, 'px-3': !multiple }"
class="text-gray-900 cursor-default select-none relative py-2 cursor-pointer group hover:text-white hover:bg-form-color focus:outline-none focus-text-white focus-nt-blue"
@click="select(item)">
<slot name="option" :option="item" :selected="isSelected(item)" />
<slot
name="option"
:option="item"
:selected="isSelected(item)"
/>
</li>
</template>
<p v-else-if="!loading && !(allowCreation && searchTerm)" class="w-full text-gray-500 text-center py-2">
<p
v-else-if="!loading && !(allowCreation && searchTerm)"
class="w-full text-gray-500 text-center py-2"
>
{{ (allowCreation ? 'Type something to add an option' : 'No option available') }}.
</p>
<li v-if="allowCreation && searchTerm" role="option" :style="optionStyle"
<li
v-if="allowCreation && searchTerm"
role="option"
:style="optionStyle"
:class="{ 'px-3 pr-9': multiple, 'px-3': !multiple }"
class="text-gray-900 cursor-default select-none relative py-2 cursor-pointer group hover:text-white hover:bg-form-color focus:outline-none focus-text-white focus-nt-blue"
class="text-gray-900 cursor-default select-none relative py-2 cursor-pointer group hover:text-white dark:text-white hover:bg-form-color focus:outline-none focus-text-white focus-nt-blue"
@click="createOption(searchTerm)">
Create <b class="px-1 bg-gray-300 rounded group-hover-text-black">{{ searchTerm }}</b>
</li>
@@ -63,9 +119,9 @@
<script>
import Collapsible from '~/components/global/transitions/Collapsible.vue'
import { themes } from '../../../lib/forms/form-themes.js'
import { themes} from "~/lib/forms/form-themes.js"
import TextInput from '../TextInput.vue'
import debounce from 'debounce'
import debounce from 'lodash/debounce'
import Fuse from 'fuse.js'
export default {
@@ -74,7 +130,7 @@ export default {
directives: {},
props: {
data: Array,
modelValue: { default: null },
modelValue: { default: null, type: [String, Number, Array, Object] },
inputClass: { type: String, default: null },
dropdownClass: { type: String, default: 'w-full' },
loading: { type: Boolean, default: false },
@@ -93,6 +149,7 @@ export default {
allowCreation: { type: Boolean, default: false },
disabled: { type: Boolean, default: false }
},
emits: ['update:modelValue', 'update-options'],
data() {
return {
isOpen: false,
@@ -210,10 +267,12 @@ export default {
if (newOption) {
const newItem = {
name: newOption,
value: newOption
value: newOption,
id: newOption
}
this.$emit('update-options', newItem)
this.select(newItem)
this.searchTerm = ''
}
}
}

View File

@@ -1,22 +1,32 @@
<template>
<div role="button" @click.stop="onClick">
<div class="inline-flex items-center h-6 w-12 p-1 bg-gray-300 border rounded-full cursor-pointer focus:outline-none transition-all transform ease-in-out duration-100" :class="{'bg-nt-blue': props.modelValue}">
<div class="inline-block h-4 w-4 rounded-full bg-white shadow transition-all transform ease-in-out duration-150 rounded-2xl scale-100" :class="{'translate-x-5.5': props.modelValue}" />
<div
role="button"
@click.stop="onClick"
>
<div
class="inline-flex items-center h-6 w-12 p-1 bg-gray-300 border rounded-full cursor-pointer focus:outline-none transition-all transform ease-in-out duration-100"
:class="{ 'bg-nt-blue': props.modelValue }"
>
<div
class="inline-block h-4 w-4 rounded-full bg-white shadow transition-all transform ease-in-out duration-150 rounded-2xl scale-100"
:class="{ 'translate-x-5.5': props.modelValue }"
/>
</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'
import { defineEmits, defineProps } from 'vue'
const props = defineProps({
modelValue: { type: Boolean, default: false },
disabled: { type: Boolean, default: false }
disabled: { type: Boolean, default: false },
})
const emit = defineEmits(['update:modelValue'])
const onClick = () => {
if (props.disabled) return
function onClick() {
if (props.disabled)
return
emit('update:modelValue', !props.modelValue)
}
</script>

View File

@@ -1,5 +1,7 @@
import { ref, computed, watch } from 'vue'
import { themes } from '~/lib/forms/form-themes.js'
import {default as _get} from 'lodash/get'
import {default as _set} from 'lodash/set'
export const inputProps = {
id: { type: String, default: null },
@@ -39,13 +41,13 @@ export function useFormInput (props, context, formPrefixKey = null) {
const compVal = computed({
get: () => {
if (props.form) {
return props.form[(formPrefixKey || '') + props.name]
return _get(props.form, (formPrefixKey || '') + props.name)
}
return content.value
},
set: (val) => {
if (props.form) {
props.form[(formPrefixKey || '') + props.name] = val
_set(props.form, (formPrefixKey || '') + props.name, val)
} else {
content.value = val
}