diff --git a/app/Http/Requests/UserFormRequest.php b/app/Http/Requests/UserFormRequest.php index ee9a88a..ecfb073 100644 --- a/app/Http/Requests/UserFormRequest.php +++ b/app/Http/Requests/UserFormRequest.php @@ -40,6 +40,7 @@ abstract class UserFormRequest extends \Illuminate\Foundation\Http\FormRequest 'notifications_include_submission' => 'boolean', 'webhook_url' => 'url|nullable', 'use_captcha' => 'boolean', + 'slack_webhook_url' => 'url|nullable', // Customization 'theme' => ['required',Rule::in(Form::THEMES)], @@ -96,7 +97,9 @@ abstract class UserFormRequest extends \Illuminate\Foundation\Http\FormRequest // Date field 'properties.*.with_time' => 'boolean|nullable', + 'properties.*.use_am_pm' => 'boolean|nullable', 'properties.*.date_range' => 'boolean|nullable', + 'properties.*.prefill_today' => 'boolean|nullable', // Select / Multi Select field 'properties.*.allow_creation' => 'boolean|nullable', diff --git a/app/Http/Resources/FormResource.php b/app/Http/Resources/FormResource.php index a8bfeda..f8ebfff 100644 --- a/app/Http/Resources/FormResource.php +++ b/app/Http/Resources/FormResource.php @@ -29,6 +29,7 @@ class FormResource extends JsonResource 'views_count' => $this->when($this->workspace->is_pro, $this->views_count), 'submissions_count' => $this->when($this->workspace->is_pro, $this->submissions_count), 'notifies' => $this->notifies, + 'notifies_slack' => $this->notifies_slack, 'send_submission_confirmation' => $this->send_submission_confirmation, 'webhook_url' => $this->webhook_url, 'redirect_url' => $this->redirect_url, @@ -42,6 +43,7 @@ class FormResource extends JsonResource 'password' => $this->password, 'tags' => $this->tags, 'notification_emails' => $this->notification_emails, + 'slack_webhook_url' => $this->slack_webhook_url, ] : []; $baseData = $this->getFilteredFormData(parent::toArray($request), $userIsFormOwner); diff --git a/app/Listeners/Forms/NotifyFormSubmission.php b/app/Listeners/Forms/NotifyFormSubmission.php index b42a3c4..5ab5a2f 100644 --- a/app/Listeners/Forms/NotifyFormSubmission.php +++ b/app/Listeners/Forms/NotifyFormSubmission.php @@ -7,6 +7,8 @@ use App\Notifications\Forms\FormSubmissionNotification; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Support\Facades\Notification; +use Spatie\WebhookServer\WebhookCall; +use App\Service\Forms\FormSubmissionFormatter; class NotifyFormSubmission implements ShouldQueue { @@ -20,18 +22,77 @@ class NotifyFormSubmission implements ShouldQueue */ public function handle(FormSubmitted $event) { - if (!$event->form->notifies || !$event->form->is_pro) return; + if (!$event->form->is_pro) return; - $subscribers = collect(preg_split("/\r\n|\n|\r/", $event->form->notification_emails))->filter(function($email) { - return filter_var($email, FILTER_VALIDATE_EMAIL); - }); - \Log::debug('Sending email notification',[ - 'recipients' => $subscribers->toArray(), - 'form_id' => $event->form->id, - 'form_slug' => $event->form->slug, - ]); - $subscribers->each(function ($subscriber) use ($event) { - Notification::route('mail', $subscriber)->notify(new FormSubmissionNotification($event)); - }); + if ($event->form->notifies) { + // Send Email Notification + $subscribers = collect(preg_split("/\r\n|\n|\r/", $event->form->notification_emails))->filter(function($email) { + return filter_var($email, FILTER_VALIDATE_EMAIL); + }); + \Log::debug('Sending email notification',[ + 'recipients' => $subscribers->toArray(), + 'form_id' => $event->form->id, + 'form_slug' => $event->form->slug, + ]); + $subscribers->each(function ($subscriber) use ($event) { + Notification::route('mail', $subscriber)->notify(new FormSubmissionNotification($event)); + }); + } + + if ($event->form->notifies_slack) { + // Send Slack Notification + $this->sendSlackNotification($event); + } + } + + private function sendSlackNotification(FormSubmitted $event) + { + if($this->validateSlackWebhookUrl($event->form->slack_webhook_url)){ + $submissionString = ""; + $formatter = (new FormSubmissionFormatter($event->form, $event->data))->outputStringsOnly(); + foreach ($formatter->getFieldsWithValue() as $field) { + $tmpVal = is_array($field['value']) ? implode(",", $field['value']) : $field['value']; + $submissionString .= ">*".ucfirst($field['name'])."*: ".$tmpVal." \n"; + } + + $formURL = url("forms/".$event->form->slug); + $editFormURL = url("forms/".$event->form->slug."/show"); + $finalSlackPostData = [ + 'blocks' => [ + [ + 'type' => 'section', + 'text' => [ + 'type' => 'mrkdwn', + 'text' => 'New submission for your form *<'.$formURL.'|'.$event->form->title.':>*', + ] + ], + [ + 'type' => 'section', + 'text' => [ + 'type' => 'mrkdwn', + 'text' => $submissionString + ] + ], + [ + 'type' => 'section', + 'text' => [ + 'type' => 'mrkdwn', + 'text' => '*<'.$formURL.'|🔗 Open Form>* *<'.$editFormURL.'|✍️ Edit Form>*', + ] + ], + ] + ]; + + WebhookCall::create() + ->url($event->form->slack_webhook_url) + ->doNotSign() + ->payload($finalSlackPostData) + ->dispatch(); + } + } + + private function validateSlackWebhookUrl($url) + { + return ($url) ? str_contains($url, 'https://hooks.slack.com/') : false; } } diff --git a/app/Models/Forms/Form.php b/app/Models/Forms/Form.php index 5e4f8df..64c029b 100644 --- a/app/Models/Forms/Form.php +++ b/app/Models/Forms/Form.php @@ -37,6 +37,7 @@ class Form extends Model 'notification_subject', 'notification_body', 'notifications_include_submission', + 'slack_webhook_url', // integrations 'webhook_url', @@ -94,6 +95,7 @@ class Form extends Model protected $hidden = [ 'workspace_id', 'notifies', + 'slack_webhook_url', 'webhook_url', 'send_submission_confirmation', 'redirect_url', @@ -223,4 +225,9 @@ class Form extends Model { return FormFactory::new(); } + + public function getNotifiesSlackAttribute() + { + return !empty($this->slack_webhook_url); + } } diff --git a/app/Models/User.php b/app/Models/User.php index 079ee5e..4618afa 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -4,7 +4,6 @@ namespace App\Models; use App\Http\Controllers\SubscriptionController; use App\Models\Forms\Form; -use App\Models\Workspace; use App\Notifications\ResetPassword; use App\Notifications\VerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -13,7 +12,6 @@ use Illuminate\Foundation\Auth\User as Authenticatable; // use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Notifications\Notifiable; use Laravel\Cashier\Billable; -use Rickycezar\Impersonate\Models\Impersonate; use Tymon\JWTAuth\Contracts\JWTSubject; class User extends Authenticatable implements JWTSubject //, MustVerifyEmail @@ -205,5 +203,5 @@ class User extends Authenticatable implements JWTSubject //, MustVerifyEmail }); }); } - + } diff --git a/app/Service/Forms/FormCleaner.php b/app/Service/Forms/FormCleaner.php index c63c199..f46af77 100644 --- a/app/Service/Forms/FormCleaner.php +++ b/app/Service/Forms/FormCleaner.php @@ -38,6 +38,7 @@ class FormCleaner 'theme' => 'default', 'use_captcha' => false, 'password' => null, + 'slack_webhook_url' => null, ]; private array $fieldDefaults = [ @@ -68,6 +69,7 @@ class FormCleaner 'logo_picture' => 'The logo was removed.', 'database_fields_update' => 'Form submission will only create new records (no updates).', 'theme' => 'Default theme was applied.', + 'slack_webhook_url' => "Slack webhook disabled.", // For fields 'hide_field_name' => 'Hide field name removed.', diff --git a/config/services.php b/config/services.php index e668b03..eaa8e0d 100644 --- a/config/services.php +++ b/config/services.php @@ -46,5 +46,9 @@ return [ 'notion' => [ 'worker' => env('NOTION_WORKER','https://notion-forms-worker.notionforms.workers.dev/v1') - ] + ], + + 'google_analytics_code' => env('GOOGLE_ANALYTICS_CODE'), + 'amplitude_code' => env('AMPLITUDE_CODE'), + 'crisp_website_id' => env('CRISP_WEBSITE_ID') ]; diff --git a/database/factories/FormFactory.php b/database/factories/FormFactory.php index 7320977..40d4850 100644 --- a/database/factories/FormFactory.php +++ b/database/factories/FormFactory.php @@ -57,7 +57,7 @@ class FormFactory extends Factory { return [ 'title' => $this->faker->text(30), - 'description' => $this->faker->randomHtml(2), + 'description' => $this->faker->randomHtml(1), 'notifies' => false, 'send_submission_confirmation' => false, 'webhook_url' => null, @@ -80,7 +80,8 @@ class FormFactory extends Factory 'use_captcha' => false, 'can_be_indexed' => true, 'password' => false, - 'tags' => [] + 'tags' => [], + 'slack_webhook_url' => null, ]; } diff --git a/database/migrations/2022_09_16_110344_add_slack_webhook_url_to_forms.php b/database/migrations/2022_09_16_110344_add_slack_webhook_url_to_forms.php new file mode 100644 index 0000000..33df31c --- /dev/null +++ b/database/migrations/2022_09_16_110344_add_slack_webhook_url_to_forms.php @@ -0,0 +1,32 @@ +string('slack_webhook_url')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('forms', function (Blueprint $table) { + $table->dropColumn('slack_webhook_url'); + }); + } +}; diff --git a/resources/js/components/forms/DateInput.vue b/resources/js/components/forms/DateInput.vue index b196de3..abddb1c 100644 --- a/resources/js/components/forms/DateInput.vue +++ b/resources/js/components/forms/DateInput.vue @@ -11,7 +11,8 @@ :style="inputStyle" :name="name" :fixed-classes="fixedClasses" :range="dateRange" :placeholder="placeholder" :timepicker="useTime" :date-format="useTime?'Z':'Y-m-d'" - :user-format="useTime?'F j, Y - H:i':'F j, Y'" + :user-format="useTime ? amPm ? 'F j, Y - G:i K' : 'F j, Y - H:i' : 'F j, Y'" + :amPm="amPm" /> {{ help }} @@ -30,7 +31,8 @@ export default { props: { withTime: { type: Boolean, default: false }, - dateRange: { type: Boolean, default: false } + dateRange: { type: Boolean, default: false }, + amPm: { type: Boolean, default: false } }, data: () => ({ diff --git a/resources/js/components/forms/FlatSelectInput.vue b/resources/js/components/forms/FlatSelectInput.vue index 1d62468..8361024 100644 --- a/resources/js/components/forms/FlatSelectInput.vue +++ b/resources/js/components/forms/FlatSelectInput.vue @@ -1,86 +1,84 @@ - +} + diff --git a/resources/js/components/open/forms/OpenForm.vue b/resources/js/components/open/forms/OpenForm.vue index 6c85d7b..427acb0 100644 --- a/resources/js/components/open/forms/OpenForm.vue +++ b/resources/js/components/open/forms/OpenForm.vue @@ -288,6 +288,12 @@ export default { } else if (urlPrefill && urlPrefill.has(field.id + '[]')) { // Array url prefills formData[field.id] = urlPrefill.getAll(field.id + '[]') + } else if (field.type === 'date' && field.prefill_today === true) { // For Prefill with 'today' + const dateObj = new Date() + const currentDate = dateObj.getFullYear() + '-' + + String(dateObj.getMonth() + 1).padStart(2, '0') + '-' + + String(dateObj.getDate()).padStart(2, '0') + formData[field.id] = currentDate } else { // Default prefill if any formData[field.id] = field.prefill } @@ -363,6 +369,9 @@ export default { } else if (field.date_range) { inputProperties.dateRange = true } + if (field.use_am_pm) { + inputProperties.amPm = true + } } else if (field.type === 'files' || (field.type === 'url' && field.file_upload)) { inputProperties.multiple = (field.multiple !== undefined && field.multiple) inputProperties.mbLimit = 5 diff --git a/resources/js/components/open/forms/components/form-components/FormIntegrations.vue b/resources/js/components/open/forms/components/form-components/FormIntegrations.vue index bdd5141..28925bd 100644 --- a/resources/js/components/open/forms/components/form-components/FormIntegrations.vue +++ b/resources/js/components/open/forms/components/form-components/FormIntegrations.vue @@ -2,11 +2,13 @@ -

- NEW - our Zapier integration is available for - beta testers! During the beta, you don't need a Pro subscription to try it out. -

-

- - - - - - Zapier Integration - - -

+ + + + + + + + + + + + + + + + + + + +
@@ -40,21 +44,19 @@ import Collapse from '../../../../common/Collapse' import ProTag from '../../../../common/ProTag' export default { - components: { Collapse, ProTag }, - props: { - }, - data () { - return { - } + components: {Collapse, ProTag}, + props: {}, + data() { + return {} }, computed: { form: { - get () { + get() { return this.$store.state['open/working_form'].content }, /* We add a setter */ - set (value) { + set(value) { this.$store.commit('open/working_form/set', value) } }, @@ -64,10 +66,9 @@ export default { watch: {}, - mounted () { + mounted() { }, - methods: { - } + methods: {} } diff --git a/resources/js/components/open/forms/components/form-components/FormNotifications.vue b/resources/js/components/open/forms/components/form-components/FormNotifications.vue index e72dc4b..2ae7118 100644 --- a/resources/js/components/open/forms/components/form-components/FormNotifications.vue +++ b/resources/js/components/open/forms/components/form-components/FormNotifications.vue @@ -15,6 +15,17 @@ + + + + + Include time. Or not. This cannot be used with the date range option yet.

+ + Use 12h AM/PM format + +

+ By default, input uses the 24 hours format +

+ + Prefill with 'today' + +

+ if enabled we will pre-fill this field with the current date +

@@ -180,6 +198,11 @@ label="Pre-filled value" :multiple="field.type==='multi_select'" /> +
@@ -383,10 +407,12 @@ export default { this.$set(this.field, 'date_range', val) if (this.field.date_range) { this.$set(this.field, 'with_time', false) + this.$set(this.field, 'prefill_today', false) } }, onFieldWithTimeChange (val) { this.$set(this.field, 'with_time', val) + this.$set(this.field, 'use_am_pm', false) if (this.field.with_time) { this.$set(this.field, 'date_range', false) } @@ -420,6 +446,15 @@ export default { }) this.$set(this.field, this.field.type, {'options': tmpOpts}) }, + onFieldPrefillTodayChange (val) { + this.$set(this.field, 'prefill_today', val) + if (this.field.prefill_today) { + this.$set(this.field, 'prefill', 'Pre-filled with current date') + this.$set(this.field, 'date_range', false) + } else { + this.$set(this.field, 'prefill', null) + } + }, onFieldAllowCreationChange (val) { this.$set(this.field, 'allow_creation', val) if(this.field.allow_creation){ diff --git a/resources/js/components/service/Amplitude.vue b/resources/js/components/service/Amplitude.vue index b0bab6a..30c3b7c 100644 --- a/resources/js/components/service/Amplitude.vue +++ b/resources/js/components/service/Amplitude.vue @@ -44,7 +44,7 @@ export default { } }, loadAmplitude () { - if (this.loaded || !typeof window.amplitude === 'undefined') return + if (this.loaded || !typeof window.amplitude === 'undefined' || !window.config.amplitude_code) return (function (e, t) { const n = e.amplitude || { _q: [], _iq: {} }; const r = t.createElement('script') @@ -88,7 +88,7 @@ export default { })(window, document) this.amplitudeInstance = window.amplitude.getInstance() - this.amplitudeInstance.init('9952c8b914ce3f2bd494fce2dba18243') + this.amplitudeInstance.init(window.config.amplitude_code) this.loaded = true this.authenticateUser() } diff --git a/resources/js/components/service/Crisp.vue b/resources/js/components/service/Crisp.vue index 6fd5d17..869b590 100644 --- a/resources/js/components/service/Crisp.vue +++ b/resources/js/components/service/Crisp.vue @@ -18,10 +18,10 @@ export default { methods: { loadCrisp () { - if (this.isIframe) return + if (this.isIframe || !window.config.crisp_website_id) return window.$crisp = [] - window.CRISP_WEBSITE_ID = '94219d77-06ff-4aec-b07a-5bf26ec8fde1' + window.CRISP_WEBSITE_ID = window.config.crisp_website_id const script = document.createElement('script') script.setAttribute('src', 'https://client.crisp.chat/l.js') diff --git a/resources/js/pages/forms/create.vue b/resources/js/pages/forms/create.vue index 49b26ef..43b66dc 100644 --- a/resources/js/pages/forms/create.vue +++ b/resources/js/pages/forms/create.vue @@ -152,6 +152,7 @@ export default { properties: [], notifies: false, + slack_notifies: false, send_submission_confirmation: false, webhook_url: null, diff --git a/resources/views/spa.blade.php b/resources/views/spa.blade.php index 8aa1281..d3c0cec 100644 --- a/resources/views/spa.blade.php +++ b/resources/views/spa.blade.php @@ -10,6 +10,9 @@ 'links' => config('links'), 'production' => App::isProduction(), 'hCaptchaSiteKey' => config('services.h_captcha.site_key'), + 'google_analytics_code' => config('services.google_analytics_code'), + 'amplitude_code' => config('services.amplitude_code'), + 'crisp_website_id' => config('services.crisp_website_id'), ]; @endphp @@ -37,8 +40,9 @@ {{-- Load the application scripts --}} +@if($config['google_analytics_code']) - + +@endif +