diff --git a/app/controllers/template_documents_controller.rb b/app/controllers/template_documents_controller.rb
index cfca1297..b29a18f6 100644
--- a/app/controllers/template_documents_controller.rb
+++ b/app/controllers/template_documents_controller.rb
@@ -3,6 +3,12 @@
class TemplateDocumentsController < ApplicationController
load_and_authorize_resource :template
+ FILES_TTL = 5.minutes
+
+ def index
+ render json: @template.schema_documents.map { |d| ActiveStorage::Blob.proxy_url(d.blob, expires_at: FILES_TTL.from_now.to_i) }
+ end
+
def create
if params[:blobs].blank? && params[:files].blank?
return render json: { error: I18n.t('file_is_missing') }, status: :unprocessable_content
diff --git a/app/javascript/application.js b/app/javascript/application.js
index 9b17172b..b839039d 100644
--- a/app/javascript/application.js
+++ b/app/javascript/application.js
@@ -167,6 +167,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
withConditions: this.dataset.withConditions === 'true',
withGoogleDrive: this.dataset.withGoogleDrive === 'true',
withReplaceAndCloneUpload: true,
+ withDownload: true,
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
acceptFileTypes: this.dataset.acceptFileTypes,
showTourStartForm: this.dataset.showTourStartForm === 'true'
diff --git a/app/javascript/template_builder/builder.vue b/app/javascript/template_builder/builder.vue
index bcf376ac..5ce17324 100644
--- a/app/javascript/template_builder/builder.vue
+++ b/app/javascript/template_builder/builder.vue
@@ -175,7 +175,10 @@
{{ t('save') }}
-
+
@@ -511,7 +538,7 @@ import DocumentPreview from './preview'
import DocumentControls from './controls'
import MobileFields from './mobile_fields'
import FieldSubmitter from './field_submitter'
-import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments } from '@tabler/icons-vue'
+import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments, IconDownload } from '@tabler/icons-vue'
import { v4 } from 'uuid'
import { ref, computed, toRaw } from 'vue'
import * as i18n from './i18n'
@@ -537,6 +564,7 @@ export default {
Contenteditable,
IconUsersPlus,
IconChevronDown,
+ IconDownload,
IconAdjustments,
IconEye,
IconDeviceFloppy
@@ -584,6 +612,11 @@ export default {
required: false,
default: null
},
+ withDownload: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
backgroundColor: {
type: String,
required: false,
@@ -805,6 +838,7 @@ export default {
return {
documentRefs: [],
isBreakpointLg: false,
+ isDownloading: false,
isLoadingBlankPage: false,
isSaving: false,
selectedSubmitter: null,
@@ -963,6 +997,75 @@ export default {
},
methods: {
toRaw,
+ download () {
+ this.isDownloading = true
+
+ this.baseFetch(`/templates/${this.template.id}/documents`).then(async (response) => {
+ if (response.ok) {
+ const urls = await response.json()
+ const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
+ const isSafariIos = isMobileSafariIos || /iPhone|iPad|iPod/i.test(navigator.userAgent)
+
+ if (isSafariIos && urls.length > 1) {
+ this.downloadSafariIos(urls)
+ } else {
+ this.downloadUrls(urls)
+ }
+ } else {
+ alert(this.t('failed_to_download_files'))
+ }
+ })
+ },
+ downloadUrls (urls) {
+ const fileRequests = urls.map((url) => {
+ return () => {
+ return fetch(url).then(async (resp) => {
+ const blobUrl = URL.createObjectURL(await resp.blob())
+ const link = document.createElement('a')
+
+ link.href = blobUrl
+ link.setAttribute('download', decodeURI(url.split('/').pop()))
+
+ link.click()
+
+ URL.revokeObjectURL(blobUrl)
+ })
+ }
+ })
+
+ fileRequests.reduce(
+ (prevPromise, request) => prevPromise.then(() => request()),
+ Promise.resolve()
+ ).finally(() => {
+ this.isDownloading = false
+ })
+ },
+ downloadSafariIos (urls) {
+ const fileRequests = urls.map((url) => {
+ return fetch(url).then(async (resp) => {
+ const blob = await resp.blob()
+ const blobUrl = URL.createObjectURL(blob.slice(0, blob.size, 'application/octet-stream'))
+ const link = document.createElement('a')
+
+ link.href = blobUrl
+ link.setAttribute('download', decodeURI(url.split('/').pop()))
+
+ return link
+ })
+ })
+
+ Promise.all(fileRequests).then((links) => {
+ links.forEach((link, index) => {
+ setTimeout(() => {
+ link.click()
+
+ URL.revokeObjectURL(link.href)
+ }, index * 50)
+ })
+ }).finally(() => {
+ this.isDownloading = false
+ })
+ },
onDragover (e) {
if (this.$refs.dragPlaceholder?.dragPlaceholder) {
this.$refs.dragPlaceholder.isMask = e.target.id === 'mask'
diff --git a/app/javascript/template_builder/i18n.js b/app/javascript/template_builder/i18n.js
index a23e5c8b..e04d37d1 100644
--- a/app/javascript/template_builder/i18n.js
+++ b/app/javascript/template_builder/i18n.js
@@ -1,4 +1,6 @@
const en = {
+ download: 'Download',
+ downloading_: 'Downloading...',
view: 'View',
autodetect_fields: 'Autodetect fields',
payment_link: 'Payment link',
@@ -185,6 +187,8 @@ const en = {
}
const es = {
+ download: 'Descargar',
+ downloading_: 'Descargando...',
view: 'Vista',
payment_link: 'Enlace de pago',
strikeout: 'Tachar',
@@ -370,6 +374,8 @@ const es = {
}
const it = {
+ download: 'Scarica',
+ downloading_: 'Download in corso...',
view: 'Vista',
payment_link: 'Link di pagamento',
strikeout: 'Barrato',
@@ -555,6 +561,8 @@ const it = {
}
const pt = {
+ download: 'Baixar',
+ downloading_: 'Baixando...',
view: 'Visualizar',
payment_link: 'Link de pagamento',
strikeout: 'Tachado',
@@ -740,6 +748,8 @@ const pt = {
}
const fr = {
+ download: 'Télécharger',
+ downloading_: 'Téléchargement...',
view: 'Voir',
payment_link: 'Lien de paiement',
strikeout: 'Rature',
@@ -925,6 +935,8 @@ const fr = {
}
const de = {
+ download: 'Download',
+ downloading_: 'Download...',
view: 'Anzeigen',
payment_link: 'Zahlungslink',
strikeout: 'Durchstreichen',
@@ -1110,6 +1122,8 @@ const de = {
}
const nl = {
+ download: 'Downloaden',
+ downloading_: 'Downloaden...',
view: 'Bekijken',
payment_link: 'Betaallink',
strikeout: 'Doorhalen',
diff --git a/config/routes.rb b/config/routes.rb
index 43701da1..4e5715a5 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -98,7 +98,7 @@ Rails.application.routes.draw do
resources :submissions_filters, only: %i[show], param: 'name'
resources :templates, only: %i[new create edit update show destroy] do
resource :debug, only: %i[show], controller: 'templates_debug' if Rails.env.development?
- resources :documents, only: %i[create], controller: 'template_documents'
+ resources :documents, only: %i[index create], controller: 'template_documents'
resources :clone_and_replace, only: %i[create], controller: 'templates_clone_and_replace'
if !Docuseal.multitenant? || Docuseal.demo?
resources :detect_fields, only: %i[create], controller: 'templates_detect_fields'