add Google Drive replace option

pull/604/merge
Alex Turchyn 2 weeks ago committed by Pete Matsyburka
parent a89aef4a89
commit da21c37254

@ -309,6 +309,8 @@
:data-document-uuid="item.attachment_uuid" :data-document-uuid="item.attachment_uuid"
:accept-file-types="acceptFileTypes" :accept-file-types="acceptFileTypes"
:with-replace-button="withUploadButton" :with-replace-button="withUploadButton"
:with-google-drive="withGoogleDrive"
:authenticity-token="authenticityToken"
:editable="editable" :editable="editable"
:dynamic-documents="dynamicDocuments" :dynamic-documents="dynamicDocuments"
:with-dynamic-documents="withDynamicDocuments" :with-dynamic-documents="withDynamicDocuments"
@ -456,6 +458,8 @@
:with-arrows="template.schema.length > 1" :with-arrows="template.schema.length > 1"
:item="template.schema.find((item) => item.attachment_uuid === document.uuid)" :item="template.schema.find((item) => item.attachment_uuid === document.uuid)"
:with-replace-button="withUploadButton" :with-replace-button="withUploadButton"
:with-google-drive="withGoogleDrive"
:authenticity-token="authenticityToken"
:accept-file-types="acceptFileTypes" :accept-file-types="acceptFileTypes"
:document="document" :document="document"
:template="template" :template="template"

@ -10,6 +10,9 @@
v-if="withReplaceButton" v-if="withReplaceButton"
:template-id="template.id" :template-id="template.id"
:accept-file-types="acceptFileTypes" :accept-file-types="acceptFileTypes"
:authenticity-token="authenticityToken"
:with-google-drive="withGoogleDrive"
:google-drive-file-id="item.google_drive_file_id"
@click.stop @click.stop
@success="$emit('replace', { replaceSchemaItem: item, ...$event })" @success="$emit('replace', { replaceSchemaItem: item, ...$event })"
/> />
@ -73,6 +76,16 @@ export default {
required: true, required: true,
default: true default: true
}, },
withGoogleDrive: {
type: Boolean,
required: false,
default: false
},
authenticityToken: {
type: String,
required: false,
default: ''
},
withArrows: { withArrows: {
type: Boolean, type: Boolean,
required: false, required: false,

@ -0,0 +1,165 @@
<template>
<Teleport :to="modalContainerEl">
<div
class="modal modal-open items-start !animate-none overflow-y-auto"
>
<div
class="absolute top-0 bottom-0 right-0 left-0"
@click.prevent="$emit('close')"
/>
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
<span class="modal-title">
Google Drive
</span>
<a
href="#"
class="text-xl modal-close-button"
@click.prevent="$emit('close')"
>&times;</a>
</div>
<div>
<form
v-if="showOauthButton"
method="post"
:action="oauthPath"
@submit="isConnectClicked = true"
>
<input
type="hidden"
name="authenticity_token"
:value="authenticityToken"
autocomplete="off"
>
<button
class="btn bg-white btn-outline w-full text-base font-medium mt-4"
data-turbo="false"
type="submit"
:disabled="isConnectClicked"
>
<span v-if="isConnectClicked">
<span class="flex items-center justify-center space-x-2">
<IconInnerShadowTop class="animate-spin" />
<span>{{ t('submitting') }}...</span>
</span>
</span>
<span v-else>
<span class="flex items-center justify-center space-x-2">
<IconBrandGoogleDrive />
<span>{{ t('connect_google_drive') }}</span>
</span>
</span>
</button>
</form>
<div
v-else
class="relative"
>
<iframe
class="border border-base-300 rounded-lg"
style="width: 100%; height: 440px; background: white;"
src="/template_google_drive"
/>
<div v-if="loading">
<div
class="bg-white absolute top-0 bottom-0 left-0 right-0 opacity-80 rounded-lg"
style="margin: 1px"
/>
<div class="absolute top-0 bottom-0 left-0 right-0 flex items-center justify-center">
<IconInnerShadowTop class="animate-spin" />
</div>
</div>
</div>
</div>
</div>
</div>
</Teleport>
</template>
<script>
import { IconBrandGoogleDrive, IconInnerShadowTop } from '@tabler/icons-vue'
export default {
name: 'GoogleDrivePickerModal',
components: {
IconBrandGoogleDrive,
IconInnerShadowTop
},
inject: ['t'],
props: {
templateId: {
type: [Number, String],
required: true
},
authenticityToken: {
type: String,
required: false,
default: ''
},
reopenAfterAuth: {
type: Boolean,
required: false,
default: false
},
modalContainerEl: {
type: [Object, String],
required: true
},
loading: {
type: Boolean,
required: false,
default: true
}
},
emits: ['close', 'picked', 'update:loading'],
data () {
return {
isConnectClicked: false,
showOauthButton: false
}
},
computed: {
oauthPath () {
const redir = this.reopenAfterAuth
? `/templates/${this.templateId}/edit?google_drive_open=1`
: `/templates/${this.templateId}/edit`
const params = {
access_type: 'offline',
include_granted_scopes: 'true',
prompt: 'consent',
scope: [
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/drive.file'
].join(' '),
oauth_data: new URLSearchParams({ redir }).toString()
}
return `/auth/google_oauth2?${new URLSearchParams(params).toString()}`
}
},
mounted () {
window.addEventListener('message', this.messageHandler)
},
beforeUnmount () {
window.removeEventListener('message', this.messageHandler)
},
methods: {
messageHandler (event) {
if (event.data.type === 'google-drive-files-picked') {
const files = event.data.files || []
if (!files.length) return
this.$emit('update:loading', true)
this.$emit('picked', files)
} else if (event.data.type === 'google-drive-picker-loaded') {
this.$emit('update:loading', false)
} else if (event.data.type === 'google-drive-picker-request-oauth') {
this.showOauthButton = true
this.$emit('update:loading', false)
}
}
}
}
</script>

@ -219,7 +219,9 @@ const en = {
revisions: 'Revisions', revisions: 'Revisions',
apply: 'Apply', apply: 'Apply',
no_revisions_yet: 'No revisions yet', no_revisions_yet: 'No revisions yet',
viewing_revision_from: 'Viewing revision from {date}' viewing_revision_from: 'Viewing revision from {date}',
connect_google_drive: 'Connect Google Drive',
submitting: 'Submitting'
} }
const es = { const es = {
@ -443,7 +445,9 @@ const es = {
revisions: 'Revisiones', revisions: 'Revisiones',
apply: 'Aplicar', apply: 'Aplicar',
no_revisions_yet: 'Aún no hay revisiones', no_revisions_yet: 'Aún no hay revisiones',
viewing_revision_from: 'Viendo revisión del {date}' viewing_revision_from: 'Viendo revisión del {date}',
connect_google_drive: 'Conectar Google Drive',
submitting: 'Enviando'
} }
const it = { const it = {
@ -667,7 +671,9 @@ const it = {
revisions: 'Revisioni', revisions: 'Revisioni',
apply: 'Applica', apply: 'Applica',
no_revisions_yet: 'Nessuna revisione ancora', no_revisions_yet: 'Nessuna revisione ancora',
viewing_revision_from: 'Visualizzazione revisione del {date}' viewing_revision_from: 'Visualizzazione revisione del {date}',
connect_google_drive: 'Connetti Google Drive',
submitting: 'Invio in corso'
} }
const pt = { const pt = {
@ -891,7 +897,9 @@ const pt = {
revisions: 'Revisões', revisions: 'Revisões',
apply: 'Aplicar', apply: 'Aplicar',
no_revisions_yet: 'Nenhuma revisão ainda', no_revisions_yet: 'Nenhuma revisão ainda',
viewing_revision_from: 'Visualizando revisão de {date}' viewing_revision_from: 'Visualizando revisão de {date}',
connect_google_drive: 'Conectar Google Drive',
submitting: 'Enviando'
} }
const fr = { const fr = {
@ -1115,7 +1123,9 @@ const fr = {
revisions: 'Révisions', revisions: 'Révisions',
apply: 'Appliquer', apply: 'Appliquer',
no_revisions_yet: 'Aucune révision pour le moment', no_revisions_yet: 'Aucune révision pour le moment',
viewing_revision_from: 'Affichage de la révision du {date}' viewing_revision_from: 'Affichage de la révision du {date}',
connect_google_drive: 'Connecter Google Drive',
submitting: 'Soumission en cours'
} }
const de = { const de = {
@ -1339,7 +1349,9 @@ const de = {
revisions: 'Revisionen', revisions: 'Revisionen',
apply: 'Anwenden', apply: 'Anwenden',
no_revisions_yet: 'Noch keine Revisionen', no_revisions_yet: 'Noch keine Revisionen',
viewing_revision_from: 'Ansicht der Revision vom {date}' viewing_revision_from: 'Ansicht der Revision vom {date}',
connect_google_drive: 'Google Drive verbinden',
submitting: 'Wird eingereicht'
} }
const nl = { const nl = {
@ -1563,7 +1575,9 @@ const nl = {
revisions: 'Revisies', revisions: 'Revisies',
apply: 'Toepassen', apply: 'Toepassen',
no_revisions_yet: 'Nog geen revisies', no_revisions_yet: 'Nog geen revisies',
viewing_revision_from: 'Revisie van {date} bekijken' viewing_revision_from: 'Revisie van {date} bekijken',
connect_google_drive: 'Verbind Google Drive',
submitting: 'Indienen'
} }
export { en, es, it, pt, fr, de, nl } export { en, es, it, pt, fr, de, nl }

@ -36,7 +36,10 @@
v-if="withReplaceButton" v-if="withReplaceButton"
:template-id="template.id" :template-id="template.id"
:accept-file-types="acceptFileTypes" :accept-file-types="acceptFileTypes"
class="opacity-0 group-hover:opacity-100" :authenticity-token="authenticityToken"
:with-google-drive="withGoogleDrive"
:google-drive-file-id="item.google_drive_file_id"
class="opacity-0 group-hover:opacity-100 has-[label:focus]:opacity-100"
@click.stop @click.stop
@success="$emit('replace', { replaceSchemaItem: item, ...$event })" @success="$emit('replace', { replaceSchemaItem: item, ...$event })"
/> />
@ -232,6 +235,16 @@ export default {
required: true, required: true,
default: true default: true
}, },
withGoogleDrive: {
type: Boolean,
required: false,
default: false
},
authenticityToken: {
type: String,
required: false,
default: ''
},
dynamicDocuments: { dynamicDocuments: {
type: Array, type: Array,
required: true required: true

@ -1,14 +1,52 @@
<template> <template>
<label <div class="inline-flex items-stretch replace-document-control">
:for="inputId" <label
class="btn btn-neutral btn-xs text-white transition-none replace-document-button" :for="inputId"
:class="{ 'opacity-100': isLoading }" class="btn btn-neutral btn-xs text-white transition-none replace-document-button"
> :class="[{ 'opacity-100': isLoading }, showGoogleDriveDropdown ? 'rounded-r-none pr-1.5' : '']"
{{ message }} >
{{ message }}
</label>
<span
v-if="showGoogleDriveDropdown"
class="dropdown dropdown-end"
@click.stop
>
<label
tabindex="0"
class="btn btn-neutral btn-xs text-white rounded-l-none border-l border-white/30 px-1 transition-none cursor-pointer flex items-center"
>
<IconChevronDown
width="12"
stroke-width="2.5"
/>
</label>
<ul
tabindex="0"
:style="{ minWidth: '130px', backgroundColor: backgroundColor }"
class="mt-1.5 dropdown-content p-1 shadow-lg rounded-lg border border-neutral-200 z-50 bg-white"
>
<li>
<button
type="button"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
@click.prevent="openGoogleDriveModal"
>
<IconBrandGoogleDrive class="w-4 h-4 flex-shrink-0" />
<span>Google Drive</span>
</button>
</li>
</ul>
</span>
<form <form
ref="form" ref="form"
class="hidden" class="hidden"
> >
<input
v-if="googleDriveFile"
name="google_drive_file_ids[]"
:value="googleDriveFile.id"
>
<input <input
:id="inputId" :id="inputId"
ref="input" ref="input"
@ -18,20 +56,51 @@
@change="upload" @change="upload"
> >
</form> </form>
</label> <GoogleDrivePickerModal
v-if="showGoogleDriveModal"
v-model:loading="isLoadingGoogleDrive"
:template-id="templateId"
:authenticity-token="authenticityToken"
:modal-container-el="modalContainerEl"
@close="showGoogleDriveModal = false"
@picked="onGoogleDrivePicked"
/>
</div>
</template> </template>
<script> <script>
import Upload from './upload' import Upload from './upload'
import GoogleDrivePickerModal from './google_drive_picker_modal'
import { IconChevronDown, IconBrandGoogleDrive } from '@tabler/icons-vue'
export default { export default {
name: 'ReplaceDocument', name: 'ReplaceDocument',
inject: ['baseFetch', 't'], components: {
IconChevronDown,
IconBrandGoogleDrive,
GoogleDrivePickerModal
},
inject: ['baseFetch', 't', 'backgroundColor'],
props: { props: {
templateId: { templateId: {
type: [Number, String], type: [Number, String],
required: true required: true
}, },
authenticityToken: {
type: String,
required: false,
default: ''
},
googleDriveFileId: {
type: String,
required: false,
default: ''
},
withGoogleDrive: {
type: Boolean,
required: false,
default: false
},
acceptFileTypes: { acceptFileTypes: {
type: String, type: String,
required: false, required: false,
@ -41,16 +110,25 @@ export default {
emits: ['success'], emits: ['success'],
data () { data () {
return { return {
isLoading: false isLoading: false,
isLoadingGoogleDrive: true,
googleDriveFile: null,
showGoogleDriveModal: false
} }
}, },
computed: { computed: {
inputId () { inputId () {
return 'el' + Math.random().toString(32).split('.')[1] return 'el' + Math.random().toString(32).split('.')[1]
}, },
showGoogleDriveDropdown () {
return this.withGoogleDrive && !!this.googleDriveFileId
},
uploadUrl () { uploadUrl () {
return `/templates/${this.templateId}/documents` return `/templates/${this.templateId}/documents`
}, },
modalContainerEl () {
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
},
message () { message () {
if (this.isLoading) { if (this.isLoading) {
return this.t('uploading_') return this.t('uploading_')
@ -60,7 +138,27 @@ export default {
} }
}, },
methods: { methods: {
upload: Upload.methods.upload upload: Upload.methods.upload,
openGoogleDriveModal () {
this.showGoogleDriveModal = true
this.isLoadingGoogleDrive = true
this.googleDriveFile = null
this.$el.getRootNode().activeElement?.blur()
},
onGoogleDrivePicked (files) {
this.googleDriveFile = files[0]
this.$nextTick(() => {
this.upload({ path: `/templates/${this.templateId}/google_drive_documents` }).then((resp) => {
if (resp.ok) {
this.showGoogleDriveModal = false
}
}).finally(() => {
this.isLoadingGoogleDrive = false
this.googleDriveFile = null
})
})
}
} }
} }
</script> </script>

@ -61,87 +61,16 @@
</span> </span>
</div> </div>
</label> </label>
<Teleport <GoogleDrivePickerModal
v-if="showGoogleDriveModal" v-if="showGoogleDriveModal"
:to="modalContainerEl" v-model:loading="isLoadingGoogleDrive"
> :template-id="templateId"
<div :authenticity-token="authenticityToken"
class="modal modal-open items-start !animate-none overflow-y-auto" :modal-container-el="modalContainerEl"
> :reopen-after-auth="true"
<div @close="showGoogleDriveModal = false"
class="absolute top-0 bottom-0 right-0 left-0" @picked="onGoogleDrivePicked"
@click.prevent="showGoogleDriveModal = false" />
/>
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
<span class="modal-title">
Google Drive
</span>
<a
href="#"
class="text-xl modal-close-button"
@click.prevent="showGoogleDriveModal = false"
>&times;</a>
</div>
<div>
<form
v-if="showGoogleDriveOauthButton"
method="post"
:action="googleDriveOauthPath"
@submit="isConnectGoogleDriveClicked = true"
>
<input
type="hidden"
name="authenticity_token"
:value="authenticityToken"
autocomplete="off"
>
<button
id="gdrive_oauth_button"
class="btn bg-white btn-outline w-full text-base font-medium mt-4"
data-turbo="false"
type="submit"
:disabled="isConnectGoogleDriveClicked"
>
<span v-if="isConnectGoogleDriveClicked">
<span class="flex items-center justify-center space-x-2">
<IconInnerShadowTop class="animate-spin" />
<span class="">Submitting...</span>
</span>
</span>
<span
v-else
>
<span class="flex items-center justify-center space-x-2">
<IconBrandGoogleDrive />
<span>Connect Google Drive</span>
</span>
</span>
</button>
</form>
<div
v-else
class="relative"
>
<iframe
class="border border-base-300 rounded-lg"
style="width: 100%; height: 440px; background: white;"
src="/template_google_drive"
/>
<div v-if="isLoadingGoogleDrive">
<div
class="bg-white absolute top-0 bottom-0 left-0 right-0 opacity-80 rounded-lg"
style="margin: 1px"
/>
<div class="absolute top-0 bottom-0 left-0 right-0 flex items-center justify-center">
<IconInnerShadowTop class="animate-spin" />
</div>
</div>
</div>
</div>
</div>
</div>
</Teleport>
<form <form
ref="form" ref="form"
class="hidden" class="hidden"
@ -167,6 +96,7 @@
<script> <script>
import { IconUpload, IconInnerShadowTop, IconChevronDown, IconBrandGoogleDrive } from '@tabler/icons-vue' import { IconUpload, IconInnerShadowTop, IconChevronDown, IconBrandGoogleDrive } from '@tabler/icons-vue'
import GoogleDrivePickerModal from './google_drive_picker_modal'
function convertImage (sourceFile, targetType, quality) { function convertImage (sourceFile, targetType, quality) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -233,7 +163,8 @@ export default {
IconUpload, IconUpload,
IconInnerShadowTop, IconInnerShadowTop,
IconChevronDown, IconChevronDown,
IconBrandGoogleDrive IconBrandGoogleDrive,
GoogleDrivePickerModal
}, },
inject: ['baseFetch', 't', 'backgroundColor'], inject: ['baseFetch', 't', 'backgroundColor'],
props: { props: {
@ -261,11 +192,9 @@ export default {
data () { data () {
return { return {
isLoading: false, isLoading: false,
isConnectGoogleDriveClicked: false, isLoadingGoogleDrive: true,
isLoadingGoogleDrive: false,
googleDriveFiles: [], googleDriveFiles: [],
showGoogleDriveModal: false, showGoogleDriveModal: false
showGoogleDriveOauthButton: false
} }
}, },
computed: { computed: {
@ -278,65 +207,35 @@ export default {
uploadUrl () { uploadUrl () {
return `/templates/${this.templateId}/documents` return `/templates/${this.templateId}/documents`
}, },
googleDriveOauthPath () {
const params = {
access_type: 'offline',
include_granted_scopes: 'true',
prompt: 'consent',
scope: [
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/drive.file'
].join(' '),
oauth_data: new URLSearchParams({
redir: `/templates/${this.templateId}/edit?google_drive_open=1`
}).toString()
}
const query = new URLSearchParams(params).toString()
return `/auth/google_oauth2?${query}`
},
modalContainerEl () { modalContainerEl () {
return this.$el.getRootNode().querySelector('#docuseal_modal_container') return this.$el.getRootNode().querySelector('#docuseal_modal_container')
} }
}, },
mounted () { mounted () {
window.addEventListener('message', this.messageHandler)
if (this.queryParams.get('google_drive_open') === '1') { if (this.queryParams.get('google_drive_open') === '1') {
this.openGoogleDriveModal() this.openGoogleDriveModal()
window.history.replaceState({}, document.title, window.location.pathname) window.history.replaceState({}, document.title, window.location.pathname)
} }
}, },
beforeUnmount () {
window.removeEventListener('message', this.messageHandler)
},
methods: { methods: {
openGoogleDriveModal () { openGoogleDriveModal () {
this.showGoogleDriveModal = true this.showGoogleDriveModal = true
this.isLoadingGoogleDrive = true this.isLoadingGoogleDrive = true
}, },
messageHandler (event) { onGoogleDrivePicked (files) {
if (event.data.type === 'google-drive-files-picked') { this.googleDriveFiles = files
this.googleDriveFiles = event.data.files || []
this.$nextTick(() => {
this.isLoadingGoogleDrive = true
this.upload({ path: `/templates/${this.templateId}/google_drive_documents` }).then((resp) => { this.$nextTick(() => {
if (resp.ok) { this.upload({ path: `/templates/${this.templateId}/google_drive_documents` }).then((resp) => {
this.showGoogleDriveModal = false if (resp.ok) {
} this.showGoogleDriveModal = false
}).finally(() => { }
this.isLoadingGoogleDrive = false }).finally(() => {
}) this.isLoadingGoogleDrive = false
this.googleDriveFiles = []
}) })
} else if (event.data.type === 'google-drive-picker-loaded') { })
this.isLoadingGoogleDrive = false
} else if (event.data.type === 'google-drive-picker-request-oauth') {
this.showGoogleDriveOauthButton = true
}
}, },
async upload ({ path } = {}) { async upload ({ path } = {}) {
this.isLoading = true this.isLoading = true

@ -20,7 +20,7 @@ RSpec.describe 'Template Builder' do
expect do expect do
doc.find('.replace-document-button').click doc.find('.replace-document-button').click
doc.find('.replace-document-button input[type="file"]', visible: false) doc.find('.replace-document-control input[type="file"]', visible: false)
.attach_file(Rails.root.join('spec/fixtures/sample-image.png')) .attach_file(Rails.root.join('spec/fixtures/sample-image.png'))
page.driver.wait_for_network_idle page.driver.wait_for_network_idle

Loading…
Cancel
Save