diff --git a/app/controllers/user_configs_controller.rb b/app/controllers/user_configs_controller.rb index 09b7e6c5..097951b4 100644 --- a/app/controllers/user_configs_controller.rb +++ b/app/controllers/user_configs_controller.rb @@ -5,7 +5,8 @@ class UserConfigsController < ApplicationController authorize_resource :user_config ALLOWED_KEYS = [ - UserConfig::RECEIVE_COMPLETED_EMAIL + UserConfig::RECEIVE_COMPLETED_EMAIL, + UserConfig::SHOW_APP_TOUR ].freeze InvalidKey = Class.new(StandardError) @@ -28,6 +29,7 @@ class UserConfigsController < ApplicationController def user_config_params params.required(:user_config).permit(:key, :value, { value: {} }, { value: [] }).tap do |attrs| attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0]) + attrs[:value] = attrs[:value] == 'true' if attrs[:value].in?(%w[true false]) end end end diff --git a/app/javascript/application.js b/app/javascript/application.js index 7ae6836f..78e542b0 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -32,6 +32,7 @@ import CheckboxGroup from './elements/checkbox_group' import MaskedInput from './elements/masked_input' import SetDateButton from './elements/set_date_button' import IndeterminateCheckbox from './elements/indeterminate_checkbox' +import AppTour from './elements/app_tour' import * as TurboInstantClick from './lib/turbo_instant_click' @@ -99,6 +100,7 @@ safeRegisterElement('checkbox-group', CheckboxGroup) safeRegisterElement('masked-input', MaskedInput) safeRegisterElement('set-date-button', SetDateButton) safeRegisterElement('indeterminate-checkbox', IndeterminateCheckbox) +safeRegisterElement('app-tour', AppTour) safeRegisterElement('template-builder', class extends HTMLElement { connectedCallback () { @@ -124,7 +126,8 @@ safeRegisterElement('template-builder', class extends HTMLElement { withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false', withConditions: this.dataset.withConditions === 'true', currencies: (this.dataset.currencies || '').split(',').filter(Boolean), - acceptFileTypes: this.dataset.acceptFileTypes + acceptFileTypes: this.dataset.acceptFileTypes, + showTourStartForm: this.dataset.showTourStartForm === 'true' }) this.component = this.app.mount(this.appElem) diff --git a/app/javascript/elements/app_tour.js b/app/javascript/elements/app_tour.js new file mode 100644 index 00000000..e98d4281 --- /dev/null +++ b/app/javascript/elements/app_tour.js @@ -0,0 +1,338 @@ +export default class extends HTMLElement { + async connectedCallback () { + this.tourType = this.dataset.type + this.nextPagePath = this.dataset.nextPagePath + this.I18n = JSON.parse(this.dataset.i18n || '{}') + + if (this.dataset.showTour === 'true') this.start() + } + + async start () { + if (window.innerWidth < 768) return + + const [{ driver }] = await Promise.all([ + import('driver.js'), + import('driver.js/dist/driver.css') + ]) + + this.driverObj = driver({ + showProgress: true, + nextBtnText: this.I18n.next, + prevBtnText: this.I18n.previous, + doneBtnText: this.I18n.done, + onDestroyStarted: () => { + this.disableAppGuide().finally(() => { this.destroy() }) + }, + onHighlightStarted: (element) => { + if (element) { + const clickHandler = () => { + this.disableAppGuide().finally(() => { this.destroy() }) + element.removeEventListener('click', clickHandler) + } + + element.addEventListener('click', clickHandler) + } + } + }) + + if (this.tourType === 'dashboard') { + this.showDashboardTour() + } else if (this.tourType === 'builder') { + this.showTemplateBuilderTour() + } else if (this.tourType === 'account') { + this.showAccountTour() + } else if (this.tourType === 'template') { + this.showTemplateTour() + } + } + + disconnectedCallback () { + if (this.driverObj) this.destroy() + } + + destroy () { + if (this.builderTemplate) this.builderTemplate.fields.shift() + if (this.driverObj) this.driverObj.destroy() + } + + showTemplateTour () { + const steps = [ + { + element: '#share_link_clipboard', + popover: { + title: this.I18n.copy_and_share_link, + description: this.I18n.copy_and_share_link_description, + side: 'bottom', + align: 'end' + } + }, + { + element: '#sign_yourself_button', + popover: { + title: this.I18n.sign_the_document, + description: this.I18n.sign_the_document_description, + side: 'top', + align: 'center' + } + }, + { + element: '#send_to_recipients_button', + popover: { + title: this.I18n.send_for_signing, + description: this.I18n.add_recipients_description, + side: 'top', + align: 'center' + } + }, + { + element: '#add_recipients_button', + popover: { + title: this.I18n.add_recipients, + description: this.I18n.add_recipients_description, + side: 'bottom', + align: 'end' + } + }, + { + element: '#account_settings_button', + popover: { + title: this.I18n.settings, + description: this.I18n.settings_template_description, + side: 'right', + align: 'start', + showButtons: this.nextPagePath ? ['next', 'previous', 'close'] : ['previous', 'close'], + onNextClick: () => { + if (this.nextPagePath) { + window.Turbo.visit(this.nextPagePath) + } + } + } + } + ].filter((step) => document.querySelector(step.element)) + + this.driverObj.setSteps(steps) + this.driverObj.drive() + } + + showDashboardTour () { + this.driverObj.setSteps([ + { + element: '#templates_submissions_toggle', + popover: { + title: this.I18n.template_and_submissions, + description: this.I18n.template_and_submissions_description, + side: 'right', + align: 'start' + } + }, + { + element: '#templates_upload_button', + popover: { + title: this.I18n.upload_a_pdf_file, + description: this.I18n.upload_a_pdf_file_description, + side: 'left', + align: 'start', + showButtons: this.nextPagePath ? ['next', 'previous', 'close'] : ['previous', 'close'], + onNextClick: () => { + if (this.nextPagePath) { + window.Turbo.visit(this.nextPagePath) + } + } + }, + onHighlightStarted: () => {} + } + ]) + + this.driverObj.drive() + } + + showAccountTour () { + this.driverObj.setSteps([ + { + element: '#account_settings_menu', + popover: { + title: this.I18n.settings, + description: this.I18n.settings_account_description, + side: 'right', + align: 'start' + } + }, + { + element: '#support_channels', + popover: { + title: this.I18n.support, + description: this.I18n.support_description, + side: 'left', + align: 'start' + } + } + ].filter((step) => document.querySelector(step.element))) + + this.driverObj.drive() + } + + showTemplateBuilderTour () { + const builderComponent = document.querySelector('template-builder')?.component + + this.builderTemplate = builderComponent?.template + + if (this.builderTemplate) { + this.builderTemplate.fields.unshift({ + uuid: 'b387399b-88dc-4345-9d37-743e97a9b2b3', + submitter_uuid: this.builderTemplate.submitters[0].uuid, + name: 'First Name', + type: 'text' + }) + + builderComponent.$nextTick(() => { + this.driverObj.setSteps([ + { + element: '.roles-dropdown', + popover: { + title: this.I18n.select_a_signer_party, + description: this.I18n.select_a_signer_party_description, + side: 'left', + align: 'start', + onPopoverRender: () => { + const rolesDropdown = document.querySelector('.roles-dropdown') + + rolesDropdown.dispatchEvent(new Event('mouseenter', { bubbles: true, cancelable: true })) + rolesDropdown.classList.add('dropdown-open') + } + } + }, + { + element: '.roles-dropdown .dropdown-content', + popover: { + title: this.I18n.available_parties, + description: this.I18n.available_parties_description, + side: 'left', + align: 'start', + onPopoverRender: () => { + document.querySelector('.roles-dropdown .dropdown-content').classList.remove('driver-active-element') + }, + onNextClick: () => { + document.querySelector('.roles-dropdown').classList.remove('dropdown-open') + this.driverObj.moveNext() + } + } + }, + { + element: '#field-types-grid', + popover: { + title: this.I18n.available_field_types, + description: this.I18n.available_field_types_description, + side: 'right', + align: 'start', + onPrevClick: () => { + document.querySelector('.roles-dropdown').classList.add('dropdown-open') + this.driverObj.movePrevious() + } + } + }, + { + element: '#text_type_field_button', + popover: { + title: this.I18n.text_input_field, + description: this.I18n.text_input_field_description, + side: 'left', + align: 'start' + } + }, + { + element: '#signature_type_field_button', + popover: { + title: this.I18n.signature_field, + description: this.I18n.signature_field_description, + side: 'left', + align: 'start' + } + }, + { + element: '.fields', + popover: { + title: this.I18n.added_fields, + description: this.I18n.added_fields_description, + side: 'right', + align: 'start' + } + }, + { + element: '.list-field label:has(svg.tabler-icon-settings)', + popover: { + title: this.I18n.open_field_settings, + description: this.I18n.open_field_settings_description, + side: 'bottom', + align: 'end', + onPopoverRender: () => { + const settingsDropdown = document.querySelector('.list-field div:first-child span:has(svg.tabler-icon-settings)') + + document.querySelectorAll('.list-field div:first-child .text-transparent').forEach((e) => e.classList.remove('text-transparent')) + settingsDropdown.dispatchEvent(new Event('mouseenter', { bubbles: true, cancelable: true })) + settingsDropdown.classList.add('dropdown-open') + } + } + }, + { + element: '.list-field div:first-child span:has(svg.tabler-icon-settings) .dropdown-content', + popover: { + title: this.I18n.field_settings, + description: this.I18n.field_settings_description, + side: 'left', + align: 'start', + onPopoverRender: () => { + document.querySelector('.list-field div:first-child span:has(svg.tabler-icon-settings) .dropdown-content').classList.remove('driver-active-element') + }, + onNextClick: () => { + document.querySelector('.list-field div:first-child span:has(svg.tabler-icon-settings)').classList.remove('dropdown-open') + this.driverObj.moveNext() + } + } + }, + { + element: '#send_button', + popover: { + title: this.I18n.send_document, + description: this.I18n.send_document_description, + side: 'bottom', + align: 'end', + onPrevClick: () => { + document.querySelector('.list-field div:first-child span:has(svg.tabler-icon-settings)').classList.add('dropdown-open') + this.driverObj.movePrevious() + } + } + }, + { + element: '#sign_yourself_button', + popover: { + title: this.I18n.sign_yourself, + description: this.I18n.sign_yourself_description, + side: 'bottom', + align: 'end', + onNextClick: () => { + if (this.nextPagePath) { + window.Turbo.visit(this.nextPagePath) + } else { + this.destroy() + } + } + } + } + ]) + + this.driverObj.drive() + }) + } + } + + async disableAppGuide () { + return fetch('/user_configs', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content + }, + body: JSON.stringify({ key: 'show_app_tour', value: false }) + }) + } +} diff --git a/app/javascript/elements/file_dropzone.js b/app/javascript/elements/file_dropzone.js index d8fbb116..76411a50 100644 --- a/app/javascript/elements/file_dropzone.js +++ b/app/javascript/elements/file_dropzone.js @@ -35,7 +35,7 @@ export default actionable(targetable(class extends HTMLElement { } toggleLoading = (e) => { - if (e && e.target && !e.target.contains(this)) { + if (e && e.target && (!e.target.contains(this) || !e.detail?.formSubmission?.formElement?.contains(this))) { return } diff --git a/app/javascript/template_builder/builder.vue b/app/javascript/template_builder/builder.vue index cf3fe21a..a830dd02 100644 --- a/app/javascript/template_builder/builder.vue +++ b/app/javascript/template_builder/builder.vue @@ -69,6 +69,7 @@