Merge from docusealco/wip

pull/603/head 2.3.6
Alex Turchyn 3 weeks ago committed by GitHub
commit d4e94ac2bc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -48,7 +48,7 @@ ENV RAILS_ENV=production
ENV BUNDLE_WITHOUT="development:test"
ENV LD_PRELOAD=/lib/libgcompat.so.0
ENV OPENSSL_CONF=/etc/openssl_legacy.cnf
ENV VIPS_MAX_COORD=10000
ENV VIPS_MAX_COORD=15000
WORKDIR /app
@ -80,7 +80,7 @@ COPY --chown=docuseal:docuseal ./log ./log
COPY --chown=docuseal:docuseal ./lib ./lib
COPY --chown=docuseal:docuseal ./public ./public
COPY --chown=docuseal:docuseal ./tmp ./tmp
COPY --chown=docuseal:docuseal LICENSE README.md Rakefile config.ru .version ./
COPY --chown=docuseal:docuseal LICENSE LICENSE_ADDITIONAL_TERMS README.md Rakefile config.ru .version ./
COPY --chown=docuseal:docuseal .version ./public/version
COPY --chown=docuseal:docuseal --from=download /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts

@ -0,0 +1,5 @@
Additional Terms
In accordance with Section 7(b) of the GNU Affero General Public License,
a covered work must retain the original DocuSeal attribution in interactive
user interfaces.

@ -97,8 +97,8 @@ At DocuSeal we have expertise and technologies to make documents creation, filli
## License
Distributed under the AGPLv3 License. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) for more information.
Unless otherwise noted, all files © 2023 DocuSeal LLC.
Distributed under the AGPLv3 License with Section 7(b) Additional Terms. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) and [LICENSE_ADDITIONAL_TERMS](https://github.com/docusealco/docuseal/blob/master/LICENSE_ADDITIONAL_TERMS) for more information.
Unless otherwise noted, all files © 2023-2026 DocuSeal LLC.
## Tools

@ -5,8 +5,9 @@ class SubmissionsExportController < ApplicationController
load_and_authorize_resource :submission, through: :template, parent: false, only: :index
def index
submissions = @submissions.active
.preload(submitters: { documents_attachments: :blob,
submissions = params[:archived] == 'true' ? @submissions.archived : @submissions.active
submissions = submissions.preload(submitters: { documents_attachments: :blob,
attachments_attachments: :blob })
.order(id: :asc)

@ -7,25 +7,34 @@ class SubmittersAutocompleteController < ApplicationController
LIMIT = 100
def index
submitters = search_submitters(@submitters)
field = SELECT_COLUMNS.find { |c| c == params[:field] }
submitters = search_submitters(@submitters, field)
arel_columns = SELECT_COLUMNS.map { |col| Submitter.arel_table[col] }
values = submitters.limit(LIMIT).group(arel_columns).pluck(arel_columns)
values =
if field
max_ids = submitters.group(field).limit(LIMIT).select(Submitter.arel_table[:id].maximum)
submitters.where(id: max_ids).order(id: :desc).pluck(arel_columns)
else
submitters.limit(LIMIT).group(arel_columns).pluck(arel_columns)
end
attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h }
attrs = attrs.uniq { |e| e[params[:field]] } if params[:field].present?
render json: attrs
end
private
def search_submitters(submitters)
if SELECT_COLUMNS.include?(params[:field])
def search_submitters(submitters, field)
if field
if Docuseal.fulltext_search?
Submitters.fulltext_search_field(current_user, submitters, params[:q], params[:field])
Submitters.fulltext_search_field(current_user, submitters, params[:q], field)
else
column = Submitter.arel_table[params[:field].to_sym]
column = Submitter.arel_table[field.to_sym]
term = "#{params[:q].downcase}%"

@ -21,7 +21,7 @@ class TemplatesCloneController < ApplicationController
authorize!(:create, @template)
if params[:account_id].present? && true_ability.authorize!(:manage, Account.find(params[:account_id]))
if params[:account_id].present? && true_ability.can?(:manage, Account.find(params[:account_id]))
@template.account_id = params[:account_id]
@template.author = true_user if true_user.account_id == @template.account_id
@template.folder = @template.account.default_template_folder if @template.account_id != current_account.id

@ -155,12 +155,15 @@ safeRegisterElement('template-builder', class extends HTMLElement {
this.appElem.classList.add('md:h-screen')
const template = reactive(JSON.parse(this.dataset.template))
this.app = createApp(TemplateBuilder, {
template: reactive(JSON.parse(this.dataset.template)),
template,
customFields: reactive(JSON.parse(this.dataset.customFields || '[]')),
backgroundColor: '#faf7f5',
locale: this.dataset.locale,
withPhone: this.dataset.withPhone === 'true',
withPrefillable: template.fields?.some((f) => f.prefillable),
withVerification: ['true', 'false'].includes(this.dataset.withVerification) ? this.dataset.withVerification === 'true' : null,
withKba: ['true', 'false'].includes(this.dataset.withKba) ? this.dataset.withKba === 'true' : null,
withLogo: this.dataset.withLogo !== 'false',

@ -36,15 +36,11 @@ function loadTiptap () {
}
class LinkTooltip {
constructor (container, editor) {
constructor (container, editor, templateEl) {
this.container = container
this.editor = editor
const template = document.createElement('template')
template.innerHTML = container.dataset.linkTooltipHtml
this.tooltip = template.content.firstElementChild
this.tooltip = templateEl.content.firstElementChild.cloneNode(true)
this.input = this.tooltip.querySelector('input')
this.saveButton = this.tooltip.querySelector('[data-role="link-save"]')
@ -140,7 +136,8 @@ export default actionable(targetable(class extends HTMLElement {
'boldButton',
'italicButton',
'underlineButton',
'linkButton'
'linkButton',
'linkTooltipTemplate'
]
async connectedCallback () {
@ -256,7 +253,7 @@ export default actionable(targetable(class extends HTMLElement {
}
})
this.linkTooltip = new LinkTooltip(this, this.editor)
this.linkTooltip = new LinkTooltip(this, this.editor, this.linkTooltipTemplate)
}
adjustShortcutsForPlatform () {

@ -720,6 +720,11 @@ export default {
required: false,
default: false
},
withPrefillable: {
type: Boolean,
required: false,
default: false
},
customFields: {
type: Array,
required: false,
@ -948,13 +953,6 @@ export default {
!submitter.email
})
},
withPrefillable () {
if (this.template.fields) {
return this.template.fields.some((f) => f.prefillable)
} else {
return false
}
},
isInlineSize () {
return CSS.supports('container-type: size')
},

@ -1,6 +1,7 @@
<template>
<div
class="list-field group"
:class="`list-field-${field.type}`"
>
<div
class="border border-base-300 rounded relative group fields-list-item"

@ -11,7 +11,7 @@
>
<label
v-if="showRequired"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
class="field-settings-required w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
@click.stop
>
<input
@ -26,7 +26,7 @@
</label>
<label
v-if="showReadOnly"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
class="field-settings-read-only w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
@click.stop
>
<input
@ -41,7 +41,7 @@
</label>
<label
v-if="showPrefillable"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
class="field-settings-prefillable w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
@click.stop
>
<input
@ -56,7 +56,7 @@
</label>
<label
v-if="showSetSigningDate"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
class="field-settings-set-signing-date w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
@click.stop
>
<input
@ -70,7 +70,7 @@
</label>
<label
v-if="showWithLogo"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
class="field-settings-with-logo w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
@click.stop
>
<input
@ -84,7 +84,7 @@
</label>
<label
v-if="showSignatureId"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
class="field-settings-signature-id w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
@click.stop
>
<input
@ -99,7 +99,7 @@
</label>
<label
v-if="showChecked"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
class="field-settings-checked w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
@click.stop
>
<input
@ -113,6 +113,7 @@
</label>
<ContextSubmenu
v-if="showVerificationMethod"
class="field-settings-verification-method"
:icon="IconId"
:label="t('method')"
:options="methodOptions"
@ -125,6 +126,7 @@
>
<ContextSubmenu
v-if="showFormatSubmenu"
class="field-settings-format"
:icon="IconAdjustmentsHorizontal"
:label="t('format')"
:options="formatOptions"
@ -133,6 +135,7 @@
/>
<ContextSubmenu
v-if="showValidationSubmenu && field.type !== 'number'"
class="field-settings-validation"
:icon="IconInputCheck"
:label="t('validation')"
:options="validationMenuItems.map(k => ({ value: k, label: t(k) }))"
@ -141,7 +144,7 @@
/>
<button
v-if="field.type === 'number'"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
class="field-settings-validation w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
@click.stop="openNumberRangeModal"
>
<IconInputCheck class="w-4 h-4" />
@ -149,6 +152,7 @@
</button>
<ContextSubmenu
v-if="showPaymentSettings"
class="field-settings-currency"
:icon="IconCash"
:label="t('currency')"
:options="currencyOptions"
@ -157,6 +161,7 @@
/>
<ContextSubmenu
v-if="showPaymentSettings"
class="field-settings-price"
:icon="IconCoins"
:label="t('price')"
:options="priceTypeOptions"
@ -165,7 +170,7 @@
/>
<button
v-if="showFont"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
class="field-settings-font w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
@click.stop="openFontModal"
>
<IconTypography class="w-4 h-4" />
@ -173,7 +178,7 @@
</button>
<button
v-if="showDescription"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
class="field-settings-description w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
@click.stop="openDescriptionModal"
>
<IconInfoCircle class="w-4 h-4" />
@ -181,7 +186,7 @@
</button>
<button
v-if="showCondition"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
class="field-settings-condition w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
@click.stop="openConditionModal"
>
<span class="flex items-center space-x-2">
@ -196,7 +201,7 @@
</button>
<button
v-if="showFormula"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
class="field-settings-formula w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
@click.stop="openFormulaModal"
>
<IconMathFunction class="w-4 h-4" />
@ -207,7 +212,7 @@
class="my-1 border-neutral-200"
>
<button
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
class="field-settings-copy w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
@click.stop="$emit('copy')"
>
<span class="flex items-center space-x-2">
@ -217,7 +222,7 @@
<span class="text-xs text-base-content/60 ml-4">{{ isMac ? '⌘C' : 'Ctrl+C' }}</span>
</button>
<button
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm text-red-600"
class="field-settings-remove w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm text-red-600"
@click.stop="$emit('delete')"
>
<span class="flex items-center space-x-2">
@ -228,12 +233,13 @@
</button>
<ContextSubmenu
v-if="showMoreSubmenu"
class="field-settings-more"
:icon="IconDots"
:label="t('more')"
>
<button
v-if="showDrawNewArea"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
class="field-settings-draw-new-area w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
@click="handleMoreSelect('draw_new_area')"
>
<IconNewSection class="w-4 h-4" />
@ -241,7 +247,7 @@
</button>
<button
v-if="showCopyToAllPages"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
class="field-settings-copy-to-all-pages w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
@click="handleMoreSelect('copy_to_all_pages')"
>
<IconCopy class="w-4 h-4" />
@ -249,7 +255,7 @@
</button>
<button
v-if="showSaveAsCustom"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
class="field-settings-save-as-custom-field w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
@click="handleMoreSelect('save_as_custom')"
>
<IconForms class="w-4 h-4" />

@ -1,7 +1,7 @@
<template>
<div
v-if="field.type === 'verification'"
class="py-1.5 px-1 relative"
class="field-settings-verification-method py-1.5 px-1 relative"
@click.stop
>
<select
@ -28,7 +28,7 @@
</div>
<div
v-if="['select', 'radio'].includes(field.type) && !defaultField"
class="py-1.5 px-1 relative"
class="field-settings-default-value py-1.5 px-1 relative"
@click.stop
>
<select
@ -62,7 +62,7 @@
</div>
<div
v-if="['text', 'number'].includes(field.type) && !defaultField"
class="py-1.5 px-1 relative"
class="field-settings-default-value py-1.5 px-1 relative"
@click.stop
>
<input
@ -84,7 +84,7 @@
</div>
<div
v-if="['text', 'cells'].includes(field.type)"
class="py-1.5 px-1 relative"
class="field-settings-validation py-1.5 px-1 relative"
@click.stop
>
<select
@ -122,7 +122,7 @@
</div>
<div
v-if="['text', 'cells'].includes(field.type) && field.validation && lengthValidation"
class="py-1.5 px-1 relative flex space-x-1"
class="field-settings-length-validation py-1.5 px-1 relative flex space-x-1"
@click.stop
>
<div class="w-1/2 relative">
@ -166,7 +166,7 @@
</div>
<div
v-if="field.type === 'number'"
class="py-1.5 px-1 relative flex space-x-1"
class="field-settings-number-range py-1.5 px-1 relative flex space-x-1"
@click.stop
>
<div class="w-1/2 relative">
@ -210,7 +210,7 @@
</div>
<div
v-if="field.type === 'number'"
class="py-1.5 px-1 relative"
class="field-settings-number-format py-1.5 px-1 relative"
@click.stop
>
<select
@ -237,7 +237,7 @@
</div>
<div
v-if="['text', 'cells'].includes(field.type) && field.validation && !validations[field.validation.pattern] && !lengthValidation"
class="py-1.5 px-1 relative"
class="field-settings-custom-validation py-1.5 px-1 relative"
@click.stop
>
<input
@ -259,7 +259,7 @@
</div>
<div
v-if="['text', 'cells'].includes(field.type) && field.validation && !validations[field.validation.pattern] && !lengthValidation"
class="py-1.5 px-1 relative"
class="field-settings-error-message py-1.5 px-1 relative"
@click.stop
>
<input
@ -280,7 +280,7 @@
</div>
<div
v-if="field.type === 'date'"
class="py-1.5 px-1 relative"
class="field-settings-date-format py-1.5 px-1 relative"
@click.stop
>
<select
@ -307,7 +307,7 @@
</div>
<div
v-if="field.type === 'signature'"
class="py-1.5 px-1 relative"
class="field-settings-signature-format py-1.5 px-1 relative"
@click.stop
>
<select
@ -340,6 +340,7 @@
</div>
<li
v-if="[true, false].includes(withSignatureId) && field.type === 'signature'"
class="field-settings-signature-id"
@click.stop
>
<label class="cursor-pointer py-1.5">
@ -355,6 +356,7 @@
</li>
<li
v-if="withRequired && field.type !== 'phone' && field.type !== 'stamp' && field.type !== 'verification' && field.type !== 'strikethrough' && field.type !== 'heading'"
class="field-settings-required"
@click.stop
>
<label class="cursor-pointer py-1.5">
@ -370,6 +372,7 @@
</li>
<li
v-if="field.type == 'stamp'"
class="field-settings-with-logo"
@click.stop
>
<label class="cursor-pointer py-1.5">
@ -384,6 +387,7 @@
</li>
<li
v-if="field.type == 'checkbox'"
class="field-settings-checked"
@click.stop
>
<label class="cursor-pointer py-1.5">
@ -398,6 +402,7 @@
</li>
<li
v-if="field.type == 'date'"
class="field-settings-set-signing-date"
@click.stop
>
<label class="cursor-pointer py-1.5">
@ -412,6 +417,7 @@
</li>
<li
v-if="['text', 'number', 'radio', 'multiple', 'select'].includes(field.type)"
class="field-settings-read-only"
@click.stop
>
<label class="cursor-pointer py-1.5">
@ -427,6 +433,7 @@
</li>
<li
v-if="withPrefillable && prefillableFieldTypes.includes(field['type'])"
class="field-settings-prefillable"
@click.stop
>
<label class="cursor-pointer py-1.5">
@ -444,7 +451,10 @@
v-if="field.type != 'stamp'"
class="pb-0.5 mt-0.5"
>
<li v-if="['text', 'number', 'date', 'select', 'heading', 'cells'].includes(field.type)">
<li
v-if="['text', 'number', 'date', 'select', 'heading', 'cells'].includes(field.type)"
class="field-settings-font"
>
<label
class="label-text cursor-pointer text-center w-full flex items-center"
@click="$emit('click-font')"
@ -459,6 +469,7 @@
</li>
<li
v-if="field.type != 'stamp' && field.type != 'heading' && field.type != 'strikethrough'"
class="field-settings-description"
>
<label
class="label-text cursor-pointer text-center w-full flex items-center"
@ -474,6 +485,7 @@
</li>
<li
v-if="withCondition && field.type != 'stamp' && field.type != 'heading'"
class="field-settings-condition"
>
<label
class="label-text cursor-pointer text-center w-full flex items-center"
@ -487,7 +499,10 @@
</span>
</label>
</li>
<li v-if="field.type == 'number'">
<li
v-if="field.type == 'number'"
class="field-settings-formula"
>
<label
class="label-text cursor-pointer text-center w-full flex items-center"
@click="$emit('click-formula')"
@ -508,6 +523,7 @@
<li
v-for="(area, index) in sortedAreas"
:key="index"
class="field-settings-area"
>
<a
href="#"
@ -527,7 +543,10 @@
/>
</a>
</li>
<li v-if="!field.areas?.length || !['radio', 'multiple'].includes(field.type)">
<li
v-if="!field.areas?.length || !['radio', 'multiple'].includes(field.type)"
class="field-settings-draw-new-area"
>
<a
href="#"
class="text-sm py-1 px-2"
@ -541,7 +560,10 @@
</a>
</li>
</template>
<li v-if="withCopyToAllPages && field.areas?.length === 1 && ['date', 'signature', 'initials', 'text', 'cells', 'stamp'].includes(field.type)">
<li
v-if="withCopyToAllPages && field.areas?.length === 1 && ['date', 'signature', 'initials', 'text', 'cells', 'stamp'].includes(field.type)"
class="field-settings-copy-to-all-pages"
>
<a
href="#"
class="text-sm py-1 px-2"
@ -554,7 +576,10 @@
{{ t('copy_to_all_pages') }}
</a>
</li>
<li v-if="withCustomFields">
<li
v-if="withCustomFields"
class="field-settings-save-as-custom-field"
>
<a
href="#"
class="text-sm py-1 px-2"

@ -22,7 +22,7 @@
>
<div
v-if="!('price_id' in field.preferences) && !('payment_link_id' in field.preferences)"
class="py-1.5 px-1 relative"
class="field-settings-currency py-1.5 px-1 relative"
@click.stop
>
<select
@ -48,7 +48,7 @@
</label>
</div>
<div
class="py-1.5 px-1 relative"
class="field-settings-price py-1.5 px-1 relative"
@click.stop
>
<input
@ -121,7 +121,7 @@
</div>
<div
v-if="!isConnected || isOauthSuccess"
class="py-1.5 px-1 relative"
class="field-settings-stripe-connect py-1.5 px-1 relative"
@click.stop
>
<div
@ -203,7 +203,7 @@
>{{ t('learn_more') }}</a>
</div>
<li
class="mb-1"
class="field-settings-formula mb-1"
>
<label
class="label-text cursor-pointer text-center w-full flex items-center"
@ -218,7 +218,7 @@
</label>
</li>
<hr>
<li>
<li class="field-settings-description">
<label
class="label-text cursor-pointer text-center w-full flex items-center"
@click="$emit('click-description')"
@ -233,7 +233,7 @@
</li>
<li
v-if="withCondition"
class="mt-1"
class="field-settings-condition mt-1"
>
<label
class="label-text cursor-pointer text-center w-full flex items-center"
@ -251,7 +251,10 @@
v-if="withCustomFields"
class="pb-0.5 mt-0.5"
>
<li v-if="withCustomFields">
<li
v-if="withCustomFields"
class="field-settings-save-as-custom-field"
>
<a
href="#"
class="text-sm py-1 px-2"

@ -29,7 +29,7 @@ class SubmitterMailer < ApplicationMailer
assign_message_metadata('submitter_invitation', @submitter)
reply_to = build_submitter_reply_to(@submitter)
reply_to = build_submitter_reply_to(@submitter, email_config: @email_config)
maybe_set_custom_domain(@submitter)

@ -1,4 +1,10 @@
<% link_tooltip_html = capture do %>
<% if value.to_s.start_with?('<html') %>
<autoresize-textarea>
<%= text_area_tag name, value, required: true, class: 'base-input w-full py-2 !rounded-2xl', dir: 'auto', style: 'max-height: 400px' %>
</autoresize-textarea>
<% else %>
<markdown-editor>
<template data-target="markdown-editor.linkTooltipTemplate">
<div class="hidden absolute flex bg-white border border-base-300 rounded-xl shadow p-1 gap-1 items-center z-50" contenteditable="false">
<input type="text" placeholder="<%= t('enter_a_url_or_variable_name') %>" class="rounded-lg border border-base-300 px-2 py-1 text-sm outline-none" style="field-sizing: content; min-width: 205px; max-width: 320px;" autocomplete="off">
<button type="button" data-role="link-save" class="flex items-center px-1 w-6 h-6 rounded hover:bg-success/10 cursor-pointer">
@ -8,8 +14,7 @@
<%= svg_icon('x', class: 'w-4 h-4 text-error') %>
</button>
</div>
<% end %>
<markdown-editor data-link-tooltip-html="<%= link_tooltip_html.squish %>">
</template>
<div class="border border-base-content/20 rounded-2xl bg-white">
<div class="flex items-center px-2 py-2 border-b" style="height: 42px;">
<div class="flex items-center gap-1">
@ -67,4 +72,5 @@
<div data-target="markdown-editor.editorElement"></div>
</div>
<%= hidden_field_tag name, value, required: true, data: { target: 'markdown-editor.textarea' } %>
</markdown-editor>
</markdown-editor>
<% end %>

@ -8,12 +8,18 @@
<div class="collapse-content">
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
<%= f.hidden_field :key %>
<%= f.fields_for :value, Struct.new(:subject, :body).new(*f.object.value.values_at('subject', 'body')) do |ff| %>
<%= f.fields_for :value, Struct.new(:subject, :body, :reply_to).new(*f.object.value.values_at('subject', 'body', 'reply_to')) do |ff| %>
<div class="form-control">
<%= ff.label :subject, t('subject'), class: 'label' %>
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
</div>
<%= render 'personalization_settings/email_body_field', ff:, config: f.object %>
<% if can?(:manage, :reply_to) || can?(:manage, :personalization_advanced) %>
<div class="form-control">
<%= ff.label :reply_to, t('reply_to'), class: 'label' %>
<%= ff.email_field :reply_to, class: 'base-input', dir: 'auto', placeholder: t(:email) %>
</div>
<% end %>
<% end %>
<div class="form-control pt-2">
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>

@ -1,4 +1,4 @@
<% filter_params = params.permit(:q, *Submissions::Filter::ALLOWED_PARAMS) %>
<% filter_params = params.permit(:q, :archived, *Submissions::Filter::ALLOWED_PARAMS) %>
<%= render 'shared/turbo_modal', title: t('export'), close_after_submit: false do %>
<div class="space-y-2">
<%= button_to template_submissions_export_index_path(@template), params: { format: :xlsx, **filter_params }, method: :get, data: { turbo_frame: :_top } do %>

@ -16,7 +16,7 @@
<% if with_filters %>
<%= render 'shared/search_input' %>
<% end %>
<%= link_to new_template_submissions_export_path(@template), class: 'btn btn-ghost text-base', data: { turbo_frame: 'modal' } do %>
<%= link_to new_template_submissions_export_path(@template, archived: true), class: 'btn btn-ghost text-base', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('download', class: 'w-6 h-6 stroke-2') %>
<span><%= t('export') %></span>
<% end %>

@ -1,6 +1,8 @@
# frozen_string_literal: true
module LoadBmp
BPPS = [1, 4, 8, 24, 32].freeze
module_function
# rubocop:disable Metrics
@ -16,6 +18,17 @@ module LoadBmp
header_data[:height]
)
if header_data[:bpp] <= 8
final_pixel_data = decode_indexed_pixel_data(
raw_pixel_data_from_file,
header_data[:bpp],
header_data[:width],
header_data[:height],
header_data[:bmp_stride],
header_data[:color_table]
)
bands = 3
else
final_pixel_data = prepare_unpadded_pixel_data_string(
raw_pixel_data_from_file,
header_data[:bpp],
@ -23,11 +36,7 @@ module LoadBmp
header_data[:height],
header_data[:bmp_stride]
)
bands = header_data[:bpp] / 8
unless header_data[:bpp] == 24 || header_data[:bpp] == 32
raise ArgumentError, "Conversion for #{header_data[:bpp]}-bpp BMP not implemented."
end
image = Vips::Image.new_from_memory(final_pixel_data, header_data[:width], header_data[:height], bands, :uchar)
@ -35,7 +44,9 @@ module LoadBmp
image = image.flip(:vertical) if header_data[:orientation] == -1
image_rgb =
if bands == 3
if header_data[:bpp] <= 8
image
elsif bands == 3
image.recomb(band3_recomb)
elsif bands == 4
image.recomb(band4_recomb)
@ -93,15 +104,31 @@ module LoadBmp
"Unsupported BMP compression type: #{compression}. Only uncompressed (0) is supported."
end
unless [24, 32].include?(bpp)
raise ArgumentError, "Unsupported BMP bits per pixel: #{bpp}. Only 24-bit and 32-bit are supported."
if BPPS.exclude?(bpp)
raise ArgumentError, "Unsupported BMP bits per pixel: #{bpp}. Only 1, 4, 8, 24, and 32-bit are supported."
end
raise ArgumentError, "Unsupported BMP planes: #{planes}. Expected 1." if planes != 1
bytes_per_pixel = bpp / 8
row_size_unpadded = width * bytes_per_pixel
bmp_stride = (row_size_unpadded + 3) & ~3
bmp_stride = (((width * bpp) + 31) / 32) * 4
color_table = nil
if bpp <= 8
num_colors = 1 << bpp
color_table_offset = 14 + info_header_size
color_table_size = num_colors * 4
if bmp_bytes.bytesize < color_table_offset + color_table_size
raise ArgumentError, 'BMP data too short for color table.'
end
color_table = Array.new(num_colors) do |i|
offset = color_table_offset + (i * 4)
b, g, r = bmp_bytes.unpack("@#{offset}CCC")
[r, g, b]
end
end
{
width:,
@ -109,7 +136,8 @@ module LoadBmp
bpp:,
pixel_data_offset:,
bmp_stride:,
orientation:
orientation:,
color_table:
}
end
@ -163,6 +191,37 @@ module LoadBmp
unpadded_rows.join
end
def decode_indexed_pixel_data(raw_data, bpp, width, height, bmp_stride, color_table)
palette = color_table.map { |r, g, b| [r, g, b].pack('CCC') }
output = String.new(capacity: width * height * 3)
height.times do |y|
row_offset = y * bmp_stride
case bpp
when 1
width.times do |x|
byte_val = raw_data.getbyte(row_offset + (x >> 3))
index = (byte_val >> (7 - (x & 7))) & 0x01
output << palette[index]
end
when 4
width.times do |x|
byte_val = raw_data.getbyte(row_offset + (x >> 1))
index = x.even? ? (byte_val >> 4) & 0x0F : byte_val & 0x0F
output << palette[index]
end
when 8
width.times do |x|
output << palette[raw_data.getbyte(row_offset + x)]
end
end
end
output
end
def band3_recomb
@band3_recomb ||=
Vips::Image.new_from_array(

Loading…
Cancel
Save