adjust templates, add timezone and locale

pull/105/head
Alex Turchyn 2 years ago
parent d0c7d449d5
commit c904d51302

@ -21,6 +21,7 @@ gem 'pg'
gem 'premailer-rails'
gem 'puma'
gem 'rails'
gem 'rails-i18n'
gem 'ruby-vips'
gem 'shakapacker'
gem 'sqlite3'

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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!

@ -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 () {

@ -0,0 +1,7 @@
export default class extends HTMLElement {
connectedCallback () {
if (this.dataset.inputId) {
document.getElementById(this.dataset.inputId).value = Intl.DateTimeFormat().resolvedOptions().timeZone
}
}
}

@ -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 ''
}

@ -41,6 +41,10 @@
>{{ currentField.name }}
<template v-if="!currentField.required">(optional)</template>
</label>
<div
v-else
class="py-1"
/>
<div>
<input
:id="currentField.uuid"
@ -62,6 +66,10 @@
>{{ currentField.name }}
<template v-if="!currentField.required">(optional)</template>
</label>
<div
v-else
class="py-1"
/>
<div class="text-center">
<input
:id="currentField.uuid"
@ -82,6 +90,10 @@
>{{ currentField.name }}
<template v-if="!currentField.required">(optional)</template>
</label>
<div
v-else
class="py-1"
/>
<select
:id="currentField.uuid"
:required="currentField.required"
@ -246,7 +258,7 @@
:key="step[0].uuid"
href="#"
class="inline border border-base-300 h-3 w-3 rounded-full mx-1"
:class="{ 'bg-base-200': index === currentStep, 'bg-base-content': index < currentStep || isCompleted, 'bg-white': index > currentStep }"
:class="{ 'bg-base-300': index === currentStep, 'bg-base-content': index < currentStep || isCompleted, 'bg-white': index > currentStep }"
@click.prevent="isCompleted ? '' : goToStep(step, true)"
/>
</div>

@ -5,7 +5,9 @@
# Table name: accounts
#
# id :bigint not null, primary key
# locale :string not null
# name :string not null
# timezone :string not null
# created_at :datetime not null
# updated_at :datetime not null
#
@ -15,4 +17,7 @@ class Account < ApplicationRecord
has_many :templates, dependent: :destroy
has_many :active_users, -> { active }, dependent: :destroy,
inverse_of: :account, class_name: 'User'
attribute :timezone, :string, default: 'UTC'
attribute :locale, :string, default: 'en-US'
end

@ -50,4 +50,8 @@ class Submitter < ApplicationRecord
'awaiting'
end
end
def status_event_at
completed_at || opened_at || sent_at || created_at
end
end

@ -52,8 +52,6 @@ class User < ApplicationRecord
scope :active, -> { where(deleted_at: nil) }
accepts_nested_attributes_for :account, update_only: true
def active_for_authentication?
!deleted_at?
end

@ -0,0 +1,36 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
<%= render 'shared/settings_nav' %>
<div class="flex-grow max-w-xl mx-auto">
<h1 class="text-4xl font-bold mb-4">Account</h1>
<%= form_for '', url: settings_account_path, method: :patch, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
<%= f.fields_for current_account do |ff| %>
<div class="form-control">
<%= ff.label :name, 'Company Name', class: 'label' %>
<%= ff.text_field :name, required: true, class: 'base-input' %>
</div>
<div class="grid md:grid-cols-2 gap-4">
<div class="form-control">
<%= ff.label :timezone, class: 'label' %>
<%= ff.select :timezone, nil, {}, class: 'select base-input w-full font-normal' do %>
<%= time_zone_options_for_select(current_account.timezone) %>
<% end %>
</div>
<div class="form-control">
<%= ff.label :locale, 'Time format', class: 'label' %>
<%= ff.select :locale, options_for_select(controller.class::LOCALE_OPTIONS.invert, current_account.locale), {}, class: 'select base-input w-full font-normal' %>
</div>
</div>
<% end %>
<%= f.fields_for @encrypted_config || EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::APP_URL_KEY) do |ff| %>
<div class="form-control">
<%= ff.label :value, 'App URL', class: 'label' %>
<%= ff.text_field :value, autocomplete: 'off', class: 'base-input' %>
</div>
<% end %>
<div class="form-control pt-2">
<%= f.button button_title(title: 'Update', disabled_with: 'Updating'), class: 'base-button' %>
</div>
<% end %>
</div>
<div class="w-0 md:w-52"></div>
</div>

@ -20,7 +20,7 @@
</p>
<p class="flex items-center space-x-1 text-xs text-base-content/60">
<%= svg_icon('calendar', class: 'w-4 h-4') %>
<span><%= l(template.created_at, format: :long) %></span>
<span><%= l(template.created_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %></span>
</p>
</div>
</a>
@ -39,18 +39,22 @@
</div>
<% end %>
</div>
<%= render 'shared/pagination', pagy: @pagy, items_name: 'Templates' %>
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates' %>
<% else %>
<div class="card bg-base-200 h-96 mb-2">
<div class="card-body text-center">
<div class="flex items-center h-full">
<div class="max-w-lg mx-auto">
<h1 class="text-5xl font-bold">Welcome to DocuSeal</h1>
<p class="py-6">Streamline document workflows, from creating customizable templates to filling and signing document forms, with DocuSeal</p>
<%= link_to new_template_path, class: 'btn btn-neutral', data: { turbo_frame: :modal } do %>
<%= svg_icon('plus', class: 'w-6 h-6') %>
<span>Create Template</span>
<% end %>
<div class="mx-auto">
<div class="max-w-xl mx-auto">
<h1 class="text-5xl font-bold text-base-content">👋 Welcome to DocuSeal</h1>
</div>
<div class="max-w-lg mx-auto">
<p class="py-6 text-gray-600">Streamline document workflows, from creating customizable templates to filling and signing document forms, with DocuSeal</p>
<%= link_to new_template_path, class: 'base-button', data: { turbo_frame: :modal } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span>Create Template</span>
<% end %>
</div>
</div>
</div>
</div>

@ -4,13 +4,21 @@
<div class="space-y-2">
<%= render 'devise/shared/error_messages', resource: %>
<%= f.fields_for resource do |ff| %>
<div class="form-control">
<%= ff.label :first_name, class: 'label' %>
<%= ff.text_field :first_name, required: true, class: 'base-input' %>
<div class="grid gap-2 md:grid-cols-2 md:gap-4">
<div class="form-control">
<%= ff.label :first_name, class: 'label' %>
<%= ff.text_field :first_name, required: true, class: 'base-input' %>
</div>
<div class="form-control">
<%= ff.label :last_name, class: 'label' %>
<%= ff.text_field :last_name, required: true, class: 'base-input' %>
</div>
</div>
<% end %>
<%= f.fields_for resource do |ff| %>
<div class="form-control">
<%= ff.label :last_name, class: 'label' %>
<%= ff.text_field :last_name, required: true, class: 'base-input' %>
<%= ff.label :email, class: 'label' %>
<%= ff.email_field :email, required: true, class: 'base-input' %>
</div>
<% end %>
<%= f.fields_for resource.account do |ff| %>
@ -20,10 +28,6 @@
</div>
<% end %>
<%= f.fields_for resource do |ff| %>
<div class="form-control">
<%= ff.label :email, class: 'label' %>
<%= ff.email_field :email, required: true, class: 'base-input' %>
</div>
<div class="form-control">
<%= ff.label :password, class: 'label' %>
<%= ff.password_field :password, required: true, class: 'base-input' %>

@ -2,9 +2,6 @@
<%= render 'shared/settings_nav' %>
<div class="flex-grow max-w-xl mx-auto">
<h1 class="text-4xl font-bold mb-4">Email SMTP</h1>
<p>
Configure your to send emails (TODO)
</p>
<% 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 @@
</div>
</div>
<div class="form-control">
<%= ff.label :from_email, 'Send from', class: 'label' %>
<%= ff.label :from_email, 'Send from Email', class: 'label' %>
<%= ff.email_field :from_email, value: value['from_email'], required: true, class: 'base-input' %>
</div>
<% end %>
<div class="form-control">
<div class="form-control pt-2">
<%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button' %>
</div>
<% end %>

@ -50,7 +50,7 @@
</div>
<div class="flex items-center space-x-1">
<%= svg_icon('calendar', class: 'w-5 h-5 inline') %>
<span><%= l(signature.signing_time, format: :long) %></span>
<span><%= l(signature.signing_time.in_time_zone(current_account.timezone), format: :long, locale: current_account.locale) %></span>
</div>
<div class="flex items-center space-x-1">
<%= svg_icon('certificate', class: 'w-5 h-5 inline') %>

@ -1,7 +1,7 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
<%= render 'shared/settings_nav' %>
<div class="flex-grow max-w-xl mx-auto">
<h1 class="text-4xl font-bold mb-4">Verify PDF Signature</h1>
<h1 class="text-4xl font-bold mb-4">PDF Signature</h1>
<div id="result">
<p class="mb-2">
Upload signed PDF file to validate its signature:

@ -1,5 +1,5 @@
<div class="max-w-xl mx-auto px-2">
<h1 class="text-4xl font-bold text-center my-8">Welcome to Docuseal</h1>
<h1 class="text-4xl font-bold text-center my-8">👋 Welcome to Docuseal</h1>
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: 'space-y-6' }) do |f| %>
<div class="space-y-2">
<%= render 'devise/shared/error_messages', resource: %>

@ -8,65 +8,61 @@
DocuSeal
</h1>
</div>
<p class="mb-16 text-lg text-center text-gray-500">
A self-hosted, web-based platform providing secure and efficient digital document signing and transaction management.
</p>
<h2 class="mb-16 text-lg text-center text-gray-500">
A self-hosted and open-source web platform that provides secure and efficient digital document signing and processing.
</h2>
</div>
</div>
<div class="grid grid-cols-1 gap-12 md:gap-10 md:grid-cols-2">
<div class="card bg-base-300">
<div class="card bg-base-200">
<div class="card-body">
<div class="text-center transition-all">
<div class="inline-block p-4 mb-6 -mt-16 bg-base-content rounded-full">
<div class="inline-block p-4 mb-4 -mt-16 bg-base-content rounded-full">
<%= svg_icon('brand_docker', class: 'w-10 h-10 text-base-100 stroke-1') %>
</div>
<h3 class="mb-4 text-2xl font-semibold">Easy to Install</h3>
<h3 class="mb-4 text-2xl font-semibold">Easy to Start</h3>
<p class="text-base text-gray-500">
Simply initiate the process on your platform, deploy the solution via <a href="https://hub.docker.com/r/docuseal/docuseal" class="link link-neutral font-bold" target="_blank">Docker</a>,
or opt for its packaged version for ease of use.
Run on your own host using <a href="https://hub.docker.com/r/docuseal/docuseal" class="link link-neutral font-bold" target="_blank">Docker</a> container, or deploy on your favorite managed PaaS with a single <a href="https:///www.docuseal.co/install" class="link link-neutral font-bold">click</a>.
</p>
</div>
</div>
</div>
<div class="card bg-base-300">
<div class="card bg-base-200">
<div class="card-body">
<div class="text-center transition-all">
<div class="inline-block p-4 mb-6 -mt-16 bg-base-content rounded-full">
<div class="inline-block p-4 mb-4 -mt-16 bg-base-content rounded-full">
<%= svg_icon('devices', class: 'w-10 h-10 text-base-100') %>
</div>
<h3 class="mb-4 text-2xl font-semibold">Mobile Optimized</h3>
<p class="text-base text-gray-500">
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.
</p>
</div>
</div>
</div>
<div class="card bg-base-300">
<div class="card bg-base-200">
<div class="card-body">
<div class="text-center transition-all">
<div class="inline-block p-4 mb-6 -mt-16 bg-base-content rounded-full">
<div class="inline-block p-4 mb-4 -mt-16 bg-base-content rounded-full">
<%= svg_icon('shield_check', class: 'w-10 h-10 text-base-100') %>
</div>
<h3 class="mb-4 text-2xl font-semibold">Secure</h3>
<p class="text-base text-gray-500">
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.
</p>
</div>
</div>
</div>
<div class="card bg-base-300">
<div class="card bg-base-200">
<div class="card-body">
<div class="text-center transition-all">
<div class="inline-block p-4 mb-6 -mt-16 bg-base-content rounded-full">
<div class="inline-block p-4 mb-4 -mt-16 bg-base-content rounded-full">
<%= svg_icon('brand_github', class: 'w-10 h-10 text-base-100') %>
</div>
<h3 class="mb-4 text-2xl font-semibold">Open Source</h3>
<p class="text-base text-gray-500">
Don't hesitate to fetch <a href="https://github.com/docusealhq" class="link link-neutral font-bold" target="_blank">github.com/docusealhq</a>.
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 <a href="https://github.com/docusealhq/docuseal" class="link link-neutral font-bold" target="_blank">github.com/docusealhq</a>.<br>Open-source contributors are always ready to help!
</p>
</div>
</div>

@ -2,9 +2,6 @@
<%= render 'shared/settings_nav' %>
<div class="flex-grow max-w-xl mx-auto">
<h1 class="text-4xl font-bold mb-4">Profile</h1>
<p>
Manage your contact information
</p>
<%= form_for current_user, url: update_contact_settings_profile_index_path, method: :patch, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
<div class="grid md:grid-cols-2 gap-4">
<div class="form-control">
@ -20,13 +17,7 @@
<%= f.label :email, 'Email', class: 'label' %>
<%= f.email_field :email, autocomplete: 'off', class: 'base-input' %>
</div>
<%= f.fields_for :account do |ff| %>
<div class="form-control">
<%= ff.label :name, 'Company Name', class: 'label' %>
<%= ff.text_field :name, required: true, class: 'base-input' %>
</div>
<% end %>
<div class="form-control">
<div class="form-control pt-2">
<%= f.button button_title(title: 'Update', disabled_with: 'Updating'), class: 'base-button' %>
</div>
<% end %>
@ -40,16 +31,7 @@
<%= f.label :password_confirmation, 'Confirm new password', class: 'label' %>
<%= f.password_field :password_confirmation, autocomplete: 'off', class: 'base-input' %>
</div>
<div class="form-control">
<%= f.button button_title(title: 'Update', disabled_with: 'Updating'), class: 'base-button' %>
</div>
<% end %>
<p class="text-2xl font-bold mt-8 mb-4">App URL</p>
<%= form_for @encrypted_config, url: update_app_url_settings_profile_index_path, method: :patch, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
<div class="form-control">
<%= f.text_field :value, required: true, class: 'base-input' %>
</div>
<div class="form-control">
<div class="form-control pt-2">
<%= f.button button_title(title: 'Update', disabled_with: 'Updating'), class: 'base-button' %>
</div>
<% end %>

@ -1 +1,18 @@
Email has bee sent
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="space-y-6 mx-auto">
<div class="space-y-6">
<div class="flex items-center justify-center">
<a href="/" class="flex items-center">
<div class="mr-3">
<%= render 'shared/logo', width: '50px', height: '50px' %>
</div>
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
</a>
</div>
<div class="text-center text-4xl font-bold">
Email has been sent
</div>
</div>
</div>
</div>
<%= render 'shared/attribution' %>

@ -14,17 +14,21 @@
</div>
</div>
<% end %>
<%= f.fields_for @user do |ff| %>
<div class="form-control">
<%= ff.label :email, class: 'label' %>
<%= ff.email_field :email, required: true, class: 'base-input' %>
</div>
<% end %>
<%= f.fields_for @account do |ff| %>
<set-timezone data-input-id="_account_timezone"></set-timezone>
<%= ff.hidden_field :timezone %>
<div class="form-control">
<%= ff.label :name, 'Company name', class: 'label' %>
<%= ff.text_field :name, required: true, class: 'base-input' %>
</div>
<% end %>
<%= f.fields_for @user do |ff| %>
<div class="form-control">
<%= ff.label :email, class: 'label' %>
<%= ff.email_field :email, required: true, class: 'base-input' %>
</div>
<div class="form-control">
<%= ff.label :password, class: 'label' %>
<%= ff.password_field :password, required: true, placeholder: '************', class: 'base-input' %>

@ -1,8 +0,0 @@
<div class="card bg-base-200">
<div class="card-body text-center">
<div class="max-w-md mx-auto">
<p class="text-4xl font-semibold text-gray-700 mb-4">Nothing to display</p>
<p class="text-gray-500">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.</p>
</div>
</div>
</div>

@ -2,7 +2,7 @@
<% if @pagy.pages > 1 %>
<div class="flex my-6 justify-center md:justify-between">
<div class="hidden md:block text-sm">
<%= @pagy.from %>-<%= @pagy.to %> of <%= @pagy.count %> of <%= local_assigns[:items_name] || 'items' %>
<%= @pagy.from %>-<%= @pagy.to %> of <%= @pagy.count %> <%= local_assigns[:items_name] || 'items' %>
</div>
<div class="join">
<% if @pagy.prev %>

@ -6,6 +6,9 @@
<li>
<%= link_to 'Profile', settings_profile_index_path, class: 'text-base hover:bg-base-300' %>
</li>
<li>
<%= link_to 'Account', settings_account_path, class: 'text-base hover:bg-base-300' %>
</li>
<li>
<%= link_to 'Email', settings_email_index_path, class: 'text-base hover:bg-base-300' %>
</li>
@ -13,7 +16,7 @@
<%= link_to 'Storage', settings_storage_index_path, class: 'text-base hover:bg-base-300' %>
</li>
<li>
<%= link_to 'eSign', settings_esign_index_path, class: 'text-base hover:bg-base-300' %>
<%= link_to 'Signature', settings_esign_index_path, class: 'text-base hover:bg-base-300' %>
</li>
<li>
<%= link_to 'Team', settings_users_path, class: 'text-base hover:bg-base-300' %>

@ -1,4 +1,37 @@
<p>
Form has been submitted already - thanks!
</p>
<%= 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' } %>
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="space-y-6 mx-auto">
<div class="space-y-6">
<div class="flex items-center justify-center">
<a href="/" class="flex items-center">
<div class="mr-3">
<%= render 'shared/logo', width: '50px', height: '50px' %>
</div>
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
</a>
</div>
<div class="flex items-center bg-base-300 rounded-xl p-4 mb-4">
<div class="flex items-center">
<div class="mr-3">
<svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 19c3.333 -2 5 -4 5 -6c0 -3 -1 -3 -2 -3s-2.032 1.085 -2 3c.034 2.048 1.658 2.877 2.5 4c1.5 2 2.5 2.5 3.5 1c.667 -1 1.167 -1.833 1.5 -2.5c1 2.333 2.333 3.5 4 3.5h2.5" />
<path d="M20 17v-12c0 -1.121 -.879 -2 -2 -2s-2 .879 -2 2v12l2 2l2 -2z" />
<path d="M16 7h4" />
</svg>
</div>
<div>
<p class="text-lg font-bold mb-1"><%= @submitter.submission.template.name %></p>
<p class="text-sm">Signed on <%= l(@submitter.completed_at.to_date, format: :long, locale: @submitter.submission.template.account.locale) %></p>
</div>
</div>
</div>
</div>
<div class="text-center text-2xl font-semibold">
Form has been submitter already
</div>
<div>
<%= button_to button_title(title: 'Send copy to Email', disabled_with: 'Sending', icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'base-button w-full' %>
</div>
</div>
</div>
<%= render 'shared/attribution' %>

@ -10,10 +10,10 @@
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
</a>
<p class="text-xl font-semibold text-center">You have been invited to submit the form</p>
<div class="flex items-center bg-neutral rounded-xl p-4">
<div class="flex items-center bg-base-content/10 rounded-xl p-4">
<div class="flex items-center">
<div class="mr-3">
<svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10 text-gray-100 " width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 19c3.333 -2 5 -4 5 -6c0 -3 -1 -3 -2 -3s-2.032 1.085 -2 3c.034 2.048 1.658 2.877 2.5 4c1.5 2 2.5 2.5 3.5 1c.667 -1 1.167 -1.833 1.5 -2.5c1 2.333 2.333 3.5 4 3.5h2.5" />
<path d="M20 17v-12c0 -1.121 -.879 -2 -2 -2s-2 .879 -2 2v12l2 2l2 -2z" />
@ -21,8 +21,8 @@
</svg>
</div>
<div>
<p class="text-gray-100 font-bold mb-1"><%= @template.name %></p>
<p class="text-sm text-gray-300">Invited by <%= @template.account.name %></p>
<p class="font-bold mb-1"><%= @template.name %></p>
<p class="text-sm">Invited by <%= @template.account.name %></p>
</div>
</div>
</div>

@ -2,9 +2,6 @@
<%= render 'shared/settings_nav' %>
<div class="flex-grow max-w-xl mx-auto">
<h1 class="text-4xl font-bold mb-4">Storage</h1>
<p class="mb-4">
Select files storage option (TODO)
</p>
<% 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 @@
<disable-hidden id="aws_s3" class="block my-4 space-y-4 <%= 'hidden' if value['service'] != 'aws_s3' %>">
<%= render 'aws_form', f:, configs:, value: %>
</disable-hidden>
<disable-hidden id="google" class="block mt-4 space-y-4 mb-8 <%= 'hidden' if value['service'] != 'google' %>">
<disable-hidden id="google" class="block mt-4 space-y-4 mb-6 <%= 'hidden' if value['service'] != 'google' %>">
<%= render 'google_cloud_form', f:, configs:, value: %>
</disable-hidden>
<disable-hidden id="azure" class="block mt-4 space-y-4 mb-8 <%= 'hidden' if value['service'] != 'azure' %>">
<disable-hidden id="azure" class="block mt-4 space-y-4 mb-6 <%= 'hidden' if value['service'] != 'azure' %>">
<%= render 'azure_form', f:, configs:, value: %>
</disable-hidden>
<div class="form-control">

@ -25,7 +25,7 @@
</div>
<% elsif field['type'] == 'date' %>
<div class="flex items-center px-0.5">
<%= l(Date.parse(value)) %>
<%= l(Date.parse(value), format: :long, locale: local_assigns[:locale]) %>
</div>
<% else %>
<div class="flex items-center px-0.5">

@ -1,9 +1,9 @@
<div style="max-width: 1600px" class="mx-auto pl-4">
<div class="flex justify-between py-1.5 items-center pr-4">
<div class="flex space-x-3">
<a href="/"><%= render 'shared/logo' %></a>
<a href="<%= template_path(@submission.template) %>" class="flex space-x-3">
<span><%= render 'shared/logo' %></span>
<span class="text-3xl font-semibold focus:text-clip"><%= @submission.template.name %></span>
</div>
</a>
<div class="space-x-3 flex items-center">
<% if last_submitter = @submission.submitters.select(&:completed_at?).max_by(&:completed_at) %>
<download-button data-src="<%= submitter_download_index_path(last_submitter.slug) %>" class="base-button">
@ -45,7 +45,7 @@
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
<% value = values[field['uuid']] %>
<% next if value.blank? %>
<%= render 'submissions/value', area:, field:, attachments_index:, value: %>
<%= render 'submissions/value', area:, field:, attachments_index:, value:, locale: current_account.locale %>
<% end %>
</div>
</div>
@ -80,7 +80,7 @@
</div>
<% if submitter && !submitter.completed_at? %>
<div class="mt-2 mb-1">
<a class="btn btn-xs btn-primary w-full" target="_blank" href="<%= submit_form_url(slug: submitter.slug) %>">
<a class="btn btn-sm btn-primary w-full" target="_blank" href="<%= submit_form_url(slug: submitter.slug) %>">
Submit Form
</a>
</div>
@ -118,7 +118,7 @@
<% elsif field['type'] == 'checkbox' %>
<%= svg_icon('check', class: 'w-6 h-6') %>
<% elsif field['type'] == 'date' %>
<%= l(Date.parse(value)) %>
<%= l(Date.parse(value), locale: current_account.locale, format: :long) %>
<% else %>
<%= Array.wrap(value).join(', ') %>
<% end %>

@ -12,7 +12,7 @@
<div class="flex items-center bg-base-300 rounded-xl p-4 mb-4">
<div class="flex items-center">
<div class="mr-3">
<svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10 text-gray-700 " width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 19c3.333 -2 5 -4 5 -6c0 -3 -1 -3 -2 -3s-2.032 1.085 -2 3c.034 2.048 1.658 2.877 2.5 4c1.5 2 2.5 2.5 3.5 1c.667 -1 1.167 -1.833 1.5 -2.5c1 2.333 2.333 3.5 4 3.5h2.5" />
<path d="M20 17v-12c0 -1.121 -.879 -2 -2 -2s-2 .879 -2 2v12l2 2l2 -2z" />
@ -20,8 +20,8 @@
</svg>
</div>
<div>
<p class="text-lg text-gray-700 font-bold mb-1"><%= @submitter.submission.template.name %></p>
<p class="text-sm text-gray-500">Signed on <%= l(@submitter.completed_at.to_date, format: :long) %></p>
<p class="text-lg font-bold mb-1"><%= @submitter.submission.template.name %></p>
<p class="text-sm">Signed on <%= l(@submitter.completed_at.to_date, format: :long, locale: @submitter.submission.template.account.locale) %></p>
</div>
</div>
</div>

@ -1,5 +1,5 @@
<% fields_index = Templates.build_field_areas_index(@submitter.submission.template) %>
<% values = @submitter.submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %>
<% values = @submitter.submission.submitters.where.not(id: @submitter.id).reduce({}) { |acc, sub| acc.merge(sub.values) } %>
<% attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments).preload(:blob).index_by(&:uuid) %>
<div class="mx-auto block pb-72" style="max-width: 1000px">
<div class="mt-4 flex">
@ -17,7 +17,7 @@
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
<% value = values[field['uuid']] %>
<% next if value.blank? %>
<%= render 'submissions/value', area:, field:, attachments_index:, value: %>
<%= render 'submissions/value', area:, field:, attachments_index:, value:, locale: @submitter.submission.template.account.locale %>
<% end %>
</div>
</div>
@ -28,7 +28,7 @@
<div class="fixed bottom-0 w-full h-0 z-20">
<div class="mx-auto" style="max-width: 1000px">
<div class="relative md:mx-32">
<div class="shadow-md bg-base-100 absolute bottom-0 md:bottom-4 w-full border border-base-200 p-4 rounded">
<div class="shadow-md bg-base-100 absolute bottom-0 md:bottom-4 w-full border border-base-200 border p-4 rounded">
<submission-form data-submitter-uuid="<%= @submitter.uuid %>" data-submitter-slug="<%= @submitter.slug %>" data-attachments="<%= attachments_index.values.select { |e| e.record_id == @submitter.id }.to_json(only: %i[uuid], methods: %i[url filename content_type]) %>" data-fields="<%= @submitter.submission.template.fields.select { |f| f['submitter_uuid'] == @submitter.uuid }.to_json %>" data-values="<%= @submitter.values.to_json %>" data-authenticity-token="<%= form_authenticity_token %>"></submission-form>
</div>
</div>

@ -16,18 +16,22 @@
<% end %>
</div>
</div>
<div class="flex justify-between mb-4 items-end">
<p class="text-3xl font-bold">Submissions</p>
<div class="flex space-x-2">
<% if @template.submitters.to_a.size == 1 %>
<%= render 'shared/clipboard_copy', text: start_form_url(slug: @template.slug), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Share Link', copied_title: 'Copied to Clipboard' %>
<% end %>
<%= link_to new_template_submission_path(@template), class: 'btn btn-primary text-base', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span class="hidden md:block">Add Recipients</span>
<% end %>
<% if !@pagy.count.zero? || @template.submitters.to_a.size == 1 %>
<div class="flex justify-between mb-4 items-end">
<p class="text-3xl font-bold">Submissions</p>
<div class="flex space-x-2">
<% if @template.submitters.to_a.size == 1 %>
<%= render 'shared/clipboard_copy', text: start_form_url(slug: @template.slug), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Share Link', copied_title: 'Copied to Clipboard' %>
<% end %>
<% unless @pagy.count.zero? %>
<%= link_to new_template_submission_path(@template), class: 'btn btn-primary text-base', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span class="hidden md:block">Add Recipients</span>
<% end %>
<% end %>
</div>
</div>
</div>
<% end %>
<% status_badges = { 'awaiting' => 'badge-info', 'sent' => 'badge-info', 'completed' => 'badge-success', 'opened' => 'badge-warning' } %>
<% if @submissions.present? %>
<div class="space-y-4">
@ -46,9 +50,11 @@
</div>
</div>
<div class="flex space-x-2 items-center">
<span class="badge <%= status_badges[submitter.status] %> w-32 badge-lg btn-sm uppercase text-sm font-medium border-1">
<%= submitter.status %>
</span>
<div class="tooltip flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
<span class="badge <%= status_badges[submitter.status] %> w-32 badge-lg btn-sm uppercase text-sm font-medium border-1">
<%= submitter.status %>
</span>
</div>
<% if submitter.completed_at? %>
<form onsubmit="event.preventDefault()">
<button onclick="event.stopPropagation()">
@ -65,7 +71,7 @@
</button>
</form>
<% else %>
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submission.submitters.first.slug), class: 'btn btn-sm btn-neutral text-white w-36', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Link' %>
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submission.submitters.first.slug), class: 'btn btn-sm btn-neutral text-white w-36 flex', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Link' %>
<% end %>
<%= button_to submitter.completed_at? ? 'Archive' : 'Remove', submission_path(submission), class: 'btn btn-outline btn-sm w-28', title: 'Delete', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %>
</div>
@ -82,10 +88,12 @@
</span>
<% unless submission.submitters.all?(&:completed_at?) %>
<div class="flex items-center space-x-3">
<span class="badge w-24 <%= status_badges[submitter.status] %> btn-xs uppercase text-xs font-medium border-1">
<%= submitter.status %>
</span>
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'btn btn-xs text-xs btn-neutral text-white w-32', icon_class: 'w-4 h-4 text-white', copy_title: 'Copy Link' %>
<div class="tooltip flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
<span class="badge w-24 <%= status_badges[submitter.status] %> btn-xs uppercase text-xs font-medium border-1">
<%= submitter.status %>
</span>
</div>
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'btn btn-xs text-xs btn-neutral text-white w-32 flex', icon_class: 'w-4 h-4 text-white', copy_title: 'Copy Link' %>
</div>
<% end %>
</div>
@ -93,9 +101,11 @@
</div>
<div class="flex space-x-2 items-center">
<% if submission.submitters.all?(&:completed_at?) %>
<span class="badge <%= status_badges[submitter.status] %> w-32 badge-lg btn-sm uppercase text-sm font-medium border-1">
<%= submitter.status %>
</span>
<div class="tooltip flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
<span class="badge <%= status_badges[submitter.status] %> w-32 badge-lg btn-sm uppercase text-sm font-medium border-1">
<%= submitter.status %>
</span>
</div>
<form onsubmit="event.preventDefault()">
<button onclick="event.stopPropagation()">
<download-button data-src="<%= submitter_download_index_path(submission.submitters.select(&:completed_at?).max_by(&:completed_at).slug) %>" class="btn btn-sm btn-neutral text-white w-36">
@ -117,7 +127,18 @@
</a>
<% end %>
</div>
<%= render 'shared/pagination', pagy: @pagy, items_name: 'Submissions' %>
<%= render 'shared/pagination', pagy: @pagy, items_name: 'submissions' %>
<% else %>
<%= render 'shared/no_data_banner' %>
<div class="card bg-base-200">
<div class="card-body text-center py-16">
<div class="max-w-md mx-auto">
<p class="text-3xl font-bold text-base-content mb-4">There are no Submissions yet</p>
<p class="text-gray-600">Send an invitation to fill and submit the documents via email</p>
<%= 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') %>
<span class="hidden md:block">Add Recipients</span>
<% end %>
</div>
</div>
</div>
<% end %>

@ -17,7 +17,7 @@
<%= f.password_field :password, required: user.new_record?, class: 'base-input' %>
</div>
</div>
<div class="form-control">
<div class="form-control pt-2">
<%= f.button button_title, class: 'base-button' %>
</div>
<% end %>

@ -43,7 +43,7 @@
</span>
</td>
<td>
<%= user.last_sign_in_at ? l(user.last_sign_in_at, format: :short) : '-' %>
<%= user.last_sign_in_at ? l(user.last_sign_in_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) : '-' %>
</td>
<td class="flex items-center space-x-2 justify-end">
<%= link_to edit_user_path(user), class: 'btn btn-outline btn-xs', title: 'Edit', data: { turbo_frame: 'modal' } do %>
@ -58,6 +58,6 @@
</tbody>
</table>
</div>
<%= render 'shared/pagination', pagy: @pagy, items_name: 'Users' %>
<%= render 'shared/pagination', pagy: @pagy, items_name: 'users' %>
</div>
</div>

@ -20,6 +20,9 @@ module DocuSeal
config.active_storage.routes_prefix = ''
config.i18n.available_locales = %i[en en-US en-GB es-ES pt-PT de-DE]
config.i18n.fallbacks = [:en]
config.action_view.frozen_string_literal = true
config.middleware.insert_before ActionDispatch::Static, Rack::Deflater

@ -6,7 +6,7 @@ if ENV['RAILS_ENV'] == 'production' && ENV['SECRET_KEY_BASE'].to_s.empty?
require 'dotenv'
require 'securerandom'
dotenv_path = './docuseal.env'
dotenv_path = "#{ENV.fetch('WORKDIR', '.')}/docuseal.env"
unless File.exist?(dotenv_path)
default_env = <<~TEXT

@ -56,6 +56,7 @@ Rails.application.routes.draw do
resources :email, only: %i[index create], controller: 'email_settings'
resources :esign, only: %i[index create], controller: 'esign_settings'
resources :users, only: %i[index]
resource :account, only: %i[show update]
resources :profile, only: %i[index] do
collection do
patch :update_contact

@ -4,6 +4,8 @@ class CreateAccounts < ActiveRecord::Migration[7.0]
def change
create_table :accounts do |t|
t.string :name, null: false
t.string :timezone, null: false
t.string :locale, null: false
t.timestamps
end

@ -16,6 +16,8 @@ ActiveRecord::Schema[7.0].define(version: 2023_06_12_182744) do
create_table "accounts", force: :cascade do |t|
t.string "name", null: false
t.string "timezone", null: false
t.string "locale", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end

@ -0,0 +1,11 @@
# frozen_string_literal: true
module Accounts
module_function
def normalize_timezone(timezone)
tzinfo = TZInfo::Timezone.get(ActiveSupport::TimeZone::MAPPING[timezone] || timezone)
::ActiveSupport::TimeZone.all.find { |e| e.tzinfo == tzinfo }&.name || timezone
end
end

@ -135,7 +135,7 @@ module Submissions
height - (area['y'] * height))
end
else
value = I18n.l(Date.parse(value)) if field['type'] == 'date'
value = I18n.l(Date.parse(value), format: :long) if field['type'] == 'date'
text = HexaPDF::Layout::TextFragment.create(Array.wrap(value).join(', '), font: pdf.fonts.add(FONT_NAME),
font_size: FONT_SIZE)

@ -8,6 +8,6 @@ module.exports = {
'./app/views/start_form/**/*.erb',
'./app/views/shared/_button_title.html.erb',
'./app/views/shared/_attribution.html.erb',
'./app/views/send_submission_copy/**/*.erb'
'./app/views/send_submission_email/**/*.erb'
]
}

Loading…
Cancel
Save