Files

392 lines
13 KiB
Vue
Raw Permalink Normal View History

2023-05-31 00:50:43 +03:00
<template>
<div>
<div class="relative">
<img
:src="previewImage.url"
:width="previewImage.metadata.width"
:height="previewImage.metadata.height"
class="rounded border"
loading="lazy"
>
<div
2025-10-11 10:58:09 +03:00
class="group flex justify-end cursor-pointer top-0 bottom-0 left-0 right-0 absolute p-1 hover:bg-black/10 transition-colors rounded"
2023-05-31 00:50:43 +03:00
@click="$emit('scroll-to', item)"
>
2023-11-04 01:19:32 +02:00
<div
v-if="editable"
class="flex justify-between w-full"
>
2024-12-07 01:35:59 +02:00
<div
style="width: 26px"
2026-02-12 09:22:08 +02:00
class="flex flex-col"
2024-12-07 01:35:59 +02:00
>
2026-02-12 09:22:08 +02:00
<button
v-if="item.conditions?.length"
class="btn border-gray-300 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors p-0 document-control-button"
@click.stop="isShowConditionsModal = true"
>
<IconRouteAltLeft
:width="14"
:stroke-width="1.6"
/>
</button>
2024-12-07 01:35:59 +02:00
</div>
2023-07-26 20:35:53 +03:00
<div class="">
<ReplaceButton
2023-11-09 10:54:56 +02:00
v-if="withReplaceButton"
2023-07-26 20:35:53 +03:00
:template-id="template.id"
2023-09-05 20:33:40 +03:00
:accept-file-types="acceptFileTypes"
2026-06-04 00:35:04 +03:00
: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"
2023-07-26 20:35:53 +03:00
@click.stop
@success="$emit('replace', { replaceSchemaItem: item, ...$event })"
/>
2023-05-31 00:50:43 +03:00
</div>
<div
2026-02-12 09:22:08 +02:00
class="flex flex-col justify-between"
2023-05-31 00:50:43 +03:00
>
2026-02-12 09:22:08 +02:00
<span
class="dropdown dropdown-end group-hover:opacity-100 has-[label:focus]:opacity-100"
:class="{ 'dropdown-open': isMakeDynamicLoading, 'opacity-0': !isMakeDynamicLoading }"
@mouseenter="renderDropdown = true"
@touchstart="renderDropdown = true"
>
<label
tabindex="0"
class="btn border-gray-300 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors document-control-button px-0"
style="width: 24px; height: 24px"
@click.stop
>
<IconDotsVertical
:width="16"
:height="16"
:stroke-width="1.6"
/>
</label>
<ul
v-if="renderDropdown"
tabindex="0"
class="mt-1.5 dropdown-content p-1 shadow-lg rounded-lg border border-neutral-200 z-50 bg-white"
style="min-width: 170px"
@click="closeDropdown"
>
2026-06-10 15:12:16 +03:00
<li v-if="!item.dynamic">
<button
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm whitespace-nowrap"
@click.stop="$emit('edit', item); closeDropdown()"
>
<IconPencil class="w-4 h-4" />
<span>{{ t('edit') }}</span>
</button>
</li>
2026-02-12 09:22:08 +02:00
<li>
<button
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
@click.stop="isShowConditionsModal = true; closeDropdown()"
>
<span class="flex items-center space-x-2">
<IconRouteAltLeft class="w-4 h-4" />
<span>{{ t('condition') }}</span>
</span>
<span
v-if="item.conditions?.length"
class="bg-neutral-200 rounded px-1 leading-3"
style="font-size: 9px;"
>{{ item.conditions.length }}</span>
</button>
</li>
<li v-if="!item.dynamic">
<button
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm whitespace-nowrap"
@click.stop="$emit('reorder', item); closeDropdown()"
>
<IconSortDescending2 class="w-4 h-4" />
<span>{{ t('reorder_fields') }}</span>
</button>
</li>
<li v-if="withDynamicDocuments && !item.dynamic && document.metadata?.original_uuid">
<button
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm whitespace-nowrap"
:disabled="isMakeDynamicLoading"
@click.stop="makeDynamic"
>
<IconInnerShadowTop
v-if="isMakeDynamicLoading"
class="w-4 h-4 animate-spin"
/>
<IconBolt
v-else
class="w-4 h-4"
/>
<span>{{ t('make_dynamic') }}</span>
</button>
</li>
<hr class="my-1 border-neutral-200">
<li>
<button
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm text-red-600"
@click.stop="$emit('remove', item); closeDropdown()"
>
<IconTrashX class="w-4 h-4" />
<span>{{ t('remove') }}</span>
</button>
</li>
</ul>
</span>
<div
v-if="withArrows"
class="flex flex-col space-y-1 opacity-0 group-hover:opacity-100"
>
2023-07-26 20:35:53 +03:00
<button
2025-10-11 10:58:09 +03:00
class="btn border-gray-300 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors document-control-button"
2023-07-26 20:35:53 +03:00
style="width: 24px; height: 24px"
2026-02-12 09:22:08 +02:00
@click.stop="$emit('up', item)"
2023-07-26 20:35:53 +03:00
>
2026-02-12 09:22:08 +02:00
&uarr;
2023-07-26 20:35:53 +03:00
</button>
2026-02-12 09:22:08 +02:00
<button
class="btn border-gray-300 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors document-control-button"
style="width: 24px; height: 24px"
@click.stop="$emit('down', item)"
2023-07-26 20:35:53 +03:00
>
2026-02-12 09:22:08 +02:00
&darr;
</button>
2023-07-26 20:35:53 +03:00
</div>
2023-05-31 00:50:43 +03:00
</div>
</div>
</div>
</div>
2025-10-09 16:43:11 +03:00
<div class="flex items-center gap-1 pb-2 pt-1.5 document-preview-name">
<GoogleDriveDocumentSettings
v-if="item.google_drive_file_id"
:template-id="template.id"
:google-drive-file-id="item.google_drive_file_id"
@success="$emit('replace', { replaceSchemaItem: item, ...$event })"
/>
2023-05-31 00:50:43 +03:00
<Contenteditable
:model-value="item.name"
:icon-width="16"
2025-10-09 16:43:11 +03:00
:icon-inline="true"
2025-10-11 10:58:09 +03:00
:float-icon="!item.google_drive_file_id"
:hide-icon="!item.google_drive_file_id"
2023-12-20 21:58:47 +02:00
:editable="editable"
2023-05-31 00:50:43 +03:00
class="mx-auto"
@update:model-value="onUpdateName"
/>
</div>
2025-01-10 16:00:14 +02:00
<Teleport
v-if="isShowConditionsModal"
:to="modalContainerEl"
>
<ConditionsModal
:item="item"
:build-default-name="buildDefaultName"
2026-01-30 21:36:33 +02:00
@save="$emit('change')"
2025-01-10 16:00:14 +02:00
@close="isShowConditionsModal = false"
/>
</Teleport>
2023-05-31 00:50:43 +03:00
</div>
</template>
<script>
import Contenteditable from './contenteditable'
2023-07-26 20:35:53 +03:00
import Upload from './upload'
2026-06-10 15:12:16 +03:00
import { IconRouteAltLeft, IconSortDescending2, IconDotsVertical, IconTrashX, IconBolt, IconInnerShadowTop, IconPencil } from '@tabler/icons-vue'
2024-12-07 01:35:59 +02:00
import ConditionsModal from './conditions_modal'
2023-07-26 20:35:53 +03:00
import ReplaceButton from './replace'
2025-10-09 16:43:11 +03:00
import GoogleDriveDocumentSettings from './google_drive_document_settings'
2024-12-07 01:35:59 +02:00
import Field from './field'
import FieldType from './field_type'
2026-03-05 10:08:29 +02:00
import { v4 } from 'uuid'
2023-05-31 00:50:43 +03:00
export default {
name: 'DocumentPreview',
components: {
2023-07-26 20:35:53 +03:00
Contenteditable,
2026-02-12 09:22:08 +02:00
IconInnerShadowTop,
2024-12-07 01:35:59 +02:00
IconRouteAltLeft,
ConditionsModal,
2025-01-04 14:01:09 +02:00
ReplaceButton,
2025-10-09 16:43:11 +03:00
GoogleDriveDocumentSettings,
2026-02-12 09:22:08 +02:00
IconSortDescending2,
IconDotsVertical,
IconTrashX,
2026-06-10 15:12:16 +03:00
IconBolt,
IconPencil
2023-05-31 00:50:43 +03:00
},
2026-02-12 09:22:08 +02:00
inject: ['t', 'getFieldTypeIndex', 'baseFetch'],
2023-05-31 00:50:43 +03:00
props: {
item: {
type: Object,
required: true
},
2023-07-26 20:35:53 +03:00
template: {
type: Object,
required: true
},
2023-05-31 00:50:43 +03:00
document: {
type: Object,
required: true
},
2023-11-04 01:19:32 +02:00
editable: {
type: Boolean,
required: false,
default: true
},
2023-09-05 20:33:40 +03:00
acceptFileTypes: {
type: String,
required: false,
2026-03-04 12:20:52 +02:00
default: 'image/*, application/pdf, application/zip, application/json'
2023-09-05 20:33:40 +03:00
},
2023-11-09 10:54:56 +02:00
withReplaceButton: {
type: Boolean,
required: true,
default: true
},
2026-06-04 00:35:04 +03:00
withGoogleDrive: {
type: Boolean,
required: false,
default: false
},
authenticityToken: {
type: String,
required: false,
default: ''
},
2026-02-12 09:22:08 +02:00
dynamicDocuments: {
type: Array,
required: true
},
withDynamicDocuments: {
type: Boolean,
required: false,
default: false
},
2023-05-31 00:50:43 +03:00
withArrows: {
type: Boolean,
required: false,
default: true
}
},
2026-06-10 15:12:16 +03:00
emits: ['scroll-to', 'change', 'remove', 'up', 'down', 'replace', 'reorder', 'edit'],
2024-12-07 01:35:59 +02:00
data () {
return {
2026-02-12 09:22:08 +02:00
isShowConditionsModal: false,
isMakeDynamicLoading: false,
renderDropdown: false
2024-12-07 01:35:59 +02:00
}
},
2023-05-31 00:50:43 +03:00
computed: {
2024-12-07 01:35:59 +02:00
fieldNames: FieldType.computed.fieldNames,
fieldLabels: FieldType.computed.fieldLabels,
2023-05-31 00:50:43 +03:00
previewImage () {
2023-10-22 01:54:20 +03:00
return [...this.document.preview_images].sort((a, b) => parseInt(a.filename) - parseInt(b.filename))[0]
2024-12-07 01:35:59 +02:00
},
modalContainerEl () {
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
2023-05-31 00:50:43 +03:00
}
},
methods: {
2023-07-26 20:35:53 +03:00
upload: Upload.methods.upload,
2024-12-07 01:35:59 +02:00
buildDefaultName: Field.methods.buildDefaultName,
2026-02-12 09:22:08 +02:00
closeDropdown () {
this.$el.getRootNode().activeElement.blur()
},
makeDynamic () {
this.isMakeDynamicLoading = true
2026-03-11 10:44:12 +02:00
Promise.all([
this.baseFetch(`/templates/${this.template.id}/dynamic_documents`, {
method: 'POST',
body: JSON.stringify({ uuid: this.document.uuid }),
headers: {
'Content-Type': 'application/json'
}
}),
import(/* webpackChunkName: "dynamic-editor" */ './dynamic_document')
]).then(async ([resp, _]) => {
2026-02-12 09:22:08 +02:00
const dynamicDocument = await resp.json()
2026-03-05 10:08:29 +02:00
this.template.schema.find((item) => item.attachment_uuid === dynamicDocument.uuid).dynamic = true
this.removeFieldAreas()
if (dynamicDocument.fields?.length) {
this.addDynamicFields(dynamicDocument.fields)
}
2026-02-12 09:22:08 +02:00
if (dynamicDocument.uuid) {
2026-03-05 10:08:29 +02:00
delete dynamicDocument.fields
2026-02-12 09:22:08 +02:00
this.dynamicDocuments.push(dynamicDocument)
}
this.$emit('change')
}).finally(() => {
this.isMakeDynamicLoading = false
})
},
2026-03-05 10:08:29 +02:00
removeFieldAreas () {
this.template.fields.forEach((field) => {
if (field.areas?.length) {
field.areas = field.areas.filter((a) => a.attachment_uuid !== this.document.uuid)
}
})
this.template.fields = this.template.fields.filter((field) => field.areas?.length)
},
addDynamicFields (fields) {
const submittersNameIndex = this.template.submitters.reduce((acc, submitter) => {
acc[submitter.name] = submitter
return acc
}, {})
fields.forEach((field) => {
const roleName = field.role || this.template.submitters[0]?.name || this.t('first_party')
let submitter = submittersNameIndex[roleName]
if (!submitter) {
submitter = { name: roleName, uuid: v4() }
this.template.submitters.push(submitter)
submittersNameIndex[roleName] = submitter
}
const existingField = this.template.fields.find((f) => {
return f.name && f.name === field.name && f.type === (field.type || 'text') && f.submitter_uuid === submitter.uuid
})
if (existingField) {
field.areas.forEach((area) => {
area.attachment_uuid = this.document.uuid
existingField.areas = existingField.areas || []
existingField.areas.push(area)
})
} else {
field.submitter_uuid = submitter.uuid
delete field.role
field.areas.forEach((area) => {
area.attachment_uuid = this.document.uuid
})
this.template.fields.push(field)
}
})
},
2023-05-31 00:50:43 +03:00
onUpdateName (value) {
this.item.name = value
this.$emit('change')
}
}
}
</script>