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

@ -10,6 +10,9 @@
v-if="withReplaceButton"
:template-id="template.id"
:accept-file-types="acceptFileTypes"
:authenticity-token="authenticityToken"
:with-google-drive="withGoogleDrive"
:google-drive-file-id="item.google_drive_file_id"
@click.stop
@success="$emit('replace', { replaceSchemaItem: item, ...$event })"
/>
@ -73,6 +76,16 @@ export default {
required: true,
default: true
},
withGoogleDrive: {
type: Boolean,
required: false,
default: false
},
authenticityToken: {
type: String,
required: false,
default: ''
},
withArrows: {
type: Boolean,
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',
apply: 'Apply',
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 = {
@ -443,7 +445,9 @@ const es = {
revisions: 'Revisiones',
apply: 'Aplicar',
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 = {
@ -667,7 +671,9 @@ const it = {
revisions: 'Revisioni',
apply: 'Applica',
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 = {
@ -891,7 +897,9 @@ const pt = {
revisions: 'Revisões',
apply: 'Aplicar',
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 = {
@ -1115,7 +1123,9 @@ const fr = {
revisions: 'Révisions',
apply: 'Appliquer',
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 = {
@ -1339,7 +1349,9 @@ const de = {
revisions: 'Revisionen',
apply: 'Anwenden',
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 = {
@ -1563,7 +1575,9 @@ const nl = {
revisions: 'Revisies',
apply: 'Toepassen',
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 }

@ -36,7 +36,10 @@
v-if="withReplaceButton"
:template-id="template.id"
: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
@success="$emit('replace', { replaceSchemaItem: item, ...$event })"
/>
@ -232,6 +235,16 @@ export default {
required: true,
default: true
},
withGoogleDrive: {
type: Boolean,
required: false,
default: false
},
authenticityToken: {
type: String,
required: false,
default: ''
},
dynamicDocuments: {
type: Array,
required: true

@ -1,13 +1,51 @@
<template>
<div class="inline-flex items-stretch replace-document-control">
<label
:for="inputId"
class="btn btn-neutral btn-xs text-white transition-none replace-document-button"
:class="{ 'opacity-100': isLoading }"
:class="[{ 'opacity-100': isLoading }, showGoogleDriveDropdown ? 'rounded-r-none pr-1.5' : '']"
>
{{ 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
ref="form"
class="hidden"
>
<input
v-if="googleDriveFile"
name="google_drive_file_ids[]"
:value="googleDriveFile.id"
>
<input
:id="inputId"
@ -18,20 +56,51 @@
@change="upload"
>
</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>
<script>
import Upload from './upload'
import GoogleDrivePickerModal from './google_drive_picker_modal'
import { IconChevronDown, IconBrandGoogleDrive } from '@tabler/icons-vue'
export default {
name: 'ReplaceDocument',
inject: ['baseFetch', 't'],
components: {
IconChevronDown,
IconBrandGoogleDrive,
GoogleDrivePickerModal
},
inject: ['baseFetch', 't', 'backgroundColor'],
props: {
templateId: {
type: [Number, String],
required: true
},
authenticityToken: {
type: String,
required: false,
default: ''
},
googleDriveFileId: {
type: String,
required: false,
default: ''
},
withGoogleDrive: {
type: Boolean,
required: false,
default: false
},
acceptFileTypes: {
type: String,
required: false,
@ -41,16 +110,25 @@ export default {
emits: ['success'],
data () {
return {
isLoading: false
isLoading: false,
isLoadingGoogleDrive: true,
googleDriveFile: null,
showGoogleDriveModal: false
}
},
computed: {
inputId () {
return 'el' + Math.random().toString(32).split('.')[1]
},
showGoogleDriveDropdown () {
return this.withGoogleDrive && !!this.googleDriveFileId
},
uploadUrl () {
return `/templates/${this.templateId}/documents`
},
modalContainerEl () {
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
},
message () {
if (this.isLoading) {
return this.t('uploading_')
@ -60,7 +138,27 @@ export default {
}
},
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>

@ -61,87 +61,16 @@
</span>
</div>
</label>
<Teleport
<GoogleDrivePickerModal
v-if="showGoogleDriveModal"
: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="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"
v-model:loading="isLoadingGoogleDrive"
:template-id="templateId"
:authenticity-token="authenticityToken"
:modal-container-el="modalContainerEl"
:reopen-after-auth="true"
@close="showGoogleDriveModal = false"
@picked="onGoogleDrivePicked"
/>
<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
ref="form"
class="hidden"
@ -167,6 +96,7 @@
<script>
import { IconUpload, IconInnerShadowTop, IconChevronDown, IconBrandGoogleDrive } from '@tabler/icons-vue'
import GoogleDrivePickerModal from './google_drive_picker_modal'
function convertImage (sourceFile, targetType, quality) {
return new Promise((resolve, reject) => {
@ -233,7 +163,8 @@ export default {
IconUpload,
IconInnerShadowTop,
IconChevronDown,
IconBrandGoogleDrive
IconBrandGoogleDrive,
GoogleDrivePickerModal
},
inject: ['baseFetch', 't', 'backgroundColor'],
props: {
@ -261,11 +192,9 @@ export default {
data () {
return {
isLoading: false,
isConnectGoogleDriveClicked: false,
isLoadingGoogleDrive: false,
isLoadingGoogleDrive: true,
googleDriveFiles: [],
showGoogleDriveModal: false,
showGoogleDriveOauthButton: false
showGoogleDriveModal: false
}
},
computed: {
@ -278,65 +207,35 @@ export default {
uploadUrl () {
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 () {
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
}
},
mounted () {
window.addEventListener('message', this.messageHandler)
if (this.queryParams.get('google_drive_open') === '1') {
this.openGoogleDriveModal()
window.history.replaceState({}, document.title, window.location.pathname)
}
},
beforeUnmount () {
window.removeEventListener('message', this.messageHandler)
},
methods: {
openGoogleDriveModal () {
this.showGoogleDriveModal = true
this.isLoadingGoogleDrive = true
},
messageHandler (event) {
if (event.data.type === 'google-drive-files-picked') {
this.googleDriveFiles = event.data.files || []
onGoogleDrivePicked (files) {
this.googleDriveFiles = files
this.$nextTick(() => {
this.isLoadingGoogleDrive = true
this.upload({ path: `/templates/${this.templateId}/google_drive_documents` }).then((resp) => {
if (resp.ok) {
this.showGoogleDriveModal = 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 } = {}) {
this.isLoading = true

@ -20,7 +20,7 @@ RSpec.describe 'Template Builder' do
expect do
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'))
page.driver.wait_for_network_idle

Loading…
Cancel
Save