add builder drawer

This commit is contained in:
Pete Matsyburka committed 2026-09-23 10:59:54 +03:00
1 parent 79f72c92b3
commit d29be7c2cf
12 files changed
+299 -57

No files matched your search

@@ -4,12 +4,9 @@ class TemplatesVersionsController < ApplicationController
load_and_authorize_resource :template
def index
@versions = @template.template_versions.order(id: :desc).preload(:author)
versions = @template.template_versions.order(id: :desc).preload(:author)
respond_to do |format|
format.json { render json: @versions.as_json(TemplateVersions::SERIALIZE_PARAMS) }
format.html
end
render json: versions.as_json(TemplateVersions::SERIALIZE_PARAMS)
end
def show
+44 -1
View File
@@ -189,6 +189,10 @@ safeRegisterElement('template-builder', class extends HTMLElement {
connectedCallback () {
document.addEventListener('turbo:submit-end', this.onSubmit)
document.addEventListener('template-builder:update', this.onSheetSubmit)
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:draw-field', this.onDrawField)
document.addEventListener('turbo:before-cache', this.onBeforeCache)
this.appElem = document.createElement('div')
@@ -233,7 +237,8 @@ safeRegisterElement('template-builder', class extends HTMLElement {
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
acceptFileTypes: this.dataset.acceptFileTypes,
showTourStartForm: this.dataset.showTourStartForm === 'true',
nativePlatform: this.dataset.nativePlatform
nativePlatform: this.dataset.nativePlatform,
view: this.dataset.view
})
this.component = this.app.mount(this.appElem)
@@ -257,6 +262,40 @@ safeRegisterElement('template-builder', class extends HTMLElement {
this.applySubmission(e.detail.form, e.detail.action, e.detail.body, {})
}
onSync = (e) => {
const { schema, documents, fields, submitters } = e.detail
Object.assign(this.component.template, { schema, documents, fields, submitters })
}
onScrollTo = (e) => {
this.component.scrollIntoDocument(e.detail)
}
onSelectField = (e) => {
const { uuid, attachment_uuid: attachmentUuid, page, x, y } = e.detail
const field = this.component.template.fields.find((f) => f.uuid === uuid)
const areas = field?.areas || []
const area = areas.find((a) => a.attachment_uuid === attachmentUuid && a.page === page && a.x === x && a.y === y) || areas[0]
if (area) this.component.scrollToArea(area)
}
onDrawField = (e) => {
const { uuid, type, option } = e.detail
if (uuid) {
const field = this.component.template.fields.find((f) => f.uuid === uuid)
if (field) {
this.component.drawField = field
this.component.drawOption = option || null
}
} else if (type) {
this.component.startFieldDraw({ type })
}
}
applySubmission (formId, action, body, data) {
if (formId === 'submitters_form') {
this.component.template.submitters = JSON.parse(body).submitters
@@ -294,6 +333,10 @@ safeRegisterElement('template-builder', class extends HTMLElement {
disconnectedCallback () {
document.removeEventListener('turbo:submit-end', this.onSubmit)
document.removeEventListener('template-builder:update', this.onSheetSubmit)
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:draw-field', this.onDrawField)
document.removeEventListener('turbo:before-cache', this.onBeforeCache)
this.app?.unmount()
+2
View File
@@ -21,6 +21,8 @@ export default class extends HTMLElement {
title: this.dataset.label || target?.textContent.trim(),
icon: this.dataset.icon,
placement: this.dataset.placement || 'menu',
side: this.dataset.side,
view: this.dataset.view,
section: this.dataset.section,
menu: this.dataset.menu,
menuIcon: this.dataset.menuIcon,
+166 -8
View File
@@ -2,7 +2,7 @@
<div
ref="dragContainer"
style="max-width: 1600px"
class="mx-auto pl-3 md:pl-4 h-full"
:class="isNativeView ? 'px-4 pt-3 pb-6' : 'mx-auto pl-3 md:pl-4 h-full'"
@dragover="onDragover"
@drop="isDragFile = false"
>
@@ -25,6 +25,85 @@
:is-default="defaultFields.includes(toRaw(dragField))"
:is-required="defaultRequiredFields.includes(toRaw(dragField))"
/>
<div
v-if="isDocumentsView"
class="space-y-3"
>
<DocumentPreview
v-for="(item, index) in template.schema"
:key="item.attachment_uuid"
:with-arrows="template.schema.length > 1"
:with-visible-controls="true"
:item="item"
:document="sortedDocuments[index]"
:accept-file-types="acceptFileTypes"
:with-replace-button="withUploadButton"
:with-google-drive="withGoogleDrive"
:authenticity-token="authenticityToken"
:editable="editable"
:dynamic-documents="dynamicDocuments"
:with-dynamic-documents="withDynamicDocuments"
:template="template"
@scroll-to="postNativeEvent('template-builder:scroll-to', { attachment_uuid: item.attachment_uuid }, true)"
@remove="onDocumentRemove"
@replace="onDocumentReplace"
@up="moveDocument(item, -1)"
@reorder="reorderFields"
@edit="editModalDocumentUuid = item.attachment_uuid"
@down="moveDocument(item, 1)"
@change="save"
/>
<Upload
v-if="editable && withUploadButton"
:accept-file-types="acceptFileTypes"
:authenticity-token="authenticityToken"
:with-google-drive="withGoogleDrive"
:template-id="template.id"
@success="updateFromUpload"
/>
</div>
<div v-if="isFieldsView">
<Fields
ref="fields"
:fields="template.fields"
:submitters="template.submitters"
:selected-submitter="selectedSubmitter"
:with-help="withHelp"
:default-submitters="defaultSubmitters"
:draw-field-type="drawFieldType"
:custom-fields="customFields"
:with-custom-fields="withCustomFields"
:with-fields-search="withFieldsSearch"
:default-fields="[...defaultRequiredFields, ...defaultFields]"
:with-custom-fields-tab="false"
:template="template"
:default-required-fields="defaultRequiredFields"
:detect-custom-fields-index="detectCustomFieldsIndex"
:field-types="fieldTypes"
:with-fields-detection="withFieldsDetection"
:with-detect-existing-fields="withDetectExistingFields"
:with-signature-id="withSignatureId"
:with-prefillable="withPrefillable"
:only-defined-fields="onlyDefinedFields"
:editable="editable"
:scroll-on-edit="false"
@add-field="addField"
@set-draw="postNativeEvent('template-builder:draw-field', { uuid: $event.field.uuid, option: $event.option }, true)"
@remove-field="onRemoveField"
@remove-submitter="onRemoveSubmitter"
@select-submitter="selectedSubmitter = $event"
@set-draw-type="postNativeEvent('template-builder:draw-field', { type: $event }, true)"
@change-submitter="selectedSubmitter = $event"
@scroll-to-area="onDrawerScrollToArea"
@rebuild-variables-schema="rebuildVariablesSchema"
/>
</div>
<div
v-if="isRevisionsView && !isRevisionsModalOpen"
class="flex justify-center py-8"
>
<IconInnerShadowTop class="w-6 h-6 animate-spin" />
</div>
<div
v-if="pendingFieldAttachmentUuids.length && editable"
class="top-1.5 sticky h-0 z-20 max-w-2xl mx-auto"
@@ -75,7 +154,7 @@
</div>
</div>
<div
v-if="$slots.buttons || withTitle"
v-if="($slots.buttons || withTitle) && !isNativeView"
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 }"
@@ -325,7 +404,7 @@
>
<a
v-if="!!nativePlatform"
:href="`/templates/${template.id}/versions`"
:href="`/templates/${template.id}/edit?view=revisions`"
data-turbo-frame="modal"
/>
<button
@@ -390,6 +469,7 @@
</div>
</div>
<div
v-if="!isNativeView"
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%]')"
@@ -685,9 +765,40 @@
</div>
</div>
</div>
<div class="sticky bottom-0 z-10">
<div
v-if="!isNativeView"
class="sticky bottom-0 z-10"
>
<native-action
v-if="nativePlatform && withDocumentsList && isBreakpointLg && sortedDocuments.length"
data-placement="drawer"
data-side="leading"
data-icon="files"
data-native="drawer"
:data-label="t('documents')"
data-view="documents"
/>
<native-action
v-if="nativePlatform && withFieldsList && isBreakpointLg && sortedDocuments.length && editable"
data-placement="drawer"
data-side="trailing"
data-icon="plus"
data-native="drawer"
:data-label="'Fields'"
data-view="fields"
/>
<native-action
v-if="nativePlatform && drawField && (isBreakpointLg || isMobile)"
data-placement="draw"
:data-label="t('draw_field').replace('{field}', fieldNames[drawField.type])"
>
<button
class="hidden"
@click="[drawField = null, drawOption = null]"
/>
</native-action>
<MobileDrawField
v-if="drawField && (isBreakpointLg || isMobile)"
v-if="drawField && (isBreakpointLg || isMobile) && !nativePlatform"
:draw-field="drawField"
:fields="template.fields"
:submitters="template.submitters"
@@ -698,7 +809,7 @@
@change-submitter="[selectedSubmitter = $event, drawField.submitter_uuid = $event.uuid]"
/>
<MobileFields
v-if="sortedDocuments.length && !drawField && editable"
v-if="sortedDocuments.length && !drawField && editable && !nativePlatform"
:fields="template.fields"
:default-fields="[...defaultRequiredFields, ...defaultFields]"
:default-required-fields="defaultRequiredFields"
@@ -784,8 +895,9 @@
:template="template"
:revisions="revisions"
:locale="locale"
:inline="isRevisionsView"
@close="isRevisionsModalOpen = false"
@apply="onRevisionApply"
@apply="isRevisionsView ? postNativeEvent('template-builder:apply-revision', $event, true) : onRevisionApply($event)"
/>
<DocumentsEditorModal
v-if="editModalDocumentUuid"
@@ -809,6 +921,7 @@ import HoverDropzone from './hover_dropzone'
import DragPlaceholder from './drag_placeholder'
import Fields from './fields'
import MobileDrawField from './mobile_draw_field'
import FieldType from './field_type'
import Document from './document'
import Logo from './logo'
import Contenteditable from './contenteditable'
@@ -887,6 +1000,7 @@ export default {
withFormula: this.withFormula,
withConditions: this.withConditions,
nativePlatform: this.nativePlatform,
isMobile: this.isMobile,
withCustomFields: this.withCustomFields,
isInlineSize: this.isInlineSize,
defaultDrawFieldType: this.defaultDrawFieldType,
@@ -1109,6 +1223,11 @@ export default {
required: false,
default: ''
},
view: {
type: String,
required: false,
default: ''
},
withFieldsSearch: {
type: Boolean,
required: false,
@@ -1287,6 +1406,19 @@ export default {
lastSelectedArea () {
return this.selectedAreasRef.value[this.selectedAreasRef.value.length - 1]
},
fieldNames: FieldType.computed.fieldNames,
isDocumentsView () {
return this.view === 'documents'
},
isFieldsView () {
return this.view === 'fields'
},
isRevisionsView () {
return this.view === 'revisions'
},
isNativeView () {
return this.isDocumentsView || this.isFieldsView || this.isRevisionsView
},
isMobile () {
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
@@ -1472,6 +1604,10 @@ export default {
this.selectedSubmitter = this.template.submitters[0]
},
mounted () {
if (this.isRevisionsView) {
this.openRevisionsModal()
}
this.undoStack = [JSON.stringify(this.template)]
this.redoStack = []
@@ -2265,7 +2401,25 @@ export default {
scrollIntoDocument (item) {
const ref = this.documentRefs.find((e) => e.document.uuid === item.attachment_uuid)
ref.$el.scrollIntoView({ behavior: 'smooth', block: 'start' })
ref?.$el.scrollIntoView({ behavior: 'smooth', block: 'start' })
},
postNativeEvent (name, detail, dismiss = false) {
window.webkit?.messageHandlers?.modal?.postMessage({ action: 'dispatch', name, detail: JSON.stringify(detail), dismiss: dismiss ? 'true' : 'false' })
},
onDrawerScrollToArea (area) {
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)
},
syncNative () {
const { schema, documents, fields, submitters } = this.template
const detail = JSON.stringify({ schema, documents, fields, submitters })
if (this.isNativeView) {
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 })
}
},
clearDrawField () {
this.drawField = null
@@ -3651,6 +3805,10 @@ export default {
this.pushUndo()
if (this.nativePlatform) {
this.syncNative()
}
if (!this.autosave && !force) {
return Promise.resolve({})
}
@@ -23,7 +23,7 @@
{{ value }}
</span>
<span
v-if="withButton"
v-if="withButton && !isMobile"
class="relative inline"
:class="{ 'peer-focus:hidden': hideIcon, 'peer-focus:invisible': !hideIcon }"
>
@@ -48,7 +48,7 @@ export default {
components: {
IconPencil
},
inject: ['t'],
inject: ['t', 'isMobile'],
props: {
modelValue: {
type: String,
+47 -9
View File
@@ -20,7 +20,7 @@
:menu-classes="'mt-1.5'"
:menu-style="{ backgroundColor: dropdownBgColor }"
@update:model-value="[maybeUpdateOptions(), $emit('save')]"
@click="scrollToFirstArea"
@click="scrollOnEdit && scrollToFirstArea()"
/>
<Contenteditable
ref="name"
@@ -29,7 +29,7 @@
:icon-inline="true"
:icon-width="18"
:icon-stroke-width="1.6"
@focus="[onNameFocus(), scrollToFirstArea()]"
@focus="[onNameFocus(), scrollOnEdit && scrollToFirstArea()]"
@blur="onNameBlur"
/>
</div>
@@ -60,7 +60,8 @@
<button
v-if="field && !field.areas?.length"
:title="t('draw')"
class="relative cursor-pointer text-transparent group-hover:text-base-content"
class="relative cursor-pointer"
:class="iconColorClass"
@click="$emit('set-draw', { field })"
>
<IconNewSection
@@ -70,7 +71,8 @@
</button>
<button
v-if="field.preferences?.formula"
class="relative cursor-pointer text-transparent group-hover:text-base-content"
class="relative cursor-pointer"
:class="iconColorClass"
:title="t('formula')"
@click="isShowFormulaModal = true"
>
@@ -81,7 +83,8 @@
</button>
<button
v-if="field.conditions?.length"
class="relative cursor-pointer text-transparent group-hover:text-base-content"
class="relative cursor-pointer"
:class="iconColorClass"
:title="t('condition')"
@click="isShowConditionsModal = true"
>
@@ -109,9 +112,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"
/>
@@ -143,10 +154,28 @@
@save="$emit('save')"
@scroll-to="$emit('scroll-to', $event)"
/>
<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
class="relative text-transparent group-hover:text-base-content pr-1 field-remove-button"
v-if="!isMobile"
class="relative pr-1 field-remove-button"
:class="iconColorClass"
:title="t('remove')"
@click="$emit('remove', field)"
>
@@ -324,7 +353,7 @@ import FormulaModal from './formula_modal'
import FontModal from './font_modal'
import ConditionsModal from './conditions_modal'
import DescriptionModal from './description_modal'
import { IconRouteAltLeft, IconMathFunction, IconNewSection, IconTrashX, IconSettings, IconChevronDown } from '@tabler/icons-vue'
import { IconRouteAltLeft, IconMathFunction, IconNewSection, IconTrashX, IconSettings, IconDotsVertical, IconChevronDown } from '@tabler/icons-vue'
import { v4 } from 'uuid'
export default {
@@ -332,6 +361,7 @@ export default {
components: {
Contenteditable,
IconSettings,
IconDotsVertical,
FieldSettings,
PaymentSettings,
IconChevronDown,
@@ -345,12 +375,17 @@ export default {
IconMathFunction,
FieldType
},
inject: ['template', 'backgroundColor', 'selectedAreasRef', 't', 'locale', 'getFieldTypeIndex', 'dateFormats'],
inject: ['template', 'backgroundColor', 'selectedAreasRef', 't', 'locale', 'getFieldTypeIndex', 'dateFormats', 'isMobile'],
props: {
field: {
type: Object,
required: true
},
scrollOnEdit: {
type: Boolean,
required: false,
default: true
},
withSignatureId: {
type: Boolean,
required: false,
@@ -396,6 +431,9 @@ export default {
}
},
computed: {
iconColorClass () {
return this.isMobile ? 'text-base-content' : 'text-transparent group-hover:text-base-content'
},
fieldNames: FieldType.computed.fieldNames,
fieldLabels: FieldType.computed.fieldLabels,
dropdownBgColor () {
@@ -48,6 +48,7 @@
:key="field.uuid"
:data-uuid="field.uuid"
:field="field"
:scroll-on-edit="scrollOnEdit"
:type-index="getFieldTypeIndex(field)"
:editable="editable"
:with-signature-id="withSignatureId"
@@ -482,6 +483,11 @@ export default {
required: false,
default: true
},
scrollOnEdit: {
type: Boolean,
required: false,
default: true
},
defaultFields: {
type: Array,
required: false,
+7
View File
@@ -98,6 +98,7 @@ const en = {
add_documents_or_images: 'Add documents or images',
add_a_new_document: 'Add a new document',
edit_documents: 'Edit documents',
documents: 'Documents',
move_forward: 'Move forward',
move_backward: 'Move backward',
remove_page: 'Remove page',
@@ -340,6 +341,7 @@ const es = {
add_documents_or_images: 'Agregar documentos o imágenes',
add_a_new_document: 'Agregar un nuevo documento',
edit_documents: 'Editar documentos',
documents: 'Documentos',
move_forward: 'Mover adelante',
move_backward: 'Mover atrás',
remove_page: 'Eliminar página',
@@ -588,6 +590,7 @@ const it = {
add_documents_or_images: 'Aggiungi documenti o immagini',
add_a_new_document: 'Aggiungi un nuovo documento',
edit_documents: 'Modifica documenti',
documents: 'Documenti',
move_forward: 'Sposta avanti',
move_backward: 'Sposta indietro',
remove_page: 'Rimuovi pagina',
@@ -830,6 +833,7 @@ const pt = {
add_documents_or_images: 'Adicionar documentos ou imagens',
add_a_new_document: 'Adicionar um novo documento',
edit_documents: 'Editar documentos',
documents: 'Documentos',
move_forward: 'Mover para frente',
move_backward: 'Mover para trás',
remove_page: 'Remover página',
@@ -1078,6 +1082,7 @@ const fr = {
add_documents_or_images: 'Ajouter des documents ou des images',
add_a_new_document: 'Ajouter un nouveau document',
edit_documents: 'Modifier les documents',
documents: 'Documents',
move_forward: 'Déplacer en avant',
move_backward: 'Déplacer en arrière',
remove_page: 'Supprimer la page',
@@ -1323,6 +1328,7 @@ const de = {
add_documents_or_images: 'Dokumente oder Bilder hinzufügen',
add_a_new_document: 'Neues Dokument hinzufügen',
edit_documents: 'Dokumente bearbeiten',
documents: 'Dokumente',
move_forward: 'Nach vorne',
move_backward: 'Nach hinten',
remove_page: 'Seite entfernen',
@@ -1568,6 +1574,7 @@ const nl = {
add_documents_or_images: 'Documenten of afbeeldingen toevoegen',
add_a_new_document: 'Nieuw document toevoegen',
edit_documents: 'Documenten bewerken',
documents: 'Documenten',
move_forward: 'Naar voren',
move_backward: 'Naar achteren',
remove_page: 'Pagina verwijderen',
+9 -3
View File
@@ -39,7 +39,7 @@
: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"
:class="{ 'opacity-0 group-hover:opacity-100 has-[label:focus]:opacity-100': !withVisibleControls }"
@click.stop
@success="$emit('replace', { replaceSchemaItem: item, ...$event })"
/>
@@ -49,7 +49,7 @@
>
<span
class="dropdown dropdown-end group-hover:opacity-100 has-[label:focus]:opacity-100"
:class="{ 'dropdown-open': isMakeDynamicLoading, 'opacity-0': !isMakeDynamicLoading }"
:class="{ 'dropdown-open': isMakeDynamicLoading, 'opacity-0': !isMakeDynamicLoading && !withVisibleControls }"
@mouseenter="renderDropdown = true"
@touchstart="renderDropdown = true"
>
@@ -137,7 +137,8 @@
</span>
<div
v-if="withArrows"
class="flex flex-col space-y-1 opacity-0 group-hover:opacity-100"
class="flex flex-col space-y-1"
:class="{ 'opacity-0 group-hover:opacity-100': !withVisibleControls }"
>
<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"
@@ -222,6 +223,11 @@ export default {
type: Object,
required: true
},
withVisibleControls: {
type: Boolean,
required: false,
default: false
},
template: {
type: Object,
required: true
@@ -1,11 +1,15 @@
<template>
<div class="modal modal-open items-start !animate-none overflow-y-auto">
<div :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 mt-20 w-full">
<div class="flex justify-between items-center border-b pb-2 mb-3 font-medium">
<div :class="{ 'modal-box pt-4 pb-6 mt-20 w-full': !inline }">
<div
v-if="!inline"
class="flex justify-between items-center border-b pb-2 mb-3 font-medium"
>
<span>{{ t('revisions') }}</span>
<a
href="#"
@@ -72,6 +76,11 @@ export default {
locale: {
type: String,
required: true
},
inline: {
type: Boolean,
required: false,
default: false
}
},
emits: ['close', 'apply'],
+2 -2
View File
@@ -1,4 +1,4 @@
<% content_for(:native_title, @template.name) if turbo_native_app? %>
<% content_for(:native_title, params[:view] == 'revisions' ? t('revisions') : @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-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-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>
@@ -1,24 +0,0 @@
<%= render 'shared/turbo_modal', title: t('revisions') do %>
<ul class="space-y-1.5">
<% @versions.each do |version| %>
<li>
<native-event class="group flex justify-between items-center gap-2 w-full rounded-lg p-3 border border-dashed border-base-200 cursor-pointer" data-name="template-builder:apply-revision" data-src="<%= template_version_path(@template, version) %>">
<div class="flex flex-col min-w-0">
<span><%= l(version.created_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %></span>
<span class="-ml-0.5 flex items-center space-x-1 text-xs text-base-content/60 mt-0.5">
<%= svg_icon('user', class: 'w-3.5 h-3.5 flex-shrink-0') %>
<span class="truncate"><%= version.author.full_name.presence || version.author.email %></span>
</span>
</div>
<span class="btn btn-sm btn-neutral text-white pointer-events-none flex-shrink-0">
<span class="group-data-[loading]:hidden"><%= t('view') %></span>
<%= svg_icon('loader', class: 'w-4 h-4 animate-spin hidden group-data-[loading]:block') %>
</span>
</native-event>
</li>
<% end %>
<% if @versions.empty? %>
<li class="py-4 text-center text-base-content/60"><%= t('no_revisions_yet') %></li>
<% end %>
</ul>
<% end %>