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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
6
client/components/forms/useFormInput.js
vendored
6
client/components/forms/useFormInput.js
vendored
@@ -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
|
||||
}
|
||||
|
||||
50
client/components/global/Badge.vue
Normal file
50
client/components/global/Badge.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div :class="classes">
|
||||
<Icon v-if="beforeIcon" :name="beforeIcon" :class="iconClasses"/>
|
||||
<slot></slot>
|
||||
<Icon v-if="afterIcon" :name="afterIcon" :class="iconClasses"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
const props = defineProps({
|
||||
color: {
|
||||
type: String,
|
||||
default: 'green'
|
||||
},
|
||||
beforeIcon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
afterIcon: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const baseClasses = {
|
||||
'green': ['bg-green-100', 'border', 'border-green-300', 'text-green-700'],
|
||||
'red': ['bg-red-100', 'border', 'border-red-300', 'text-red-700'],
|
||||
'gray': ['bg-gray-100', 'border', 'border-gray-300', 'text-gray-700'],
|
||||
}
|
||||
|
||||
const iconBaseClasses = {
|
||||
'green': ['text-green-500'],
|
||||
'red': ['text-red-500'],
|
||||
'gray': ['text-gray-500'],
|
||||
}
|
||||
|
||||
const activeColor = computed(() => {
|
||||
return Object.hasOwn(baseClasses, props.color) ? props.color : 'gray'
|
||||
})
|
||||
|
||||
const classes = computed(() => {
|
||||
const classes = ['border', 'text-xs', 'px-2', 'inline-flex', 'items-center', 'rounded-full'].concat(baseClasses[activeColor.value])
|
||||
return classes.join(' ')
|
||||
})
|
||||
|
||||
const iconClasses = computed(() => {
|
||||
return iconBaseClasses[activeColor.value].concat(['w-2 h-2 mr-1']).join(' ')
|
||||
})
|
||||
</script>
|
||||
@@ -7,7 +7,7 @@
|
||||
@click.self="close"
|
||||
>
|
||||
<div ref="content"
|
||||
class="self-start bg-white dark:bg-notion-dark w-full relative p-4 md:p-6 my-6 rounded-xl shadow-xl"
|
||||
class="self-start bg-white dark:bg-notion-dark w-full relative my-6 rounded-xl shadow-xl"
|
||||
:class="maxWidthClass"
|
||||
>
|
||||
<div v-if="closeable" class="absolute top-4 right-4">
|
||||
@@ -19,15 +19,17 @@
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="sm:flex sm:flex-col sm:items-start">
|
||||
<div v-if="$slots.hasOwnProperty('icon')" class="flex w-full justify-center mb-4">
|
||||
<div class="flex border-b pb-4"
|
||||
v-if="$slots.hasOwnProperty('icon') || $slots.hasOwnProperty('title')"
|
||||
:class="[{'flex-col sm:items-start':!compactHeader, 'items-center justify-center py-6 gap-x-4':compactHeader},headerInnerPadding]">
|
||||
<div v-if="$slots.hasOwnProperty('icon')" :class="{'w-full mb-4 flex justify-center':!compactHeader}">
|
||||
<div class="w-14 h-14 rounded-full flex justify-center items-center"
|
||||
:class="'bg-'+iconColor+'-100 text-'+iconColor+'-600'"
|
||||
>
|
||||
<slot name="icon"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 text-center sm:mt-0 w-full">
|
||||
<div class="mt-3 text-center sm:mt-0" :class="{'w-full':!compactHeader}">
|
||||
<h2 v-if="$slots.hasOwnProperty('title')"
|
||||
class="text-2xl font-semibold text-center text-gray-900"
|
||||
>
|
||||
@@ -36,11 +38,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full">
|
||||
<div class="w-full" :class="innerPadding">
|
||||
<slot/>
|
||||
</div>
|
||||
|
||||
<div v-if="$slots.hasOwnProperty('footer')" class="px-6 py-4 bg-gray-100 text-right">
|
||||
<div v-if="$slots.hasOwnProperty('footer')" class="bg-gray-50 border-t rounded-b-xl text-right" :class="footerInnerPadding">
|
||||
<slot name="footer"/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -66,19 +68,34 @@ const props = defineProps({
|
||||
maxWidth: {
|
||||
default: '2xl'
|
||||
},
|
||||
innerPadding: {
|
||||
default: 'p-6'
|
||||
},
|
||||
headerInnerPadding: {
|
||||
default: 'p-6'
|
||||
},
|
||||
footerInnerPadding: {
|
||||
default: 'p-6'
|
||||
},
|
||||
closeable: {
|
||||
default: true
|
||||
}
|
||||
},
|
||||
compactHeader: {
|
||||
default: false,
|
||||
type: Boolean
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
useHead({
|
||||
bodyAttrs: {
|
||||
class: {
|
||||
'overflow-hidden': props.show
|
||||
bodyAttrs: computed(() => {
|
||||
return {
|
||||
class: {
|
||||
'overflow-hidden': props.show
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const closeOnEscape = (e) => {
|
||||
@@ -146,7 +163,8 @@ const motionSlideBottom = {
|
||||
}
|
||||
|
||||
const onLeave = (el, done) => {
|
||||
contentMotion.value.leave(()=>{})
|
||||
contentMotion.value.leave(() => {
|
||||
})
|
||||
backdropMotion.value.leave(done)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,43 +1,45 @@
|
||||
<template>
|
||||
<div class="inline" v-if="shouldDisplayProTag">
|
||||
<div class="bg-nt-blue text-white px-2 text-xs uppercase inline rounded-full font-semibold cursor-pointer"
|
||||
@click.prevent="showPremiumModal=true"
|
||||
>
|
||||
PRO
|
||||
</div>
|
||||
<modal :show="showPremiumModal" @close="showPremiumModal=false">
|
||||
<h2 class="text-nt-blue">
|
||||
OpnForm PRO
|
||||
</h2>
|
||||
<h4 v-if="user && user.is_subscribed" class="text-center mt-5">
|
||||
We're happy to have you as a Pro customer. If you're having any issue with OpnForm, or if you have a
|
||||
feature request, please <a href="mailto:contact@opnform.com">contact us</a>.
|
||||
</h4>
|
||||
<div v-if="!user || !user.is_subscribed" class="mt-4">
|
||||
<p>
|
||||
All the features with a<span
|
||||
class="bg-nt-blue text-white px-2 text-xs uppercase inline rounded-full font-semibold mx-1"
|
||||
>
|
||||
<UTooltip text="Upgrade to use this feature">
|
||||
<div role="button" class="bg-nt-blue text-white px-2 text-xs uppercase inline rounded-full font-semibold cursor-pointer"
|
||||
@click="showPremiumModal=true">
|
||||
PRO
|
||||
</div>
|
||||
<modal :show="showPremiumModal" @close="showPremiumModal=false">
|
||||
<h2 class="text-nt-blue">
|
||||
OpnForm PRO
|
||||
</h2>
|
||||
<h4 v-if="user && user.is_subscribed" class="text-center mt-5">
|
||||
We're happy to have you as a Pro customer. If you're having any issue with OpnForm, or if you have a
|
||||
feature request, please <a href="mailto:contact@opnform.com">contact us</a>.
|
||||
</h4>
|
||||
<div v-if="!user || !user.is_subscribed" class="mt-4">
|
||||
<p>
|
||||
All the features with a<span
|
||||
class="bg-nt-blue text-white px-2 text-xs uppercase inline rounded-full font-semibold mx-1"
|
||||
>
|
||||
PRO
|
||||
</span> tag are available in the Pro plan of OpnForm. <b>You can play around and try all Pro features
|
||||
within
|
||||
the form editor, but you can't use them in your real forms</b>. You can subscribe now to gain unlimited access
|
||||
to
|
||||
all our pro features!
|
||||
</p>
|
||||
</div>
|
||||
within
|
||||
the form editor, but you can't use them in your real forms</b>. You can subscribe now to gain unlimited
|
||||
access
|
||||
to
|
||||
all our pro features!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="my-4 text-center">
|
||||
<v-button color="white" @click="showPremiumModal=false">
|
||||
Close
|
||||
</v-button>
|
||||
</div>
|
||||
</modal>
|
||||
<div class="my-4 text-center">
|
||||
<v-button color="white" @click="showPremiumModal=false">
|
||||
Close
|
||||
</v-button>
|
||||
</div>
|
||||
</modal>
|
||||
</UTooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import {computed} from 'vue'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const workspacesStore = useWorkspacesStore()
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import throttle from 'lodash.throttle'
|
||||
import throttle from 'lodash/throttle'
|
||||
function newResizeObserver (callback) {
|
||||
// Skip this feature for browsers which
|
||||
// do not support ResizeObserver.
|
||||
|
||||
@@ -5,18 +5,18 @@
|
||||
help="With form submission answers"
|
||||
/>
|
||||
<toggle-switch-input name="link_open_form" v-model="compVal.link_open_form" class="mt-4"
|
||||
label="Open Form"
|
||||
label="'Open Form' Link"
|
||||
help="Link to the form public page"
|
||||
/>
|
||||
<toggle-switch-input name="link_edit_form" v-model="compVal.link_edit_form" class="mt-4"
|
||||
label="Edit Form"
|
||||
label="'Edit Form' Link"
|
||||
help="Link to the form admin page"
|
||||
/>
|
||||
<toggle-switch-input name="views_submissions_count" v-model="compVal.views_submissions_count" class="mt-4"
|
||||
label="Analytics (views & submissions)"
|
||||
label="Form Analytics" help="Form views and submissions count"
|
||||
/>
|
||||
<toggle-switch-input name="link_edit_submission" v-model="compVal.link_edit_submission" class="mt-4"
|
||||
label="Link to the Edit Submission Record"
|
||||
label="Edit Submission Link"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
33
client/components/open/integrations/DiscordIntegration.vue
Normal file
33
client/components/open/integrations/DiscordIntegration.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<IntegrationWrapper :integration="props.integration" :form="form" v-model="props.integrationData">
|
||||
<text-input :form="integrationData" name="settings.discord_webhook_url"
|
||||
label="Discord webhook url" help="help" required>
|
||||
<template #help>
|
||||
<InputHelp>
|
||||
<template #help>
|
||||
<span>
|
||||
Receive a discord message on each form submission.
|
||||
<a href="https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks" target="_blank"> Click
|
||||
here </a> to learn how to get a discord webhook url.
|
||||
</span>
|
||||
</template>
|
||||
</InputHelp>
|
||||
</template>
|
||||
</text-input>
|
||||
<h4 class="font-bold mt-4">Discord message options</h4>
|
||||
<form-notifications-message-actions v-model="integrationData.settings"/>
|
||||
</IntegrationWrapper>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import IntegrationWrapper from "./components/IntegrationWrapper.vue"
|
||||
import FormNotificationsMessageActions
|
||||
from "~/components/open/forms/components/form-components/components/FormNotificationsMessageActions.vue"
|
||||
|
||||
const props = defineProps({
|
||||
integration: {type: Object, required: true},
|
||||
form: {type: Object, required: true},
|
||||
integrationData: {type: Object, required: true},
|
||||
formIntegrationId: {type: Number, required: false, default: null}
|
||||
})
|
||||
</script>
|
||||
34
client/components/open/integrations/EmailIntegration.vue
Normal file
34
client/components/open/integrations/EmailIntegration.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<IntegrationWrapper :integration="props.integration" :form="form" v-model="props.integrationData">
|
||||
<text-area-input :form="integrationData" name="settings.notification_emails" required
|
||||
label="Notification Emails" help="Add one email per line" />
|
||||
<text-input :form="integrationData" name="settings.notification_reply_to"
|
||||
label="Notification Reply To" :help="notifiesHelp" />
|
||||
</IntegrationWrapper>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import IntegrationWrapper from './components/IntegrationWrapper.vue'
|
||||
|
||||
const props = defineProps({
|
||||
integration: { type: Object, required: true },
|
||||
form: { type: Object, required: true },
|
||||
integrationData: { type: Object, required: true },
|
||||
formIntegrationId: { type: Number, required: false, default: null }
|
||||
})
|
||||
|
||||
const replayToEmailField = computed(() => {
|
||||
const emailFields = props.form.properties.filter((field) => {
|
||||
return field.type === 'email' && !field.hidden
|
||||
})
|
||||
if (emailFields.length === 1) return emailFields[0]
|
||||
return null
|
||||
})
|
||||
|
||||
const notifiesHelp = computed(() => {
|
||||
if (replayToEmailField.value) {
|
||||
return 'If empty, Reply-to for this notification will be the email filled in the field "' + replayToEmailField.value.name + '".'
|
||||
}
|
||||
return 'If empty, Reply-to for this notification will be your own email. Add a single email field to your form, and it will automatically become the reply to value.'
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<IntegrationWrapper :integration="props.integration" :form="form" v-model="props.integrationData">
|
||||
<div class="my-5">
|
||||
Coming Soon...
|
||||
</div>
|
||||
</IntegrationWrapper>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import IntegrationWrapper from './components/IntegrationWrapper.vue'
|
||||
|
||||
const props = defineProps({
|
||||
integration: { type: Object, required: true },
|
||||
form: { type: Object, required: true },
|
||||
integrationData: { type: Object, required: true },
|
||||
formIntegrationId: { type: Number, required: false, default: null }
|
||||
})
|
||||
</script>
|
||||
32
client/components/open/integrations/SlackIntegration.vue
Normal file
32
client/components/open/integrations/SlackIntegration.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<IntegrationWrapper :integration="props.integration" :form="form" v-model="props.integrationData">
|
||||
<text-input :form="integrationData" name="settings.slack_webhook_url"
|
||||
label="Slack webhook url" help="help" required>
|
||||
<template #help>
|
||||
<InputHelp>
|
||||
<template #help>
|
||||
<span>
|
||||
Receive slack message on each form submission. <a href="https://api.slack.com/messaging/webhooks" target="_blank"> Click here </a>
|
||||
to learn how to get a slack webhook url
|
||||
</span>
|
||||
</template>
|
||||
</InputHelp>
|
||||
</template>
|
||||
</text-input>
|
||||
<h4 class="font-bold mt-4">Slack message actions</h4>
|
||||
<form-notifications-message-actions v-model="integrationData.settings"/>
|
||||
</IntegrationWrapper>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import IntegrationWrapper from "./components/IntegrationWrapper.vue"
|
||||
import FormNotificationsMessageActions
|
||||
from "~/components/open/forms/components/form-components/components/FormNotificationsMessageActions.vue"
|
||||
|
||||
const props = defineProps({
|
||||
integration: {type: Object, required: true},
|
||||
form: {type: Object, required: true},
|
||||
integrationData: {type: Object, required: true},
|
||||
formIntegrationId: {type: Number, required: false, default: null}
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<IntegrationWrapper :integration="props.integration" :form="form" v-model="props.integrationData">
|
||||
<div>{{ emailSubmissionConfirmationHelp }}</div>
|
||||
|
||||
<div v-if="emailSubmissionConfirmationField">
|
||||
<text-input :form="integrationData" name="settings.notification_sender" class="mt-4" required
|
||||
label="Confirmation Email Sender Name"
|
||||
help="Emails will be sent from our email address but you can customize the name of the Sender" />
|
||||
<text-input :form="integrationData" name="settings.notification_subject" class="mt-4" required
|
||||
label="Confirmation email subject" help="Subject of the confirmation email that will be sent" />
|
||||
<rich-text-area-input :form="integrationData" name="settings.notification_body" class="mt-4" required
|
||||
label="Confirmation email content" help="Content of the confirmation email that will be sent" />
|
||||
<text-input :form="integrationData" name="settings.confirmation_reply_to" class="mt-4"
|
||||
label="Confirmation Reply To" help="If empty, Reply-to will be your own email."/>
|
||||
<toggle-switch-input :form="integrationData" name="settings.notifications_include_submission" class="mt-4"
|
||||
label="Include submission data" help="If enabled the confirmation email will contain form submission answers" />
|
||||
</div>
|
||||
</IntegrationWrapper>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import IntegrationWrapper from "./components/IntegrationWrapper.vue"
|
||||
|
||||
const props = defineProps({
|
||||
integration: { type: Object, required: true },
|
||||
form: { type: Object, required: true },
|
||||
integrationData: { type: Object, required: true },
|
||||
formIntegrationId: { type: Number, required: false, default: null }
|
||||
})
|
||||
|
||||
const emailSubmissionConfirmationField = computed(() => {
|
||||
if (!props.form.properties || !Array.isArray(props.form.properties)) return null
|
||||
const emailFields = props.form.properties.filter((field) => {
|
||||
return field.type === 'email' && !field.hidden
|
||||
})
|
||||
if (emailFields.length === 1) return emailFields[0]
|
||||
return null
|
||||
})
|
||||
const emailSubmissionConfirmationHelp = computed(() => {
|
||||
if (emailSubmissionConfirmationField.value) {
|
||||
return 'Confirmation will be sent to the email in the "' + emailSubmissionConfirmationField.value.name + '" field.'
|
||||
}
|
||||
return 'Only available if your form contains 1 email field.'
|
||||
})
|
||||
|
||||
onBeforeMount(() => {
|
||||
for (const [keyname, defaultValue] of Object.entries({
|
||||
'notification_sender': 'OpnForm',
|
||||
'notification_subject': 'We saved your answers',
|
||||
'notification_body': 'Hello there 👋 <br>This is a confirmation that your submission was successfully saved.',
|
||||
'notifications_include_submission': true,
|
||||
})) {
|
||||
if (props.integrationData.settings[keyname] === undefined) {
|
||||
props.integrationData.settings[keyname] = defaultValue
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
17
client/components/open/integrations/WebhookIntegration.vue
Normal file
17
client/components/open/integrations/WebhookIntegration.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<IntegrationWrapper :integration="props.integration" :form="form" v-model="props.integrationData">
|
||||
<text-input :form="integrationData" name="settings.webhook_url" class="mt-4" label="Webhook url"
|
||||
help="We will post form submissions to this endpoint" required />
|
||||
</IntegrationWrapper>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import IntegrationWrapper from "./components/IntegrationWrapper.vue"
|
||||
|
||||
const props = defineProps({
|
||||
integration: { type: Object, required: true },
|
||||
form: { type: Object, required: true },
|
||||
integrationData: { type: Object, required: true },
|
||||
formIntegrationId: { type: Number, required: false, default: null }
|
||||
})
|
||||
</script>
|
||||
18
client/components/open/integrations/ZapierIntegration.vue
Normal file
18
client/components/open/integrations/ZapierIntegration.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<IntegrationWrapper :integration="props.integration" :form="form" v-model="props.integrationData">
|
||||
<div class="my-5">
|
||||
Coming Soon...
|
||||
</div>
|
||||
</IntegrationWrapper>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import IntegrationWrapper from './components/IntegrationWrapper.vue'
|
||||
|
||||
const props = defineProps({
|
||||
integration: { type: Object, required: true },
|
||||
form: { type: Object, required: true },
|
||||
integrationData: { type: Object, required: true },
|
||||
formIntegrationId: { type: Number, required: false, default: null }
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<div class="text-gray-500 border shadow rounded-md p-5 mt-4 relative flex items-center">
|
||||
<div class="flex-grow flex items-center">
|
||||
<div class="mr-4"
|
||||
:class="{ 'text-blue-500': integration.status === 'active', 'text-gray-400': integration.status !== 'active' }">
|
||||
<Icon :name="integrationTypeInfo.icon" size="32px"/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex space-x-3 font-semibold mr-2">{{ integrationTypeInfo.name }}</div>
|
||||
<Badge :color="integration.status === 'active' ? 'green' : 'gray'"
|
||||
:before-icon="integration.status === 'active' ? 'solar:play-bold' : 'solar:pause-bold'"
|
||||
>
|
||||
{{ integration.status === 'active' ? 'Active' : 'Paused' }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loadingDelete" class="pr-4 pt-2">
|
||||
<Loader class="h-6 w-6 mx-auto"/>
|
||||
</div>
|
||||
<dropdown v-else class="inline">
|
||||
<template #trigger="{ toggle }">
|
||||
<v-button color="white" @click="toggle">
|
||||
<svg class="w-4 h-4 inline -mt-1" viewBox="0 0 16 4" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M8.00016 2.83366C8.4604 2.83366 8.8335 2.46056 8.8335 2.00033C8.8335 1.54009 8.4604 1.16699 8.00016 1.16699C7.53993 1.16699 7.16683 1.54009 7.16683 2.00033C7.16683 2.46056 7.53993 2.83366 8.00016 2.83366Z"
|
||||
stroke="#344054" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path
|
||||
d="M13.8335 2.83366C14.2937 2.83366 14.6668 2.46056 14.6668 2.00033C14.6668 1.54009 14.2937 1.16699 13.8335 1.16699C13.3733 1.16699 13.0002 1.54009 13.0002 2.00033C13.0002 2.46056 13.3733 2.83366 13.8335 2.83366Z"
|
||||
stroke="#344054" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path
|
||||
d="M2.16683 2.83366C2.62707 2.83366 3.00016 2.46056 3.00016 2.00033C3.00016 1.54009 2.62707 1.16699 2.16683 1.16699C1.70659 1.16699 1.3335 1.54009 1.3335 2.00033C1.3335 2.46056 1.70659 2.83366 2.16683 2.83366Z"
|
||||
stroke="#344054" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</v-button>
|
||||
</template>
|
||||
<a v-track.edit_form_integration_click="{ form_slug: form.slug, form_integration_id: integration.id }" href="#"
|
||||
@click.prevent="showIntegrationModal = true"
|
||||
class="flex px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:no-underline hover:text-gray-900 items-center">
|
||||
<Icon name="heroicons:pencil" class="w-5 h-5 mr-2"/>
|
||||
Edit
|
||||
</a>
|
||||
<a v-track.past_events_form_integration_click="{ form_slug: form.slug, form_integration_id: integration.id }"
|
||||
href="#"
|
||||
@click.prevent="showIntegrationEventsModal = true"
|
||||
class="flex px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:no-underline hover:text-gray-900 items-center">
|
||||
<Icon name="heroicons:clock" class="w-5 h-5 mr-2"/>
|
||||
Past Events
|
||||
</a>
|
||||
<a v-track.delete_form_integration_click="{ form_integration_id: integration.id }" href="#"
|
||||
class="flex px-4 py-2 text-md text-red-600 hover:bg-red-50 hover:no-underline items-center"
|
||||
@click.prevent="deleteFormIntegration(integration.id)">
|
||||
<Icon name="heroicons:trash" class="w-5 h-5 mr-2"/>
|
||||
|
||||
Delete Integration
|
||||
</a>
|
||||
</dropdown>
|
||||
<IntegrationModal v-if="form && integration && integrationTypeInfo" :form="form" :integration="integrationTypeInfo"
|
||||
:integrationKey="integration.integration_id" :formIntegrationId="integration.id"
|
||||
:show="showIntegrationModal"
|
||||
@close="showIntegrationModal = false"/>
|
||||
|
||||
<IntegrationEventsModal v-if="form && integration" :form="form" :formIntegrationId="integration.id"
|
||||
:show="showIntegrationEventsModal"
|
||||
@close="showIntegrationEventsModal = false"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed} from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
integration: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
form: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const alert = useAlert()
|
||||
const formIntegrationsStore = useFormIntegrationsStore()
|
||||
const integrations = computed(() => formIntegrationsStore.availableIntegrations)
|
||||
const integrationTypeInfo = computed(() => integrations.value.get(props.integration.integration_id))
|
||||
|
||||
let showIntegrationModal = ref(false)
|
||||
let showIntegrationEventsModal = ref(false)
|
||||
let loadingDelete = ref(false)
|
||||
|
||||
const deleteFormIntegration = (integrationid) => {
|
||||
alert.confirm('Do you really want to delete this form integration?', () => {
|
||||
opnFetch('/open/forms/{formid}/integration/{integrationid}'.replace('{formid}', props.form.id).replace('{integrationid}', integrationid), {method: 'DELETE'}).then((data) => {
|
||||
if (data.type === 'success') {
|
||||
alert.success(data.message)
|
||||
formIntegrationsStore.remove(integrationid)
|
||||
} else {
|
||||
alert.error('Something went wrong!')
|
||||
}
|
||||
}).catch((error) => {
|
||||
alert.error(error.data.message)
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<modal :show="show" @close="emit('close')" compact-header inner-padding="">
|
||||
<template #icon>
|
||||
<Icon name="heroicons:clock" size="40px"/>
|
||||
</template>
|
||||
<template #title>
|
||||
Past Events
|
||||
</template>
|
||||
|
||||
<UTable :loading="integrationEventsLoading" :columns="columns" :rows="integrationEvents">
|
||||
<template #status-data="{ row }">
|
||||
<Badge :color="(row.status==='Success') ? 'green' : 'red'">
|
||||
{{row.status}}
|
||||
</Badge>
|
||||
</template>
|
||||
<template #data-data="{ row }">
|
||||
<vue-json-pretty v-if="row.data && Object.keys(row.data).length > 0" :data="row.data" :collapsedNodeLength="0" :showLength="true" :showIcon="true" />
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</UTable>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-center gap-x-2">
|
||||
<v-button color="white" @click.prevent="emit('close')">
|
||||
Close
|
||||
</v-button>
|
||||
</div>
|
||||
</template>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import VueJsonPretty from 'vue-json-pretty'
|
||||
import 'vue-json-pretty/lib/styles.css'
|
||||
|
||||
const props = defineProps({
|
||||
show: { type: Boolean, required: true },
|
||||
form: {type: Object, required: true},
|
||||
formIntegrationId: {type: Number, required: true}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
const formIntegrationEventEndpoint = '/open/forms/{formid}/integration/{integrationid}/events'
|
||||
const columns = [
|
||||
{ key: 'date', label: 'Date', sortable: true },
|
||||
{ key: 'status', label: 'Status', sortable: true },
|
||||
{ key: 'data', label: 'Info'}
|
||||
]
|
||||
let integrationEvents = ref([])
|
||||
let integrationEventsLoading = ref(false)
|
||||
|
||||
watch(() => props.show, () => {
|
||||
fetchEvents()
|
||||
})
|
||||
|
||||
const fetchEvents = () => {
|
||||
if (props.show) {
|
||||
nextTick(() => {
|
||||
integrationEventsLoading.value = true
|
||||
integrationEvents.value = []
|
||||
opnFetch(formIntegrationEventEndpoint.replace('{formid}', props.form.id).replace('{integrationid}', props.formIntegrationId)).then((data) => {
|
||||
integrationEvents.value = data
|
||||
integrationEventsLoading.value = false
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<UTooltip :text="tooltipText" :prevent="!unavailable">
|
||||
<div role="button" @click="onClick"
|
||||
v-track.new_integration_click="{ name: integration.id }"
|
||||
:class="{'hover:bg-blue-50 group cursor-pointer': !unavailable, 'cursor-not-allowed': unavailable}"
|
||||
class="bg-gray-50 border border-gray-200 rounded-md transition-colors p-4 pb-2 items-center justify-center w-[170px] h-[110px] flex flex-col relative">
|
||||
<div class="flex justify-center">
|
||||
<div class="h-10 w-10 text-gray-500 group-hover:text-blue-500 transition-colors flex items-center">
|
||||
<Icon :name="integration.icon" size="40px"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow flex items-center">
|
||||
<div class="text-gray-400 font-medium text-sm text-center">
|
||||
{{ integration.name }}<span class="text-xs" v-if="integration.coming_soon"> (coming soon)</span>
|
||||
</div>
|
||||
</div>
|
||||
<pro-tag v-if="integration?.is_pro === true" class="absolute top-0 right-1"/>
|
||||
</div>
|
||||
</UTooltip>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
const emit = defineEmits(['select'])
|
||||
|
||||
const props = defineProps({
|
||||
integration: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const unavailable = computed(() => {
|
||||
return props.integration.coming_soon || props.integration.requires_subscription
|
||||
})
|
||||
|
||||
const tooltipText = computed(() => {
|
||||
if (props.integration.coming_soon) return 'This integration is coming soon'
|
||||
if (props.integration.requires_subscription) return 'You need a subscription to use this integration.'
|
||||
return ''
|
||||
})
|
||||
|
||||
const onClick = () => {
|
||||
if (props.integration.coming_soon || props.integration.requires_subscription) return
|
||||
emit('select', props.integration.id)
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<modal :show="show" @close="emit('close')" compact-header>
|
||||
<template #icon>
|
||||
<Icon :name="integration?.icon" size="40px"/>
|
||||
</template>
|
||||
<template #title>
|
||||
{{ integration?.name }}
|
||||
<pro-tag v-if="integration?.is_pro === true"/>
|
||||
</template>
|
||||
|
||||
<component v-if="integration && component" :is="component" :form="form" :integration="integration"
|
||||
:integrationData="integrationData"/>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-center gap-x-2">
|
||||
<v-button class="px-8" @click.prevent="save">
|
||||
Save
|
||||
</v-button>
|
||||
<v-button color="white" @click.prevent="emit('close')">
|
||||
Close
|
||||
</v-button>
|
||||
</div>
|
||||
</template>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed} from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
show: {type: Boolean, required: true},
|
||||
form: {type: Object, required: true},
|
||||
integrationKey: {type: String, required: true},
|
||||
integration: {type: Object, required: true},
|
||||
formIntegrationId: {type: Number, required: false, default: null}
|
||||
})
|
||||
|
||||
const alert = useAlert()
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const formIntegrationsStore = useFormIntegrationsStore()
|
||||
const formIntegration = computed(() => (props.formIntegrationId) ? formIntegrationsStore.getByKey(props.formIntegrationId) : null)
|
||||
|
||||
const component = computed(() => {
|
||||
if (!props.integration) return null
|
||||
return resolveComponent(props.integration.file_name)
|
||||
})
|
||||
|
||||
const integrationData = ref(null)
|
||||
|
||||
watch(() => props.integrationKey, () => {
|
||||
initIntegrationData()
|
||||
})
|
||||
|
||||
const initIntegrationData = () => {
|
||||
integrationData.value = useForm({
|
||||
integration_id: (props.formIntegrationId) ? formIntegration.value.integration_id : props.integrationKey,
|
||||
status: (props.formIntegrationId) ? formIntegration.value.status === 'active' : true,
|
||||
settings: (props.formIntegrationId) ? formIntegration.value.data ?? {} : {},
|
||||
logic: (props.formIntegrationId) ? (!Array.isArray(formIntegration.value.logic) && formIntegration.value.logic) ? formIntegration.value.logic : null : null
|
||||
})
|
||||
}
|
||||
initIntegrationData()
|
||||
|
||||
const save = () => {
|
||||
if (!integrationData.value) return
|
||||
integrationData.value.submit(
|
||||
(props.formIntegrationId) ? 'PUT' : 'POST',
|
||||
'/open/forms/{formid}/integration'.replace('{formid}', props.form.id) + ((props.formIntegrationId) ? '/' + props.formIntegrationId : '')
|
||||
).then(data => {
|
||||
alert.success(data.message)
|
||||
formIntegrationsStore.save(data.form_integration)
|
||||
emit('close')
|
||||
}).catch((error) => {
|
||||
try {
|
||||
alert.error(error.data.message)
|
||||
} catch (e) {
|
||||
alert.error('An error occurred while saving the integration')
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div :class="wrapperClass" :style="inputStyle">
|
||||
<div class="flex justify-between">
|
||||
<slot name="status">
|
||||
<toggle-switch-input name="status" v-model="modelValue.status" label="Enabled"/>
|
||||
</slot>
|
||||
<slot name="help">
|
||||
<v-button class="flex" color="white" size="small" @click="openHelp">
|
||||
<Icon name="heroicons:question-mark-circle-16-solid" class="w-4 h-4 text-gray-500 -mt-[3px]"/>
|
||||
<span class="text-gray-500">
|
||||
Help
|
||||
</span>
|
||||
</v-button>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<slot/>
|
||||
|
||||
<slot name="logic">
|
||||
<div class="-mx-6 px-6 border-t pt-6">
|
||||
<collapse class="w-full" v-model="showLogic">
|
||||
<template #title>
|
||||
<div class="flex gap-x-3 items-start pr-8">
|
||||
<div class="transition-colors" :class="{ 'text-blue-600': showLogic, 'text-gray-300': !showLogic }">
|
||||
<Icon name="material-symbols:settings" size="30px"/>
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<h3 class="font-semibold">
|
||||
Logic
|
||||
</h3>
|
||||
<p class="text-gray-400 text-xs">
|
||||
Only run integration when a condition is met
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<condition-editor ref="filter-editor" v-model="modelValue.logic" class="mt-4 border-t border rounded-md integration-logic"
|
||||
:form="form"/>
|
||||
</collapse>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ConditionEditor from '~/components/open/forms/components/form-logic-components/ConditionEditor.client.vue'
|
||||
|
||||
const props = defineProps({
|
||||
integration: {type: Object, required: true},
|
||||
modelValue: {required: false},
|
||||
wrapperClass: {type: String, required: false},
|
||||
inputStyle: {type: Object, required: false},
|
||||
form: {type: Object, required: false}
|
||||
})
|
||||
|
||||
const crisp = useCrisp()
|
||||
const emit = defineEmits(['close'])
|
||||
const showLogic = ref(!!props.modelValue.logic)
|
||||
|
||||
const openHelp = () => {
|
||||
if (props.integration && props.integration?.crisp_help_page_slug) {
|
||||
crisp.openHelpdeskArticle(props.integration?.crisp_help_page_slug)
|
||||
return
|
||||
}
|
||||
crisp.openHelpdesk()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.integration-logic {
|
||||
.query-builder-group__group-children {
|
||||
margin: 4px 0px 0px 0px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,35 +1,32 @@
|
||||
<template>
|
||||
<Modal :show="show" :closeable="!aiForm.busy" @close="$emit('close')">
|
||||
<template #icon>
|
||||
<template v-if="state=='default'">
|
||||
<template v-if="state == 'default'">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-10 h-10 text-blue">
|
||||
<path fill-rule="evenodd"
|
||||
d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</template>
|
||||
<template v-else-if="state=='ai'">
|
||||
<template v-else-if="state == 'ai'">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-8 h-8 text-blue-500">
|
||||
<path fill-rule="evenodd"
|
||||
d="M14.615 1.595a.75.75 0 01.359.852L12.982 9.75h7.268a.75.75 0 01.548 1.262l-10.5 11.25a.75.75 0 01-1.272-.71l1.992-7.302H3.75a.75.75 0 01-.548-1.262l10.5-11.25a.75.75 0 01.913-.143z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
d="M14.615 1.595a.75.75 0 01.359.852L12.982 9.75h7.268a.75.75 0 01.548 1.262l-10.5 11.25a.75.75 0 01-1.272-.71l1.992-7.302H3.75a.75.75 0 01-.548-1.262l10.5-11.25a.75.75 0 01.913-.143z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</template>
|
||||
</template>
|
||||
<template #title>
|
||||
<template v-if="state=='default'">
|
||||
<template v-if="state == 'default'">
|
||||
Choose a base for your form
|
||||
</template>
|
||||
<template v-else-if="state=='ai'">
|
||||
<template v-else-if="state == 'ai'">
|
||||
AI-powered form generator
|
||||
</template>
|
||||
</template>
|
||||
<div v-if="state=='default'" class="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-8">
|
||||
<div v-track.select_form_base="{base:'contact-form'}" role="button"
|
||||
class="rounded-md border p-4 flex flex-col items-center cursor-pointer hover:bg-gray-50" @click="$emit('close')"
|
||||
>
|
||||
<div v-if="state == 'default'" class="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-8">
|
||||
<div v-track.select_form_base="{ base: 'contact-form' }" role="button"
|
||||
class="rounded-md border p-4 flex flex-col items-center cursor-pointer hover:bg-gray-50" @click="$emit('close')">
|
||||
<div class="p-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-8 h-8 text-blue-500">
|
||||
<path d="M1.5 8.67v8.58a3 3 0 003 3h15a3 3 0 003-3V8.67l-8.928 5.493a3 3 0 01-3.144 0L1.5 8.67z" />
|
||||
@@ -40,15 +37,14 @@
|
||||
Start from a simple contact form
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="aiFeaturesEnabled" v-track.select_form_base="{base:'ai'}"
|
||||
class="rounded-md border p-4 flex flex-col items-center cursor-pointer hover:bg-gray-50" role="button" @click="state='ai'"
|
||||
>
|
||||
<div v-if="aiFeaturesEnabled" v-track.select_form_base="{ base: 'ai' }"
|
||||
class="rounded-md border p-4 flex flex-col items-center cursor-pointer hover:bg-gray-50" role="button"
|
||||
@click="state = 'ai'">
|
||||
<div class="p-4 relative">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-8 h-8 text-blue-500">
|
||||
<path fill-rule="evenodd"
|
||||
d="M14.615 1.595a.75.75 0 01.359.852L12.982 9.75h7.268a.75.75 0 01.548 1.262l-10.5 11.25a.75.75 0 01-1.272-.71l1.992-7.302H3.75a.75.75 0 01-.548-1.262l10.5-11.25a.75.75 0 01.913-.143z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
d="M14.615 1.595a.75.75 0 01.359.852L12.982 9.75h7.268a.75.75 0 01.548 1.262l-10.5 11.25a.75.75 0 01-1.272-.71l1.992-7.302H3.75a.75.75 0 01-.548-1.262l10.5-11.25a.75.75 0 01.913-.143z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="font-medium text-blue-700">
|
||||
@@ -60,26 +56,27 @@
|
||||
<div class="p-4 relative">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-8 h-8 text-blue-500">
|
||||
<path
|
||||
d="M11.25 5.337c0-.355-.186-.676-.401-.959a1.647 1.647 0 01-.349-1.003c0-1.036 1.007-1.875 2.25-1.875S15 2.34 15 3.375c0 .369-.128.713-.349 1.003-.215.283-.401.604-.401.959 0 .332.278.598.61.578 1.91-.114 3.79-.342 5.632-.676a.75.75 0 01.878.645 49.17 49.17 0 01.376 5.452.657.657 0 01-.66.664c-.354 0-.675-.186-.958-.401a1.647 1.647 0 00-1.003-.349c-1.035 0-1.875 1.007-1.875 2.25s.84 2.25 1.875 2.25c.369 0 .713-.128 1.003-.349.283-.215.604-.401.959-.401.31 0 .557.262.534.571a48.774 48.774 0 01-.595 4.845.75.75 0 01-.61.61c-1.82.317-3.673.533-5.555.642a.58.58 0 01-.611-.581c0-.355.186-.676.401-.959.221-.29.349-.634.349-1.003 0-1.035-1.007-1.875-2.25-1.875s-2.25.84-2.25 1.875c0 .369.128.713.349 1.003.215.283.401.604.401.959a.641.641 0 01-.658.643 49.118 49.118 0 01-4.708-.36.75.75 0 01-.645-.878c.293-1.614.504-3.257.629-4.924A.53.53 0 005.337 15c-.355 0-.676.186-.959.401-.29.221-.634.349-1.003.349-1.036 0-1.875-1.007-1.875-2.25s.84-2.25 1.875-2.25c.369 0 .713.128 1.003.349.283.215.604.401.959.401a.656.656 0 00.659-.663 47.703 47.703 0 00-.31-4.82.75.75 0 01.83-.832c1.343.155 2.703.254 4.077.294a.64.64 0 00.657-.642z"
|
||||
/>
|
||||
d="M11.25 5.337c0-.355-.186-.676-.401-.959a1.647 1.647 0 01-.349-1.003c0-1.036 1.007-1.875 2.25-1.875S15 2.34 15 3.375c0 .369-.128.713-.349 1.003-.215.283-.401.604-.401.959 0 .332.278.598.61.578 1.91-.114 3.79-.342 5.632-.676a.75.75 0 01.878.645 49.17 49.17 0 01.376 5.452.657.657 0 01-.66.664c-.354 0-.675-.186-.958-.401a1.647 1.647 0 00-1.003-.349c-1.035 0-1.875 1.007-1.875 2.25s.84 2.25 1.875 2.25c.369 0 .713-.128 1.003-.349.283-.215.604-.401.959-.401.31 0 .557.262.534.571a48.774 48.774 0 01-.595 4.845.75.75 0 01-.61.61c-1.82.317-3.673.533-5.555.642a.58.58 0 01-.611-.581c0-.355.186-.676.401-.959.221-.29.349-.634.349-1.003 0-1.035-1.007-1.875-2.25-1.875s-2.25.84-2.25 1.875c0 .369.128.713.349 1.003.215.283.401.604.401.959a.641.641 0 01-.658.643 49.118 49.118 0 01-4.708-.36.75.75 0 01-.645-.878c.293-1.614.504-3.257.629-4.924A.53.53 0 005.337 15c-.355 0-.676.186-.959.401-.29.221-.634.349-1.003.349-1.036 0-1.875-1.007-1.875-2.25s.84-2.25 1.875-2.25c.369 0 .713.128 1.003.349.283.215.604.401.959.401a.656.656 0 00.659-.663 47.703 47.703 0 00-.31-4.82.75.75 0 01.83-.832c1.343.155 2.703.254 4.077.294a.64.64 0 00.657-.642z" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="font-medium">
|
||||
Start from a template
|
||||
</p>
|
||||
<NuxtLink v-track.select_form_base="{base:'template'}" :to="{name:'templates'}" class="absolute inset-0" />
|
||||
<NuxtLink v-track.select_form_base="{ base: 'template' }" :to="{ name: 'templates' }" class="absolute inset-0" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="state=='ai'">
|
||||
<a class="absolute top-4 left-4" href="#" role="button" @click.prevent="state='default'">
|
||||
<div v-else-if="state == 'ai'">
|
||||
<a class="absolute top-4 left-4" href="#" role="button" @click.prevent="state = 'default'">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4 inline -mt-1">
|
||||
<path fill-rule="evenodd" d="M7.72 12.53a.75.75 0 010-1.06l7.5-7.5a.75.75 0 111.06 1.06L9.31 12l6.97 6.97a.75.75 0 11-1.06 1.06l-7.5-7.5z" clip-rule="evenodd" />
|
||||
<path fill-rule="evenodd"
|
||||
d="M7.72 12.53a.75.75 0 010-1.06l7.5-7.5a.75.75 0 111.06 1.06L9.31 12l6.97 6.97a.75.75 0 11-1.06 1.06l-7.5-7.5z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
Back
|
||||
</a>
|
||||
<text-area-input label="Form Description" :disabled="loading?true:null" :form="aiForm" name="form_prompt" help="Give us a description of the form you want to build (the more details the better)"
|
||||
placeholder="A simple contact form, with a name, email and message field"
|
||||
/>
|
||||
<text-area-input label="Form Description" :disabled="loading ? true : null" :form="aiForm" name="form_prompt"
|
||||
help="Give us a description of the form you want to build (the more details the better)"
|
||||
placeholder="A simple contact form, with a name, email and message field" />
|
||||
<v-button class="w-full" :loading="loading" @click.prevent="generateForm">
|
||||
Generate a form
|
||||
</v-button>
|
||||
@@ -92,10 +89,11 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
emits: ['close', 'form-generated'],
|
||||
props: {
|
||||
show: { type: Boolean, required: true }
|
||||
},
|
||||
setup () {
|
||||
setup() {
|
||||
return {
|
||||
useAlert: useAlert(),
|
||||
runtimeConfig: useRuntimeConfig()
|
||||
@@ -110,13 +108,13 @@ export default {
|
||||
}),
|
||||
|
||||
computed: {
|
||||
aiFeaturesEnabled () {
|
||||
aiFeaturesEnabled() {
|
||||
return this.runtimeConfig.public.aiFeaturesEnabled
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
generateForm () {
|
||||
generateForm() {
|
||||
if (this.loading) return
|
||||
|
||||
this.loading = true
|
||||
@@ -126,10 +124,10 @@ export default {
|
||||
}).catch(error => {
|
||||
console.error(error)
|
||||
this.loading = false
|
||||
this.state = 'default'
|
||||
this.state = 'ai'
|
||||
})
|
||||
},
|
||||
fetchGeneratedForm (generationId) {
|
||||
fetchGeneratedForm(generationId) {
|
||||
// check every 4 seconds if form is generated
|
||||
setTimeout(() => {
|
||||
opnFetch('/forms/ai/' + generationId).then(data => {
|
||||
@@ -145,7 +143,7 @@ export default {
|
||||
this.fetchGeneratedForm(generationId)
|
||||
}
|
||||
}).catch(error => {
|
||||
if (error?.data?.message){
|
||||
if (error?.data?.message) {
|
||||
this.useAlert.error(error.data.message)
|
||||
}
|
||||
this.state = 'default'
|
||||
|
||||
Reference in New Issue
Block a user