diff --git a/Gemfile b/Gemfile index 51fbd5e9..ba270748 100644 --- a/Gemfile +++ b/Gemfile @@ -21,6 +21,7 @@ gem 'pg' gem 'premailer-rails' gem 'puma' gem 'rails' +gem 'rails-i18n' gem 'ruby-vips' gem 'shakapacker' gem 'sqlite3' diff --git a/Gemfile.lock b/Gemfile.lock index 0bd3e1cd..9c95070f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -352,6 +352,9 @@ GEM rails-html-sanitizer (1.6.0) loofah (~> 2.21) nokogiri (~> 1.14) + rails-i18n (7.0.7) + i18n (>= 0.7, < 2) + railties (>= 6.0.0, < 8) railties (7.0.5) actionpack (= 7.0.5) activesupport (= 7.0.5) @@ -511,6 +514,7 @@ DEPENDENCIES pry-rails puma rails + rails-i18n rspec-rails rubocop rubocop-performance diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb new file mode 100644 index 00000000..523dcf71 --- /dev/null +++ b/app/controllers/accounts_controller.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +class AccountsController < ApplicationController + LOCALE_OPTIONS = { + 'en-US' => 'English (United States)', + 'en-GB' => 'English (United Kingdom)', + 'es-ES' => 'Spanish (Spain)', + 'pt-PT' => 'Portuguese (Portugal)', + 'de-DE' => 'German (Germany)' + }.freeze + + def show; end + + def update + current_account.update!(account_params) + + @encrypted_config = EncryptedConfig.find_or_initialize_by(account: current_account, + key: EncryptedConfig::APP_URL_KEY) + @encrypted_config.update!(app_url_params) + + Docuseal.refresh_default_url_options! + + redirect_to settings_account_path, notice: 'Account information has been updated' + rescue ActiveRecord::RecordInvalid + render :show, status: :unprocessable_entity + end + + private + + def account_params + params.require(:account).permit(:name, :timezone, :locale) + end + + def app_url_params + params.require(:encrypted_config).permit(:value) + end +end diff --git a/app/controllers/profile_controller.rb b/app/controllers/profile_controller.rb index 033f12a8..62e047a1 100644 --- a/app/controllers/profile_controller.rb +++ b/app/controllers/profile_controller.rb @@ -1,13 +1,11 @@ # frozen_string_literal: true class ProfileController < ApplicationController - before_action :load_encrypted_config, only: %i[index update_app_url] - def index; end def update_contact if current_user.update(contact_params) - redirect_to settings_profile_index_path, notice: 'Contact information successfully updated' + redirect_to settings_profile_index_path, notice: 'Contact information has been updated' else render :index, status: :unprocessable_entity end @@ -16,17 +14,7 @@ class ProfileController < ApplicationController def update_password if current_user.update(password_params) bypass_sign_in(current_user) - redirect_to settings_profile_index_path, notice: 'Password successfully changed' - else - render :index, status: :unprocessable_entity - end - end - - def update_app_url - if @encrypted_config.update(app_url_params) - Docuseal.refresh_default_url_options! - - redirect_to settings_profile_index_path, notice: 'App URL successfully changed' + redirect_to settings_profile_index_path, notice: 'Password has been changed' else render :index, status: :unprocessable_entity end @@ -34,20 +22,11 @@ class ProfileController < ApplicationController private - def load_encrypted_config - @encrypted_config = - EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::APP_URL_KEY) - end - def contact_params - params.require(:user).permit(:first_name, :last_name, :email, account_attributes: %i[name]) + params.require(:user).permit(:first_name, :last_name, :email) end def password_params params.require(:user).permit(:password, :password_confirmation) end - - def app_url_params - params.require(:encrypted_config).permit(:value) - end end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 63f5523c..f0f0028c 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -5,6 +5,7 @@ class RegistrationsController < Devise::RegistrationsController def build_resource(_hash = {}) account = Account.new(account_params) + account.timezone = Accounts.normalize_timezone(account.timezone) self.resource = account.users.new(user_params) end @@ -18,6 +19,6 @@ class RegistrationsController < Devise::RegistrationsController def account_params return {} if params[:account].blank? - params.require(:account).permit(:name) + params.require(:account).permit(:name, :timezone) end end diff --git a/app/controllers/setup_controller.rb b/app/controllers/setup_controller.rb index 8fe0bbc2..e9a8a806 100644 --- a/app/controllers/setup_controller.rb +++ b/app/controllers/setup_controller.rb @@ -15,6 +15,8 @@ class SetupController < ApplicationController def create @account = Account.new(account_params) + @account.timezone = Accounts.normalize_timezone(@account.timezone) + @user = @account.users.new(user_params) if @user.save @@ -24,6 +26,8 @@ class SetupController < ApplicationController ] @account.encrypted_configs.create!(encrypted_configs) + Docuseal.refresh_default_url_options! + sign_in(@user) redirect_to root_path @@ -43,7 +47,7 @@ class SetupController < ApplicationController def account_params return {} unless params[:account] - params.require(:account).permit(:name) + params.require(:account).permit(:name, :timezone) end def encrypted_config_params diff --git a/app/controllers/start_form_controller.rb b/app/controllers/start_form_controller.rb index bdde91a0..93ca4fbf 100644 --- a/app/controllers/start_form_controller.rb +++ b/app/controllers/start_form_controller.rb @@ -16,7 +16,7 @@ class StartFormController < ApplicationController .find_or_initialize_by(**submitter_params) if @submitter.completed_at? - redirect_to start_form_completed_path(@template.slug, email: submission_params[:email]) + redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email]) else @submitter.assign_attributes( uuid: @template.submitters.first['uuid'], @@ -36,7 +36,7 @@ class StartFormController < ApplicationController end def completed - @submitter = Submitter.where(submission: @template.submitters).find_by(email: params[:email]) + @submitter = Submitter.where(submission: @template.submissions).find_by!(email: params[:email]) end private @@ -46,7 +46,7 @@ class StartFormController < ApplicationController end def load_template - slug = params[:slug] || params[:start_template_slug] + slug = params[:slug] || params[:start_form_slug] @template = Template.find_by!(slug:) end diff --git a/app/controllers/submit_form_controller.rb b/app/controllers/submit_form_controller.rb index 00f3e19a..c014b6d7 100644 --- a/app/controllers/submit_form_controller.rb +++ b/app/controllers/submit_form_controller.rb @@ -19,6 +19,7 @@ class SubmitFormController < ApplicationController submitter = Submitter.find_by!(slug: params[:slug]) submitter.values.merge!(normalized_values) submitter.completed_at = Time.current if params[:completed] == 'true' + submitter.opened_at ||= Time.current submitter.save! diff --git a/app/javascript/application.js b/app/javascript/application.js index a12535d2..ac414dcb 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -12,6 +12,7 @@ import ClipboardCopy from './elements/clipboard_copy' import DynamicList from './elements/dynamic_list' import DownloadButton from './elements/download_button' import SetOriginUrl from './elements/set_origin_url' +import SetTimezone from './elements/set_timezone' document.addEventListener('turbo:before-cache', () => { window.flash?.remove() @@ -32,6 +33,7 @@ window.customElements.define('clipboard-copy', ClipboardCopy) window.customElements.define('dynamic-list', DynamicList) window.customElements.define('download-button', DownloadButton) window.customElements.define('set-origin-url', SetOriginUrl) +window.customElements.define('set-timezone', SetTimezone) window.customElements.define('template-builder', class extends HTMLElement { connectedCallback () { diff --git a/app/javascript/elements/set_timezone.js b/app/javascript/elements/set_timezone.js new file mode 100644 index 00000000..be4c05e1 --- /dev/null +++ b/app/javascript/elements/set_timezone.js @@ -0,0 +1,7 @@ +export default class extends HTMLElement { + connectedCallback () { + if (this.dataset.inputId) { + document.getElementById(this.dataset.inputId).value = Intl.DateTimeFormat().resolvedOptions().timeZone + } + } +} diff --git a/app/javascript/submission_form/area.vue b/app/javascript/submission_form/area.vue index 7aa500ff..76d33091 100644 --- a/app/javascript/submission_form/area.vue +++ b/app/javascript/submission_form/area.vue @@ -207,7 +207,7 @@ export default { }, formattedDate () { if (this.field.type === 'date' && this.modelValue) { - return new Intl.DateTimeFormat({ year: 'numeric', month: 'numeric', day: 'numeric' }).format(new Date(this.modelValue)) + return new Intl.DateTimeFormat([], { year: 'numeric', month: 'long', day: 'numeric' }).format(new Date(this.modelValue)) } else { return '' } diff --git a/app/javascript/submission_form/form.vue b/app/javascript/submission_form/form.vue index 8c424b99..76b5988b 100644 --- a/app/javascript/submission_form/form.vue +++ b/app/javascript/submission_form/form.vue @@ -41,6 +41,10 @@ >{{ currentField.name }} (optional) +
<%= svg_icon('calendar', class: 'w-4 h-4') %> - <%= l(template.created_at, format: :long) %> + <%= l(template.created_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>
Streamline document workflows, from creating customizable templates to filling and signing document forms, with DocuSeal
- <%= link_to new_template_path, class: 'btn btn-neutral', data: { turbo_frame: :modal } do %> - <%= svg_icon('plus', class: 'w-6 h-6') %> - Create Template - <% end %> +Streamline document workflows, from creating customizable templates to filling and signing document forms, with DocuSeal
+ <%= link_to new_template_path, class: 'base-button', data: { turbo_frame: :modal } do %> + <%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %> + Create Template + <% end %> +- Configure your to send emails (TODO) -
<% value = @encrypted_config.value || {} %> <%= form_for @encrypted_config, url: settings_email_index_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %> <%= f.fields_for :value do |ff| %> @@ -29,11 +26,11 @@Upload signed PDF file to validate its signature: diff --git a/app/views/invitations/edit.html.erb b/app/views/invitations/edit.html.erb index 75bbb9e5..9acd372f 100644 --- a/app/views/invitations/edit.html.erb +++ b/app/views/invitations/edit.html.erb @@ -1,5 +1,5 @@
- A self-hosted, web-based platform providing secure and efficient digital document signing and transaction management. -
+- Simply initiate the process on your platform, deploy the solution via Docker, - or opt for its packaged version for ease of use. + Run on your own host using Docker container, or deploy on your favorite managed PaaS with a single click.
- This self-hosted solution is mobile-ready, designed to offer a seamless user experience on any device. - Manage documents with ease, right from your smartphone or tablet. + Review and sign digital documents online from any device. + Docuseal document forms are optimized for screens of all sizes.
- With a focus on security, this solution integrates top-level encryption within your own hosting infrastructure. - Comprehensive tracking of every action assures a secure environment for all digital transactions. + Host it on your hardware under a VPN to ensure that important documents can be accesses only within your organization.
- Don't hesitate to fetch github.com/docusealhq.
- We warmly invite you to participate and help us enhance this project.
- There's no need to shy away from becoming a contributor!
+ Source code is available under github.com/docusealhq.
Open-source contributors are always ready to help!
- Manage your contact information -
<%= form_for current_user, url: update_contact_settings_profile_index_path, method: :patch, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>App URL
- <%= form_for @encrypted_config, url: update_app_url_settings_profile_index_path, method: :patch, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %> -Nothing to display
-We apologize, but currently there is no data available to be displayed. You can try adding new entries or refreshing the page to see if any data becomes available.
-- Form has been submitted already - thanks! -
-<%= button_to button_title(title: 'Send copy to Email', disabled_with: 'Sending'), send_submission_email_index_path, params: { template_slug: @template.slug, email: params[:email] }, form: { onsubmit: 'event.submitter.disabled = true' } %> +<%= @submitter.submission.template.name %>
+Signed on <%= l(@submitter.completed_at.to_date, format: :long, locale: @submitter.submission.template.account.locale) %>
+You have been invited to submit the form
-<%= @template.name %>
-Invited by <%= @template.account.name %>
+<%= @template.name %>
+Invited by <%= @template.account.name %>
- Select files storage option (TODO) -
<% value = @encrypted_config.value || { 'service' => 'disk' } %> <% configs = value['configs'] || {} %> <%= form_for @encrypted_config, url: settings_storage_index_path, method: :post, html: { autocomplete: 'off', class: 'w-full' } do |f| %> @@ -29,10 +26,10 @@<%= @submitter.submission.template.name %>
-Signed on <%= l(@submitter.completed_at.to_date, format: :long) %>
+<%= @submitter.submission.template.name %>
+Signed on <%= l(@submitter.completed_at.to_date, format: :long, locale: @submitter.submission.template.account.locale) %>
Submissions
-Submissions
+There are no Submissions yet
+Send an invitation to fill and submit the documents via email
+ <%= link_to new_template_submission_path(@template), class: 'base-button mt-6', data: { turbo_frame: 'modal' } do %> + <%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %> + Add Recipients + <% end %> +