diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae1b667c..c4d56d7b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: - name: Install Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: 3.4.1 + ruby-version: 3.4.2 - name: Cache gems uses: actions/cache@v4 with: @@ -35,7 +35,7 @@ jobs: - name: Install Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: 3.4.1 + ruby-version: 3.4.2 - name: Cache gems uses: actions/cache@v4 with: @@ -85,7 +85,7 @@ jobs: - name: Install Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: 3.4.1 + ruby-version: 3.4.2 - name: Cache gems uses: actions/cache@v4 with: @@ -127,7 +127,7 @@ jobs: - name: Install Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: 3.4.1 + ruby-version: 3.4.2 - name: Set up Node uses: actions/setup-node@v1 with: diff --git a/Dockerfile b/Dockerfile index 23fa1ad7..99753c93 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:3.4.1-alpine AS fonts +FROM ruby:3.4.2-alpine AS fonts WORKDIR /fonts @@ -6,7 +6,7 @@ RUN apk --no-cache add fontforge wget && wget https://github.com/satbyy/go-noto- RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")' -FROM ruby:3.4.1-alpine AS webpack +FROM ruby:3.4.2-alpine AS webpack ENV RAILS_ENV=production ENV NODE_ENV=production @@ -32,7 +32,7 @@ COPY ./app/views ./app/views RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker -FROM ruby:3.4.1-alpine AS app +FROM ruby:3.4.2-alpine AS app ENV RAILS_ENV=production ENV BUNDLE_WITHOUT="development:test" diff --git a/Gemfile b/Gemfile index 6961ac80..7820977c 100644 --- a/Gemfile +++ b/Gemfile @@ -2,7 +2,7 @@ source 'https://rubygems.org' -ruby '3.4.1' +ruby '3.4.2' gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic' gem 'aws-sdk-s3', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 43018559..88d95b09 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -652,7 +652,7 @@ DEPENDENCIES webmock RUBY VERSION - ruby 3.4.1p0 + ruby 3.4.2p28 BUNDLED WITH 2.5.3 diff --git a/app/controllers/api/submissions_controller.rb b/app/controllers/api/submissions_controller.rb index 51a8e43e..89d361ec 100644 --- a/app/controllers/api/submissions_controller.rb +++ b/app/controllers/api/submissions_controller.rb @@ -17,6 +17,10 @@ module Api submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] }) end + if params.key?(:archived) + submissions = params[:archived].in?(['true', true]) ? submissions.archived : submissions.active + end + submissions = Submissions::Filter.call(submissions, current_user, params) submissions = paginate(submissions.preload(:created_by_user, :submitters, 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 1cc9dbf1..c83c3873 100644 --- a/app/javascript/template_builder/builder.vue +++ b/app/javascript/template_builder/builder.vue @@ -1,9 +1,18 @@