diff --git a/Gemfile b/Gemfile index b0974208..68f2b403 100644 --- a/Gemfile +++ b/Gemfile @@ -2,7 +2,7 @@ source 'https://rubygems.org' -ruby '3.4.2' +ruby '~> 3.2.0' gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic' gem 'aws-sdk-s3', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 72f12d84..fa2f9371 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -685,7 +685,7 @@ DEPENDENCIES webmock RUBY VERSION - ruby 3.4.2p28 + ruby 3.2.2p53 BUNDLED WITH 2.5.3 diff --git a/app/controllers/account_logos_controller.rb b/app/controllers/account_logos_controller.rb new file mode 100644 index 00000000..e4079787 --- /dev/null +++ b/app/controllers/account_logos_controller.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class AccountLogosController < ApplicationController + before_action :load_account + authorize_resource :account + + def update + file = params[:file] + + return redirect_to settings_personalization_path, alert: I18n.t('file_is_missing') if file.blank? + + @account.logo.attach( + io: file.open, + filename: file.original_filename, + content_type: file.content_type + ) + + redirect_to settings_personalization_path, notice: I18n.t('logo_has_been_uploaded') + end + + def destroy + @account.logo.purge + + redirect_to settings_personalization_path, notice: I18n.t('logo_has_been_uploaded') + end + + private + + def load_account + @account = current_account + end +end diff --git a/app/controllers/console_redirect_controller.rb b/app/controllers/console_redirect_controller.rb index 4b910263..ab569c80 100644 --- a/app/controllers/console_redirect_controller.rb +++ b/app/controllers/console_redirect_controller.rb @@ -10,9 +10,22 @@ class ConsoleRedirectController < ApplicationController end params[:redir] = "#{Docuseal::CONSOLE_URL}/manage" if request.path == '/manage' + + if request.path == '/sign_up' + params[:redir] = Docuseal.multitenant? ? "#{Docuseal::CONSOLE_URL}/plans" : "#{Docuseal::CONSOLE_URL}/on_premises" + end return redirect_to(new_user_session_path({ redir: params[:redir] }.compact)) if true_user.blank? + # In development, if console URL is localhost and doesn't exist, redirect to cloud URL instead + if Rails.env.development? && Docuseal::CONSOLE_URL.include?('localhost') && !Docuseal.multitenant? + if params[:redir].to_s.include?('/on_premises') + return redirect_to 'https://console.docuseal.com/on_premises', allow_other_host: true + elsif params[:redir].to_s.include?('/plans') + return redirect_to 'https://console.docuseal.com/plans', allow_other_host: true + end + end + auth = JsonWebToken.encode(uuid: true_user.uuid, scope: :console, exp: 1.minute.from_now.to_i) @@ -20,7 +33,8 @@ class ConsoleRedirectController < ApplicationController redir_uri = Addressable::URI.parse(params[:redir]) path = redir_uri.path if params[:redir].to_s.starts_with?(Docuseal::CONSOLE_URL) - redirect_to "#{Docuseal::CONSOLE_URL}#{path}?#{{ **redir_uri&.query_values, 'auth' => auth }.to_query}", + query_values = redir_uri&.query_values || {} + redirect_to "#{Docuseal::CONSOLE_URL}#{path}?#{{ **query_values, 'auth' => auth }.to_query}", allow_other_host: true end end diff --git a/app/controllers/submissions_archived_controller.rb b/app/controllers/submissions_archived_controller.rb index 793da755..acec1dfe 100644 --- a/app/controllers/submissions_archived_controller.rb +++ b/app/controllers/submissions_archived_controller.rb @@ -7,7 +7,7 @@ class SubmissionsArchivedController < ApplicationController @submissions = @submissions.left_joins(:template) @submissions = @submissions.where.not(archived_at: nil) .or(@submissions.where.not(templates: { archived_at: nil })) - .preload(:template_accesses, :created_by_user, template: :author) + .preload(:created_by_user, template: :author) @submissions = Submissions.search(current_user, @submissions, params[:q], search_template: true) @submissions = Submissions::Filter.call(@submissions, current_user, params) diff --git a/app/controllers/submissions_dashboard_controller.rb b/app/controllers/submissions_dashboard_controller.rb index f0851741..884fb146 100644 --- a/app/controllers/submissions_dashboard_controller.rb +++ b/app/controllers/submissions_dashboard_controller.rb @@ -8,7 +8,7 @@ class SubmissionsDashboardController < ApplicationController @submissions = @submissions.where(archived_at: nil) .where(templates: { archived_at: nil }) - .preload(:template_accesses, :created_by_user, template: :author) + .preload(:created_by_user, template: :author) @submissions = Submissions.search(current_user, @submissions, params[:q], search_template: true) @submissions = Submissions::Filter.call(@submissions, current_user, params) diff --git a/app/controllers/template_folders_controller.rb b/app/controllers/template_folders_controller.rb index 25e489ce..fdd913d2 100644 --- a/app/controllers/template_folders_controller.rb +++ b/app/controllers/template_folders_controller.rb @@ -11,7 +11,6 @@ class TemplateFoldersController < ApplicationController def show @templates = Template.active.accessible_by(current_ability) .where(folder: [@template_folder, *(params[:q].present? ? @template_folder.subfolders : [])]) - .preload(:author, :template_accesses) @template_folders = @template_folder.subfolders.where(id: Template.accessible_by(current_ability).active.select(:folder_id)) @@ -22,6 +21,7 @@ class TemplateFoldersController < ApplicationController if @templates.exists? @templates = Templates.search(current_user, @templates, params[:q]) @templates = Templates::Order.call(@templates, current_user, selected_order) + @templates = @templates.preload(:author, :template_accesses) limit = if @template_folders.size < 4 diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb index 39b45044..b5fb1df5 100644 --- a/app/controllers/templates_controller.rb +++ b/app/controllers/templates_controller.rb @@ -19,7 +19,7 @@ class TemplatesController < ApplicationController submissions.order(id: :desc) end - @pagy, @submissions = pagy_auto(submissions.preload(:template_accesses, submitters: :start_form_submission_events)) + @pagy, @submissions = pagy_auto(submissions.preload(submitters: :start_form_submission_events)) rescue ActiveRecord::RecordNotFound redirect_to root_path end diff --git a/app/javascript/application.js b/app/javascript/application.js index 4b5988c9..ca83685e 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -155,7 +155,7 @@ safeRegisterElement('template-builder', class extends HTMLElement { this.app = createApp(TemplateBuilder, { template: reactive(JSON.parse(this.dataset.template)), - backgroundColor: '#faf7f5', + backgroundColor: '#FFFFFF', locale: this.dataset.locale, withPhone: this.dataset.withPhone === 'true', withVerification: ['true', 'false'].includes(this.dataset.withVerification) ? this.dataset.withVerification === 'true' : null, @@ -171,9 +171,10 @@ safeRegisterElement('template-builder', class extends HTMLElement { withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false', withConditions: this.dataset.withConditions === 'true', withGoogleDrive: this.dataset.withGoogleDrive === 'true', + withAddPageButton: true, withReplaceAndCloneUpload: true, withDownload: true, - currencies: (this.dataset.currencies || '').split(',').filter(Boolean), + caurrencies: (this.dataset.currencies || '').split(',').filter(Boolean), acceptFileTypes: this.dataset.acceptFileTypes, showTourStartForm: this.dataset.showTourStartForm === 'true' }) diff --git a/app/javascript/application.scss b/app/javascript/application.scss index 47eeb4f6..253cd2fa 100644 --- a/app/javascript/application.scss +++ b/app/javascript/application.scss @@ -4,6 +4,11 @@ @import "tailwindcss/components"; @import "tailwindcss/utilities"; +body { + @apply antialiased text-base-content; + font-feature-settings: "cv02", "cv03", "cv04", "cv11"; +} + a[href], input[type='checkbox'], input[type='submit'], @@ -31,50 +36,92 @@ button[disabled] .enabled, button.btn-disabled .enabled { display: none; } -.input-bordered { - @apply border-base-content/20; +/* Form controls - enterprise-grade inputs */ +.input-bordered, +.select-bordered, +.textarea-bordered { + @apply border-base-content/15 transition-all duration-200; } -.select-bordered { - @apply border-base-content/20; +.input-bordered:focus, +.select-bordered:focus, +.textarea-bordered:focus { + @apply border-primary outline-none ring-2 ring-primary/20; } -.textarea-bordered { - @apply border-base-content/20; +.input-bordered:hover:not(:focus):not(:disabled), +.textarea-bordered:hover:not(:focus):not(:disabled) { + @apply border-base-content/25; } -.btn { - @apply no-animation; +.base-input { + @apply input input-bordered bg-white h-11 px-4 text-base rounded-lg font-normal transition-all duration-200; } -.base-input { - @apply input input-bordered bg-white; +.base-input::placeholder { + @apply text-base-content/50; +} + +.base-input:disabled, +.base-input[readonly] { + @apply bg-base-200 cursor-not-allowed opacity-90; } .base-textarea { - @apply textarea textarea-bordered bg-white rounded-3xl; + @apply textarea textarea-bordered bg-white rounded-lg px-4 py-3 text-base font-normal transition-all duration-200 min-h-[100px]; +} + +.base-select { + @apply select base-input w-full font-normal; +} + +/* Form labels */ +.form-control .label { + @apply mb-1.5; +} + +.form-control .label .label-text { + @apply font-medium text-base-content text-sm; +} + +.form-control .label-text-alt { + @apply text-base-content/70 text-sm mt-1; +} + +/* Buttons - clear hierarchy with transitions */ +.btn { + @apply transition-all duration-200 font-medium; } .base-button { - @apply btn btn-neutral text-white text-base; + @apply btn btn-primary text-primary-content text-base px-6 h-11 rounded-lg hover:opacity-90 active:scale-[0.98] focus:outline-none focus:ring-2 focus:ring-primary/30 focus:ring-offset-2; } .white-button { - @apply btn btn-outline text-base bg-white border-2; + @apply btn btn-outline text-base bg-white border-2 border-base-300 rounded-lg px-6 h-11 hover:bg-base-200 hover:border-base-content/20 active:scale-[0.98] focus:outline-none focus:ring-2 focus:ring-primary/20 focus:ring-offset-2 transition-all duration-200; +} + +.btn-neutral, +.btn-primary { + @apply transition-all duration-200 hover:opacity-90 active:scale-[0.98] focus:outline-none focus:ring-2 focus:ring-offset-2; +} + +.btn-primary { + @apply focus:ring-primary/30; +} + +.btn-neutral { + @apply focus:ring-neutral/20; } .base-checkbox { - @apply checkbox rounded bg-white checkbox-sm no-animation; + @apply checkbox rounded bg-white checkbox-sm no-animation transition-colors duration-200; } .base-radio { @apply radio bg-white radio-sm no-animation; } -.base-select { - @apply select base-input w-full font-normal; -} - .tooltip-bottom-end:before { transform: translateX(-95%); top: var(--tooltip-offset); @@ -117,10 +164,10 @@ button[disabled] .enabled, button.btn-disabled .enabled { .autocomplete { background: white; z-index: 1000; - font: 16px/25px "-apple-system", BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + font: inherit; overflow: auto; box-sizing: border-box; - @apply border border-base-300 mt-1 rounded-md; + @apply border border-base-300 mt-1 rounded-lg shadow-soft-lg; } .autocomplete * { @@ -128,16 +175,16 @@ button[disabled] .enabled, button.btn-disabled .enabled { } .autocomplete > div { - @apply px-2 py-1.5 font-normal; + @apply px-3 py-2.5 font-normal transition-colors duration-150; } .autocomplete .group { - background: #eee; + @apply bg-base-200; } .autocomplete > div:hover:not(.group), .autocomplete > div.selected { - @apply bg-base-300; + @apply bg-base-200; cursor: pointer; } diff --git a/app/javascript/elements/toggle_cookies.js b/app/javascript/elements/toggle_cookies.js index 8d030cb3..788c3d8e 100644 --- a/app/javascript/elements/toggle_cookies.js +++ b/app/javascript/elements/toggle_cookies.js @@ -1,13 +1,16 @@ export default class extends HTMLElement { connectedCallback () { - this.button.addEventListener('click', () => { + this.button.addEventListener('click', (e) => { const expirationDate = new Date() - expirationDate.setFullYear(expirationDate.getFullYear() + 10) - const expires = expirationDate.toUTCString() - document.cookie = this.dataset.key + '=' + this.dataset.value + '; expires=' + expires + '; path=/' + + const form = this.closest('form') + if (form) { + e.preventDefault() + form.requestSubmit(this.button) + } }) } diff --git a/app/javascript/elements/turbo_modal.js b/app/javascript/elements/turbo_modal.js index 69c20549..be7e9726 100644 --- a/app/javascript/elements/turbo_modal.js +++ b/app/javascript/elements/turbo_modal.js @@ -4,8 +4,8 @@ export default actionable(class extends HTMLElement { connectedCallback () { document.body.classList.add('overflow-hidden') + this.addEventListener('click', this.onClick) document.addEventListener('keyup', this.onEscKey) - document.addEventListener('turbo:before-cache', this.close) if (this.dataset.closeAfterSubmit !== 'false') { @@ -16,11 +16,22 @@ export default actionable(class extends HTMLElement { disconnectedCallback () { document.body.classList.remove('overflow-hidden') + this.removeEventListener('click', this.onClick) document.removeEventListener('keyup', this.onEscKey) document.removeEventListener('turbo:submit-end', this.onSubmit) document.removeEventListener('turbo:before-cache', this.close) } + onClick = (e) => { + const isCloseButton = e.target.closest('[data-turbo-modal-close]') + const isOutsideContent = !e.target.closest('[data-turbo-modal-content]') + if (isCloseButton || isOutsideContent) { + e.preventDefault() + e.stopPropagation() + this.close() + } + } + onSubmit = (e) => { if (e.detail.success && e.detail?.formSubmission?.formElement?.dataset?.closeOnSubmit !== 'false') { this.close() diff --git a/app/javascript/form.scss b/app/javascript/form.scss index 92fc6f6a..7009a391 100644 --- a/app/javascript/form.scss +++ b/app/javascript/form.scss @@ -31,36 +31,36 @@ button[disabled] .enabled, button.btn-disabled .enabled { display: none; } -.input-bordered { - @apply border-base-content/20; +.input-bordered, +.select-bordered, +.textarea-bordered { + @apply border-base-content/15 transition-all duration-200; } -.select-bordered { - @apply border-base-content/20; +.input-bordered:focus, +.select-bordered:focus, +.textarea-bordered:focus { + @apply border-primary outline-none ring-2 ring-primary/20; } -.textarea-bordered { - @apply border-base-content/20; +.base-input { + @apply input input-bordered bg-white h-11 px-4 text-base rounded-lg font-normal transition-all duration-200; } .base-textarea { - @apply textarea textarea-bordered bg-white rounded-3xl; + @apply textarea textarea-bordered bg-white rounded-lg px-4 py-3 text-base font-normal transition-all duration-200 min-h-[100px]; } .btn { - @apply no-animation; -} - -.base-input { - @apply input input-bordered bg-white; + @apply transition-all duration-200 font-medium; } .base-button { - @apply btn btn-neutral text-white text-base; + @apply btn btn-primary text-primary-content text-base px-6 h-11 rounded-lg hover:opacity-90 active:scale-[0.98] focus:outline-none focus:ring-2 focus:ring-primary/30 focus:ring-offset-2; } .white-button { - @apply btn btn-outline text-base bg-white border-2; + @apply btn btn-outline text-base bg-white border-2 border-base-300 rounded-lg px-6 h-11 hover:bg-base-200 hover:border-base-content/20 active:scale-[0.98] focus:outline-none focus:ring-2 focus:ring-primary/20 focus:ring-offset-2 transition-all duration-200; } .base-checkbox { diff --git a/app/javascript/submission_form/area.vue b/app/javascript/submission_form/area.vue index 30a0255e..b4887476 100644 --- a/app/javascript/submission_form/area.vue +++ b/app/javascript/submission_form/area.vue @@ -3,10 +3,10 @@ class="flex absolute lg:text-base -outline-offset-1 field-area" dir="auto" :style="[computedStyle, fontStyle]" - :class="{ 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'outline-red-500 outline-dashed outline-2 z-10 field-area-active': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }" + :class="{ 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable && field.type !== 'redact', 'border border-red-100': !isActive && submittable && field.type !== 'redact', 'bg-opacity-80': !isActive && !isValueSet && submittable && field.type !== 'redact', 'outline-red-500 outline-dashed outline-2 z-10 field-area-active': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable && field.type !== 'redact', '!bg-black !opacity-100 border-0': field.type === 'redact' }" >