add template download
This commit is contained in:
@@ -3,6 +3,12 @@
|
|||||||
class TemplateDocumentsController < ApplicationController
|
class TemplateDocumentsController < ApplicationController
|
||||||
load_and_authorize_resource :template
|
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
|
def create
|
||||||
if params[:blobs].blank? && params[:files].blank?
|
if params[:blobs].blank? && params[:files].blank?
|
||||||
return render json: { error: I18n.t('file_is_missing') }, status: :unprocessable_content
|
return render json: { error: I18n.t('file_is_missing') }, status: :unprocessable_content
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
|||||||
withConditions: this.dataset.withConditions === 'true',
|
withConditions: this.dataset.withConditions === 'true',
|
||||||
withGoogleDrive: this.dataset.withGoogleDrive === 'true',
|
withGoogleDrive: this.dataset.withGoogleDrive === 'true',
|
||||||
withReplaceAndCloneUpload: true,
|
withReplaceAndCloneUpload: true,
|
||||||
|
withDownload: true,
|
||||||
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
||||||
acceptFileTypes: this.dataset.acceptFileTypes,
|
acceptFileTypes: this.dataset.acceptFileTypes,
|
||||||
showTourStartForm: this.dataset.showTourStartForm === 'true'
|
showTourStartForm: this.dataset.showTourStartForm === 'true'
|
||||||
|
|||||||
@@ -175,7 +175,10 @@
|
|||||||
{{ t('save') }}
|
{{ t('save') }}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="dropdown dropdown-end">
|
<div
|
||||||
|
class="dropdown dropdown-end"
|
||||||
|
:class="{ 'dropdown-open': isDownloading }"
|
||||||
|
>
|
||||||
<label
|
<label
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="base-button !rounded-l-none !pl-1 !pr-2 !border-l-neutral-500"
|
class="base-button !rounded-l-none !pl-1 !pr-2 !border-l-neutral-500"
|
||||||
@@ -209,6 +212,30 @@
|
|||||||
<span class="whitespace-nowrap">{{ t('preferences') }}</span>
|
<span class="whitespace-nowrap">{{ t('preferences') }}</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li v-if="withDownload">
|
||||||
|
<button
|
||||||
|
class="flex space-x-2"
|
||||||
|
:disabled="isDownloading"
|
||||||
|
@click.stop.prevent="download"
|
||||||
|
>
|
||||||
|
<IconInnerShadowTop
|
||||||
|
v-if="isDownloading"
|
||||||
|
class="animate-spin w-6 h-6 flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<IconDownload
|
||||||
|
v-else
|
||||||
|
class="w-6 h-6 flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="isDownloading"
|
||||||
|
class="whitespace-nowrap"
|
||||||
|
>{{ t('downloading_') }}</span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="whitespace-nowrap"
|
||||||
|
>{{ t('download') }}</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
@@ -511,7 +538,7 @@ import DocumentPreview from './preview'
|
|||||||
import DocumentControls from './controls'
|
import DocumentControls from './controls'
|
||||||
import MobileFields from './mobile_fields'
|
import MobileFields from './mobile_fields'
|
||||||
import FieldSubmitter from './field_submitter'
|
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 { v4 } from 'uuid'
|
||||||
import { ref, computed, toRaw } from 'vue'
|
import { ref, computed, toRaw } from 'vue'
|
||||||
import * as i18n from './i18n'
|
import * as i18n from './i18n'
|
||||||
@@ -537,6 +564,7 @@ export default {
|
|||||||
Contenteditable,
|
Contenteditable,
|
||||||
IconUsersPlus,
|
IconUsersPlus,
|
||||||
IconChevronDown,
|
IconChevronDown,
|
||||||
|
IconDownload,
|
||||||
IconAdjustments,
|
IconAdjustments,
|
||||||
IconEye,
|
IconEye,
|
||||||
IconDeviceFloppy
|
IconDeviceFloppy
|
||||||
@@ -584,6 +612,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
withDownload: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
backgroundColor: {
|
backgroundColor: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -805,6 +838,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
documentRefs: [],
|
documentRefs: [],
|
||||||
isBreakpointLg: false,
|
isBreakpointLg: false,
|
||||||
|
isDownloading: false,
|
||||||
isLoadingBlankPage: false,
|
isLoadingBlankPage: false,
|
||||||
isSaving: false,
|
isSaving: false,
|
||||||
selectedSubmitter: null,
|
selectedSubmitter: null,
|
||||||
@@ -963,6 +997,75 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toRaw,
|
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) {
|
onDragover (e) {
|
||||||
if (this.$refs.dragPlaceholder?.dragPlaceholder) {
|
if (this.$refs.dragPlaceholder?.dragPlaceholder) {
|
||||||
this.$refs.dragPlaceholder.isMask = e.target.id === 'mask'
|
this.$refs.dragPlaceholder.isMask = e.target.id === 'mask'
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
const en = {
|
const en = {
|
||||||
|
download: 'Download',
|
||||||
|
downloading_: 'Downloading...',
|
||||||
view: 'View',
|
view: 'View',
|
||||||
autodetect_fields: 'Autodetect fields',
|
autodetect_fields: 'Autodetect fields',
|
||||||
payment_link: 'Payment link',
|
payment_link: 'Payment link',
|
||||||
@@ -185,6 +187,8 @@ const en = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
|
download: 'Descargar',
|
||||||
|
downloading_: 'Descargando...',
|
||||||
view: 'Vista',
|
view: 'Vista',
|
||||||
payment_link: 'Enlace de pago',
|
payment_link: 'Enlace de pago',
|
||||||
strikeout: 'Tachar',
|
strikeout: 'Tachar',
|
||||||
@@ -370,6 +374,8 @@ const es = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
|
download: 'Scarica',
|
||||||
|
downloading_: 'Download in corso...',
|
||||||
view: 'Vista',
|
view: 'Vista',
|
||||||
payment_link: 'Link di pagamento',
|
payment_link: 'Link di pagamento',
|
||||||
strikeout: 'Barrato',
|
strikeout: 'Barrato',
|
||||||
@@ -555,6 +561,8 @@ const it = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pt = {
|
const pt = {
|
||||||
|
download: 'Baixar',
|
||||||
|
downloading_: 'Baixando...',
|
||||||
view: 'Visualizar',
|
view: 'Visualizar',
|
||||||
payment_link: 'Link de pagamento',
|
payment_link: 'Link de pagamento',
|
||||||
strikeout: 'Tachado',
|
strikeout: 'Tachado',
|
||||||
@@ -740,6 +748,8 @@ const pt = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
|
download: 'Télécharger',
|
||||||
|
downloading_: 'Téléchargement...',
|
||||||
view: 'Voir',
|
view: 'Voir',
|
||||||
payment_link: 'Lien de paiement',
|
payment_link: 'Lien de paiement',
|
||||||
strikeout: 'Rature',
|
strikeout: 'Rature',
|
||||||
@@ -925,6 +935,8 @@ const fr = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
|
download: 'Download',
|
||||||
|
downloading_: 'Download...',
|
||||||
view: 'Anzeigen',
|
view: 'Anzeigen',
|
||||||
payment_link: 'Zahlungslink',
|
payment_link: 'Zahlungslink',
|
||||||
strikeout: 'Durchstreichen',
|
strikeout: 'Durchstreichen',
|
||||||
@@ -1110,6 +1122,8 @@ const de = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nl = {
|
const nl = {
|
||||||
|
download: 'Downloaden',
|
||||||
|
downloading_: 'Downloaden...',
|
||||||
view: 'Bekijken',
|
view: 'Bekijken',
|
||||||
payment_link: 'Betaallink',
|
payment_link: 'Betaallink',
|
||||||
strikeout: 'Doorhalen',
|
strikeout: 'Doorhalen',
|
||||||
|
|||||||
+1
-1
@@ -98,7 +98,7 @@ Rails.application.routes.draw do
|
|||||||
resources :submissions_filters, only: %i[show], param: 'name'
|
resources :submissions_filters, only: %i[show], param: 'name'
|
||||||
resources :templates, only: %i[new create edit update show destroy] do
|
resources :templates, only: %i[new create edit update show destroy] do
|
||||||
resource :debug, only: %i[show], controller: 'templates_debug' if Rails.env.development?
|
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'
|
resources :clone_and_replace, only: %i[create], controller: 'templates_clone_and_replace'
|
||||||
if !Docuseal.multitenant? || Docuseal.demo?
|
if !Docuseal.multitenant? || Docuseal.demo?
|
||||||
resources :detect_fields, only: %i[create], controller: 'templates_detect_fields'
|
resources :detect_fields, only: %i[create], controller: 'templates_detect_fields'
|
||||||
|
|||||||
Reference in New Issue
Block a user