Merge from docusealco/wip
This commit is contained in:
@@ -68,6 +68,7 @@ COPY ./lib ./lib
|
|||||||
COPY ./public ./public
|
COPY ./public ./public
|
||||||
COPY ./tmp ./tmp
|
COPY ./tmp ./tmp
|
||||||
COPY LICENSE README.md Rakefile config.ru .version ./
|
COPY LICENSE README.md Rakefile config.ru .version ./
|
||||||
|
COPY .version ./public/version
|
||||||
|
|
||||||
COPY --from=fonts /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
|
COPY --from=fonts /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
|
||||||
COPY --from=fonts /fonts/FreeSans.ttf /usr/share/fonts/freefont
|
COPY --from=fonts /fonts/FreeSans.ttf /usr/share/fonts/freefont
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
if params[:permanently] == 'true'
|
if params[:permanently].in?(['true', true])
|
||||||
@submission.destroy!
|
@submission.destroy!
|
||||||
else
|
else
|
||||||
@submission.update!(archived_at: Time.current)
|
@submission.update!(archived_at: Time.current)
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ module Api
|
|||||||
cloned_template.source = :api
|
cloned_template.source = :api
|
||||||
cloned_template.save!
|
cloned_template.save!
|
||||||
|
|
||||||
schema_documents = Templates::CloneAttachments.call(template: cloned_template, original_template: @template)
|
schema_documents = Templates::CloneAttachments.call(template: cloned_template,
|
||||||
|
original_template: @template,
|
||||||
|
documents: params[:documents])
|
||||||
|
|
||||||
WebhookUrls.for_account_id(cloned_template.account_id, 'template.created').each do |webhook_url|
|
WebhookUrls.for_account_id(cloned_template.account_id, 'template.created').each do |webhook_url|
|
||||||
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id,
|
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id,
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
if params[:permanently] == 'true'
|
if params[:permanently].in?(['true', true])
|
||||||
@template.destroy!
|
@template.destroy!
|
||||||
else
|
else
|
||||||
@template.update!(archived_at: Time.current)
|
@template.update!(archived_at: Time.current)
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class SubmissionsController < ApplicationController
|
|||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
notice =
|
notice =
|
||||||
if params[:permanently].present?
|
if params[:permanently].in?(['true', true])
|
||||||
@submission.destroy!
|
@submission.destroy!
|
||||||
|
|
||||||
I18n.t('submission_has_been_removed')
|
I18n.t('submission_has_been_removed')
|
||||||
|
|||||||
@@ -15,9 +15,6 @@ class SubmissionsDashboardController < ApplicationController
|
|||||||
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
|
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
|
||||||
@submissions = Submissions::Filter.call(@submissions, current_user, params)
|
@submissions = Submissions::Filter.call(@submissions, current_user, params)
|
||||||
|
|
||||||
@submissions = @submissions.pending if params[:status] == 'pending'
|
|
||||||
@submissions = @submissions.completed if params[:status] == 'completed'
|
|
||||||
|
|
||||||
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
|
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
|
||||||
@submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
|
@submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ class SubmissionsFiltersController < ApplicationController
|
|||||||
ALLOWED_NAMES = %w[
|
ALLOWED_NAMES = %w[
|
||||||
author
|
author
|
||||||
completed_at
|
completed_at
|
||||||
|
status
|
||||||
created_at
|
created_at
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ class TemplateFoldersController < ApplicationController
|
|||||||
load_and_authorize_resource :template_folder
|
load_and_authorize_resource :template_folder
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@templates = @template_folder.templates.active.preload(:author, :template_accesses).order(id: :desc)
|
@templates = @template_folder.templates.active.accessible_by(current_ability)
|
||||||
|
.preload(:author, :template_accesses).order(id: :desc)
|
||||||
@templates = Templates.search(@templates, params[:q])
|
@templates = Templates.search(@templates, params[:q])
|
||||||
|
|
||||||
@pagy, @templates = pagy(@templates, limit: 12)
|
@pagy, @templates = pagy(@templates, limit: 12)
|
||||||
|
|||||||
@@ -9,12 +9,11 @@ class TemplatesController < ApplicationController
|
|||||||
submissions = @template.submissions.accessible_by(current_ability)
|
submissions = @template.submissions.accessible_by(current_ability)
|
||||||
submissions = submissions.active if @template.archived_at.blank?
|
submissions = submissions.active if @template.archived_at.blank?
|
||||||
submissions = Submissions.search(submissions, params[:q], search_values: true)
|
submissions = Submissions.search(submissions, params[:q], search_values: true)
|
||||||
submissions = Submissions::Filter.call(submissions, current_user, params)
|
submissions = Submissions::Filter.call(submissions, current_user, params.except(:status))
|
||||||
|
|
||||||
@base_submissions = submissions
|
@base_submissions = submissions
|
||||||
|
|
||||||
submissions = submissions.pending if params[:status] == 'pending'
|
submissions = Submissions::Filter.filter_by_status(submissions, params)
|
||||||
submissions = submissions.completed if params[:status] == 'completed'
|
|
||||||
|
|
||||||
submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
|
submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
|
||||||
submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
|
submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
|
||||||
@@ -93,7 +92,7 @@ class TemplatesController < ApplicationController
|
|||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
notice =
|
notice =
|
||||||
if params[:permanently].present?
|
if params[:permanently].in?(['true', true])
|
||||||
@template.destroy!
|
@template.destroy!
|
||||||
|
|
||||||
I18n.t('template_has_been_removed')
|
I18n.t('template_has_been_removed')
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ class TemplatesDashboardController < ApplicationController
|
|||||||
FOLDERS_PER_PAGE = 18
|
FOLDERS_PER_PAGE = 18
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@template_folders = filter_template_folders(@template_folders)
|
@template_folders = @template_folders.where(id: @templates.active.select(:folder_id)).order(id: :desc)
|
||||||
|
|
||||||
|
@template_folders = TemplateFolders.search(@template_folders, params[:q])
|
||||||
|
|
||||||
@pagy, @template_folders = pagy(
|
@pagy, @template_folders = pagy(
|
||||||
@template_folders,
|
@template_folders,
|
||||||
@@ -36,14 +38,6 @@ class TemplatesDashboardController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def filter_template_folders(template_folders)
|
|
||||||
rel = template_folders.joins(:active_templates)
|
|
||||||
.order(id: :desc)
|
|
||||||
.distinct
|
|
||||||
|
|
||||||
TemplateFolders.search(rel, params[:q])
|
|
||||||
end
|
|
||||||
|
|
||||||
def filter_templates(templates)
|
def filter_templates(templates)
|
||||||
rel = templates.active.preload(:author, :template_accesses).order(id: :desc)
|
rel = templates.active.preload(:author, :template_accesses).order(id: :desc)
|
||||||
|
|
||||||
|
|||||||
@@ -24,14 +24,10 @@ class UsersController < ApplicationController
|
|||||||
def edit; end
|
def edit; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
existing_user = User.accessible_by(current_ability).find_by(email: @user.email)
|
if User.accessible_by(current_ability).exists?(email: @user.email)
|
||||||
|
@user.errors.add(:email, I18n.t('already_exists'))
|
||||||
|
|
||||||
if existing_user
|
return render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity
|
||||||
existing_user.archived_at = nil
|
|
||||||
existing_user.assign_attributes(user_params)
|
|
||||||
existing_user.account = current_account
|
|
||||||
|
|
||||||
@user = existing_user
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@user.role = User::ADMIN_ROLE unless role_valid?(@user.role)
|
@user.role = User::ADMIN_ROLE unless role_valid?(@user.role)
|
||||||
@@ -83,14 +79,7 @@ class UsersController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def build_user
|
def build_user
|
||||||
@user = current_account.users.find_by(email: user_params[:email])&.tap do |user|
|
@user = current_account.users.new(user_params)
|
||||||
user.assign_attributes(user_params)
|
|
||||||
user.archived_at = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
@user ||= current_account.users.new(user_params)
|
|
||||||
|
|
||||||
@user
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_params
|
def user_params
|
||||||
|
|||||||
@@ -74,7 +74,7 @@
|
|||||||
ID: {{ signature.uuid }}
|
ID: {{ signature.uuid }}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{{ t('reason') }}: {{ values[field.preferences?.reason_field_uuid] || t('digitally_signed_by') }} {{ submitter.name }}
|
<span v-if="values[field.preferences?.reason_field_uuid]">{{ t('reason') }}: </span>{{ values[field.preferences?.reason_field_uuid] || t('digitally_signed_by') }} {{ submitter.name }}
|
||||||
<template v-if="submitter.email">
|
<template v-if="submitter.email">
|
||||||
<{{ submitter.email }}>
|
<{{ submitter.email }}>
|
||||||
</template>
|
</template>
|
||||||
@@ -168,6 +168,7 @@
|
|||||||
<div
|
<div
|
||||||
v-else-if="field.type === 'cells'"
|
v-else-if="field.type === 'cells'"
|
||||||
class="w-full flex items-center"
|
class="w-full flex items-center"
|
||||||
|
:class="{ 'justify-end': field.preferences?.align === 'right' }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(char, index) in modelValue"
|
v-for="(char, index) in modelValue"
|
||||||
|
|||||||
@@ -1304,6 +1304,16 @@ export default {
|
|||||||
fieldArea.h = lastArea.h
|
fieldArea.h = lastArea.h
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (field.width) {
|
||||||
|
fieldArea.w = field.width / area.maskW
|
||||||
|
delete field.width
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.height) {
|
||||||
|
fieldArea.h = field.height / area.maskH
|
||||||
|
delete field.height
|
||||||
|
}
|
||||||
|
|
||||||
field.areas.push(fieldArea)
|
field.areas.push(fieldArea)
|
||||||
|
|
||||||
this.selectedAreaRef.value = fieldArea
|
this.selectedAreaRef.value = fieldArea
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="['number'].includes(field.type)"
|
v-if="['number', 'cells'].includes(field.type)"
|
||||||
class="py-1.5 px-1 relative"
|
class="py-1.5 px-1 relative"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
@change="[field.preferences ||= {}, field.preferences.align = $event.target.value, save()]"
|
@change="[field.preferences ||= {}, field.preferences.align = $event.target.value, save()]"
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
v-for="value in ['left', 'right', 'center']"
|
v-for="value in ['left', 'right', field.type === 'cells' ? null : 'center'].filter(Boolean)"
|
||||||
:key="value"
|
:key="value"
|
||||||
:selected="field.preferences?.align ? value === field.preferences.align : value === 'left'"
|
:selected="field.preferences?.align ? value === field.preferences.align : value === 'left'"
|
||||||
:value="value"
|
:value="value"
|
||||||
@@ -240,7 +240,7 @@
|
|||||||
<input
|
<input
|
||||||
v-model="field.required"
|
v-model="field.required"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:disabled="!editable || defaultField"
|
:disabled="!editable || (defaultField && [true, false].includes(defaultField.required))"
|
||||||
class="toggle toggle-xs"
|
class="toggle toggle-xs"
|
||||||
@update:model-value="save"
|
@update:model-value="save"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -346,7 +346,7 @@ export default {
|
|||||||
return this.$el.closest('form')
|
return this.$el.closest('form')
|
||||||
},
|
},
|
||||||
fieldTypes () {
|
fieldTypes () {
|
||||||
return ['text', 'cells', 'date', 'number', 'radio', 'select', 'checkbox']
|
return ['text', 'cells', 'date', 'number', 'radio', 'select', 'checkbox', 'image']
|
||||||
},
|
},
|
||||||
defaultFields () {
|
defaultFields () {
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ class Submission < ApplicationRecord
|
|||||||
where.not(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
|
where.not(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
|
||||||
.and(Submitter.arel_table[:completed_at].eq(nil))).select(1).arel.exists)
|
.and(Submitter.arel_table[:completed_at].eq(nil))).select(1).arel.exists)
|
||||||
}
|
}
|
||||||
|
scope :declined, -> { joins(:submitters).where.not(submitters: { declined_at: nil }).group(:id) }
|
||||||
|
scope :expired, -> { where(expire_at: ..Time.current) }
|
||||||
|
|
||||||
enum :source, {
|
enum :source, {
|
||||||
invite: 'invite',
|
invite: 'invite',
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" 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="M20.997 12.25a9 9 0 1 0 -8.718 8.745" /><path d="M19 19m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" /><path d="M17 21l4 -4" /><path d="M12 7v5l2 2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 433 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" 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="M21 12a9 9 0 1 0 -9.972 8.948c.32 .034 .644 .052 .972 .052" /><path d="M12 7v5l2 2" /><path d="M18.42 15.61a2.1 2.1 0 0 1 2.97 2.97l-3.39 3.42h-3v-3l3.42 -3.39z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 456 B |
@@ -12,7 +12,8 @@
|
|||||||
ID: <%= attachment.uuid %>
|
ID: <%= attachment.uuid %>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<%= t('reason') %>: <%= submitter.values[field.dig('preferences', 'reason_field_uuid')].presence || t('digitally_signed_by') %> <%= submitter.name %>
|
<% reason_value = submitter.values[field.dig('preferences', 'reason_field_uuid')].presence %>
|
||||||
|
<% if reason_value %><%= t('reason') %>: <% end %><%= reason_value || t('digitally_signed_by') %> <%= submitter.name %>
|
||||||
<% if submitter.email %>
|
<% if submitter.email %>
|
||||||
<<%= submitter.email %>>
|
<<%= submitter.email %>>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -49,7 +50,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% elsif field['type'] == 'cells' && area['cell_w'].to_f > 0.0 %>
|
<% elsif field['type'] == 'cells' && area['cell_w'].to_f > 0.0 %>
|
||||||
<% cell_width = area['cell_w'] / area['w'] * 100 %>
|
<% cell_width = area['cell_w'] / area['w'] * 100 %>
|
||||||
<div class="w-full flex items-center">
|
<div class="w-full flex items-center <%= 'justify-end' if align == 'right' %>">
|
||||||
<% (0..(area['w'] / area['cell_w']).ceil).each do |index| %>
|
<% (0..(area['w'] / area['cell_w']).ceil).each do |index| %>
|
||||||
<% if value[index] %>
|
<% if value[index] %>
|
||||||
<div class="text-center flex-none" style="width: <%= cell_width %>%;"><%= value[index] %></div>
|
<div class="text-center flex-none" style="width: <%= cell_width %>%;"><%= value[index] %></div>
|
||||||
|
|||||||
@@ -1,4 +1,26 @@
|
|||||||
<% query_params = params.permit(:q, :status).merge(filter_params) %>
|
<% query_params = params.permit(:q).merge(filter_params) %>
|
||||||
|
<% if icon = { 'declined' => 'x_circle', 'expired' => 'clock_cancel', 'partially_completed' => 'clock_edit' }[params[:status]] %>
|
||||||
|
<div class="flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-34 border-neutral-700">
|
||||||
|
<%= link_to submissions_filter_path('status', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 w-full pr-1 md:max-w-[140px]' do %>
|
||||||
|
<%= svg_icon(icon, class: 'w-5 h-5 shrink-0') %>
|
||||||
|
<span class="font-normal truncate"><%= t(params[:status]) %></span>
|
||||||
|
<% end %>
|
||||||
|
<%= link_to url_for(params.to_unsafe_h.except(:status)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
|
||||||
|
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% if params[:author].present? %>
|
||||||
|
<div class="tooltip tooltip-bottom flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-34 border-neutral-700" data-tip="<%= t('author') %>">
|
||||||
|
<%= link_to submissions_filter_path('author', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 w-full pr-1 md:max-w-[140px]' do %>
|
||||||
|
<%= svg_icon('user', class: 'w-5 h-5 shrink-0') %>
|
||||||
|
<span class="font-normal truncate"><%= current_account.users.accessible_by(current_ability).where(account: current_account).find_by(email: params[:author])&.full_name || 'NA' %></span>
|
||||||
|
<% end %>
|
||||||
|
<%= link_to url_for(params.to_unsafe_h.except(:author)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
|
||||||
|
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
<% if query_params[:completed_at_from].present? || query_params[:completed_at_to].present? %>
|
<% if query_params[:completed_at_from].present? || query_params[:completed_at_to].present? %>
|
||||||
<div class="tooltip tooltip-bottom flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-34 border-neutral-700" data-tip="<%= t('completed_at') %>">
|
<div class="tooltip tooltip-bottom flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-34 border-neutral-700" data-tip="<%= t('completed_at') %>">
|
||||||
<%= link_to submissions_filter_path('completed_at', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 w-full pr-1 md:max-w-[140px]' do %>
|
<%= link_to submissions_filter_path('completed_at', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 w-full pr-1 md:max-w-[140px]' do %>
|
||||||
@@ -37,14 +59,3 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if params[:author].present? %>
|
|
||||||
<div class="tooltip tooltip-bottom flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-34 border-neutral-700" data-tip="<%= t('author') %>">
|
|
||||||
<%= link_to submissions_filter_path('author', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 w-full pr-1 md:max-w-[140px]' do %>
|
|
||||||
<%= svg_icon('user', class: 'w-5 h-5 shrink-0') %>
|
|
||||||
<span class="font-normal truncate"><%= current_account.users.accessible_by(current_ability).where(account: current_account).find_by(email: params[:author])&.full_name || 'NA' %></span>
|
|
||||||
<% end %>
|
|
||||||
<%= link_to url_for(params.to_unsafe_h.except(:author)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
|
|
||||||
<%= svg_icon('x', class: 'w-5 h-5') %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<% query_params = params.permit(:q, :status).merge(filter_params) %>
|
<% query_params = params.permit(:q).merge(filter_params) %>
|
||||||
<div class="dropdown dropdown-end">
|
<div class="dropdown dropdown-end">
|
||||||
<label tabindex="0" class="cursor-pointer flex h-10 px-3 py-1 space-x-1 text-lg items-center justify-between border text-center text-neutral rounded-xl border-neutral-300 hover:border-neutral-700">
|
<label tabindex="0" class="cursor-pointer flex h-10 px-3 py-1 space-x-1 text-lg items-center justify-between border text-center text-neutral rounded-xl border-neutral-300 hover:border-neutral-700">
|
||||||
<%= svg_icon('filter', class: 'w-5 h-5 flex-shrink-0 stroke-2') %>
|
<%= svg_icon('filter', class: 'w-5 h-5 flex-shrink-0 stroke-2') %>
|
||||||
<span class="<%= filter_params.present? ? 'md:hidden' : '' %>">Filter</span>
|
<span class="<%= filter_params.then { |f| f[:status].in?(%w[pending completed]) ? f.except(:status) : f }.present? ? 'md:hidden' : '' %>">Filter</span>
|
||||||
</label>
|
</label>
|
||||||
<ul tabindex="0" class="z-10 dropdown-content p-2 mt-2 shadow menu text-base bg-base-100 rounded-box min-w-[180px] text-right">
|
<ul tabindex="0" class="z-10 dropdown-content p-2 mt-2 shadow menu text-base bg-base-100 rounded-box min-w-[180px] text-right">
|
||||||
<li class="flex">
|
<li class="flex">
|
||||||
@@ -17,6 +17,12 @@
|
|||||||
<span><%= t('created_at') %></span>
|
<span><%= t('created_at') %></span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="flex">
|
||||||
|
<%= link_to submissions_filter_path('status', query_params.merge(path: url_for)), data: { turbo_frame: 'modal' } do %>
|
||||||
|
<%= svg_icon('info_circle', class: 'w-5 h-5 flex-shrink-0 stroke-2') %>
|
||||||
|
<span><%= t('status') %></span>
|
||||||
|
<% end %>
|
||||||
|
</li>
|
||||||
<li class="flex">
|
<li class="flex">
|
||||||
<%= link_to submissions_filter_path('author', query_params.merge(path: url_for)), data: { turbo_frame: 'modal' } do %>
|
<%= link_to submissions_filter_path('author', query_params.merge(path: url_for)), data: { turbo_frame: 'modal' } do %>
|
||||||
<%= svg_icon('user', class: 'w-5 h-5 flex-shrink-0 stroke-2') %>
|
<%= svg_icon('user', class: 'w-5 h-5 flex-shrink-0 stroke-2') %>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<%= render 'shared/turbo_modal', title: local_assigns[:title] do %>
|
<%= render 'shared/turbo_modal', title: local_assigns[:title] do %>
|
||||||
<%= form_for '', url: params[:path], method: :get, data: { turbo_frame: :_top }, html: { autocomplete: :off } do |f| %>
|
<%= form_for '', url: params[:path], method: :get, data: { turbo_frame: :_top }, html: { autocomplete: :off } do |f| %>
|
||||||
<%= hidden_field_tag :status, params[:status] if params[:status].present? %>
|
|
||||||
<%= hidden_field_tag :q, params[:q] if params[:q].present? %>
|
<%= hidden_field_tag :q, params[:q] if params[:q].present? %>
|
||||||
<% local_assigns[:default_params].each do |key, value| %>
|
<% local_assigns[:default_params].each do |key, value| %>
|
||||||
<%= hidden_field_tag(key, value) if value.present? %>
|
<%= hidden_field_tag(key, value) if value.present? %>
|
||||||
@@ -11,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<% if params[:with_remove] %>
|
<% if params[:with_remove] %>
|
||||||
<div class="text-center w-full mt-4">
|
<div class="text-center w-full mt-4">
|
||||||
<%= link_to t('remove_filter'), "#{params[:path]}?#{params.to_unsafe_h.slice(:q, :status).merge(local_assigns[:default_params]).to_query}", class: 'link', data: { turbo_frame: :_top } %>
|
<%= link_to t('remove_filter'), "#{params[:path]}?#{params.to_unsafe_h.slice(:q).merge(local_assigns[:default_params]).to_query}", class: 'link', data: { turbo_frame: :_top } %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<%= render 'filter_modal', title: t('status'), default_params: params.permit(*(Submissions::Filter::ALLOWED_PARAMS - %w[status])) do %>
|
||||||
|
<div class="flex flex-col md:flex-row gap-2 mt-5">
|
||||||
|
<div class="form-control w-full">
|
||||||
|
<div id="status" class="radio-select grid grid-cols-2 gap-2 px-1">
|
||||||
|
<% ['', 'pending', 'completed', 'partially_completed', 'declined', 'expired'].each do |status| %>
|
||||||
|
<label class="radio-label cursor-pointer inline-flex items-center space-x-2">
|
||||||
|
<%= radio_button_tag 'status', status, params[:status] == status || (status == '' && params[:status].blank?), class: 'base-radio' %>
|
||||||
|
<span><%= t(status.presence || 'all') %></span>
|
||||||
|
</label>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
@@ -20,6 +20,8 @@ en: &en
|
|||||||
language_ko: 한국어
|
language_ko: 한국어
|
||||||
hi_there: Hi there
|
hi_there: Hi there
|
||||||
thanks: Thanks
|
thanks: Thanks
|
||||||
|
pending_by_me: Pending by me
|
||||||
|
partially_completed: Partially completed
|
||||||
unarchive: Unarchive
|
unarchive: Unarchive
|
||||||
first_party: 'First Party'
|
first_party: 'First Party'
|
||||||
remove_filter: Remove filter
|
remove_filter: Remove filter
|
||||||
@@ -660,6 +662,7 @@ en: &en
|
|||||||
policy_links: Policy Links
|
policy_links: Policy Links
|
||||||
markdown_content_e_g: Markdown content, e.g.
|
markdown_content_e_g: Markdown content, e.g.
|
||||||
privacy_policy: Privacy Policy
|
privacy_policy: Privacy Policy
|
||||||
|
use_the_edit_form_to_move_it_to_another_team: Use the edit form to move it to another team.
|
||||||
submission_event_names:
|
submission_event_names:
|
||||||
send_email_to_html: '<b>Email sent</b> to %{submitter_name}'
|
send_email_to_html: '<b>Email sent</b> to %{submitter_name}'
|
||||||
send_reminder_email_to_html: '<b>Reminder email sent</b> to %{submitter_name}'
|
send_reminder_email_to_html: '<b>Reminder email sent</b> to %{submitter_name}'
|
||||||
@@ -698,6 +701,8 @@ en: &en
|
|||||||
read: Read your data
|
read: Read your data
|
||||||
|
|
||||||
es: &es
|
es: &es
|
||||||
|
partially_completed: Parcialmente completado
|
||||||
|
pending_by_me: Pendiente por mi
|
||||||
add: Agregar
|
add: Agregar
|
||||||
adding: Agregando
|
adding: Agregando
|
||||||
owner: Propietario
|
owner: Propietario
|
||||||
@@ -1340,6 +1345,7 @@ es: &es
|
|||||||
policy_links: Enlaces de Políticas
|
policy_links: Enlaces de Políticas
|
||||||
markdown_content_e_g: Contenido Markdown, por ej.
|
markdown_content_e_g: Contenido Markdown, por ej.
|
||||||
privacy_policy: Política de Privacidad
|
privacy_policy: Política de Privacidad
|
||||||
|
use_the_edit_form_to_move_it_to_another_team: Usa el formulario de edición para moverlo a otro equipo.
|
||||||
submission_event_names:
|
submission_event_names:
|
||||||
send_email_to_html: '<b>Correo electrónico enviado</b> a %{submitter_name}'
|
send_email_to_html: '<b>Correo electrónico enviado</b> a %{submitter_name}'
|
||||||
send_reminder_email_to_html: '<b>Correo de recordatorio enviado</b> a %{submitter_name}'
|
send_reminder_email_to_html: '<b>Correo de recordatorio enviado</b> a %{submitter_name}'
|
||||||
@@ -1378,6 +1384,7 @@ es: &es
|
|||||||
read: Leer tus datos
|
read: Leer tus datos
|
||||||
|
|
||||||
it: &it
|
it: &it
|
||||||
|
pending_by_me: In sospeso da me
|
||||||
add: Aggiungi
|
add: Aggiungi
|
||||||
adding: Aggiungendo
|
adding: Aggiungendo
|
||||||
owner: Proprietario
|
owner: Proprietario
|
||||||
@@ -2020,6 +2027,7 @@ it: &it
|
|||||||
policy_links: Collegamenti alle Politiche
|
policy_links: Collegamenti alle Politiche
|
||||||
markdown_content_e_g: Contenuto Markdown, ad es.
|
markdown_content_e_g: Contenuto Markdown, ad es.
|
||||||
privacy_policy: Politica sulla Privacy
|
privacy_policy: Politica sulla Privacy
|
||||||
|
use_the_edit_form_to_move_it_to_another_team: Usa il modulo di modifica per spostarlo in un altro team.
|
||||||
submission_event_names:
|
submission_event_names:
|
||||||
send_email_to_html: '<b>E-mail inviato</b> a %{submitter_name}'
|
send_email_to_html: '<b>E-mail inviato</b> a %{submitter_name}'
|
||||||
send_reminder_email_to_html: '<b>E-mail di promemoria inviato</b> a %{submitter_name}'
|
send_reminder_email_to_html: '<b>E-mail di promemoria inviato</b> a %{submitter_name}'
|
||||||
@@ -2058,6 +2066,8 @@ it: &it
|
|||||||
read: Leggi i tuoi dati
|
read: Leggi i tuoi dati
|
||||||
|
|
||||||
fr: &fr
|
fr: &fr
|
||||||
|
partially_completed: Partiellement complété
|
||||||
|
pending_by_me: En attente par moi
|
||||||
add: Ajouter
|
add: Ajouter
|
||||||
adding: Ajout
|
adding: Ajout
|
||||||
owner: Propriétaire
|
owner: Propriétaire
|
||||||
@@ -2701,6 +2711,7 @@ fr: &fr
|
|||||||
policy_links: Liens des Politiques
|
policy_links: Liens des Politiques
|
||||||
markdown_content_e_g: Contenu Markdown, par ex.
|
markdown_content_e_g: Contenu Markdown, par ex.
|
||||||
privacy_policy: Politique de Confidentialité
|
privacy_policy: Politique de Confidentialité
|
||||||
|
use_the_edit_form_to_move_it_to_another_team: Utilisez le formulaire de modification pour le déplacer vers une autre équipe.
|
||||||
submission_event_names:
|
submission_event_names:
|
||||||
send_email_to_html: '<b>E-mail envoyé</b> à %{submitter_name}'
|
send_email_to_html: '<b>E-mail envoyé</b> à %{submitter_name}'
|
||||||
send_reminder_email_to_html: '<b>E-mail de rappel envoyé</b> à %{submitter_name}'
|
send_reminder_email_to_html: '<b>E-mail de rappel envoyé</b> à %{submitter_name}'
|
||||||
@@ -2739,6 +2750,8 @@ fr: &fr
|
|||||||
read: Lire vos données
|
read: Lire vos données
|
||||||
|
|
||||||
pt: &pt
|
pt: &pt
|
||||||
|
partially_completed: Parcialmente concluído
|
||||||
|
pending_by_me: Pendente por mim
|
||||||
add: Adicionar
|
add: Adicionar
|
||||||
adding: Adicionando
|
adding: Adicionando
|
||||||
owner: Proprietário
|
owner: Proprietário
|
||||||
@@ -3381,6 +3394,7 @@ pt: &pt
|
|||||||
policy_links: Links de Políticas
|
policy_links: Links de Políticas
|
||||||
markdown_content_e_g: Conteúdo Markdown, ex.
|
markdown_content_e_g: Conteúdo Markdown, ex.
|
||||||
privacy_policy: Política de Privacidade
|
privacy_policy: Política de Privacidade
|
||||||
|
use_the_edit_form_to_move_it_to_another_team: Use o formulário de edição para movê-lo para outra equipe.
|
||||||
submission_event_names:
|
submission_event_names:
|
||||||
send_email_to_html: '<b>E-mail enviado</b> para %{submitter_name}'
|
send_email_to_html: '<b>E-mail enviado</b> para %{submitter_name}'
|
||||||
send_reminder_email_to_html: '<b>E-mail de lembrete enviado</b> para %{submitter_name}'
|
send_reminder_email_to_html: '<b>E-mail de lembrete enviado</b> para %{submitter_name}'
|
||||||
@@ -3419,6 +3433,8 @@ pt: &pt
|
|||||||
read: Ler seus dados
|
read: Ler seus dados
|
||||||
|
|
||||||
de: &de
|
de: &de
|
||||||
|
partially_completed: Teilweise abgeschlossen
|
||||||
|
pending_by_me: Ausstehend von mir
|
||||||
add: Hinzufügen
|
add: Hinzufügen
|
||||||
adding: Hinzufügen
|
adding: Hinzufügen
|
||||||
owner: Eigentümer
|
owner: Eigentümer
|
||||||
@@ -4061,6 +4077,7 @@ de: &de
|
|||||||
policy_links: Richtlinien-Links
|
policy_links: Richtlinien-Links
|
||||||
markdown_content_e_g: Markdown-Inhalt, z. B.
|
markdown_content_e_g: Markdown-Inhalt, z. B.
|
||||||
privacy_policy: Datenschutzrichtlinie
|
privacy_policy: Datenschutzrichtlinie
|
||||||
|
use_the_edit_form_to_move_it_to_another_team: Verwenden Sie das Bearbeitungsformular, um ihn in ein anderes Team zu verschieben.
|
||||||
submission_event_names:
|
submission_event_names:
|
||||||
send_email_to_html: '<b>E-Mail gesendet</b> an %{submitter_name}'
|
send_email_to_html: '<b>E-Mail gesendet</b> an %{submitter_name}'
|
||||||
send_reminder_email_to_html: '<b>Erinnerungs-E-Mail gesendet</b> an %{submitter_name}'
|
send_reminder_email_to_html: '<b>Erinnerungs-E-Mail gesendet</b> an %{submitter_name}'
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
queues:
|
queues:
|
||||||
- [default, 1]
|
- [default, 1]
|
||||||
- [webhooks, 1]
|
- [webhooks, 1]
|
||||||
|
- [sms, 2]
|
||||||
- [images, 1]
|
- [images, 1]
|
||||||
- [mailers, 1]
|
- [mailers, 1]
|
||||||
- [recurrent, 1]
|
- [recurrent, 1]
|
||||||
|
|||||||
@@ -89,12 +89,13 @@ module Params
|
|||||||
raise_error(message || "#{key} must be unique")
|
raise_error(message || "#{key} must be unique")
|
||||||
end
|
end
|
||||||
|
|
||||||
def in_path(params, path = [])
|
def in_path(params, path = [], skip_blank: false)
|
||||||
old_path = @current_path
|
old_path = @current_path
|
||||||
|
|
||||||
@current_path = [old_path, *path].compact_blank.map(&:to_s).join('.')
|
@current_path = [old_path, *path].compact_blank.map(&:to_s).join('.')
|
||||||
|
|
||||||
param = params.dig(*path)
|
param = params.dig(*path)
|
||||||
|
param = nil if skip_blank && param.blank?
|
||||||
|
|
||||||
yield params.dig(*path) if param
|
yield params.dig(*path) if param
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ module Params
|
|||||||
type(params, :message, Hash)
|
type(params, :message, Hash)
|
||||||
type(params, :submitters, Array)
|
type(params, :submitters, Array)
|
||||||
|
|
||||||
in_path(params, :message) do |message_params|
|
in_path(params, :message, skip_blank: true) do |message_params|
|
||||||
type(message_params, :subject, String)
|
type(message_params, :subject, String)
|
||||||
type(message_params, :body, String)
|
type(message_params, :body, String)
|
||||||
|
|
||||||
|
|||||||
+51
-13
@@ -4,6 +4,7 @@ module Submissions
|
|||||||
module Filter
|
module Filter
|
||||||
ALLOWED_PARAMS = %w[
|
ALLOWED_PARAMS = %w[
|
||||||
author
|
author
|
||||||
|
status
|
||||||
completed_at_from
|
completed_at_from
|
||||||
completed_at_to
|
completed_at_to
|
||||||
created_at_from
|
created_at_from
|
||||||
@@ -22,31 +23,68 @@ module Submissions
|
|||||||
def call(submissions, current_user, params)
|
def call(submissions, current_user, params)
|
||||||
filters = normalize_filter_params(params, current_user)
|
filters = normalize_filter_params(params, current_user)
|
||||||
|
|
||||||
if filters[:author].present?
|
submissions = filter_by_author(submissions, filters, current_user)
|
||||||
user = current_user.account.users.find_by(email: filters[:author])
|
submissions = filter_by_status(submissions, filters)
|
||||||
submissions = submissions.where(created_by_user_id: user&.id || -1)
|
submissions = filter_by_created_at(submissions, filters)
|
||||||
|
|
||||||
|
filter_by_completed_at(submissions, filters)
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_by_author(submissions, filters, current_user)
|
||||||
|
return submissions if filters[:author].blank?
|
||||||
|
|
||||||
|
user = current_user.account.users.find_by(email: filters[:author])
|
||||||
|
submissions.where(created_by_user_id: user&.id || -1)
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_by_status(submissions, filters)
|
||||||
|
submissions = submissions.pending if filters[:status] == 'pending'
|
||||||
|
submissions = submissions.completed if filters[:status] == 'completed'
|
||||||
|
submissions = submissions.declined if filters[:status] == 'declined'
|
||||||
|
submissions = submissions.expired if filters[:status] == 'expired'
|
||||||
|
|
||||||
|
if filters[:status] == 'partially_completed'
|
||||||
|
submissions =
|
||||||
|
submissions.joins(:submitters)
|
||||||
|
.group(:id)
|
||||||
|
.having(Arel::Nodes::NamedFunction.new(
|
||||||
|
'COUNT', [Arel::Nodes::NamedFunction.new('NULLIF',
|
||||||
|
[Submitter.arel_table[:completed_at].eq(nil),
|
||||||
|
Arel::Nodes.build_quoted(false)])]
|
||||||
|
).gt(0))
|
||||||
|
.having(Arel::Nodes::NamedFunction.new(
|
||||||
|
'COUNT', [Arel::Nodes::NamedFunction.new('NULLIF',
|
||||||
|
[Submitter.arel_table[:completed_at].not_eq(nil),
|
||||||
|
Arel::Nodes.build_quoted(false)])]
|
||||||
|
).gt(0))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
submissions
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_by_created_at(submissions, filters)
|
||||||
submissions = submissions.where(created_at: filters[:created_at_from]..) if filters[:created_at_from].present?
|
submissions = submissions.where(created_at: filters[:created_at_from]..) if filters[:created_at_from].present?
|
||||||
|
|
||||||
if filters[:created_at_to].present?
|
if filters[:created_at_to].present?
|
||||||
submissions = submissions.where(created_at: ..filters[:created_at_to].end_of_day)
|
submissions = submissions.where(created_at: ..filters[:created_at_to].end_of_day)
|
||||||
end
|
end
|
||||||
|
|
||||||
if filters[:completed_at_from].present? || filters[:completed_at_to].present?
|
submissions
|
||||||
completed_arel = Submitter.arel_table[:completed_at].maximum
|
end
|
||||||
submissions = submissions.completed.joins(:submitters).group(:id)
|
|
||||||
|
|
||||||
if filters[:completed_at_from].present?
|
def filter_by_completed_at(submissions, filters)
|
||||||
submissions = submissions.having(completed_arel.gteq(filters[:completed_at_from]))
|
return submissions unless filters[:completed_at_from].present? || filters[:completed_at_to].present?
|
||||||
end
|
|
||||||
|
|
||||||
if filters[:completed_at_to].present?
|
completed_arel = Submitter.arel_table[:completed_at].maximum
|
||||||
submissions = submissions.having(completed_arel.lteq(filters[:completed_at_to].end_of_day))
|
submissions = submissions.completed.joins(:submitters).group(:id)
|
||||||
end
|
|
||||||
|
if filters[:completed_at_from].present?
|
||||||
|
submissions = submissions.having(completed_arel.gteq(filters[:completed_at_from]))
|
||||||
end
|
end
|
||||||
|
|
||||||
submissions
|
return submissions if filters[:completed_at_to].blank?
|
||||||
|
|
||||||
|
submissions.having(completed_arel.lteq(filters[:completed_at_to].end_of_day))
|
||||||
end
|
end
|
||||||
|
|
||||||
def normalize_filter_params(params, current_user)
|
def normalize_filter_params(params, current_user)
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ module Submissions
|
|||||||
|
|
||||||
reason_string =
|
reason_string =
|
||||||
I18n.with_locale(submitter.account.locale) do
|
I18n.with_locale(submitter.account.locale) do
|
||||||
"#{I18n.t('reason')}: #{reason_value || I18n.t('digitally_signed_by')} " \
|
"#{reason_value ? "#{I18n.t('reason')}: " : ''}#{reason_value || I18n.t('digitally_signed_by')} " \
|
||||||
"#{submitter.name}#{submitter.email.present? ? " <#{submitter.email}>" : ''}\n" \
|
"#{submitter.name}#{submitter.email.present? ? " <#{submitter.email}>" : ''}\n" \
|
||||||
"#{I18n.l(attachment.created_at.in_time_zone(submitter.account.timezone), format: :long)} " \
|
"#{I18n.l(attachment.created_at.in_time_zone(submitter.account.timezone), format: :long)} " \
|
||||||
"#{TimeUtils.timezone_abbr(submitter.account.timezone, attachment.created_at)}"
|
"#{TimeUtils.timezone_abbr(submitter.account.timezone, attachment.created_at)}"
|
||||||
@@ -382,7 +382,10 @@ module Submissions
|
|||||||
when ->(type) { type == 'cells' && !area['cell_w'].to_f.zero? }
|
when ->(type) { type == 'cells' && !area['cell_w'].to_f.zero? }
|
||||||
cell_width = area['cell_w'] * width
|
cell_width = area['cell_w'] * width
|
||||||
|
|
||||||
TextUtils.maybe_rtl_reverse(value).chars.each_with_index do |char, index|
|
chars = TextUtils.maybe_rtl_reverse(value).chars
|
||||||
|
chars = chars.reverse if field.dig('preferences', 'align') == 'right'
|
||||||
|
|
||||||
|
chars.each_with_index do |char, index|
|
||||||
next if char.blank?
|
next if char.blank?
|
||||||
|
|
||||||
text = HexaPDF::Layout::TextFragment.create(char, font:,
|
text = HexaPDF::Layout::TextFragment.create(char, font:,
|
||||||
@@ -409,9 +412,15 @@ module Submissions
|
|||||||
line_height = layouter.fit([text], cell_width, height).lines.first.height
|
line_height = layouter.fit([text], cell_width, height).lines.first.height
|
||||||
end
|
end
|
||||||
|
|
||||||
|
x =
|
||||||
|
if field.dig('preferences', 'align') == 'right'
|
||||||
|
((area['x'] + area['w']) * width) - (cell_width * (index + 1))
|
||||||
|
else
|
||||||
|
(area['x'] * width) + (cell_width * index)
|
||||||
|
end
|
||||||
|
|
||||||
cell_layouter.fit([text], cell_width, [line_height, area['h'] * height].max)
|
cell_layouter.fit([text], cell_width, [line_height, area['h'] * height].max)
|
||||||
.draw(canvas, ((area['x'] * width) + (cell_width * index)),
|
.draw(canvas, x, height - (area['y'] * height))
|
||||||
height - (area['y'] * height))
|
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
if field['type'] == 'date'
|
if field['type'] == 'date'
|
||||||
|
|||||||
+29
-13
@@ -10,24 +10,32 @@ module Templates
|
|||||||
template.external_id = external_id
|
template.external_id = external_id
|
||||||
template.author = author
|
template.author = author
|
||||||
template.preferences = original_template.preferences.deep_dup
|
template.preferences = original_template.preferences.deep_dup
|
||||||
template.name = name || "#{original_template.name} (#{I18n.t('clone')})"
|
template.name = name.presence || "#{original_template.name} (#{I18n.t('clone')})"
|
||||||
|
|
||||||
template.assign_attributes(original_template.slice(:folder_id, :schema))
|
if folder_name.present?
|
||||||
|
template.folder = TemplateFolders.find_or_create_by_name(author, folder_name)
|
||||||
|
else
|
||||||
|
template.folder_id = original_template.folder_id
|
||||||
|
end
|
||||||
|
|
||||||
template.folder = TemplateFolders.find_or_create_by_name(author, folder_name) if folder_name.present?
|
template.submitters, template.fields, template.schema =
|
||||||
|
update_submitters_and_fields_and_schema(original_template.submitters.deep_dup,
|
||||||
|
original_template.fields.deep_dup,
|
||||||
|
original_template.schema.deep_dup)
|
||||||
|
|
||||||
template.submitters, template.fields = clone_submitters_and_fields(original_template)
|
if name.present? && template.schema.size == 1 &&
|
||||||
|
original_template.schema.first['name'] == original_template.name &&
|
||||||
|
template.name != "#{original_template.name} (#{I18n.t('clone')})"
|
||||||
|
template.schema.first['name'] = template.name
|
||||||
|
end
|
||||||
|
|
||||||
template
|
template
|
||||||
end
|
end
|
||||||
|
|
||||||
def clone_submitters_and_fields(original_template)
|
def update_submitters_and_fields_and_schema(cloned_submitters, cloned_fields, cloned_schema)
|
||||||
submitter_uuids_replacements = {}
|
submitter_uuids_replacements = {}
|
||||||
field_uuids_replacements = {}
|
field_uuids_replacements = {}
|
||||||
|
|
||||||
cloned_submitters = original_template['submitters'].deep_dup
|
|
||||||
cloned_fields = original_template['fields'].deep_dup
|
|
||||||
|
|
||||||
cloned_submitters.each do |submitter|
|
cloned_submitters.each do |submitter|
|
||||||
new_submitter_uuid = SecureRandom.uuid
|
new_submitter_uuid = SecureRandom.uuid
|
||||||
|
|
||||||
@@ -44,20 +52,28 @@ module Templates
|
|||||||
field['submitter_uuid'] = submitter_uuids_replacements[field['submitter_uuid']]
|
field['submitter_uuid'] = submitter_uuids_replacements[field['submitter_uuid']]
|
||||||
end
|
end
|
||||||
|
|
||||||
replace_fields_regexp = Regexp.union(field_uuids_replacements.keys)
|
replace_fields_regexp = nil
|
||||||
|
|
||||||
cloned_fields.each do |field|
|
cloned_fields.each do |field|
|
||||||
Array.wrap(field['conditions']).each do |condition|
|
Array.wrap(field['conditions']).each do |condition|
|
||||||
condition['field_uuid'] = field_uuids_replacements[condition['field_uuid']]
|
condition['field_uuid'] = field_uuids_replacements[condition['field_uuid']]
|
||||||
end
|
end
|
||||||
|
|
||||||
if field.dig('preferences', 'formula').present?
|
next if field.dig('preferences', 'formula').blank?
|
||||||
field['preferences']['formula'] =
|
|
||||||
field['preferences']['formula'].gsub(replace_fields_regexp, field_uuids_replacements)
|
replace_fields_regexp ||= Regexp.union(field_uuids_replacements.keys)
|
||||||
|
|
||||||
|
field['preferences']['formula'] =
|
||||||
|
field['preferences']['formula'].gsub(replace_fields_regexp, field_uuids_replacements)
|
||||||
|
end
|
||||||
|
|
||||||
|
cloned_schema.each do |field|
|
||||||
|
Array.wrap(field['conditions']).each do |condition|
|
||||||
|
condition['field_uuid'] = field_uuids_replacements[condition['field_uuid']]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
[cloned_submitters, cloned_fields]
|
[cloned_submitters, cloned_fields, cloned_schema]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,20 +4,21 @@ module Templates
|
|||||||
module CloneAttachments
|
module CloneAttachments
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
def call(template:, original_template:)
|
def call(template:, original_template:, documents: [])
|
||||||
schema_uuids_replacements = {}
|
schema_uuids_replacements = {}
|
||||||
|
|
||||||
cloned_schema = original_template.schema.deep_dup
|
template.schema.each_with_index do |schema_item, index|
|
||||||
cloned_fields = template.fields.deep_dup
|
|
||||||
|
|
||||||
cloned_schema.each do |schema_item|
|
|
||||||
new_schema_item_uuid = SecureRandom.uuid
|
new_schema_item_uuid = SecureRandom.uuid
|
||||||
|
|
||||||
schema_uuids_replacements[schema_item['attachment_uuid']] = new_schema_item_uuid
|
schema_uuids_replacements[schema_item['attachment_uuid']] = new_schema_item_uuid
|
||||||
schema_item['attachment_uuid'] = new_schema_item_uuid
|
schema_item['attachment_uuid'] = new_schema_item_uuid
|
||||||
|
|
||||||
|
new_name = documents&.dig(index, 'name')
|
||||||
|
|
||||||
|
schema_item['name'] = new_name if new_name.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
cloned_fields.each do |field|
|
template.fields.each do |field|
|
||||||
next if field['areas'].blank?
|
next if field['areas'].blank?
|
||||||
|
|
||||||
field['areas'].each do |area|
|
field['areas'].each do |area|
|
||||||
@@ -25,7 +26,7 @@ module Templates
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
template.update!(schema: cloned_schema, fields: cloned_fields)
|
template.save!
|
||||||
|
|
||||||
original_template.schema_documents.map do |document|
|
original_template.schema_documents.map do |document|
|
||||||
new_document =
|
new_document =
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ FactoryBot.define do
|
|||||||
locale { 'en-US' }
|
locale { 'en-US' }
|
||||||
timezone { 'UTC' }
|
timezone { 'UTC' }
|
||||||
|
|
||||||
|
transient do
|
||||||
|
teams_count { 2 }
|
||||||
|
end
|
||||||
|
|
||||||
trait :with_testing_account do
|
trait :with_testing_account do
|
||||||
after(:create) do |account|
|
after(:create) do |account|
|
||||||
testing_account = account.dup.tap { |a| a.name = "Testing - #{account.name}" }
|
testing_account = account.dup.tap { |a| a.name = "Testing - #{account.name}" }
|
||||||
@@ -14,5 +18,16 @@ FactoryBot.define do
|
|||||||
account.save!
|
account.save!
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
trait :with_teams do
|
||||||
|
after(:create) do |account, evaluator|
|
||||||
|
Array.new(evaluator.teams_count) do |i|
|
||||||
|
Account.create!(
|
||||||
|
name: "Team #{i}",
|
||||||
|
linked_account_account: AccountLinkedAccount.new(account_type: :linked, account:)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -68,6 +68,10 @@ RSpec.configure do |config|
|
|||||||
config.before do |example|
|
config.before do |example|
|
||||||
Sidekiq::Testing.inline! if example.metadata[:sidekiq] == :inline
|
Sidekiq::Testing.inline! if example.metadata[:sidekiq] == :inline
|
||||||
end
|
end
|
||||||
|
|
||||||
|
config.before(multitenant: true) do
|
||||||
|
allow(Docuseal).to receive(:multitenant?).and_return(true)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
ActiveSupport.run_load_hooks(:rails_specs, self)
|
ActiveSupport.run_load_hooks(:rails_specs, self)
|
||||||
|
|||||||
@@ -89,6 +89,21 @@ describe 'Submission API', type: :request do
|
|||||||
expect(response.parsed_body).to eq(JSON.parse(create_submission_body(submission).to_json))
|
expect(response.parsed_body).to eq(JSON.parse(create_submission_body(submission).to_json))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'creates a submission when the message is empty' do
|
||||||
|
post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: {
|
||||||
|
template_id: templates[0].id,
|
||||||
|
send_email: true,
|
||||||
|
submitters: [{ role: 'First Party', email: 'john.doe@example.com' }],
|
||||||
|
message: {}
|
||||||
|
}.to_json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
|
submission = Submission.last
|
||||||
|
|
||||||
|
expect(response.parsed_body).to eq(JSON.parse(create_submission_body(submission).to_json))
|
||||||
|
end
|
||||||
|
|
||||||
it 'creates a submission when some submitter roles are not provided' do
|
it 'creates a submission when some submitter roles are not provided' do
|
||||||
post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: {
|
post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: {
|
||||||
template_id: multiple_submitters_template.id,
|
template_id: multiple_submitters_template.id,
|
||||||
@@ -168,6 +183,22 @@ describe 'Submission API', type: :request do
|
|||||||
expect(response).to have_http_status(:unprocessable_entity)
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
expect(response.parsed_body).to eq({ 'error' => 'Defined more signing parties than in template' })
|
expect(response.parsed_body).to eq({ 'error' => 'Defined more signing parties than in template' })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'returns an error if the message has no body value' do
|
||||||
|
post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: {
|
||||||
|
template_id: templates[0].id,
|
||||||
|
send_email: true,
|
||||||
|
submitters: [
|
||||||
|
{ role: 'First Party', email: 'john.doe@example.com' }
|
||||||
|
],
|
||||||
|
message: {
|
||||||
|
subject: 'Custom Email Subject'
|
||||||
|
}
|
||||||
|
}.to_json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
expect(response.parsed_body).to eq({ 'error' => 'body is required in `message`.' })
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'POST /api/submissions/emails' do
|
describe 'POST /api/submissions/emails' do
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ require 'rails_helper'
|
|||||||
|
|
||||||
RSpec.describe 'Team Settings' do
|
RSpec.describe 'Team Settings' do
|
||||||
let(:account) { create(:account) }
|
let(:account) { create(:account) }
|
||||||
|
let(:second_account) { create(:account) }
|
||||||
let(:current_user) { create(:user, account:) }
|
let(:current_user) { create(:user, account:) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
@@ -56,6 +57,43 @@ RSpec.describe 'Team Settings' do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "doesn't create a new user if a user already exists" do
|
||||||
|
click_link 'New User'
|
||||||
|
|
||||||
|
within '#modal' do
|
||||||
|
fill_in 'First name', with: 'Michael'
|
||||||
|
fill_in 'Last name', with: 'Jordan'
|
||||||
|
fill_in 'Email', with: users.first.email
|
||||||
|
fill_in 'Password', with: 'password'
|
||||||
|
|
||||||
|
expect do
|
||||||
|
click_button 'Submit'
|
||||||
|
end.not_to change(User, :count)
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(page).to have_content('Email already exists')
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't create a new user if a user belongs to another account" do
|
||||||
|
user = create(:user, account: second_account)
|
||||||
|
visit settings_users_path
|
||||||
|
|
||||||
|
click_link 'New User'
|
||||||
|
|
||||||
|
within '#modal' do
|
||||||
|
fill_in 'First name', with: 'Michael'
|
||||||
|
fill_in 'Last name', with: 'Jordan'
|
||||||
|
fill_in 'Email', with: user.email
|
||||||
|
fill_in 'Password', with: 'password'
|
||||||
|
|
||||||
|
expect do
|
||||||
|
click_button 'Submit'
|
||||||
|
end.not_to change(User, :count)
|
||||||
|
|
||||||
|
expect(page).to have_content('Email has already been taken')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it 'updates a user' do
|
it 'updates a user' do
|
||||||
first(:link, 'Edit').click
|
first(:link, 'Edit').click
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user