native modals

This commit is contained in:
Pete Matsyburka committed 2026-09-23 10:59:54 +03:00
1 parent 0609a70544
commit 27e5c2c81e
21 files changed
+725 -169

No files matched your search

+37 -3
View File
@@ -194,6 +194,8 @@ safeRegisterElement('template-builder', class extends HTMLElement {
document.addEventListener('template-builder:sync', this.onSync)
document.addEventListener('template-builder:scroll-to', this.onScrollTo)
document.addEventListener('template-builder:select-field', this.onSelectField)
document.addEventListener('template-builder:select-submitter', this.onSelectSubmitter)
document.addEventListener('template-builder:sync-custom-fields', this.onSyncCustomFields)
document.addEventListener('template-builder:draw-field', this.onDrawField)
document.addEventListener('turbo:before-cache', this.onBeforeCache)
@@ -201,6 +203,10 @@ safeRegisterElement('template-builder', class extends HTMLElement {
this.appElem.classList.add('md:h-screen')
if (this.dataset.nativeView) {
this.appElem.classList.add('min-w-0')
}
const template = reactive(JSON.parse(this.dataset.template))
this.app = createApp(TemplateBuilder, {
@@ -240,7 +246,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
acceptFileTypes: this.dataset.acceptFileTypes,
showTourStartForm: this.dataset.showTourStartForm === 'true',
nativePlatform: this.dataset.nativePlatform,
view: this.dataset.view
nativeView: this.dataset.nativeView
})
this.component = this.app.mount(this.appElem)
@@ -266,14 +272,33 @@ safeRegisterElement('template-builder', class extends HTMLElement {
onSync = (e) => {
const { schema, documents, fields, submitters } = e.detail
const selectedAreas = this.component.selectedAreasRef.value.map((area) => {
const field = this.component.template.fields.find((f) => f.areas?.includes(area))
return field && { uuid: field.uuid, index: field.areas.indexOf(area) }
})
const selectedSubmitterUuid = this.component.selectedSubmitter?.uuid
Object.assign(this.component.template, { schema, documents, fields, submitters })
this.component.selectedAreasRef.value = selectedAreas.map((item) => item && fields.find((f) => f.uuid === item.uuid)?.areas?.[item.index]).filter(Boolean)
this.component.selectedSubmitter = submitters.find((s) => s.uuid === selectedSubmitterUuid) || submitters[0]
}
onScrollTo = (e) => {
this.component.scrollIntoDocument(e.detail)
}
onSyncCustomFields = (e) => {
this.component.customFields.splice(0, this.component.customFields.length, ...e.detail)
}
onSelectSubmitter = (e) => {
const submitter = this.component.template.submitters.find((s) => s.uuid === e.detail.uuid)
if (submitter) this.component.selectedSubmitter = submitter
}
onSelectField = (e) => {
const { uuid, attachment_uuid: attachmentUuid, page, x, y } = e.detail
const field = this.component.template.fields.find((f) => f.uuid === uuid)
@@ -284,9 +309,16 @@ safeRegisterElement('template-builder', class extends HTMLElement {
}
onDrawField = (e) => {
const { uuid, type, option } = e.detail
const { uuid, type, option, custom_field_uuid: customFieldUuid } = e.detail
if (uuid) {
if (customFieldUuid) {
const field = this.component.customFields.find((f) => f.uuid === customFieldUuid)
if (field) {
this.component.drawCustomField = field
this.component.showDrawField = true
}
} else if (uuid) {
const field = this.component.template.fields.find((f) => f.uuid === uuid)
if (field) {
@@ -338,6 +370,8 @@ safeRegisterElement('template-builder', class extends HTMLElement {
document.removeEventListener('template-builder:sync', this.onSync)
document.removeEventListener('template-builder:scroll-to', this.onScrollTo)
document.removeEventListener('template-builder:select-field', this.onSelectField)
document.removeEventListener('template-builder:select-submitter', this.onSelectSubmitter)
document.removeEventListener('template-builder:sync-custom-fields', this.onSyncCustomFields)
document.removeEventListener('template-builder:draw-field', this.onDrawField)
document.removeEventListener('turbo:before-cache', this.onBeforeCache)
+115 -57
View File
@@ -40,7 +40,7 @@
@blur="onNameBlur"
>{{ optionIndexText }} {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || defaultName }}</span>
<div
v-if="isSettingsFocus || isSelectInput || (isValueInput && field.type !== 'heading') || (isNameFocus && !['checkbox', 'phone'].includes(field.type))"
v-if="isShowControls"
class="flex items-center ml-1.5"
>
<input
@@ -74,62 +74,80 @@
@click.prevent="field.readonly = !(field.readonly ?? true)"
@mousedown.prevent
>{{ t('editable') }}</label>
<span
v-if="field.type !== 'payment' && !isValueInput"
class="dropdown dropdown-end field-area-settings-dropdown"
@mouseenter="renderDropdown = true"
@touchstart="renderDropdown = true"
>
<label
ref="settingsButton"
tabindex="0"
:title="t('settings')"
class="cursor-pointer flex items-center"
style="height: 25px"
@focus="isSettingsFocus = true"
@blur="maybeBlurSettings"
>
<IconDotsVertical class="w-5 h-5" />
</label>
<ul
v-if="renderDropdown"
ref="settingsDropdown"
tabindex="0"
class="dropdown-content menu menu-xs px-2 pb-2 pt-1 shadow rounded-box w-52 z-10 rounded-t-none"
:style="{ backgroundColor: 'white' }"
@dragstart.prevent.stop
@click="closeDropdown"
@focusout="maybeBlurSettings"
>
<FieldSettings
v-if="isMobile"
:field="field"
:default-field="defaultField"
:editable="editable"
:background-color="'white'"
:with-required="false"
:with-areas="false"
:with-signature-id="withSignatureId"
:with-prefillable="withPrefillable"
@click-formula="isShowFormulaModal = true"
@click-font="isShowFontModal = true"
@click-description="isShowDescriptionModal = true"
@add-custom-field="$emit('add-custom-field')"
@click-condition="isShowConditionsModal = true"
@save="$emit('change')"
@scroll-to="[selectedAreasRef.value = [$event], $emit('scroll-to', $event)]"
/>
<div
v-else
class="whitespace-normal"
>
The dots menu is retired in favor of the field context menu. Right-click the field to access field settings. Double-click the field to set a default value.
</div>
</ul>
</span>
</div>
<span
v-if="isShowSettingsMenu"
class="dropdown dropdown-end field-area-settings-dropdown"
:class="isMobile ? '-ml-1' : { 'ml-1.5': !isShowControls }"
@mouseenter="renderDropdown = true"
@touchstart="renderDropdown = true"
>
<label
ref="settingsButton"
tabindex="0"
:title="t('settings')"
class="cursor-pointer flex items-center"
:class="{ 'pr-0.5': isMobile }"
style="height: 25px"
@focus="isSettingsFocus = true"
@blur="maybeBlurSettings"
>
<IconDotsVertical class="w-5 h-5" />
</label>
<ul
v-if="renderDropdown"
ref="settingsDropdown"
tabindex="0"
class="dropdown-content menu menu-xs px-2 pb-2 pt-1 shadow rounded-box w-52 z-10 rounded-t-none"
:style="{ backgroundColor: 'white' }"
@dragstart.prevent.stop
@click="closeDropdown"
@focusout="maybeBlurSettings"
>
<FieldSettings
v-if="isMobile"
:field="field"
:default-field="defaultField"
:editable="editable"
:background-color="'white'"
:with-required="false"
:with-areas="false"
:with-signature-id="withSignatureId"
:with-prefillable="withPrefillable"
@click-formula="openFormulaModal"
@click-font="openFontModal"
@click-description="openDescriptionModal"
@add-custom-field="$emit('add-custom-field')"
@click-condition="openConditionsModal"
@save="$emit('change')"
@scroll-to="[selectedAreasRef.value = [$event], $emit('scroll-to', $event)]"
/>
<div
v-else
class="whitespace-normal"
>
The dots menu is retired in favor of the field context menu. Right-click the field to access field settings. Double-click the field to set a default value.
</div>
<li
v-if="isMobile && editable"
class="field-settings-remove"
>
<a
href="#"
class="text-sm py-1 px-2 text-red-600"
@click.prevent="$emit('remove')"
>
<IconTrashX
:width="20"
:stroke-width="1.6"
/>
{{ t('remove') }}
</a>
</li>
</ul>
</span>
<button
v-else-if="editable"
v-else-if="editable && !isShowControls"
class="pr-1"
:title="t('remove')"
@click.prevent="$emit('remove')"
@@ -199,12 +217,13 @@ import FormulaModal from './formula_modal'
import FontModal from './font_modal'
import ConditionsModal from './conditions_modal'
import DescriptionModal from './description_modal'
import { IconX, IconDotsVertical } from '@tabler/icons-vue'
import { IconX, IconDotsVertical, IconTrashX } from '@tabler/icons-vue'
import { v4 } from 'uuid'
export default {
name: 'AreaTitle',
components: {
IconTrashX,
FieldType,
FieldSettings,
FormulaModal,
@@ -215,7 +234,7 @@ export default {
FieldSubmitter,
IconX
},
inject: ['t'],
inject: ['t', 'isMobile'],
props: {
template: {
type: Object,
@@ -292,6 +311,17 @@ export default {
}
},
computed: {
isShowControls () {
if (this.isMobile) return false
return this.isSettingsFocus || this.isSelectInput || (this.isValueInput && this.field.type !== 'heading') || (this.isNameFocus && !['checkbox', 'phone'].includes(this.field.type))
},
isShowSettingsMenu () {
if (this.field.type === 'payment' || this.isValueInput) return false
if (this.isMobile) return this.editable
return this.isShowControls
},
fieldNames: FieldType.computed.fieldNames,
fieldLabels: FieldType.computed.fieldLabels,
submitter () {
@@ -318,6 +348,34 @@ export default {
}
},
methods: {
openFormulaModal () {
if (window.webkit?.messageHandlers?.modal) {
window.webkit.messageHandlers.modal.postMessage({ action: 'sheet', name: 'formula', uuid: this.field.uuid })
} else {
this.isShowFormulaModal = true
}
},
openFontModal () {
if (window.webkit?.messageHandlers?.modal) {
window.webkit.messageHandlers.modal.postMessage({ action: 'sheet', name: 'font', detent: 'medium', uuid: this.field.uuid })
} else {
this.isShowFontModal = true
}
},
openConditionsModal () {
if (window.webkit?.messageHandlers?.modal) {
window.webkit.messageHandlers.modal.postMessage({ action: 'sheet', name: 'conditions', detent: 'medium', uuid: this.field.uuid })
} else {
this.isShowConditionsModal = true
}
},
openDescriptionModal () {
if (window.webkit?.messageHandlers?.modal) {
window.webkit.messageHandlers.modal.postMessage({ action: 'sheet', name: 'description', uuid: this.field.uuid })
} else {
this.isShowDescriptionModal = true
}
},
buildDefaultName: Field.methods.buildDefaultName,
closeDropdown () {
this.$el.getRootNode().activeElement.blur()
+67 -45
View File
@@ -2,7 +2,7 @@
<div
ref="dragContainer"
style="max-width: 1600px"
:class="isNativeView ? (isRevisionsView ? 'px-4 pt-3 pb-6' : 'px-4') : 'mx-auto pl-3 md:pl-4 h-full'"
:class="nativeView ? (isSheetView ? 'bg-base-100' : 'px-4') : 'mx-auto pl-3 md:pl-4 h-full'"
@dragover="onDragover"
@drop="isDragFile = false"
>
@@ -53,7 +53,7 @@
@replace="onDocumentReplace"
@up="moveDocument(item, -1)"
@reorder="reorderFields"
@edit="editModalDocumentUuid = item.attachment_uuid"
@edit="openDocumentsEditor(item)"
@down="moveDocument(item, 1)"
@change="save"
/>
@@ -119,29 +119,36 @@
:with-signature-id="withSignatureId"
:with-prefillable="withPrefillable"
:only-defined-fields="onlyDefinedFields"
:with-sticky-submitters="!!nativeView"
:editable="editable"
:scroll-on-edit="false"
@add-field="addField"
@set-draw="onDrawerSetDraw"
@remove-field="onRemoveField"
@remove-submitter="onRemoveSubmitter"
@select-submitter="selectedSubmitter = $event"
@select-submitter="onDrawerSelectSubmitter"
@set-draw-type="onDrawerSetDrawType"
@set-draw-custom-field="onDrawerSetDrawCustomField"
@set-drag="dragField = $event"
@set-drag-placeholder="$refs.dragPlaceholder.dragPlaceholder = $event"
@drag-end="[dragField = null, $refs.dragPlaceholder.dragPlaceholder = null]"
@change-submitter="selectedSubmitter = $event"
@change-submitter="onDrawerSelectSubmitter"
@scroll-to-area="onDrawerScrollToArea"
@rebuild-variables-schema="rebuildVariablesSchema"
/>
</div>
</MobileDrawer>
<div
v-if="isRevisionsView && !isRevisionsModalOpen"
class="flex justify-center py-8"
>
<IconInnerShadowTop class="w-6 h-6 animate-spin" />
</div>
<NativeModals
v-if="isSheetView"
:default-fields="[...defaultRequiredFields, ...defaultFields]"
:custom-fields="customFields"
:editable="editable"
:authenticity-token="authenticityToken"
:accept-file-types="acceptFileTypes"
:base-url="baseUrl"
:page-preview-format="pagePreviewFormat"
@documents-modified="onDocumentsModified"
/>
<div
v-if="pendingFieldAttachmentUuids.length && editable"
class="top-1.5 sticky h-0 z-20 max-w-2xl mx-auto"
@@ -192,7 +199,7 @@
</div>
</div>
<div
v-if="($slots.buttons || withTitle) && !isNativeView"
v-if="($slots.buttons || withTitle) && !nativeView"
id="title_container"
class="flex justify-between py-1.5 items-center pr-4 top-0 z-10 title-container"
:class="{ sticky: withStickySubmitters || isBreakpointLg, hidden: !!nativePlatform }"
@@ -440,13 +447,7 @@
:data-label="t('revisions')"
data-icon="history"
>
<a
v-if="!!nativePlatform"
:href="`/templates/${template.id}/edit?view=revisions`"
data-turbo-frame="modal"
/>
<button
v-else
class="flex space-x-2"
@click.prevent="openRevisionsModal"
@mouseenter="preloadRevisions"
@@ -507,7 +508,7 @@
</div>
</div>
<div
v-if="!isNativeView"
v-if="!nativeView"
id="main_container"
class="flex main-container"
:class="($slots.buttons || withTitle) && !nativePlatform ? (isMobile ? 'max-h-[calc(100%_-_60px)]' : 'md:max-h-[calc(100%_-_60px)]') : (isMobile ? 'max-h-[100%]' : 'md:max-h-[100%]')"
@@ -539,7 +540,7 @@
@replace="onDocumentReplace"
@up="moveDocument(item, -1)"
@reorder="reorderFields"
@edit="editModalDocumentUuid = item.attachment_uuid"
@edit="openDocumentsEditor(item)"
@down="moveDocument(item, 1)"
@change="save"
/>
@@ -758,7 +759,7 @@
</div>
</div>
<div
v-if="!isNativeView"
v-if="!nativeView"
class="sticky bottom-0 z-10"
>
<native-action
@@ -780,13 +781,13 @@
data-view="fields"
/>
<native-action
v-if="nativePlatform && drawField && (isBreakpointLg || isMobile)"
v-if="nativePlatform && (drawField || drawCustomField) && (isBreakpointLg || isMobile)"
data-placement="draw"
:data-label="t('draw_field').replace('{field}', fieldNames[drawField.type])"
:data-label="t('draw_field').replace('{field}', drawCustomField ? drawCustomField.name : fieldNames[drawField.type])"
>
<button
class="hidden"
@click="[drawField = null, drawOption = null]"
@click="[drawField = null, drawOption = null, drawCustomField = null, showDrawField = false]"
/>
</native-action>
<button
@@ -912,9 +913,8 @@
:template="template"
:revisions="revisions"
:locale="locale"
:inline="isRevisionsView"
@close="isRevisionsModalOpen = false"
@apply="isRevisionsView ? postNativeEvent('template-builder:apply-revision', $event, true) : onRevisionApply($event)"
@apply="onRevisionApply($event)"
/>
<DocumentsEditorModal
v-if="editModalDocumentUuid"
@@ -946,6 +946,7 @@ import DocumentPreview from './preview'
import FieldSubmitter from './field_submitter'
import RevisionsModal from './revisions_modal'
import DocumentsEditorModal from './documents_editor_modal'
import NativeModals from './native_modals'
import { IconPlus, IconFiles, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments, IconDownload, IconHistory, IconX } from '@tabler/icons-vue'
import { v4 } from 'uuid'
import { ref, computed, toRaw, defineAsyncComponent } from 'vue'
@@ -989,7 +990,8 @@ export default {
IconDeviceFloppy,
IconX,
RevisionsModal,
DocumentsEditorModal
DocumentsEditorModal,
NativeModals
},
provide () {
return {
@@ -1237,7 +1239,7 @@ export default {
required: false,
default: ''
},
view: {
nativeView: {
type: String,
required: false,
default: ''
@@ -1425,19 +1427,16 @@ export default {
fieldNames: FieldType.computed.fieldNames,
fieldIcons: FieldType.computed.fieldIcons,
isDocumentsView () {
return this.view === 'documents'
return this.nativeView === 'documents'
},
isFieldsView () {
return this.view === 'fields'
return this.nativeView === 'fields'
},
isSheetView () {
return this.nativeView === 'sheet'
},
withMobileDrawers () {
return !this.nativePlatform && !this.isNativeView && this.sortedDocuments.length > 0
},
isRevisionsView () {
return this.view === 'revisions'
},
isNativeView () {
return this.isDocumentsView || this.isFieldsView || this.isRevisionsView
return !this.nativePlatform && !this.nativeView && this.sortedDocuments.length > 0
},
isMobile () {
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
@@ -1624,10 +1623,6 @@ export default {
this.selectedSubmitter = this.template.submitters[0]
},
mounted () {
if (this.isRevisionsView) {
this.openRevisionsModal()
}
this.undoStack = [JSON.stringify(this.template)]
this.redoStack = []
@@ -2200,6 +2195,10 @@ export default {
this.loadRevisionsPromise ||= this.baseFetch(`/templates/${this.template.id}/versions`)
},
openRevisionsModal () {
if (window.webkit?.messageHandlers?.modal) {
return window.webkit.messageHandlers.modal.postMessage({ action: 'sheet', name: 'revisions', detent: 'medium' })
}
this.closeDropdown()
this.loadRevisionsPromise ||= this.baseFetch(`/templates/${this.template.id}/versions`)
@@ -2212,6 +2211,13 @@ export default {
this.loadRevisionsPromise = null
})
},
openDocumentsEditor (item) {
if (window.webkit?.messageHandlers?.modal) {
window.webkit.messageHandlers.modal.postMessage({ action: 'sheet', name: 'documents', viewport: 'fixed', uuid: item.attachment_uuid })
} else {
this.editModalDocumentUuid = item.attachment_uuid
}
},
onApplyRevisionEvent (e) {
this.onRevisionApply(e.detail)
},
@@ -2427,7 +2433,7 @@ export default {
window.webkit?.messageHandlers?.modal?.postMessage({ action: 'dispatch', name, detail: JSON.stringify(detail), dismiss: dismiss ? 'true' : 'false' })
},
onDrawerScrollTo (item) {
if (this.isNativeView) {
if (this.nativeView) {
this.postNativeEvent('template-builder:scroll-to', { attachment_uuid: item.attachment_uuid }, true)
} else {
this.isDocumentsDrawerOpen = false
@@ -2435,7 +2441,7 @@ export default {
}
},
onDrawerSetDraw ({ field, option }) {
if (this.isNativeView) {
if (this.nativeView) {
this.postNativeEvent('template-builder:draw-field', { uuid: field.uuid, option }, true)
} else {
this.isFieldsDrawerOpen = false
@@ -2444,15 +2450,31 @@ export default {
}
},
onDrawerSetDrawType (type) {
if (this.isNativeView) {
if (this.nativeView) {
this.postNativeEvent('template-builder:draw-field', { type }, true)
} else {
this.isFieldsDrawerOpen = false
this.startFieldDraw({ type })
}
},
onDrawerSelectSubmitter (submitter) {
this.selectedSubmitter = submitter
if (this.nativeView) {
this.$nextTick(() => this.postNativeEvent('template-builder:select-submitter', { uuid: submitter.uuid }))
}
},
onDrawerSetDrawCustomField (field) {
if (this.nativeView) {
this.postNativeEvent('template-builder:draw-field', { custom_field_uuid: field.uuid }, true)
} else {
this.isFieldsDrawerOpen = false
this.drawCustomField = field
this.showDrawField = true
}
},
onDrawerScrollToArea (area) {
if (this.isNativeView) {
if (this.nativeView) {
const field = this.template.fields.find((f) => (f.areas || []).includes(area))
this.postNativeEvent('template-builder:select-field', { uuid: field?.uuid, attachment_uuid: area.attachment_uuid, page: area.page, x: area.x, y: area.y }, true)
@@ -2465,7 +2487,7 @@ export default {
const { schema, documents, fields, submitters } = this.template
const detail = JSON.stringify({ schema, documents, fields, submitters })
if (this.isNativeView) {
if (this.nativeView) {
window.webkit?.messageHandlers?.modal?.postMessage({ action: 'dispatch', name: 'template-builder:sync', detail, dismiss: 'false' })
} else {
window.webkit?.messageHandlers?.native?.postMessage({ type: 'drawer', op: 'sync', detail })
@@ -1,13 +1,17 @@
<template>
<div
class="modal modal-open items-start !animate-none overflow-y-auto"
:class="{ 'modal modal-open items-start !animate-none overflow-y-auto': !inline }"
>
<div
v-if="!inline"
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 w-full max-w-xl">
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
<div :class="inline ? 'px-4 pt-3 pb-6' : 'modal-box pt-4 pb-6 px-6 mt-20 w-full max-w-xl'">
<div
v-if="!inline"
class="flex justify-between items-center border-b pb-2 mb-2 font-medium"
>
<span class="modal-title">
{{ t('condition') }} - {{ (defaultField ? (defaultField.title || item.title || item.name) : item.name) || buildDefaultName(item) }}
</span>
@@ -183,6 +187,11 @@ export default {
type: Array,
required: false,
default: () => []
},
inline: {
type: Boolean,
required: false,
default: false
}
},
emits: ['close', 'save'],
@@ -49,8 +49,8 @@
:field="field"
:with-condition="false"
:with-force-open="false"
@click-description="isShowDescriptionModal = true"
@click-formula="isShowFormulaModal = true"
@click-description="openDescriptionModal"
@click-formula="openFormulaModal"
/>
<span
v-else-if="!isNew"
@@ -61,9 +61,17 @@
<label
tabindex="0"
:title="t('settings')"
class="cursor-pointer text-transparent group-hover:text-base-content"
class="cursor-pointer flex items-center"
:class="[iconColorClass, { 'pr-1': isMobile }]"
>
<IconDotsVertical
v-if="isMobile"
:width="18"
:height="18"
:stroke-width="1.6"
/>
<IconSettings
v-else
:width="18"
:stroke-width="1.6"
/>
@@ -85,11 +93,27 @@
:with-areas="false"
:with-copy-to-all-pages="false"
:with-condition="false"
@click-formula="isShowFormulaModal = true"
@click-font="isShowFontModal = true"
@click-description="isShowDescriptionModal = true"
@click-formula="openFormulaModal"
@click-font="openFontModal"
@click-description="openDescriptionModal"
@save="$emit('save')"
/>
<li
v-if="isMobile"
class="field-settings-remove"
>
<a
href="#"
class="text-sm py-1 px-2 text-red-600"
@click.prevent="$emit('remove', field)"
>
<IconTrashX
:width="20"
:stroke-width="1.6"
/>
{{ t('remove') }}
</a>
</li>
</ul>
</span>
<button
@@ -104,8 +128,9 @@
/>
</button>
<button
v-if="isNew || !isMobile || field.type === 'payment'"
class="relative group-hover:text-base-content pr-1 field-remove-button"
:class="isNew ? 'text-base-content' : 'text-transparent group-hover:text-base-content'"
:class="isNew ? 'text-base-content' : iconColorClass"
:title="t('remove')"
@click="onRemoveClick"
>
@@ -164,7 +189,7 @@ import FieldSettings from './field_settings'
import FormulaModal from './formula_modal'
import FontModal from './font_modal'
import DescriptionModal from './description_modal'
import { IconTrashX, IconSettings, IconCheck } from '@tabler/icons-vue'
import { IconTrashX, IconSettings, IconCheck, IconDotsVertical } from '@tabler/icons-vue'
import IconDrag from './icon_drag'
export default {
@@ -172,6 +197,7 @@ export default {
components: {
Contenteditable,
IconSettings,
IconDotsVertical,
IconCheck,
FieldSettings,
PaymentSettings,
@@ -182,7 +208,7 @@ export default {
IconTrashX,
FieldType
},
inject: ['backgroundColor', 't'],
inject: ['backgroundColor', 't', 'isMobile'],
props: {
field: {
type: Object,
@@ -217,6 +243,9 @@ export default {
dropdownBgColor () {
return ['', null, 'transparent'].includes(this.backgroundColor) ? 'white' : this.backgroundColor
},
iconColorClass () {
return this.isMobile ? 'text-base-content' : 'text-transparent group-hover:text-base-content'
},
modalContainerEl () {
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
}
@@ -230,6 +259,27 @@ export default {
}
},
methods: {
openFormulaModal () {
if (window.webkit?.messageHandlers?.modal) {
window.webkit.messageHandlers.modal.postMessage({ action: 'sheet', name: 'formula', custom_field_uuid: this.field.uuid })
} else {
this.isShowFormulaModal = true
}
},
openFontModal () {
if (window.webkit?.messageHandlers?.modal) {
window.webkit.messageHandlers.modal.postMessage({ action: 'sheet', name: 'font', detent: 'medium', custom_field_uuid: this.field.uuid })
} else {
this.isShowFontModal = true
}
},
openDescriptionModal () {
if (window.webkit?.messageHandlers?.modal) {
window.webkit.messageHandlers.modal.postMessage({ action: 'sheet', name: 'description', custom_field_uuid: this.field.uuid })
} else {
this.isShowDescriptionModal = true
}
},
buildDefaultName () {
return this.t('custom')
},
@@ -1,13 +1,17 @@
<template>
<div
class="modal modal-open items-start !animate-none overflow-y-auto"
:class="{ 'modal modal-open items-start !animate-none overflow-y-auto': !inline }"
>
<div
v-if="!inline"
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">
<div :class="inline ? 'px-4 pt-3 pb-6' : 'modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl'">
<div
v-if="!inline"
class="flex justify-between items-center border-b pb-2 mb-2 font-medium"
>
<span class="modal-title">
{{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field) }}
</span>
@@ -86,6 +90,11 @@ export default {
buildDefaultName: {
type: Function,
required: true
},
inline: {
type: Boolean,
required: false,
default: false
}
},
emits: ['close', 'save'],
@@ -49,7 +49,7 @@
<div class="flex-none px-4 border-t py-3 md:w-56 md:border-t-0 md:border-l md:py-4 md:space-y-2 md:flex md:flex-col">
<div class="grid grid-cols-3 gap-2 md:block md:space-y-2">
<button
class="btn btn-sm normal-case font-normal rounded disabled:bg-base-300 flex-col gap-1 h-auto min-h-0 py-2 px-1 text-xs leading-tight whitespace-normal md:flex-row md:gap-2 md:h-8 md:min-h-[2rem] md:py-0 md:px-3 md:text-sm md:whitespace-nowrap md:w-full md:justify-start"
class="btn btn-sm normal-case font-normal rounded order-last disabled:bg-base-300 flex-col gap-1 h-auto min-h-0 py-2 px-1 text-xs leading-tight whitespace-normal md:flex-row md:gap-2 md:h-8 md:min-h-[2rem] md:py-0 md:px-3 md:text-sm md:whitespace-nowrap md:w-full md:justify-start"
:disabled="!!isProcessing"
@click.prevent="submit(true)"
>
@@ -64,7 +64,7 @@
{{ t('crop_and_scan') }}
</button>
<button
class="btn btn-sm normal-case font-normal rounded disabled:bg-base-300 flex-col gap-1 h-auto min-h-0 py-2 px-1 text-xs leading-tight whitespace-normal md:flex-row md:gap-2 md:h-8 md:min-h-[2rem] md:py-0 md:px-3 md:text-sm md:whitespace-nowrap md:w-full md:justify-start"
class="btn btn-sm normal-case font-normal rounded order-last disabled:bg-base-300 flex-col gap-1 h-auto min-h-0 py-2 px-1 text-xs leading-tight whitespace-normal md:flex-row md:gap-2 md:h-8 md:min-h-[2rem] md:py-0 md:px-3 md:text-sm md:whitespace-nowrap md:w-full md:justify-start"
:disabled="!!isProcessing"
@click.prevent="submit(false)"
>
@@ -74,17 +74,15 @@
/>
<IconCrop
v-else
width="22"
height="22"
style="margin-left: -3px"
class="w-4 h-4 md:w-[22px] md:h-[22px] md:-ml-[3px]"
:stroke-width="1.5"
/>
<span :style="{ 'margin-left': isProcessing === 'crop' ? '0px' : '-3px' }">
<span :class="{ 'md:-ml-[3px]': isProcessing !== 'crop' }">
{{ t('crop') }}
</span>
</button>
<button
class="btn btn-sm normal-case font-normal rounded flex-col gap-1 h-auto min-h-0 py-2 px-1 text-xs leading-tight whitespace-normal md:flex-row md:gap-2 md:h-8 md:min-h-[2rem] md:py-0 md:px-3 md:text-sm md:whitespace-nowrap md:w-full md:justify-start"
class="btn btn-sm normal-case font-normal rounded order-last flex-col gap-1 h-auto min-h-0 py-2 px-1 text-xs leading-tight whitespace-normal md:flex-row md:gap-2 md:h-8 md:min-h-[2rem] md:py-0 md:px-3 md:text-sm md:whitespace-nowrap md:w-full md:justify-start"
@click.prevent="$emit('cancel')"
>
<IconX class="w-4 h-4" />
@@ -1,19 +1,23 @@
<template>
<div
class="modal modal-open items-center !animate-none"
:class="inline ? 'flex flex-col h-full' : 'modal modal-open items-center !animate-none'"
@dragover.prevent="onModalDragover"
@dragleave="onModalDragleave"
@drop.prevent="onModalDrop"
>
<div
v-if="!inline"
class="absolute top-0 bottom-0 right-0 left-0"
@click.prevent="$emit('close')"
/>
<div
class="modal-box relative flex flex-col p-0 w-full h-full rounded-none md:rounded-2xl overflow-x-hidden !max-h-full md:!max-h-[92vh]"
style="max-width: 1240px"
:class="inline ? 'relative flex flex-col flex-1 min-h-0' : 'modal-box relative flex flex-col p-0 w-full h-full rounded-none md:rounded-2xl overflow-x-hidden !max-h-full md:!max-h-[92vh]'"
:style="inline ? null : { maxWidth: '1240px' }"
>
<div class="flex justify-between items-center border-b py-2 px-6 font-medium">
<div
v-if="!inline"
class="flex justify-between items-center border-b py-2 px-6 font-medium"
>
<span
class="modal-title"
style="padding-top: 2px"
@@ -32,6 +36,7 @@
<CropView
v-if="cropPageItem"
class="absolute inset-0 z-10 bg-base-100"
:style="inline ? { paddingTop: '70px' } : null"
:template-id="template.id"
:page="cropPageItem"
:image-url="thumbUrl(cropPageItem)"
@@ -42,6 +47,7 @@
<RedactView
v-else-if="redactPageItem"
class="absolute inset-0 z-10 bg-base-100"
:style="inline ? { paddingTop: '70px' } : null"
:template-id="template.id"
:page="redactPageItem"
:image-url="thumbUrl(redactPageItem)"
@@ -57,12 +63,14 @@
>
<div
class="flex-1 overflow-y-auto overflow-x-hidden px-4 md:px-6 py-4 space-y-4"
:style="inline ? { paddingTop: '86px' } : null"
>
<div
v-for="(doc, docIndex) in layout"
:key="doc.attachmentUuid"
:data-uuid="doc.attachmentUuid"
class="relative"
:style="inline ? { scrollMarginTop: '86px' } : null"
>
<div
class="flex items-center pb-2"
@@ -80,7 +88,7 @@
:class="{ 'rotate-90': !collapsedDocumentsIndex[doc.attachmentUuid] }"
/>
<span
class="truncate"
class="truncate min-w-0"
:class="{ 'line-through': !doc.pages.length }"
>{{ doc.name }}</span>
</button>
@@ -174,7 +182,7 @@
</div>
<div
v-if="(!collapsedDocumentsIndex[doc.attachmentUuid] || layout.length === 1) && doc.pages.length"
class="grid grid-cols-2 md:grid-cols-4 gap-3"
class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3"
@dragover.prevent="onGridDragover(docIndex, $event)"
@drop.prevent="onPageDrop"
>
@@ -183,6 +191,7 @@
:key="page.id"
:data-page-id="page.id"
class="relative min-w-0 cursor-pointer group"
:style="inline ? { scrollMarginTop: '86px' } : null"
draggable="true"
@click="selectedPageId = page.id"
@mousedown="onPageMousedown(page, $event)"
@@ -250,7 +259,8 @@
</div>
</div>
<div
class="flex-none px-4 border-t py-3 flex flex-col gap-3 md:w-56 md:border-t-0 md:border-l md:py-4 md:gap-0"
class="flex-none px-4 border-t pt-3 flex flex-col gap-3 md:w-56 md:border-t-0 md:border-l md:py-4 md:gap-0"
:class="{ 'pb-3': !inline }"
>
<div class="grid grid-cols-4 gap-2 md:block md:space-y-2">
<button
@@ -324,12 +334,10 @@
@click.prevent="openCropView"
>
<IconCrop
width="22"
height="22"
style="margin-left: -3px"
class="w-4 h-4 md:w-[22px] md:h-[22px] md:-ml-[3px]"
:stroke-width="1.5"
/>
<span style="margin-left: -3px">
<span class="md:-ml-[3px]">
{{ t('crop') }}
</span>
</button>
@@ -441,6 +449,11 @@ export default {
type: String,
required: false,
default: 'image/*, application/pdf'
},
inline: {
type: Boolean,
required: false,
default: false
}
},
emits: ['close', 'saved'],
@@ -698,12 +711,14 @@ export default {
if (targetIndex < 0) {
if (docIndex > 0) {
this.layout[docIndex - 1].pages.push(page)
this.collapsedDocumentsIndex[this.layout[docIndex - 1].attachmentUuid] = false
} else {
this.layout[docIndex].pages.unshift(page)
}
} else if (targetIndex > this.layout[docIndex].pages.length) {
if (docIndex < this.layout.length - 1) {
this.layout[docIndex + 1].pages.unshift(page)
this.collapsedDocumentsIndex[this.layout[docIndex + 1].attachmentUuid] = false
} else {
this.layout[docIndex].pages.push(page)
}
@@ -712,6 +727,12 @@ export default {
}
this.pushUndo()
this.scrollIntoList(`[data-page-id="${page.id}"]`)
},
scrollIntoList (selector) {
this.$nextTick(() => {
this.$el.querySelector(selector)?.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
})
},
removePage () {
const position = this.selectedPosition
@@ -737,6 +758,7 @@ export default {
this.layout.splice(docIndex + direction, 0, doc)
this.pushUndo()
this.scrollIntoList(`[data-uuid="${doc.attachmentUuid}"]`)
},
mergeDocument (docIndex, direction) {
const doc = this.layout[docIndex]
+37 -9
View File
@@ -74,7 +74,7 @@
class="relative cursor-pointer"
:class="iconColorClass"
:title="t('formula')"
@click="isShowFormulaModal = true"
@click="openFormulaModal"
>
<IconMathFunction
:width="18"
@@ -86,7 +86,7 @@
class="relative cursor-pointer"
:class="iconColorClass"
:title="t('condition')"
@click="isShowConditionsModal = true"
@click="openConditionsModal"
>
<IconRouteAltLeft
:width="18"
@@ -97,10 +97,10 @@
v-if="field.type === 'payment'"
:field="field"
:with-custom-fields="withCustomFields"
@click-condition="isShowConditionsModal = true"
@click-description="isShowDescriptionModal = true"
@click-condition="openConditionsModal"
@click-description="openDescriptionModal"
@add-custom-field="$emit('add-custom-field', $event)"
@click-formula="isShowFormulaModal = true"
@click-formula="openFormulaModal"
@save="$emit('save')"
/>
<span
@@ -150,10 +150,10 @@
:with-prefillable="withPrefillable"
:background-color="dropdownBgColor"
:with-custom-fields="withCustomFields"
@click-formula="isShowFormulaModal = true"
@click-font="isShowFontModal = true"
@click-description="isShowDescriptionModal = true"
@click-condition="isShowConditionsModal = true"
@click-formula="openFormulaModal"
@click-font="openFontModal"
@click-description="openDescriptionModal"
@click-condition="openConditionsModal"
@set-draw="$emit('set-draw', $event)"
@add-custom-field="$emit('add-custom-field', $event)"
@remove-area="removeArea"
@@ -515,6 +515,34 @@ export default {
this.$emit('save')
},
openFormulaModal () {
if (window.webkit?.messageHandlers?.modal) {
window.webkit.messageHandlers.modal.postMessage({ action: 'sheet', name: 'formula', uuid: this.field.uuid })
} else {
this.isShowFormulaModal = true
}
},
openFontModal () {
if (window.webkit?.messageHandlers?.modal) {
window.webkit.messageHandlers.modal.postMessage({ action: 'sheet', name: 'font', detent: 'medium', uuid: this.field.uuid })
} else {
this.isShowFontModal = true
}
},
openConditionsModal () {
if (window.webkit?.messageHandlers?.modal) {
window.webkit.messageHandlers.modal.postMessage({ action: 'sheet', name: 'conditions', detent: 'medium', uuid: this.field.uuid })
} else {
this.isShowConditionsModal = true
}
},
openDescriptionModal () {
if (window.webkit?.messageHandlers?.modal) {
window.webkit.messageHandlers.modal.postMessage({ action: 'sheet', name: 'description', uuid: this.field.uuid })
} else {
this.isShowDescriptionModal = true
}
},
buildDefaultName (field) {
if (field.type === 'payment' && field.preferences?.price && !field.preferences?.formula) {
const { price, currency } = field.preferences || {}
@@ -164,7 +164,10 @@
v-if="!compact && submitters.length > 1 && editable"
class="flex"
>
<div class="flex-col pr-1 flex invisible group-hover:visible -mt-1 h-0">
<div
class="flex-col pr-1 flex -mt-1 h-0"
:class="{ 'invisible group-hover:visible': !isMobile }"
>
<button
:title="t('up')"
class="relative w-2"
@@ -184,7 +187,8 @@
</div>
<button
v-if="!compact && submitters.length > 1 && editable"
class="invisible group-hover:visible px-2"
class="px-2"
:class="{ 'invisible group-hover:visible': !isMobile }"
@click.prevent.stop="remove(submitter)"
>
<IconTrashX :width="18" />
@@ -237,7 +241,7 @@ export default {
IconTrashX,
IconChevronUp
},
inject: ['t', 'save'],
inject: ['t', 'save', 'isMobile'],
props: {
submitters: {
type: Array,
+43 -2
View File
@@ -172,6 +172,7 @@
@click="setFieldsTab('custom')"
>{{ t('custom') }}</a>
</div>
<div ref="addFieldsMarker" />
<div
v-if="!isShowVariables && showCustomTab && editable && (customFields.length || newCustomField)"
ref="customFields"
@@ -390,6 +391,26 @@
</label>
</div>
</div>
<div
v-if="isMobile"
class="sticky bottom-4 h-0 z-[5]"
>
<Transition
enter-active-class="transition duration-100 ease-out"
enter-from-class="translate-y-6 opacity-0"
leave-active-class="transition duration-100 ease-in"
leave-to-class="translate-y-6 opacity-0"
>
<button
v-if="isShowAddFieldsPill"
class="absolute bottom-0 left-0 right-0 mx-auto w-fit btn btn-neutral btn-sm rounded-full normal-case font-normal text-white gap-1 px-4 shadow"
@click.prevent="scrollToFieldTypes"
>
<IconPlus class="w-4 h-4" />
{{ t('add_fields') }}
</button>
</Transition>
</div>
</template>
<script>
@@ -398,13 +419,14 @@ import CustomField from './custom_field'
import FieldType from './field_type'
import FieldSubmitter from './field_submitter'
import { defineAsyncComponent } from 'vue'
import { IconLock, IconCirclePlus, IconInnerShadowTop, IconSparkles, IconBracketsContain } from '@tabler/icons-vue'
import { IconLock, IconCirclePlus, IconPlus, IconInnerShadowTop, IconSparkles, IconBracketsContain } from '@tabler/icons-vue'
import IconDrag from './icon_drag'
import { v4 } from 'uuid'
export default {
name: 'TemplateFields',
components: {
IconPlus,
Field,
CustomField,
FieldType,
@@ -417,7 +439,7 @@ export default {
IconBracketsContain,
DynamicVariables: defineAsyncComponent(() => import(/* webpackChunkName: "dynamic-editor" */ './dynamic_variables'))
},
inject: ['save', 'backgroundColor', 'withPhone', 'withVerification', 'withKba', 'withPayment', 'nativePlatform', 't', 'fieldsDragFieldRef', 'customDragFieldRef', 'baseFetch', 'selectedAreasRef', 'getFieldTypeIndex'],
inject: ['save', 'backgroundColor', 'withPhone', 'withVerification', 'withKba', 'withPayment', 'nativePlatform', 't', 'isMobile', 'fieldsDragFieldRef', 'customDragFieldRef', 'baseFetch', 'selectedAreasRef', 'getFieldTypeIndex'],
props: {
fields: {
type: Array,
@@ -544,6 +566,7 @@ export default {
emits: ['add-field', 'set-draw', 'set-draw-type', 'set-draw-custom-field', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter', 'set-drag-placeholder', 'select-submitter', 'rebuild-variables-schema', 'remove-field', 'remove-submitter'],
data () {
return {
isShowAddFieldsPill: false,
fieldPagesLoaded: null,
analyzingProgress: 0,
newCustomField: null,
@@ -627,8 +650,22 @@ export default {
} catch (e) {
console.error(e)
}
if (this.isMobile) {
this.fieldTypesObserver = new IntersectionObserver(([entry]) => {
this.isShowAddFieldsPill = !entry.isIntersecting
})
this.fieldTypesObserver.observe(this.$refs.addFieldsMarker)
}
},
unmounted () {
this.fieldTypesObserver?.disconnect()
},
methods: {
scrollToFieldTypes () {
this.$refs.addFieldsMarker.scrollIntoView({ block: 'start', behavior: 'smooth' })
},
toggleVariables () {
this.$emit('rebuild-variables-schema')
this.isShowVariables = !this.isShowVariables
@@ -697,6 +734,10 @@ export default {
const fields = await resp.json()
this.customFields.splice(0, this.customFields.length, ...fields)
if (this.nativePlatform) {
window.webkit?.messageHandlers?.modal?.postMessage({ action: 'dispatch', name: 'template-builder:sync-custom-fields', detail: JSON.stringify(fields), dismiss: 'false' })
}
})
},
buildExistingFields () {
+14 -5
View File
@@ -1,13 +1,17 @@
<template>
<div
class="modal modal-open items-start !animate-none overflow-y-auto"
:class="{ 'modal modal-open items-start !animate-none overflow-y-auto': !inline }"
>
<div
v-if="!inline"
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">
<div :class="inline ? 'px-4 pt-3 pb-6' : 'modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl'">
<div
v-if="!inline"
class="flex justify-between items-center border-b pb-2 mb-2 font-medium"
>
<span class="modal-title">
{{ t('font') }} - {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field) }}
</span>
@@ -19,7 +23,7 @@
</div>
<div class="mt-4">
<div>
<div class="flex items-center space-x-1.5">
<div class="flex flex-wrap items-center gap-1.5">
<span>
<div class="dropdown modal-field-font-dropdown">
<label
@@ -168,7 +172,7 @@
:class="textClasses"
>
<span
contenteditable="true"
:contenteditable="!inline"
class="outline-none whitespace-nowrap truncate"
>
{{ field.default_value || field.name || buildDefaultName(field) }}
@@ -215,6 +219,11 @@ export default {
buildDefaultName: {
type: Function,
required: true
},
inline: {
type: Boolean,
required: false,
default: false
}
},
emits: ['close', 'save'],
@@ -1,13 +1,17 @@
<template>
<div
class="modal modal-open items-start !animate-none overflow-y-auto"
:class="{ 'modal modal-open items-start !animate-none overflow-y-auto': !inline }"
>
<div
v-if="!inline"
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">
<div :class="inline ? 'px-4 pt-3 pb-6' : 'modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl'">
<div
v-if="!inline"
class="flex justify-between items-center border-b pb-2 mb-2 font-medium"
>
<span class="modal-title">
{{ t('formula') }} - {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field) }}
</span>
@@ -153,6 +157,11 @@ export default {
buildDefaultName: {
type: Function,
required: true
},
inline: {
type: Boolean,
required: false,
default: false
}
},
emits: ['close', 'save'],
+7
View File
@@ -200,6 +200,7 @@ const en = {
replace: 'Replace',
uploading_: 'Uploading...',
add_document: 'Add Document',
add_fields: 'Add Fields',
none: 'None',
ssn: 'SSN',
ein: 'EIN',
@@ -443,6 +444,7 @@ const es = {
replace: 'Reemplazar',
uploading_: 'Subiendo...',
add_document: 'Subir',
add_fields: 'Agregar campos',
any: 'Cualquier',
drawn: 'Dibujado',
drawn_or_typed: 'Dibujado o Escrito',
@@ -698,6 +700,7 @@ const it = {
replace: 'Sostituisci',
uploading_: 'Caricamento in corso...',
add_document: 'Aggiungi',
add_fields: 'Aggiungi campi',
none: 'Nessuno',
ssn: 'SSN',
ein: 'EIN',
@@ -941,6 +944,7 @@ const pt = {
replace: 'Substituir',
uploading_: 'Carregando...',
add_document: 'Enviar',
add_fields: 'Adicionar campos',
any: 'Qualquer',
drawn: 'Desenhado',
drawn_or_typed: 'Desenhado ou Digitado',
@@ -1196,6 +1200,7 @@ const fr = {
replace: 'Remplacer',
uploading_: 'Téléversement...',
add_document: 'Téléverser',
add_fields: 'Ajouter des champs',
none: 'Aucun',
ssn: 'SSN',
ein: 'EIN',
@@ -1445,6 +1450,7 @@ const de = {
replace: 'Ersetzen',
uploading_: 'Hochladen...',
add_document: 'Hochladen',
add_fields: 'Felder hinzufügen',
none: 'Keine',
ssn: 'SSN',
ein: 'EIN',
@@ -1694,6 +1700,7 @@ const nl = {
replace: 'Vervangen',
uploading_: 'Bezig met uploaden...',
add_document: 'Doc. toevoegen',
add_fields: 'Velden toevoegen',
none: 'Geen',
ssn: 'SSN',
ein: 'EIN',
@@ -0,0 +1,244 @@
<template>
<FormulaModal
v-if="modal?.name === 'formula' && field"
:field="field"
:editable="editable && !defaultField"
:default-field="defaultField"
:build-default-name="buildDefaultName"
:inline="true"
@save="onSave"
@close="close"
/>
<FontModal
v-if="modal?.name === 'font' && field"
:field="field"
:editable="editable && !defaultField"
:default-field="defaultField"
:build-default-name="buildDefaultName"
:inline="true"
@save="onSave"
@close="close"
/>
<ConditionsModal
v-if="modal?.name === 'conditions' && item"
:item="item"
:default-field="defaultField"
:build-default-name="buildDefaultName"
:inline="true"
@save="onSave"
@close="close"
/>
<DescriptionModal
v-if="modal?.name === 'description' && field"
:field="field"
:editable="editable && !defaultField"
:default-field="defaultField"
:build-default-name="buildDefaultName"
:inline="true"
@save="onSave"
@close="close"
/>
<div
v-if="modal?.name === 'documents'"
class="h-screen flex flex-col"
>
<DocumentsEditorModal
:template="template"
:authenticity-token="authenticityToken"
:accept-file-types="acceptFileTypes"
:base-url="baseUrl"
:page-preview-format="pagePreviewFormat"
:scroll-to-attachment-uuid="modal.uuid"
:inline="true"
@saved="onDocumentsSaved"
@close="close"
/>
</div>
<RevisionsModal
v-if="modal?.name === 'revisions' && revisions"
:template="template"
:revisions="revisions"
:locale="locale"
:inline="true"
@close="close"
@apply="applyRevision"
/>
<div
v-else-if="modal?.name === 'revisions'"
class="flex justify-center py-8"
>
<IconInnerShadowTop class="w-6 h-6 animate-spin" />
</div>
</template>
<script>
import FormulaModal from './formula_modal'
import FontModal from './font_modal'
import ConditionsModal from './conditions_modal'
import DescriptionModal from './description_modal'
import DocumentsEditorModal from './documents_editor_modal'
import RevisionsModal from './revisions_modal'
import Field from './field'
import FieldType from './field_type'
import { IconInnerShadowTop } from '@tabler/icons-vue'
export default {
name: 'NativeModals',
components: {
FormulaModal,
FontModal,
ConditionsModal,
DescriptionModal,
DocumentsEditorModal,
RevisionsModal,
IconInnerShadowTop
},
inject: ['t', 'template', 'locale', 'baseFetch', 'getFieldTypeIndex', 'save'],
props: {
defaultFields: {
type: Array,
required: false,
default: () => []
},
customFields: {
type: Array,
required: false,
default: () => []
},
editable: {
type: Boolean,
required: false,
default: true
},
authenticityToken: {
type: String,
required: false,
default: ''
},
acceptFileTypes: {
type: String,
required: false,
default: 'image/*, application/pdf'
},
baseUrl: {
type: String,
required: false,
default: ''
},
pagePreviewFormat: {
type: String,
required: false,
default: '.jpg'
}
},
emits: ['documents-modified'],
data () {
return {
modal: null,
revisions: null
}
},
computed: {
fieldNames: FieldType.computed.fieldNames,
fieldLabels: FieldType.computed.fieldLabels,
field () {
if (this.modal?.custom_field_uuid) {
return this.customFields.find((f) => f.uuid === this.modal.custom_field_uuid)
}
return this.template.fields.find((f) => f.uuid === this.modal?.uuid)
},
item () {
if (this.modal?.attachment_uuid) {
return this.template.schema.find((item) => item.attachment_uuid === this.modal.attachment_uuid)
}
return this.field
},
defaultField () {
if (this.modal?.custom_field_uuid) return null
return this.field && this.defaultFields.find((f) => f.name === this.field.name)
},
title () {
if (!this.modal) return ''
if (this.modal.name === 'documents') return this.t('edit_documents')
if (this.modal.name === 'revisions') return this.t('revisions')
const item = this.item
const itemName = item ? ((this.defaultField ? (this.defaultField.title || item.title || item.name) : item.name) || this.buildDefaultName(item)) : ''
if (this.modal.name === 'description') return itemName
const prefix = { formula: this.t('formula'), font: this.t('font'), conditions: this.t('condition') }[this.modal.name]
return itemName ? `${prefix} - ${itemName}` : prefix
}
},
mounted () {
document.title = ''
document.addEventListener('template-builder:open-modal', this.onOpen)
document.addEventListener('template-builder:close-modal', this.onClose)
},
unmounted () {
document.removeEventListener('template-builder:open-modal', this.onOpen)
document.removeEventListener('template-builder:close-modal', this.onClose)
},
methods: {
buildDefaultName: Field.methods.buildDefaultName,
onOpen (e) {
this.modal = e.detail
document.title = this.title
document.body.style.overflow = e.detail.name === 'documents' ? 'hidden' : ''
if (e.detail.name === 'revisions') {
this.loadRevisions()
}
},
onClose () {
this.modal = null
this.revisions = null
document.body.style.overflow = ''
},
close () {
window.webkit?.messageHandlers?.modal?.postMessage({ action: 'closeSheet' })
},
onSave () {
if (this.modal?.custom_field_uuid) {
this.saveCustomFields()
} else {
this.save()
}
},
saveCustomFields () {
return this.baseFetch('/account_custom_fields', {
method: 'POST',
body: JSON.stringify({ value: this.customFields }),
headers: { 'Content-Type': 'application/json' }
}).then(async (resp) => {
const fields = await resp.json()
this.customFields.splice(0, this.customFields.length, ...fields)
window.webkit?.messageHandlers?.modal?.postMessage({ action: 'dispatch', name: 'template-builder:sync-custom-fields', detail: JSON.stringify(fields), dismiss: 'false' })
})
},
loadRevisions () {
this.revisions = null
this.baseFetch(`/templates/${this.template.id}/versions`).then(async (resp) => {
this.revisions = await resp.json()
})
},
applyRevision (revision) {
window.webkit?.messageHandlers?.modal?.postMessage({ action: 'dispatch', name: 'template-builder:apply-revision', detail: JSON.stringify(revision), dismiss: 'true' })
},
onDocumentsSaved (data) {
this.$emit('documents-modified', data)
this.close()
}
}
}
</script>
+3 -3
View File
@@ -1,7 +1,7 @@
<template>
<div
class="relative select-none mb-4 before:border before:rounded before:top-0 before:bottom-0 before:left-0 before:right-0 before:absolute"
:class="{ 'cursor-crosshair': allowDraw && editable && !isSelectMode, 'touch-none': !!drawField }"
:class="{ 'cursor-crosshair': allowDraw && editable && !isSelectMode, 'touch-none': !!(drawField || drawCustomField) }"
style="container-type: size"
:style="{ aspectRatio: `${width} / ${height}`}"
>
@@ -115,11 +115,11 @@
/>
</div>
<div
v-show="resizeDirection || isDrag || showMask || (drawField && isMobile) || fieldsDragFieldRef.value || customDragFieldRef?.value || selectionRect"
v-show="resizeDirection || isDrag || showMask || ((drawField || drawCustomField) && isMobile) || fieldsDragFieldRef.value || customDragFieldRef?.value || selectionRect"
id="mask"
ref="mask"
class="top-0 bottom-0 left-0 right-0 absolute"
:class="{ 'z-10': !isMobile, 'cursor-grab': isDrag, 'cursor-nwse-resize': drawField && !isSelectMode, [resizeDirectionClasses[resizeDirection]]: !!resizeDirectionClasses }"
:class="{ 'z-10': !isMobile, 'cursor-grab': isDrag, 'cursor-nwse-resize': (drawField || drawCustomField) && !isSelectMode, [resizeDirectionClasses[resizeDirection]]: !!resizeDirectionClasses }"
@pointermove="onPointermove"
@pointerdown="onStartDraw"
@contextmenu="openContextMenu"
+9 -2
View File
@@ -23,7 +23,7 @@
<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"
@click.stop="openConditionsModal"
>
<IconRouteAltLeft
:width="14"
@@ -84,7 +84,7 @@
<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()"
@click.stop="openConditionsModal(); closeDropdown()"
>
<span class="flex items-center space-x-2">
<IconRouteAltLeft class="w-4 h-4" />
@@ -295,6 +295,13 @@ export default {
}
},
methods: {
openConditionsModal () {
if (window.webkit?.messageHandlers?.modal) {
window.webkit.messageHandlers.modal.postMessage({ action: 'sheet', name: 'conditions', detent: 'medium', attachment_uuid: this.item.attachment_uuid })
} else {
this.isShowConditionsModal = true
}
},
upload: Upload.methods.upload,
buildDefaultName: Field.methods.buildDefaultName,
closeDropdown () {
@@ -5,7 +5,7 @@
class="absolute top-0 bottom-0 right-0 left-0"
@click.prevent="$emit('close')"
/>
<div :class="{ 'modal-box pt-4 pb-6 mt-20 w-full': !inline }">
<div :class="inline ? 'px-4 pt-3 pb-6' : 'modal-box pt-4 pb-6 mt-20 w-full'">
<div
v-if="!inline"
class="flex justify-between items-center border-b pb-2 mb-3 font-medium"
+1 -1
View File
@@ -2,7 +2,7 @@
<div class="bg-base-200 rounded-2xl flex flex-col sm:flex-row" data-native-title="<%= submission.name.presence || (local_assigns[:template] || submission.template)&.name if turbo_native_app? %>">
<% if local_assigns[:with_template] %>
<% template = submission.template %>
<a href="<%= template ? template_path(template) : submission_path(submission) %>" class="px-5 sm:pr-3 py-3 group sm:rounded-l-2xl sm:rounded-tr-none rounded-t-2xl flex sm:flex-col justify-between sm:w-52 w-full flex-shrink-0 bg-base-300/60 space-x-2 sm:space-x-0">
<a href="<%= template ? template_path(template) : submission_path(submission) %>" data-native-title="<%= template&.name %>" class="px-5 sm:pr-3 py-3 group sm:rounded-l-2xl sm:rounded-tr-none rounded-t-2xl flex sm:flex-col justify-between sm:w-52 w-full flex-shrink-0 bg-base-300/60 space-x-2 sm:space-x-0">
<div>
<div class="font-medium items-start w-full group-hover:link text-sm flex space-x-1">
<%= svg_icon('file_text', class: 'w-4 h-4 mt-0.5 flex-shrink-0') %>
+5
View File
@@ -115,6 +115,11 @@
<a href="<%= new_template_submissions_export_path(template, params.permit(:q, *Submissions::Filter::ALLOWED_PARAMS)) %>" data-turbo-frame="modal"></a>
</native-action>
<% end %>
<% if !template.archived_at? && can?(:update, template) && @template.account_id == current_account.id %>
<native-action data-section="edit" data-icon="folder" data-label="<%= t('move') %>">
<a href="<%= edit_template_folder_path(template.id, subfolder: @template.folder.parent_folder.present?) %>" data-turbo-frame="modal"></a>
</native-action>
<% end %>
<% if can?(:destroy, template) %>
<% if template.archived_at? %>
<native-action data-section="edit" data-icon="rotate" data-label="<%= t('restore') %>">
+2 -2
View File
@@ -1,4 +1,4 @@
<% content_for(:native_title, params[:view] == 'revisions' ? t('revisions') : @template.name) if turbo_native_app? %>
<% content_for(:native_title, @template.name) if turbo_native_app? %>
<% if !turbo_native_app? && (current_user.created_at > 2.weeks.ago || params[:tour] == 'true') %>
<% user_config = current_user.user_configs.find_or_initialize_by(key: UserConfig::SHOW_APP_TOUR) %>
<% @show_tour_start_form = user_config.new_record? && !params.key?(:tour) %>
@@ -10,4 +10,4 @@
<% account_configs = current_account.account_configs.where(key: [AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY, AccountConfig::TEMPLATE_DATE_FORMATS_KEY]).to_a %>
<% custom_fields = account_configs.find { |e| e.key == AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY }&.value || [] %>
<% date_formats = account_configs.find { |e| e.key == AccountConfig::TEMPLATE_DATE_FORMATS_KEY }&.value || [] %>
<template-builder data-view="<%= params[:view] %>" data-native-platform="<%= turbo_native_ios? ? 'ios' : (turbo_native_android? ? 'android' : '') %>" class="grid" data-template="<%= @template_data.to_json %>" data-custom-fields="<%= custom_fields.to_json %>" data-date-formats="<%= date_formats.to_json %>" data-with-sign-yourself-button="<%= !@template.archived_at? %>" data-with-fields-detection="true" data-with-send-button="<%= !@template.archived_at? && can?(:create, @template.submissions.new(account: current_account)) %>" data-with-revisions-menu="<%= @template.template_versions.exists? %>" data-locale="<%= I18n.locale %>" data-show-tour-start-form="<%= @show_tour_start_form %>"></template-builder>
<template-builder data-native-platform="<%= turbo_native_ios? ? 'ios' : (turbo_native_android? ? 'android' : '') %>" class="grid" data-template="<%= @template_data.to_json %>" data-custom-fields="<%= custom_fields.to_json %>" data-date-formats="<%= date_formats.to_json %>" data-with-sign-yourself-button="<%= !@template.archived_at? %>" data-with-fields-detection="true" data-with-send-button="<%= !@template.archived_at? && can?(:create, @template.submissions.new(account: current_account)) %>" data-with-revisions-menu="<%= @template.template_versions.exists? %>" data-locale="<%= I18n.locale %>" data-show-tour-start-form="<%= @show_tour_start_form %>"></template-builder>