Compare commits

...
10 Commits
38 changed files with 809 additions and 306 deletions

No files matched your search

+3
View File
@@ -16,6 +16,8 @@ gem 'image_processing'
gem 'lograge' gem 'lograge'
gem 'mysql2', require: false gem 'mysql2', require: false
gem 'oj' gem 'oj'
gem 'omniauth-google-oauth2'
gem 'omniauth-rails_csrf_protection'
gem 'pagy' gem 'pagy'
gem 'pg', require: false gem 'pg', require: false
gem 'premailer-rails' gem 'premailer-rails'
@@ -25,6 +27,7 @@ gem 'rails-i18n'
gem 'rollbar', require: ENV.key?('ROLLBAR_ACCESS_TOKEN') gem 'rollbar', require: ENV.key?('ROLLBAR_ACCESS_TOKEN')
gem 'ruby-vips' gem 'ruby-vips'
gem 'shakapacker' gem 'shakapacker'
gem 'sidekiq', require: ENV.key?('REDIS_URL')
gem 'sqlite3', require: false gem 'sqlite3', require: false
gem 'strip_attributes' gem 'strip_attributes'
gem 'turbo-rails' gem 'turbo-rails'
+40
View File
@@ -232,6 +232,7 @@ GEM
os (>= 0.9, < 2.0) os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a) signet (>= 0.16, < 2.a)
hashdiff (1.0.1) hashdiff (1.0.1)
hashie (5.0.0)
hexapdf (0.32.2) hexapdf (0.32.2)
cmdparse (~> 3.0, >= 3.0.3) cmdparse (~> 3.0, >= 3.0.3)
geom2d (~> 0.3) geom2d (~> 0.3)
@@ -282,6 +283,7 @@ GEM
minitest (5.18.1) minitest (5.18.1)
msgpack (1.7.1) msgpack (1.7.1)
multi_json (1.15.0) multi_json (1.15.0)
multi_xml (0.6.0)
multipart-post (2.3.0) multipart-post (2.3.0)
mysql2 (0.5.5) mysql2 (0.5.5)
net-http-persistent (4.0.2) net-http-persistent (4.0.2)
@@ -298,7 +300,29 @@ GEM
nio4r (2.5.9) nio4r (2.5.9)
nokogiri (1.15.2-arm64-darwin) nokogiri (1.15.2-arm64-darwin)
racc (~> 1.4) racc (~> 1.4)
oauth2 (2.0.9)
faraday (>= 0.17.3, < 3.0)
jwt (>= 1.0, < 3.0)
multi_xml (~> 0.5)
rack (>= 1.2, < 4)
snaky_hash (~> 2.0)
version_gem (~> 1.1)
oj (3.15.0) oj (3.15.0)
omniauth (2.1.1)
hashie (>= 3.4.6)
rack (>= 2.2.3)
rack-protection
omniauth-google-oauth2 (1.1.1)
jwt (>= 2.0)
oauth2 (~> 2.0.6)
omniauth (~> 2.0)
omniauth-oauth2 (~> 1.8.0)
omniauth-oauth2 (1.8.0)
oauth2 (>= 1.4, < 3)
omniauth (~> 2.0)
omniauth-rails_csrf_protection (1.0.1)
actionpack (>= 4.2)
omniauth (~> 2.0)
openssl (3.1.0) openssl (3.1.0)
orm_adapter (0.5.0) orm_adapter (0.5.0)
os (1.1.4) os (1.1.4)
@@ -326,6 +350,8 @@ GEM
nio4r (~> 2.0) nio4r (~> 2.0)
racc (1.7.1) racc (1.7.1)
rack (2.2.7) rack (2.2.7)
rack-protection (3.0.6)
rack
rack-proxy (0.7.6) rack-proxy (0.7.6)
rack rack
rack-test (2.1.0) rack-test (2.1.0)
@@ -362,6 +388,8 @@ GEM
zeitwerk (~> 2.5) zeitwerk (~> 2.5)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.0.6) rake (13.0.6)
redis-client (0.14.1)
connection_pool
regexp_parser (2.8.1) regexp_parser (2.8.1)
reline (0.3.5) reline (0.3.5)
io-console (~> 0.5) io-console (~> 0.5)
@@ -432,6 +460,11 @@ GEM
rack-proxy (>= 0.6.1) rack-proxy (>= 0.6.1)
railties (>= 5.2) railties (>= 5.2)
semantic_range (>= 2.3.0) semantic_range (>= 2.3.0)
sidekiq (7.1.2)
concurrent-ruby (< 2)
connection_pool (>= 2.3.0)
rack (>= 2.2.4)
redis-client (>= 0.14.0)
signet (0.17.0) signet (0.17.0)
addressable (~> 2.8) addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a) faraday (>= 0.17.5, < 3.a)
@@ -444,6 +477,9 @@ GEM
simplecov-html (0.12.3) simplecov-html (0.12.3)
simplecov_json_formatter (0.1.4) simplecov_json_formatter (0.1.4)
smart_properties (1.17.0) smart_properties (1.17.0)
snaky_hash (2.0.1)
hashie
version_gem (~> 1.1, >= 1.1.1)
sqlite3 (1.5.4) sqlite3 (1.5.4)
mini_portile2 (~> 2.8.0) mini_portile2 (~> 2.8.0)
strip_attributes (1.13.0) strip_attributes (1.13.0)
@@ -462,6 +498,7 @@ GEM
uber (0.1.0) uber (0.1.0)
unicode-display_width (2.4.2) unicode-display_width (2.4.2)
uniform_notifier (1.16.0) uniform_notifier (1.16.0)
version_gem (1.1.3)
warden (1.2.9) warden (1.2.9)
rack (>= 2.0.9) rack (>= 2.0.9)
web-console (4.2.0) web-console (4.2.0)
@@ -507,6 +544,8 @@ DEPENDENCIES
lograge lograge
mysql2 mysql2
oj oj
omniauth-google-oauth2
omniauth-rails_csrf_protection
pagy pagy
pg pg
premailer-rails premailer-rails
@@ -522,6 +561,7 @@ DEPENDENCIES
rubocop-rspec rubocop-rspec
ruby-vips ruby-vips
shakapacker shakapacker
sidekiq
simplecov simplecov
sqlite3 sqlite3
strip_attributes strip_attributes
+2
View File
@@ -40,6 +40,8 @@ class AccountsController < ApplicationController
end end
def app_url_params def app_url_params
return {} if params[:encrypted_config].blank?
params.require(:encrypted_config).permit(:value) params.require(:encrypted_config).permit(:value)
end end
end end
@@ -3,6 +3,8 @@
module Api module Api
class TemplatesDocumentsController < ApiBaseController class TemplatesDocumentsController < ApiBaseController
def create def create
return head :unprocessable_entity if params[:blobs].blank? && params[:files].blank?
@template = current_account.templates.find(params[:template_id]) @template = current_account.templates.find(params[:template_id])
documents = documents =
+1 -1
View File
@@ -23,7 +23,7 @@ class ApplicationController < ActionController::Base
private private
def sign_in_for_demo def sign_in_for_demo
sign_in(User.first) unless signed_in? sign_in(User.order('random()').take) unless signed_in?
end end
def current_account def current_account
+1
View File
@@ -4,6 +4,7 @@ class DashboardController < ApplicationController
skip_before_action :authenticate_user!, only: %i[index] skip_before_action :authenticate_user!, only: %i[index]
def index def index
return redirect_to Docuseal::PRODUCT_URL, allow_other_host: true if Docuseal.multitenant? && !signed_in?
return render 'pages/landing' unless signed_in? return render 'pages/landing' unless signed_in?
templates = current_account.templates.active.preload(:author).order(id: :desc) templates = current_account.templates.active.preload(:author).order(id: :desc)
@@ -0,0 +1,16 @@
# frozen_string_literal: true
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
def google_oauth2
@user = Users.from_omniauth(request.env['omniauth.auth'])
if @user.persisted?
flash[:notice] = I18n.t('devise.omniauth_callbacks.success', kind: 'Google')
sign_in_and_redirect @user, event: :authentication
else
redirect_to new_registration_path(oauth_callback: true, user: @user.slice(:email, :first_name, :last_name)),
notice: 'Please complete registration with Google auth'
end
end
end
+22 -2
View File
@@ -1,24 +1,44 @@
# frozen_string_literal: true # frozen_string_literal: true
class RegistrationsController < Devise::RegistrationsController class RegistrationsController < Devise::RegistrationsController
prepend_before_action :require_no_authentication, only: [:show]
def show; end
def create
super
Accounts.create_default_template(resource.account) if resource.account.persisted?
end
private private
def require_no_authentication
super
flash.clear
end
def build_resource(_hash = {}) def build_resource(_hash = {})
account = Account.new(account_params) account = Account.new(account_params)
account.timezone = Accounts.normalize_timezone(account.timezone) account.timezone = Accounts.normalize_timezone(account.timezone)
self.resource = account.users.new(user_params) self.resource = account.users.new(user_params)
account.name ||= "#{resource.full_name}'s Company" if params[:action] == 'create'
end end
def user_params def user_params
return {} if params[:user].blank? return {} if params[:user].blank?
params.require(:user).permit(:first_name, :last_name, :email, :password) params.require(:user).permit(:first_name, :last_name, :email, :password).compact_blank.tap do |attrs|
attrs[:password] ||= SecureRandom.hex if params[:action] == 'create'
end
end end
def account_params def account_params
return {} if params[:account].blank? return {} if params[:account].blank?
params.require(:account).permit(:name, :timezone) params.require(:account).permit(:name, :timezone).compact_blank
end end
end end
+20
View File
@@ -0,0 +1,20 @@
# frozen_string_literal: true
class SessionsController < Devise::SessionsController
def create
if Docuseal.multitenant? && !User.exists?(email: sign_in_params[:email])
return redirect_to new_registration_path(sign_up: true, user: sign_in_params.slice(:email)),
notice: 'Create a new account'
end
super
end
private
def require_no_authentication
super
flash.clear
end
end
+1
View File
@@ -14,6 +14,7 @@ window.customElements.define('submission-form', class extends HTMLElement {
authenticityToken: this.dataset.authenticityToken, authenticityToken: this.dataset.authenticityToken,
canSendEmail: this.dataset.canSendEmail === 'true', canSendEmail: this.dataset.canSendEmail === 'true',
isDirectUpload: this.dataset.isDirectUpload === 'true', isDirectUpload: this.dataset.isDirectUpload === 'true',
isDemo: this.dataset.isDemo === 'true',
values: reactive(JSON.parse(this.dataset.values)), values: reactive(JSON.parse(this.dataset.values)),
attachments: reactive(JSON.parse(this.dataset.attachments)), attachments: reactive(JSON.parse(this.dataset.attachments)),
fields: JSON.parse(this.dataset.fields) fields: JSON.parse(this.dataset.fields)
+19 -2
View File
@@ -12,7 +12,7 @@
</p> </p>
<div class="space-y-3 mt-5"> <div class="space-y-3 mt-5">
<button <button
v-if="canSendEmail" v-if="canSendEmail && !isDemo"
class="white-button flex items-center space-x-1 w-full" class="white-button flex items-center space-x-1 w-full"
:disabled="isSendingCopy" :disabled="isSendingCopy"
@click.prevent="sendCopyToEmail" @click.prevent="sendCopyToEmail"
@@ -40,6 +40,17 @@
Download Download
</span> </span>
</button> </button>
<a
v-if="isDemo"
target="_blank"
href="https://github.com/docusealco/docuseal"
class="white-button flex items-center space-x-1 w-full"
>
<IconBrandGithub />
<span>
Star on Github
</span>
</a>
</div> </div>
<div class="text-center mt-4"> <div class="text-center mt-4">
Signed with Signed with
@@ -53,13 +64,14 @@
</template> </template>
<script> <script>
import { IconCircleCheck, IconMail, IconDownload, IconInnerShadowTop } from '@tabler/icons-vue' import { IconCircleCheck, IconBrandGithub, IconMail, IconDownload, IconInnerShadowTop } from '@tabler/icons-vue'
export default { export default {
name: 'FormCompleted', name: 'FormCompleted',
components: { components: {
IconCircleCheck, IconCircleCheck,
IconInnerShadowTop, IconInnerShadowTop,
IconBrandGithub,
IconMail, IconMail,
IconDownload IconDownload
}, },
@@ -68,6 +80,11 @@ export default {
type: String, type: String,
required: true required: true
}, },
isDemo: {
type: Boolean,
required: false,
default: false
},
canSendEmail: { canSendEmail: {
type: Boolean, type: Boolean,
required: false, required: false,
+268 -228
View File
@@ -7,258 +7,290 @@
:current-step="currentStepFields" :current-step="currentStepFields"
@focus-step="[saveStep(), goToStep($event, false, true)]" @focus-step="[saveStep(), goToStep($event, false, true)]"
/> />
<div> <button
<form v-if="!isFormVisible"
class="btn btn-neutral text-white absolute rounded-none border-x-0 md:border md:rounded-full bottom-0 w-full md:mb-4 text-base"
@click.prevent="isFormVisible = true"
>
Submit Form
<IconArrowsDiagonal
class="absolute right-0 mr-4"
:width="20"
:height="20"
/>
</button>
<div
v-else
id="form_container"
class="shadow-md bg-base-100 absolute bottom-0 md:bottom-4 w-full border-base-200 border p-4 rounded"
>
<button
v-if="!isCompleted" v-if="!isCompleted"
ref="form" class="absolute right-0 mr-2 mt-2 top-0 hidden md:block"
:action="submitPath" title="Minimize"
method="post" @click.prevent="isFormVisible = false"
class="md:mx-16"
@submit.prevent="submitStep"
> >
<input <IconArrowsDiagonalMinimize2
type="hidden" :width="20"
name="authenticity_token" :height="20"
:value="authenticityToken" />
</button>
<div>
<form
v-if="!isCompleted"
ref="form"
:action="submitPath"
method="post"
class="md:mx-16"
@submit.prevent="submitStep"
> >
<input <input
value="put" type="hidden"
name="_method" name="authenticity_token"
type="hidden" :value="authenticityToken"
> >
<div class="md:mt-4"> <input
<div v-if="['cells', 'text'].includes(currentField.type)"> value="put"
<label name="_method"
v-if="currentField.name" type="hidden"
:for="currentField.uuid" >
class="label text-2xl mb-2" <div class="md:mt-4">
>{{ currentField.name }} <div v-if="['cells', 'text'].includes(currentField.type)">
<template v-if="!currentField.required">(optional)</template> <label
</label> v-if="currentField.name"
<div :for="currentField.uuid"
v-else class="label text-2xl mb-2"
class="py-1" >{{ currentField.name }}
/> <template v-if="!currentField.required">(optional)</template>
<div> </label>
<input <div
v-else
class="py-1"
/>
<div>
<input
:id="currentField.uuid"
v-model="values[currentField.uuid]"
class="base-input !text-2xl w-full"
:required="currentField.required"
:placeholder="`Type here...${currentField.required ? '' : ' (optional)'}`"
type="text"
:name="`values[${currentField.uuid}]`"
@focus="$refs.areas.scrollIntoField(currentField)"
>
</div>
</div>
<div v-else-if="currentField.type === 'date'">
<label
v-if="currentField.name"
:for="currentField.uuid"
class="label text-2xl mb-2"
>{{ currentField.name }}
<template v-if="!currentField.required">(optional)</template>
</label>
<div
v-else
class="py-1"
/>
<div class="text-center">
<input
:id="currentField.uuid"
v-model="values[currentField.uuid]"
class="base-input !text-2xl text-center w-full"
:required="currentField.required"
type="date"
:name="`values[${currentField.uuid}]`"
@focus="$refs.areas.scrollIntoField(currentField)"
>
</div>
</div>
<div v-else-if="currentField.type === 'select'">
<label
v-if="currentField.name"
:for="currentField.uuid"
class="label text-2xl mb-2"
>{{ currentField.name }}
<template v-if="!currentField.required">(optional)</template>
</label>
<div
v-else
class="py-1"
/>
<select
:id="currentField.uuid" :id="currentField.uuid"
v-model="values[currentField.uuid]"
class="base-input !text-2xl w-full"
:required="currentField.required" :required="currentField.required"
:placeholder="`Type here...${currentField.required ? '' : ' (optional)'}`" class="select base-input !text-2xl w-full text-center font-normal"
type="text"
:name="`values[${currentField.uuid}]`" :name="`values[${currentField.uuid}]`"
@change="values[currentField.uuid] = $event.target.value"
@focus="$refs.areas.scrollIntoField(currentField)" @focus="$refs.areas.scrollIntoField(currentField)"
> >
</div> <option
</div> value=""
<div v-else-if="currentField.type === 'date'"> :selected="!values[currentField.uuid]"
<label >
v-if="currentField.name" Select your option
:for="currentField.uuid" </option>
class="label text-2xl mb-2" <option
>{{ currentField.name }}
<template v-if="!currentField.required">(optional)</template>
</label>
<div
v-else
class="py-1"
/>
<div class="text-center">
<input
:id="currentField.uuid"
v-model="values[currentField.uuid]"
class="base-input !text-2xl text-center w-full"
:required="currentField.required"
type="date"
:name="`values[${currentField.uuid}]`"
@focus="$refs.areas.scrollIntoField(currentField)"
>
</div>
</div>
<div v-else-if="currentField.type === 'select'">
<label
v-if="currentField.name"
:for="currentField.uuid"
class="label text-2xl mb-2"
>{{ currentField.name }}
<template v-if="!currentField.required">(optional)</template>
</label>
<div
v-else
class="py-1"
/>
<select
:id="currentField.uuid"
:required="currentField.required"
class="select base-input !text-2xl w-full text-center font-normal"
:name="`values[${currentField.uuid}]`"
@change="values[currentField.uuid] = $event.target.value"
@focus="$refs.areas.scrollIntoField(currentField)"
>
<option
value=""
:selected="!values[currentField.uuid]"
>
Select your option
</option>
<option
v-for="(option, index) in currentField.options"
:key="index"
:selected="values[currentField.uuid] == option"
:value="option"
>
{{ option }}
</option>
</select>
</div>
<div v-else-if="currentField.type === 'radio'">
<label
v-if="currentField.name"
:for="currentField.uuid"
class="label text-2xl mb-2"
>{{ currentField.name }}
<template v-if="!currentField.required">(optional)</template>
</label>
<div class="flex w-full">
<div class="space-y-3.5 mx-auto">
<div
v-for="(option, index) in currentField.options" v-for="(option, index) in currentField.options"
:key="index" :key="index"
:selected="values[currentField.uuid] == option"
:value="option"
>
{{ option }}
</option>
</select>
</div>
<div v-else-if="currentField.type === 'radio'">
<label
v-if="currentField.name"
:for="currentField.uuid"
class="label text-2xl mb-2"
>{{ currentField.name }}
<template v-if="!currentField.required">(optional)</template>
</label>
<div class="flex w-full">
<div class="space-y-3.5 mx-auto">
<div
v-for="(option, index) in currentField.options"
:key="index"
>
<label
:for="currentField.uuid + option"
class="flex items-center space-x-3"
>
<input
:id="currentField.uuid + option"
v-model="values[currentField.uuid]"
type="radio"
class="base-radio !h-7 !w-7"
:name="`values[${currentField.uuid}]`"
:value="option"
:required="currentField.required"
>
<span class="text-xl">
{{ option }}
</span>
</label>
</div>
</div>
</div>
</div>
<MultiSelectStep
v-else-if="currentField.type === 'multiple'"
v-model="values[currentField.uuid]"
:field="currentField"
/>
<div
v-else-if="currentField.type === 'checkbox'"
class="flex w-full"
>
<input
type="hidden"
name="cast_boolean"
value="true"
>
<div
class="space-y-3.5 mx-auto"
>
<div
v-for="(field, index) in currentStepFields"
:key="field.uuid"
> >
<label <label
:for="currentField.uuid + option" :for="field.uuid"
class="flex items-center space-x-3" class="flex items-center space-x-3"
> >
<input <input
:id="currentField.uuid + option" type="hidden"
v-model="values[currentField.uuid]" :name="`values[${field.uuid}]`"
type="radio" :value="!!values[field.uuid]"
class="base-radio !h-7 !w-7" >
:name="`values[${currentField.uuid}]`" <input
:value="option" :id="field.uuid"
:required="currentField.required" type="checkbox"
class="base-checkbox !h-7 !w-7"
:checked="!!values[field.uuid]"
@click="[$refs.areas.scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
> >
<span class="text-xl"> <span class="text-xl">
{{ option }} {{ currentField.name || currentField.type + ' ' + (index + 1) }}
</span> </span>
</label> </label>
</div> </div>
</div> </div>
</div> </div>
<ImageStep
v-else-if="currentField.type === 'image'"
v-model="values[currentField.uuid]"
:field="currentField"
:is-direct-upload="isDirectUpload"
:attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug"
@attached="[attachments.push($event), $refs.areas.scrollIntoField(currentField)]"
/>
<SignatureStep
v-else-if="currentField.type === 'signature'"
ref="currentStep"
v-model="values[currentField.uuid]"
:field="currentField"
:is-direct-upload="isDirectUpload"
:attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug"
@attached="attachments.push($event)"
@start="$refs.areas.scrollIntoField(currentField)"
@minimize="isFormVisible = false"
/>
<AttachmentStep
v-else-if="currentField.type === 'file'"
v-model="values[currentField.uuid]"
:is-direct-upload="isDirectUpload"
:field="currentField"
:attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug"
@attached="[attachments.push($event), $refs.areas.scrollIntoField(currentField)]"
/>
</div> </div>
<MultiSelectStep <div class="mt-6 md:mt-8">
v-else-if="currentField.type === 'multiple'" <button
v-model="values[currentField.uuid]" type="submit"
:field="currentField" class="base-button w-full flex justify-center"
/> :disabled="isButtonDisabled"
<div
v-else-if="currentField.type === 'checkbox'"
class="flex w-full"
>
<input
type="hidden"
name="cast_boolean"
value="true"
> >
<div <span class="flex">
class="space-y-3.5 mx-auto" <IconInnerShadowTop
> v-if="isSubmitting"
<div class="mr-1 animate-spin"
v-for="(field, index) in currentStepFields" />
:key="field.uuid" <span v-if="stepFields.length === currentStep + 1">
> Submit
<label </span>
:for="field.uuid" <span v-else>
class="flex items-center space-x-3" Next
> </span><span
<input v-if="isSubmitting"
type="hidden" class="w-6 flex justify-start mr-1"
:name="`values[${field.uuid}]`" ><span>...</span></span>
:value="!!values[field.uuid]"
>
<input
:id="field.uuid"
type="checkbox"
class="base-checkbox !h-7 !w-7"
:checked="!!values[field.uuid]"
@click="[$refs.areas.scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
>
<span class="text-xl">
{{ currentField.name || currentField.type + ' ' + (index + 1) }}
</span>
</label>
</div>
</div>
</div>
<ImageStep
v-else-if="currentField.type === 'image'"
v-model="values[currentField.uuid]"
:field="currentField"
:is-direct-upload="isDirectUpload"
:attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug"
@attached="[attachments.push($event), $refs.areas.scrollIntoField(currentField)]"
/>
<SignatureStep
v-else-if="currentField.type === 'signature'"
ref="currentStep"
v-model="values[currentField.uuid]"
:field="currentField"
:is-direct-upload="isDirectUpload"
:attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug"
@attached="attachments.push($event)"
/>
<AttachmentStep
v-else-if="currentField.type === 'file'"
v-model="values[currentField.uuid]"
:is-direct-upload="isDirectUpload"
:field="currentField"
:attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug"
@attached="[attachments.push($event), $refs.areas.scrollIntoField(currentField)]"
/>
</div>
<div class="mt-6 md:mt-8">
<button
type="submit"
class="base-button w-full flex justify-center"
:disabled="isButtonDisabled"
>
<span class="flex">
<IconInnerShadowTop
v-if="isSubmitting"
class="mr-1 animate-spin"
/>
<span v-if="stepFields.length === currentStep + 1">
Submit
</span> </span>
<span v-else> </button>
Next </div>
</span><span </form>
v-if="isSubmitting" <FormCompleted
class="w-6 flex justify-start mr-1" v-else
><span>...</span></span> :is-demo="isDemo"
</span> :can-send-email="canSendEmail"
</button> :submitter-slug="submitterSlug"
</div> />
</form> <div class="flex justify-center">
<FormCompleted <div class="flex items-center mt-5 mb-1">
v-else <a
:can-send-email="canSendEmail" v-for="(step, index) in stepFields"
:submitter-slug="submitterSlug" :key="step[0].uuid"
/> href="#"
<div class="flex justify-center"> class="inline border border-base-300 h-3 w-3 rounded-full mx-1"
<div class="flex items-center mt-5 mb-1"> :class="{ 'bg-base-300': index === currentStep, 'bg-base-content': index < currentStep || isCompleted, 'bg-white': index > currentStep }"
<a @click.prevent="isCompleted ? '' : [saveStep(), goToStep(step, true)]"
v-for="(step, index) in stepFields" />
:key="step[0].uuid" </div>
href="#"
class="inline border border-base-300 h-3 w-3 rounded-full mx-1"
:class="{ 'bg-base-300': index === currentStep, 'bg-base-content': index < currentStep || isCompleted, 'bg-white': index > currentStep }"
@click.prevent="isCompleted ? '' : [saveStep(), goToStep(step, true)]"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -271,7 +303,7 @@ import SignatureStep from './signature_step'
import AttachmentStep from './attachment_step' import AttachmentStep from './attachment_step'
import MultiSelectStep from './multi_select_step' import MultiSelectStep from './multi_select_step'
import FormCompleted from './completed' import FormCompleted from './completed'
import { IconInnerShadowTop } from '@tabler/icons-vue' import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
export default { export default {
name: 'SubmissionForm', name: 'SubmissionForm',
@@ -282,6 +314,8 @@ export default {
AttachmentStep, AttachmentStep,
MultiSelectStep, MultiSelectStep,
IconInnerShadowTop, IconInnerShadowTop,
IconArrowsDiagonal,
IconArrowsDiagonalMinimize2,
FormCompleted FormCompleted
}, },
props: { props: {
@@ -317,6 +351,11 @@ export default {
required: true, required: true,
default: false default: false
}, },
isDemo: {
type: Boolean,
required: true,
default: false
},
values: { values: {
type: Object, type: Object,
required: false, required: false,
@@ -326,6 +365,7 @@ export default {
data () { data () {
return { return {
isCompleted: false, isCompleted: false,
isFormVisible: true,
currentStep: 0, currentStep: 0,
isSubmitting: false isSubmitting: false
} }
+206 -23
View File
@@ -4,22 +4,62 @@
<label <label
class="label text-2xl" class="label text-2xl"
>{{ field.name || 'Signature' }}</label> >{{ field.name || 'Signature' }}</label>
<button <div class="space-x-2 flex">
v-if="modelValue" <span
class="btn btn-outline btn-sm" class="tooltip"
@click.prevent="remove" data-tip="Type text"
> >
<IconReload :width="16" /> <button
Redraw class="btn btn-sm btn-circle"
</button> :class="{ 'btn-neutral': isTextSignature, 'btn-outline': !isTextSignature }"
<button @click.prevent="toggleTextInput"
v-else >
class="btn btn-outline btn-sm" <IconTextSize :width="16" />
@click.prevent="clear" </button>
> </span>
<IconReload :width="16" /> <span
Clear class="tooltip"
</button> data-tip="Take photo"
>
<label
class="btn btn-outline btn-sm btn-circle"
>
<IconCamera :width="16" />
<input
type="file"
hidden
accept="image/*"
@change="drawImage"
>
</label>
</span>
<button
v-if="modelValue"
class="btn btn-outline btn-sm"
@click.prevent="remove"
>
<IconReload :width="16" />
Redraw
</button>
<button
v-else
class="btn btn-outline btn-sm"
@click.prevent="clear"
>
<IconReload :width="16" />
Clear
</button>
<button
title="Minimize"
class="py-1.5 inline md:hidden"
@click.prevent="$emit('minimize')"
>
<IconArrowsDiagonalMinimize2
:width="20"
:height="20"
/>
</button>
</div>
</div> </div>
<input <input
:value="modelValue" :value="modelValue"
@@ -29,23 +69,35 @@
<img <img
v-if="modelValue" v-if="modelValue"
:src="attachmentsIndex[modelValue].url" :src="attachmentsIndex[modelValue].url"
class="w-full bg-white border border-base-300 rounded" class="mx-auto bg-white border border-base-300 rounded max-h-72"
> >
<canvas <canvas
v-show="!modelValue" v-show="!modelValue"
ref="canvas" ref="canvas"
class="bg-white border border-base-300 rounded" class="bg-white border border-base-300 rounded"
/> />
<input
v-if="isTextSignature"
ref="textInput"
class="base-input !text-2xl w-full mt-6"
:required="field.required"
:placeholder="`Type signature here...`"
type="text"
@input="updateWrittenSignature"
>
</div> </div>
</template> </template>
<script> <script>
import { IconReload } from '@tabler/icons-vue' import { IconReload, IconCamera, IconTextSize, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
export default { export default {
name: 'SignatureStep', name: 'SignatureStep',
components: { components: {
IconReload IconReload,
IconCamera,
IconTextSize,
IconArrowsDiagonalMinimize2
}, },
props: { props: {
field: { field: {
@@ -72,10 +124,11 @@ export default {
default: '' default: ''
} }
}, },
emits: ['attached', 'update:model-value'], emits: ['attached', 'update:model-value', 'start', 'minimize'],
data () { data () {
return { return {
isSignatureStarted: false isSignatureStarted: false,
isTextSignature: false
} }
}, },
async mounted () { async mounted () {
@@ -94,6 +147,8 @@ export default {
this.pad.addEventListener('beginStroke', () => { this.pad.addEventListener('beginStroke', () => {
this.isSignatureStarted = true this.isSignatureStarted = true
this.$emit('start')
}) })
}, },
methods: { methods: {
@@ -104,6 +159,134 @@ export default {
this.pad.clear() this.pad.clear()
this.isSignatureStarted = false this.isSignatureStarted = false
if (this.$refs.textInput) {
this.$refs.textInput.value = ''
}
},
updateWrittenSignature (e) {
this.isSignatureStarted = true
const canvas = this.$refs.canvas
const context = canvas.getContext('2d')
const fontFamily = 'Arial'
const fontSize = '44px'
const fontStyle = 'italic'
const fontWeight = ''
context.font = fontStyle + ' ' + fontWeight + ' ' + fontSize + ' ' + fontFamily
context.textAlign = 'center'
context.clearRect(0, 0, canvas.width, canvas.height)
context.fillText(e.target.value, canvas.width / 2, canvas.height / 2 + 11)
},
toggleTextInput () {
this.remove()
this.isTextSignature = !this.isTextSignature
if (this.isTextSignature) {
this.$nextTick(() => {
this.$refs.textInput.focus()
this.$emit('start')
})
}
},
drawImage (event) {
this.remove()
this.isSignatureStarted = true
const file = event.target.files[0]
if (file && file.type.match('image.*')) {
const reader = new FileReader()
reader.onload = (event) => {
const img = new Image()
img.src = event.target.result
img.onload = () => {
const canvas = this.$refs.canvas
const context = canvas.getContext('2d')
const aspectRatio = img.width / img.height
let targetWidth = canvas.width
let targetHeight = canvas.height
if (canvas.width / canvas.height > aspectRatio) {
targetWidth = canvas.height * aspectRatio
} else {
targetHeight = canvas.width / aspectRatio
}
if (targetHeight > targetWidth) {
const scale = targetHeight / targetWidth
targetWidth = targetWidth * scale
targetHeight = targetHeight * scale
}
const x = (canvas.width - targetWidth) / 2
const y = (canvas.height - targetHeight) / 2
context.clearRect(0, 0, canvas.width, canvas.height)
context.drawImage(img, x, y, targetWidth, targetHeight)
this.$emit('start')
}
}
reader.readAsDataURL(file)
}
},
cropCanvasAndExportToPNG (canvas) {
const ctx = canvas.getContext('2d')
const width = canvas.width
const height = canvas.height
let topmost = height
let bottommost = 0
let leftmost = width
let rightmost = 0
const imageData = ctx.getImageData(0, 0, width, height)
const pixels = imageData.data
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const pixelIndex = (y * width + x) * 4
const alpha = pixels[pixelIndex + 3]
if (alpha !== 0) {
topmost = Math.min(topmost, y)
bottommost = Math.max(bottommost, y)
leftmost = Math.min(leftmost, x)
rightmost = Math.max(rightmost, x)
}
}
}
const croppedWidth = rightmost - leftmost + 1
const croppedHeight = bottommost - topmost + 1
const croppedCanvas = document.createElement('canvas')
croppedCanvas.width = croppedWidth
croppedCanvas.height = croppedHeight
const croppedCtx = croppedCanvas.getContext('2d')
croppedCtx.drawImage(canvas, leftmost, topmost, croppedWidth, croppedHeight, 0, 0, croppedWidth, croppedHeight)
return new Promise((resolve, reject) => {
croppedCanvas.toBlob((blob) => {
if (blob) {
resolve(blob)
} else {
reject(new Error('Failed to create a PNG blob.'))
}
}, 'image/png')
})
}, },
async submit () { async submit () {
if (this.modelValue) { if (this.modelValue) {
@@ -111,7 +294,7 @@ export default {
} }
return new Promise((resolve) => { return new Promise((resolve) => {
this.$refs.canvas.toBlob(async (blob) => { this.cropCanvasAndExportToPNG(this.$refs.canvas).then(async (blob) => {
const file = new File([blob], 'signature.png', { type: 'image/png' }) const file = new File([blob], 'signature.png', { type: 'image/png' })
if (this.isDirectUpload) { if (this.isDirectUpload) {
@@ -153,7 +336,7 @@ export default {
return resolve(attachment) return resolve(attachment)
}) })
} }
}, 'image/png') })
}) })
} }
} }
+2 -2
View File
@@ -71,7 +71,7 @@
v-if="fields.length < 4" v-if="fields.length < 4"
class="text-xs p-2 border border-base-200 rounded" class="text-xs p-2 border border-base-200 rounded"
> >
<ui> <ul class="list-disc list-outside ml-3">
<li> <li>
Draw a text field on the page with a mouse Draw a text field on the page with a mouse
</li> </li>
@@ -84,7 +84,7 @@
<li> <li>
Click on the field type above to add it to the form without drawing it on the document Click on the field type above to add it to the form without drawing it on the document
</li> </li>
</ui> </ul>
</div> </div>
</template> </template>
+1 -1
View File
@@ -32,7 +32,7 @@ class Submitter < ApplicationRecord
belongs_to :submission belongs_to :submission
attribute :values, :string, default: -> { {} } attribute :values, :string, default: -> { {} }
attribute :slug, :string, default: -> { SecureRandom.base58(10) } attribute :slug, :string, default: -> { SecureRandom.base58(14) }
serialize :values, JSON serialize :values, JSON
+1 -1
View File
@@ -36,7 +36,7 @@ class Template < ApplicationRecord
attribute :fields, :string, default: -> { [] } attribute :fields, :string, default: -> { [] }
attribute :schema, :string, default: -> { [] } attribute :schema, :string, default: -> { [] }
attribute :submitters, :string, default: -> { [{ name: DEFAULT_SUBMITTER_NAME, uuid: SecureRandom.uuid }] } attribute :submitters, :string, default: -> { [{ name: DEFAULT_SUBMITTER_NAME, uuid: SecureRandom.uuid }] }
attribute :slug, :string, default: -> { SecureRandom.base58(10) } attribute :slug, :string, default: -> { SecureRandom.base58(14) }
serialize :fields, JSON serialize :fields, JSON
serialize :schema, JSON serialize :schema, JSON
+1 -1
View File
@@ -46,7 +46,7 @@ class User < ApplicationRecord
belongs_to :account belongs_to :account
devise :database_authenticatable, :recoverable, :rememberable, :validatable, :trackable devise :database_authenticatable, :recoverable, :rememberable, :validatable, :trackable
devise :registerable if Docuseal.multitenant? devise :registerable, :omniauthable, omniauth_providers: [:google_oauth2] if Docuseal.multitenant?
attribute :role, :string, default: 'admin' attribute :role, :string, default: 'admin'
+1
View File
@@ -1,3 +1,4 @@
<% if Docuseal.demo? %><%= render 'shared/demo_alert' %><% end %>
<% if @templates.any? %> <% if @templates.any? %>
<div class="flex justify-between mb-4 items-center"> <div class="flex justify-between mb-4 items-center">
<h1 class="text-4xl font-bold">Templates</h1> <h1 class="text-4xl font-bold">Templates</h1>
+7 -7
View File
@@ -1,6 +1,6 @@
<div class="max-w-xl mx-auto px-2"> <div class="max-w-xl mx-auto px-2">
<h1 class="text-4xl font-bold text-center my-8">Sign up</h1> <h1 class="text-3xl font-bold text-center my-8">Profile Details</h1>
<%= form_for('', as: resource_name, html: { class: 'space-y-6' }, url: registration_path) do |f| %> <%= form_for('', as: resource_name, html: { class: 'space-y-6' }, url: new_registration_path) do |f| %>
<div class="space-y-2"> <div class="space-y-2">
<%= render 'devise/shared/error_messages', resource: %> <%= render 'devise/shared/error_messages', resource: %>
<%= f.fields_for resource do |ff| %> <%= f.fields_for resource do |ff| %>
@@ -16,7 +16,7 @@
</div> </div>
<% end %> <% end %>
<%= f.fields_for resource do |ff| %> <%= f.fields_for resource do |ff| %>
<div class="form-control"> <div class="form-control <%= 'hidden' if (params[:oauth_callback] || params[:sign_up]) && resource.email? %>">
<%= ff.label :email, class: 'label' %> <%= ff.label :email, class: 'label' %>
<%= ff.email_field :email, required: true, class: 'base-input' %> <%= ff.email_field :email, required: true, class: 'base-input' %>
</div> </div>
@@ -25,14 +25,14 @@
<set-timezone data-input-id="_account_timezone"></set-timezone> <set-timezone data-input-id="_account_timezone"></set-timezone>
<%= ff.hidden_field :timezone %> <%= ff.hidden_field :timezone %>
<div class="form-control"> <div class="form-control">
<%= ff.label :name, 'Company name', class: 'label' %> <%= ff.label :name, 'Company name (optional)', class: 'label' %>
<%= ff.text_field :name, required: true, class: 'base-input' %> <%= ff.text_field :name, class: 'base-input' %>
</div> </div>
<% end %> <% end %>
<%= f.fields_for resource do |ff| %> <%= f.fields_for resource do |ff| %>
<div class="form-control"> <div class="form-control <%= 'hidden' if params[:oauth_callback] %>">
<%= ff.label :password, class: 'label' %> <%= ff.label :password, class: 'label' %>
<%= ff.password_field :password, required: true, class: 'base-input' %> <%= ff.password_field :password, required: !params[:oauth_callback], class: 'base-input' %>
</div> </div>
<% end %> <% end %>
</div> </div>
@@ -0,0 +1,18 @@
<div class="max-w-xl mx-auto px-2">
<h1 class="text-3xl font-bold text-center my-8">Create Free Account</h1>
<%= form_for(User.new, html: { class: 'space-y-6' }, url: new_registration_path, method: :get) do |f| %>
<div class="space-y-2">
<div class="form-control">
<%= f.label :email, class: 'label' %>
<%= f.email_field :email, autofocus: true, autocomplete: 'email', class: 'base-input' %>
</div>
</div>
<div class="form-control">
<%= f.button button_title(title: 'Sign up', disabled_with: 'Sign up'), name: 'sign_up', value: true, class: 'base-button' %>
</div>
<% end %>
<% if devise_mapping.omniauthable? %>
<%= button_to button_title(title: 'Sign up with Google', icon: svg_icon('brand_google', class: 'w-6 h-6')), omniauth_authorize_path(resource_name, :google_oauth2), class: 'white-button w-full mt-4', data: { turbo: false }, method: :post %>
<% end %>
<%= render 'devise/shared/links' %>
</div>
+3
View File
@@ -23,5 +23,8 @@
<%= f.button button_title(title: 'Log In', disabled_with: 'Logging In'), class: 'base-button' %> <%= f.button button_title(title: 'Log In', disabled_with: 'Logging In'), class: 'base-button' %>
</div> </div>
<% end %> <% end %>
<% if devise_mapping.omniauthable? %>
<%= button_to button_title(title: 'Log in with Google', icon: svg_icon('brand_google', class: 'w-6 h-6')), omniauth_authorize_path(resource_name, :google_oauth2), class: 'white-button w-full mt-4', data: { turbo: false }, method: :post %>
<% end %>
<%= render 'devise/shared/links' %> <%= render 'devise/shared/links' %>
</div> </div>
+1 -6
View File
@@ -3,7 +3,7 @@
<%= link_to 'Log in', new_session_path(resource_name), class: 'link link-hover' %> <%= link_to 'Log in', new_session_path(resource_name), class: 'link link-hover' %>
<% end %> <% end %>
<%- if devise_mapping.registerable? && controller_name != 'registrations' %> <%- if devise_mapping.registerable? && controller_name != 'registrations' %>
<%= link_to 'Sign up', new_registration_path, class: 'link link-hover' %> <%= link_to 'Create free account', registration_path, class: 'link link-hover' %>
<% end %> <% end %>
<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> <%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
<%= link_to 'Forgot your password?', new_password_path(resource_name), class: 'link link-hover' %> <%= link_to 'Forgot your password?', new_password_path(resource_name), class: 'link link-hover' %>
@@ -14,9 +14,4 @@
<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> <%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
<%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name), class: 'link link-hover' %> <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name), class: 'link link-hover' %>
<% end %> <% end %>
<%- if devise_mapping.omniauthable? %>
<%- resource_class.omniauth_providers.each do |provider| %>
<%= button_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), class: 'link link-hover', data: { turbo: false } %>
<% end %>
<% end %>
</div> </div>
+16
View File
@@ -0,0 +1,16 @@
<svg width="800" height="800" viewBox="-0.5 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>">
<g id="Icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Color-" transform="translate(-401.000000, -860.000000)">
<g id="Google" transform="translate(401.000000, 860.000000)">
<path d="M9.82727273,24 C9.82727273,22.4757333 10.0804318,21.0144 10.5322727,19.6437333 L2.62345455,13.6042667 C1.08206818,16.7338667 0.213636364,20.2602667 0.213636364,24 C0.213636364,27.7365333 1.081,31.2608 2.62025,34.3882667 L10.5247955,28.3370667 C10.0772273,26.9728 9.82727273,25.5168 9.82727273,24" id="Fill-1" fill="#FBBC05">
</path>
<path d="M23.7136364,10.1333333 C27.025,10.1333333 30.0159091,11.3066667 32.3659091,13.2266667 L39.2022727,6.4 C35.0363636,2.77333333 29.6954545,0.533333333 23.7136364,0.533333333 C14.4268636,0.533333333 6.44540909,5.84426667 2.62345455,13.6042667 L10.5322727,19.6437333 C12.3545909,14.112 17.5491591,10.1333333 23.7136364,10.1333333" id="Fill-2" fill="#EB4335">
</path>
<path d="M23.7136364,37.8666667 C17.5491591,37.8666667 12.3545909,33.888 10.5322727,28.3562667 L2.62345455,34.3946667 C6.44540909,42.1557333 14.4268636,47.4666667 23.7136364,47.4666667 C29.4455,47.4666667 34.9177955,45.4314667 39.0249545,41.6181333 L31.5177727,35.8144 C29.3995682,37.1488 26.7323182,37.8666667 23.7136364,37.8666667" id="Fill-3" fill="#34A853">
</path>
<path d="M46.1454545,24 C46.1454545,22.6133333 45.9318182,21.12 45.6113636,19.7333333 L23.7136364,19.7333333 L23.7136364,28.8 L36.3181818,28.8 C35.6879545,31.8912 33.9724545,34.2677333 31.5177727,35.8144 L39.0249545,41.6181333 C43.3393409,37.6138667 46.1454545,31.6490667 46.1454545,24" id="Fill-4" fill="#4285F4">
</path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

-1
View File
@@ -15,7 +15,6 @@
<body> <body>
<turbo-frame id="modal"></turbo-frame> <turbo-frame id="modal"></turbo-frame>
<%= render 'shared/navbar' %> <%= render 'shared/navbar' %>
<% if Docuseal.demo? %><%= render 'shared/demo_alert' %><% end %>
<% if flash.present? %><%= render 'shared/flash' %><% end %> <% if flash.present? %><%= render 'shared/flash' %><% end %>
<div class="max-w-6xl mx-auto px-4 md:px-2 mb-8"> <div class="max-w-6xl mx-auto px-4 md:px-2 mb-8">
<%= yield %> <%= yield %>
+8 -6
View File
@@ -1,9 +1,11 @@
<div class="absolute top-0 w-full h-0"> <div class="mx-auto mt-1.5">
<div class="max-w-xl mx-auto mt-1.5"> <div class="py-3 rounded-2xl flex items-center justify-between mx-4 md:mx-0">
<div class="px-4 py-3 rounded-2xl flex items-center justify-between mx-4 md:mx-0"> <div class="w-full text-center">
<a target="_blank" href="<%= start_form_url(slug: ::Template.first&.slug) %>" class="font-medium w-full text-center underline"> <span class="font-bold">Demo Environment</span>
Demo Environment <br>
</a> Feel free to
<a href="<%= new_template_path %>" data-turbo-frame="modal" class="inline underline font-medium">create a new template</a> document form or
<a href="<%= start_form_url(slug: ::Template.first&.slug) %>" target="_blank" class="inline underline font-medium">submit the existing one</a> 😊
</div> </div>
</div> </div>
</div> </div>
+10
View File
@@ -0,0 +1,10 @@
<a href="https://github.com/docusealco/docuseal" class="btn btn-neutral btn-sm btn-outline inline-flex items-center justify-center" target="_blank" alt="Star on GitHub" style="height: 37px">
<span>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor" d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
</span>
<span class="flex">
<span class="hidden lg:block">Star on&nbsp</span>GitHub
</span>
</a>
+29 -13
View File
@@ -4,13 +4,19 @@
<%= render 'shared/logo' %> <%= render 'shared/logo' %>
<span>DocuSeal</span> <span>DocuSeal</span>
</a> </a>
<a href="<%= Docuseal::GITHUB_URL %>" target="_blank" class="inline"> <% unless Docuseal.demo? %>
<img alt="GitHub Repo stars" src="https://www.docuseal.co/github-badge.svg" style="height: 22px"> <a href="<%= Docuseal::GITHUB_URL %>" target="_blank" class="inline">
</a> <img alt="GitHub Repo stars" src="https://www.docuseal.co/github-badge.svg" style="height: 22px">
</a>
<% end %>
</div> </div>
<% if signed_in? %> <% if signed_in? %>
<div class="space-x-6"> <div class="space-x-4 flex items-center">
<%= link_to 'Settings', settings_profile_index_path, class: 'hidden md:inline font-medium text-lg' %> <% if Docuseal.demo? %>
<%= render 'shared/github_button' %>
<% else %>
<%= link_to 'Settings', settings_profile_index_path, class: 'hidden md:inline font-medium text-lg mr-2' %>
<% end %>
<div class="dropdown dropdown-end"> <div class="dropdown dropdown-end">
<label tabindex="0" class="cursor-pointer bg-base-content text-purple-300 rounded-full p-2 w-9 justify-center flex"> <label tabindex="0" class="cursor-pointer bg-base-content text-purple-300 rounded-full p-2 w-9 justify-center flex">
<span class="text-sm align-text-top"><%= current_user.initials %></span> <span class="text-sm align-text-top"><%= current_user.initials %></span>
@@ -23,8 +29,8 @@
<% end %> <% end %>
</li> </li>
<li> <li>
<%= link_to destroy_user_session_path, data: { turbo_method: :delete } do %> <%= button_to destroy_user_session_path, method: :delete, data: { turbo: false } do %>
<%= svg_icon('logout', class: 'w-5 h-5 stroke-2') %> <%= svg_icon('logout', class: 'w-5 h-5 stroke-2 inline') %>
<span class="mr-1">Sign out</span> <span class="mr-1">Sign out</span>
<% end %> <% end %>
</li> </li>
@@ -32,11 +38,21 @@
</div> </div>
</div> </div>
<% else %> <% else %>
<%= link_to new_user_session_path, class: 'font-medium text-lg' do %> <div class="flex space-x-6">
<span class="flex items-center justify-center space-x-1"> <%= link_to new_user_session_path, class: 'font-medium text-lg' do %>
<%= svg_icon('login', class: 'w-6 h-6') %> <span class="flex items-center justify-center space-x-1">
<span>Sign in</span> <%= svg_icon('login', class: 'w-6 h-6') %>
</span> <span>Sign in</span>
<% end %> </span>
<% end %>
<% if Docuseal.multitenant? %>
<%= link_to registration_path, class: 'font-medium text-lg hidden md:inline' do %>
<span class="flex items-center justify-center space-x-1">
<%= svg_icon('user_plus', class: 'w-6 h-6') %>
<span>Try for Free</span>
</span>
<% end %>
<% end %>
</div>
<% end %> <% end %>
</div> </div>
+1 -3
View File
@@ -32,9 +32,7 @@
<div class="fixed bottom-0 w-full h-0 z-20"> <div class="fixed bottom-0 w-full h-0 z-20">
<div class="mx-auto" style="max-width: 1000px"> <div class="mx-auto" style="max-width: 1000px">
<div class="relative md:mx-32"> <div class="relative md:mx-32">
<div id="form_container" 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-is-demo="<%= Docuseal.demo? %>" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-submitter-uuid="<%= @submitter.uuid %>" data-submitter-slug="<%= @submitter.slug %>" data-can-send-email="<%= Accounts.can_send_emails?(Struct.new(:id).new(@submitter.submission.template.account_id)) %>" 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>
<submission-form data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-submitter-uuid="<%= @submitter.uuid %>" data-submitter-slug="<%= @submitter.slug %>" data-can-send-email="<%= Accounts.can_send_emails?(Struct.new(:id).new(@submitter.submission.template.account_id)) %>" 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> </div>
</div> </div>
</div> </div>
+11 -5
View File
@@ -157,13 +157,19 @@
<% else %> <% else %>
<div class="card bg-base-200"> <div class="card bg-base-200">
<div class="card-body text-center py-16"> <div class="card-body text-center py-16">
<div class="max-w-md mx-auto"> <div class="max-w-lg mx-auto">
<p class="text-3xl font-bold text-base-content mb-4">There are no Submissions</p> <p class="text-3xl font-bold text-base-content mb-4">There are no Submissions</p>
<p class="text-gray-600">Send an invitation to fill and complete the form</p> <p class="text-gray-600">Send an invitation to fill and complete the form</p>
<%= link_to new_template_submission_path(@template), class: 'base-button mt-6', data: { turbo_frame: 'modal' } do %> <div class="space-x-2">
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %> <%= link_to new_template_submission_path(@template), class: 'base-button mt-6', data: { turbo_frame: 'modal' } do %>
<span class="mr-1">Add Recipients</span> <%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<% end %> <span class="mr-1">Add Recipients</span>
<% end %>
<%= link_to start_form_url(slug: @template.slug), class: 'white-button mt-6', target: '_blank', rel: 'noopener' do %>
<%= svg_icon('writing', class: 'w-6 h-6') %>
<span class="mr-1">Submit it Yourself</span>
<% end %>
</div>
</div> </div>
</div> </div>
</div> </div>
+2
View File
@@ -45,6 +45,8 @@ Rails.application.configure do
config.cache_store = :null_store config.cache_store = :null_store
end end
config.active_job.queue_adapter = :sidekiq if defined?(Sidekiq)
# Store uploaded files on the local file system (see config/storage.yml for options). # Store uploaded files on the local file system (see config/storage.yml for options).
config.active_storage.service = :disk config.active_storage.service = :disk
config.active_storage.resolve_model_to_route = :rails_storage_proxy config.active_storage.resolve_model_to_route = :rails_storage_proxy
+2
View File
@@ -25,6 +25,8 @@ Rails.application.configure do
config.action_controller.perform_caching = true config.action_controller.perform_caching = true
config.active_record.sqlite3_production_warning = false config.active_record.sqlite3_production_warning = false
config.active_job.queue_adapter = :sidekiq if defined?(Sidekiq)
# Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
# or in config/master.key. This key is used to decrypt credentials (and other encrypted files). # or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
# config.require_master_key = true # config.require_master_key = true
+2 -2
View File
@@ -120,7 +120,7 @@ Devise.setup do |config|
# a value less than 10 in other environments. Note that, for bcrypt (the default # a value less than 10 in other environments. Note that, for bcrypt (the default
# algorithm), the cost increases exponentially with the number of stretches (e.g. # algorithm), the cost increases exponentially with the number of stretches (e.g.
# a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation). # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation).
config.stretches = Rails.env.test? ? 1 : 12 config.stretches = Rails.env.test? ? 1 : 10
# Set up a pepper to generate the hashed password. # Set up a pepper to generate the hashed password.
# Send a notification to the original email when the user's email is changed. # Send a notification to the original email when the user's email is changed.
@@ -266,7 +266,7 @@ Devise.setup do |config|
# ==> OmniAuth # ==> OmniAuth
# Add a new OmniAuth provider. Check the wiki for more information on setting # Add a new OmniAuth provider. Check the wiki for more information on setting
# up on your models and hooks. # up on your models and hooks.
# config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' config.omniauth :google_oauth2, ENV.fetch('GOOGLE_CLIENT_ID', nil), ENV.fetch('GOOGLE_CLIENT_SECRET', nil), {}
# ==> Warden configuration # ==> Warden configuration
# If you want to use other strategies, that are not supported by Devise, or # If you want to use other strategies, that are not supported by Devise, or
+16
View File
@@ -0,0 +1,16 @@
# frozen_string_literal: true
if defined?(Sidekiq)
require 'sidekiq/web'
Sidekiq::Web.use(Rack::Auth::Basic) do |_, password|
next true if Rails.env.development?
ActiveSupport::SecurityUtils.secure_compare(
Digest::SHA256.hexdigest(password),
Digest::SHA256.hexdigest(ENV.fetch('SIDEKIQ_BASIC_AUTH_PASSWORD'))
)
end
Sidekiq.strict_args!
end
+10 -2
View File
@@ -2,14 +2,22 @@
Rails.application.routes.draw do Rails.application.routes.draw do
mount LetterOpenerWeb::Engine, at: '/letter_opener' if Rails.env.development? mount LetterOpenerWeb::Engine, at: '/letter_opener' if Rails.env.development?
mount Sidekiq::Web => '/sidekiq' if defined?(Sidekiq)
root 'dashboard#index' root 'dashboard#index'
devise_for :users, path: '/', only: %i[sessions passwords] devise_for :users,
path: '/', only: %i[sessions passwords omniauth_callbacks],
controllers: begin
options = { sessions: 'sessions' }
options[:omniauth_callbacks] = 'omniauth_callbacks' if Docuseal.multitenant?
options
end
devise_scope :user do devise_scope :user do
if Docuseal.multitenant? if Docuseal.multitenant?
resource :registration, only: %i[create], path: 'sign_up' do resource :registration, only: %i[show], path: 'sign_up'
resource :registration, only: %i[create], path: 'new' do
get '' => :new, as: :new get '' => :new, as: :new
end end
end end
+6
View File
@@ -0,0 +1,6 @@
queues:
- [default, 1]
- [mailers, 1]
production:
:concurrency: 15
+39
View File
@@ -3,6 +3,45 @@
module Accounts module Accounts
module_function module_function
def create_duplicate(account)
new_account = account.dup
new_user = account.users.first.dup
new_user.account = new_account
new_user.encrypted_password = SecureRandom.hex
new_user.email = "#{SecureRandom.hex}@docuseal.co"
account.templates.each do |template|
new_template = template.dup
new_template.account = new_account
new_template.slug = SecureRandom.base58(14)
new_template.save!
Templates::CloneAttachments.call(template: new_template, original_template: template)
end
new_user.save!(validate: false)
new_account
end
def create_default_template(account)
template = Template.find(1)
new_template = Template.find(1).dup
new_template.account_id = account.id
new_template.slug = SecureRandom.base58(14)
new_template.save!
Templates::CloneAttachments.call(template: new_template, original_template: template)
new_template
end
def load_signing_certs(account) def load_signing_certs(account)
certs = certs =
if Docuseal.multitenant? if Docuseal.multitenant?
+6
View File
@@ -4,6 +4,12 @@ module ActionMailerConfigsInterceptor
module_function module_function
def delivering_email(message) def delivering_email(message)
if Docuseal.demo?
message.delivery_method(:test)
return message
end
if Rails.env.production? && Rails.application.config.action_mailer.delivery_method if Rails.env.production? && Rails.application.config.action_mailer.delivery_method
message.from = ENV.fetch('SMTP_FROM') message.from = ENV.fetch('SMTP_FROM')
+15
View File
@@ -0,0 +1,15 @@
# frozen_string_literal: true
module Users
module_function
def from_omniauth(oauth)
user = User.find_by(email: oauth.info.email)
return user if user
User.new(email: oauth.info.email,
first_name: oauth.extra.id_info.given_name,
last_name: oauth.extra.id_info.family_name)
end
end