add field area controls
This commit is contained in:
@@ -14,6 +14,12 @@ document.addEventListener('turbo:before-cache', () => {
|
|||||||
window.flash?.remove()
|
window.flash?.remove()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
document.addEventListener('keyup', (e) => {
|
||||||
|
if (e.code === 'Escape') {
|
||||||
|
document.activeElement?.blur()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
window.customElements.define('toggle-visible', ToggleVisible)
|
window.customElements.define('toggle-visible', ToggleVisible)
|
||||||
window.customElements.define('disable-hidden', DisableHidden)
|
window.customElements.define('disable-hidden', DisableHidden)
|
||||||
window.customElements.define('turbo-modal', TurboModal)
|
window.customElements.define('turbo-modal', TurboModal)
|
||||||
@@ -29,6 +35,8 @@ window.customElements.define('template-builder', class extends HTMLElement {
|
|||||||
template: reactive(JSON.parse(this.dataset.template))
|
template: reactive(JSON.parse(this.dataset.template))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.app.config.globalProperties.$t = (key) => TemplateBuilder.i18n[key] || key
|
||||||
|
|
||||||
this.app.mount(this.appElem)
|
this.app.mount(this.appElem)
|
||||||
|
|
||||||
this.appendChild(this.appElem)
|
this.appendChild(this.appElem)
|
||||||
|
|||||||
@@ -130,7 +130,7 @@
|
|||||||
@attached="attachments.push($event)"
|
@attached="attachments.push($event)"
|
||||||
/>
|
/>
|
||||||
<AttachmentStep
|
<AttachmentStep
|
||||||
v-else-if="currentField.type === 'attachment'"
|
v-else-if="currentField.type === 'file'"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
|
|||||||
@@ -7,54 +7,96 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="field"
|
v-if="field"
|
||||||
class="absolute bg-white rounded-t border overflow-visible whitespace-nowrap hidden group-hover:block group-hover:z-10"
|
class="absolute bg-white rounded-t border overflow-visible whitespace-nowrap group-hover:flex group-hover:z-10"
|
||||||
|
:class="{ flex: isNameFocus || isSelected, hidden: !isNameFocus && !isSelected }"
|
||||||
style="top: -25px; height: 25px"
|
style="top: -25px; height: 25px"
|
||||||
@mousedown.stop
|
@mousedown.stop
|
||||||
@pointerdown.stop
|
@pointerdown.stop
|
||||||
>
|
>
|
||||||
|
<span class="dropdown dropdown-start border-r">
|
||||||
|
<label
|
||||||
|
tabindex="0"
|
||||||
|
title="Submitter"
|
||||||
|
class="cursor-pointer text-base-100"
|
||||||
|
@click="selectedAreaRef.value = area"
|
||||||
|
>
|
||||||
|
<button class="mx-1 w-3 h-3 rounded-full bg-yellow-600" />
|
||||||
|
</label>
|
||||||
|
<ul
|
||||||
|
tabindex="0"
|
||||||
|
class="dropdown-content bg-white menu menu-xs p-2 shadow rounded-box w-52 rounded-t-none"
|
||||||
|
style="left: -1px"
|
||||||
|
@click="closeDropdown"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="text-sm py-1 px-2"
|
||||||
|
@click.prevent
|
||||||
|
>
|
||||||
|
Submitter 1
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</span>
|
||||||
|
<FieldType
|
||||||
|
v-model="field.type"
|
||||||
|
:button-width="27"
|
||||||
|
:button-classes="'px-1'"
|
||||||
|
:menu-classes="'bg-white rounded-t-none'"
|
||||||
|
@click="selectedAreaRef.value = area"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="withName"
|
||||||
|
ref="name"
|
||||||
|
contenteditable
|
||||||
|
class="pr-1 cursor-text outline-none block"
|
||||||
|
style="min-width: 2px"
|
||||||
|
@keydown.enter.prevent="onNameEnter"
|
||||||
|
@focus="onNameFocus"
|
||||||
|
@blur="onNameBlur"
|
||||||
|
>{{ field.name || defaultName }}</span>
|
||||||
<button
|
<button
|
||||||
v-for="(component, type, index) in iconComponents"
|
class="pl-0.5 pr-1.5"
|
||||||
:key="type"
|
@click.prevent="$emit('remove')"
|
||||||
class="px-0.5 hover:text-base-100 hover:bg-base-content transition-colors"
|
|
||||||
:class="{ 'bg-base-content text-base-100': field.type === type, 'rounded-tl': index === 0, 'rounded-tr': index === 4 }"
|
|
||||||
@click="changeTypeTo(type)"
|
|
||||||
>
|
>
|
||||||
<component
|
<span>×</span>
|
||||||
:is="component"
|
|
||||||
:width="20"
|
|
||||||
stroke-width="1.5"
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="bg-red-100 opacity-70 flex items-center justify-center h-full w-full"
|
class="bg-red-100 opacity-50 flex items-center justify-center h-full w-full"
|
||||||
>
|
>
|
||||||
{{ field?.name || field?.type }}
|
<span
|
||||||
|
v-if="field"
|
||||||
|
class="flex justify-center items-center space-x-1"
|
||||||
|
>
|
||||||
|
<component :is="fieldIcons[field.type]" />
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
class="absolute top-0 bottom-0 right-0 left-0"
|
||||||
|
/>
|
||||||
<span
|
<span
|
||||||
class="h-2 w-2 right-0 bottom-0 bg-red-900 absolute cursor-nwse-resize"
|
class="h-2 w-2 -right-1 rounded-full -bottom-1 bg-red-900 absolute cursor-nwse-resize"
|
||||||
@mousedown.stop="startResize"
|
@mousedown.stop="startResize"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconTextSize, IconWriting, IconCalendarEvent, IconPhoto, IconCheckbox } from '@tabler/icons-vue'
|
import FieldType from './field_type'
|
||||||
|
import Field from './field'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FieldArea',
|
name: 'FieldArea',
|
||||||
|
components: {
|
||||||
|
FieldType
|
||||||
|
},
|
||||||
|
inject: ['template', 'selectedAreaRef'],
|
||||||
props: {
|
props: {
|
||||||
bounds: {
|
area: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: true
|
||||||
default () {
|
|
||||||
return {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
w: 0,
|
|
||||||
h: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -62,25 +104,25 @@ export default {
|
|||||||
default: null
|
default: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag'],
|
emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isResize: false,
|
isResize: false,
|
||||||
|
isNameFocus: false,
|
||||||
dragFrom: { x: 0, y: 0 }
|
dragFrom: { x: 0, y: 0 }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
iconComponents () {
|
defaultName: Field.computed.defaultName,
|
||||||
return {
|
fieldIcons: FieldType.computed.fieldIcons,
|
||||||
text: IconTextSize,
|
withName () {
|
||||||
signature: IconWriting,
|
return !['checkbox', 'radio'].includes(this.field.type) || this.field.options
|
||||||
date: IconCalendarEvent,
|
},
|
||||||
image: IconPhoto,
|
isSelected () {
|
||||||
checkbox: IconCheckbox
|
return this.selectedAreaRef.value === this.area
|
||||||
}
|
|
||||||
},
|
},
|
||||||
positionStyle () {
|
positionStyle () {
|
||||||
const { x, y, w, h } = this.bounds
|
const { x, y, w, h } = this.area
|
||||||
|
|
||||||
return {
|
return {
|
||||||
top: y * 100 + '%',
|
top: y * 100 + '%',
|
||||||
@@ -91,22 +133,47 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
changeTypeTo (type) {
|
onNameFocus (e) {
|
||||||
this.field.type = type
|
this.selectedAreaRef.value = this.area
|
||||||
|
|
||||||
|
this.isNameFocus = true
|
||||||
|
this.$refs.name.style.minWidth = this.$refs.name.clientWidth + 'px'
|
||||||
|
|
||||||
|
if (!this.field.name) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$refs.name.innerText = ' '
|
||||||
|
}, 1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onNameBlur (e) {
|
||||||
|
this.isNameFocus = false
|
||||||
|
this.$refs.name.style.minWidth = ''
|
||||||
|
|
||||||
|
if (e.target.innerText.trim()) {
|
||||||
|
this.field.name = e.target.innerText.trim()
|
||||||
|
} else {
|
||||||
|
this.field.name = ''
|
||||||
|
this.$refs.name.innerText = this.defaultName
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onNameEnter (e) {
|
||||||
|
this.$refs.name.blur()
|
||||||
},
|
},
|
||||||
resize (e) {
|
resize (e) {
|
||||||
if (e.toElement.id === 'mask') {
|
if (e.toElement.id === 'mask') {
|
||||||
this.bounds.w = e.layerX / e.toElement.clientWidth - this.bounds.x
|
this.area.w = e.layerX / e.toElement.clientWidth - this.area.x
|
||||||
this.bounds.h = e.layerY / e.toElement.clientHeight - this.bounds.y
|
this.area.h = e.layerY / e.toElement.clientHeight - this.area.y
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
drag (e) {
|
drag (e) {
|
||||||
if (e.toElement.id === 'mask') {
|
if (e.toElement.id === 'mask') {
|
||||||
this.bounds.x = (e.layerX - this.dragFrom.x) / e.toElement.clientWidth
|
this.area.x = (e.layerX - this.dragFrom.x) / e.toElement.clientWidth
|
||||||
this.bounds.y = (e.layerY - this.dragFrom.y) / e.toElement.clientHeight
|
this.area.y = (e.layerY - this.dragFrom.y) / e.toElement.clientHeight
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
startDrag (e) {
|
startDrag (e) {
|
||||||
|
this.selectedAreaRef.value = this.area
|
||||||
|
|
||||||
const rect = e.target.getBoundingClientRect()
|
const rect = e.target.getBoundingClientRect()
|
||||||
|
|
||||||
this.dragFrom = { x: e.clientX - rect.left, y: e.clientY - rect.top }
|
this.dragFrom = { x: e.clientX - rect.left, y: e.clientY - rect.top }
|
||||||
@@ -123,6 +190,8 @@ export default {
|
|||||||
this.$emit('stop-drag')
|
this.$emit('stop-drag')
|
||||||
},
|
},
|
||||||
startResize () {
|
startResize () {
|
||||||
|
this.selectedAreaRef.value = this.area
|
||||||
|
|
||||||
document.addEventListener('mousemove', this.resize)
|
document.addEventListener('mousemove', this.resize)
|
||||||
document.addEventListener('mouseup', this.stopResize)
|
document.addEventListener('mouseup', this.stopResize)
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
<Contenteditable
|
<Contenteditable
|
||||||
:model-value="template.name"
|
:model-value="template.name"
|
||||||
class="text-3xl font-semibold focus:text-clip"
|
class="text-3xl font-semibold focus:text-clip"
|
||||||
|
:icon-stroke-width="2.3"
|
||||||
@update:model-value="updateName"
|
@update:model-value="updateName"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -42,7 +43,7 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref="previews"
|
ref="previews"
|
||||||
class="overflow-auto w-52 flex-none pr-3 mt-0.5 pt-0.5"
|
class="overflow-y-auto overflow-x-hidden w-52 flex-none pr-3 mt-0.5 pt-0.5"
|
||||||
>
|
>
|
||||||
<DocumentPreview
|
<DocumentPreview
|
||||||
v-for="(item, index) in template.schema"
|
v-for="(item, index) in template.schema"
|
||||||
@@ -64,7 +65,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5">
|
<div class="w-full overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5">
|
||||||
<div class="pr-3.5 pl-0.5">
|
<div
|
||||||
|
ref="documents"
|
||||||
|
class="pr-3.5 pl-0.5"
|
||||||
|
>
|
||||||
<Document
|
<Document
|
||||||
v-for="document in sortedDocuments"
|
v-for="document in sortedDocuments"
|
||||||
:key="document.uuid"
|
:key="document.uuid"
|
||||||
@@ -75,6 +79,7 @@
|
|||||||
:is-drag="!!dragFieldType"
|
:is-drag="!!dragFieldType"
|
||||||
@draw="onDraw"
|
@draw="onDraw"
|
||||||
@drop-field="onDropfield"
|
@drop-field="onDropfield"
|
||||||
|
@remove-area="removeArea"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -115,9 +120,14 @@ import Contenteditable from './contenteditable'
|
|||||||
import DocumentPreview from './preview'
|
import DocumentPreview from './preview'
|
||||||
import { IconUsersPlus, IconDeviceFloppy } from '@tabler/icons-vue'
|
import { IconUsersPlus, IconDeviceFloppy } from '@tabler/icons-vue'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
|
import i18n from './i18n'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
const selectedAreaRef = ref(null)
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TemplateBuilder',
|
name: 'TemplateBuilder',
|
||||||
|
i18n,
|
||||||
components: {
|
components: {
|
||||||
Upload,
|
Upload,
|
||||||
Document,
|
Document,
|
||||||
@@ -128,6 +138,12 @@ export default {
|
|||||||
IconUsersPlus,
|
IconUsersPlus,
|
||||||
IconDeviceFloppy
|
IconDeviceFloppy
|
||||||
},
|
},
|
||||||
|
provide () {
|
||||||
|
return {
|
||||||
|
template: this.template,
|
||||||
|
selectedAreaRef: computed(() => selectedAreaRef)
|
||||||
|
}
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
template: {
|
template: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -158,6 +174,9 @@ export default {
|
|||||||
|
|
||||||
return areas
|
return areas
|
||||||
},
|
},
|
||||||
|
selectedField () {
|
||||||
|
return this.template.fields.find((f) => f.areas?.includes(selectedAreaRef.value))
|
||||||
|
},
|
||||||
sortedDocuments () {
|
sortedDocuments () {
|
||||||
return this.template.schema.map((item) => {
|
return this.template.schema.map((item) => {
|
||||||
return this.template.documents.find(doc => doc.uuid === item.attachment_uuid)
|
return this.template.documents.find(doc => doc.uuid === item.attachment_uuid)
|
||||||
@@ -189,6 +208,13 @@ export default {
|
|||||||
this.drawField = null
|
this.drawField = null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
removeArea ({ field, area }) {
|
||||||
|
field.areas.splice(field.areas.indexOf(area), 1)
|
||||||
|
|
||||||
|
if (!field.areas.length) {
|
||||||
|
this.template.fields.splice(this.template.fields.indexOf(field), 1)
|
||||||
|
}
|
||||||
|
},
|
||||||
onDraw (area) {
|
onDraw (area) {
|
||||||
if (this.drawField) {
|
if (this.drawField) {
|
||||||
this.drawField.areas ||= []
|
this.drawField.areas ||= []
|
||||||
@@ -196,19 +222,84 @@ export default {
|
|||||||
|
|
||||||
this.drawField = null
|
this.drawField = null
|
||||||
} else {
|
} else {
|
||||||
|
const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid)
|
||||||
|
const pageMask = documentRef.pageRefs[area.page].$refs.mask
|
||||||
|
|
||||||
|
const type = (pageMask.clientWidth * area.w) < 35 ? 'checkbox' : 'text'
|
||||||
|
|
||||||
|
if (type === 'checkbox') {
|
||||||
|
if ((pageMask.clientWidth * area.w) < 5) {
|
||||||
|
area.x = area.x - (30 / pageMask.clientWidth) / 2
|
||||||
|
area.y = area.y - (30 / pageMask.clientHeight) / 2
|
||||||
|
}
|
||||||
|
area.w = 30 / pageMask.clientWidth
|
||||||
|
area.h = 30 / pageMask.clientHeight
|
||||||
|
}
|
||||||
|
|
||||||
const field = {
|
const field = {
|
||||||
name: '',
|
name: '',
|
||||||
uuid: v4(),
|
uuid: v4(),
|
||||||
required: true,
|
required: true,
|
||||||
type: 'text',
|
type,
|
||||||
areas: [area]
|
areas: [area]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
selectedAreaRef.value = area
|
||||||
|
|
||||||
this.template.fields.push(field)
|
this.template.fields.push(field)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDropfield (area) {
|
onDropfield (area) {
|
||||||
this.$refs.fields.addField(this.dragFieldType, area)
|
const field = {
|
||||||
|
name: '',
|
||||||
|
type: this.dragFieldType,
|
||||||
|
uuid: v4(),
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldArea = {
|
||||||
|
x: (area.x - 6) / area.maskW,
|
||||||
|
y: area.y / area.maskH,
|
||||||
|
page: area.page,
|
||||||
|
attachment_uuid: area.attachment_uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousField = [...this.template.fields].reverse().find((f) => f.type === field.type)
|
||||||
|
|
||||||
|
let baseArea
|
||||||
|
|
||||||
|
if (this.selectedField?.type === this.dragFieldType) {
|
||||||
|
baseArea = selectedAreaRef.value
|
||||||
|
} else if (previousField) {
|
||||||
|
baseArea = previousField.areas[previousField.areas.length - 1]
|
||||||
|
} else {
|
||||||
|
if (['checkbox', 'radio'].includes(this.dragFieldType)) {
|
||||||
|
baseArea = {
|
||||||
|
w: area.maskW / 30 / area.maskW,
|
||||||
|
h: area.maskW / 30 / area.maskW * (area.maskW / area.maskH)
|
||||||
|
}
|
||||||
|
} else if (this.dragFieldType === 'image') {
|
||||||
|
baseArea = {
|
||||||
|
w: area.maskW / 5 / area.maskW,
|
||||||
|
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
baseArea = {
|
||||||
|
w: area.maskW / 5 / area.maskW,
|
||||||
|
h: area.maskW / 35 / area.maskW
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldArea.w = baseArea.w
|
||||||
|
fieldArea.h = baseArea.h
|
||||||
|
fieldArea.y = fieldArea.y - baseArea.h / 2
|
||||||
|
|
||||||
|
field.areas = [fieldArea]
|
||||||
|
|
||||||
|
selectedAreaRef.value = fieldArea
|
||||||
|
|
||||||
|
this.template.fields.push(field)
|
||||||
},
|
},
|
||||||
updateFromUpload ({ schema, documents }) {
|
updateFromUpload ({ schema, documents }) {
|
||||||
this.template.schema.push(...schema)
|
this.template.schema.push(...schema)
|
||||||
@@ -264,6 +355,8 @@ export default {
|
|||||||
documentRef.scrollToArea(area)
|
documentRef.scrollToArea(area)
|
||||||
},
|
},
|
||||||
save () {
|
save () {
|
||||||
|
this.$el.closest('template-builder').dataset.template = JSON.stringify(this.template)
|
||||||
|
|
||||||
return fetch(`/api/templates/${this.template.id}`, {
|
return fetch(`/api/templates/${this.template.id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({ template: this.template }),
|
body: JSON.stringify({ template: this.template }),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="group relative overflow-visible"
|
class="group/contenteditable relative overflow-visible"
|
||||||
:class="{ 'flex items-center': !iconInline }"
|
:class="{ 'flex items-center': !iconInline }"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -17,10 +17,12 @@
|
|||||||
</span>
|
</span>
|
||||||
<IconPencil
|
<IconPencil
|
||||||
contenteditable="false"
|
contenteditable="false"
|
||||||
class="cursor-pointer ml-1 flex-none opacity-0 group-hover:opacity-100 align-middle peer-focus:hidden"
|
class="cursor-pointer ml-1 flex-none opacity-0 group-hover/contenteditable:opacity-100 align-middle peer-focus:hidden"
|
||||||
:style="iconInline ? {} : { right: -(1.1 * iconWidth) + 'px' }"
|
:style="iconInline ? {} : { right: -(1.1 * iconWidth) + 'px' }"
|
||||||
|
title="Edit"
|
||||||
:class="{ 'absolute': !iconInline, 'inline align-bottom': iconInline }"
|
:class="{ 'absolute': !iconInline, 'inline align-bottom': iconInline }"
|
||||||
:width="iconWidth"
|
:width="iconWidth"
|
||||||
|
:stroke-width="iconStrokeWidth"
|
||||||
@click="onPencilClick"
|
@click="onPencilClick"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -49,6 +51,11 @@ export default {
|
|||||||
type: Number,
|
type: Number,
|
||||||
required: false,
|
required: false,
|
||||||
default: 30
|
default: 30
|
||||||
|
},
|
||||||
|
iconStrokeWidth: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['update:model-value', 'focus', 'blur'],
|
emits: ['update:model-value', 'focus', 'blur'],
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
:is-drag="isDrag"
|
:is-drag="isDrag"
|
||||||
:image="image"
|
:image="image"
|
||||||
@drop-field="$emit('drop-field', {...$event, attachment_uuid: document.uuid })"
|
@drop-field="$emit('drop-field', {...$event, attachment_uuid: document.uuid })"
|
||||||
|
@remove-area="$emit('remove-area', $event)"
|
||||||
@draw="$emit('draw', {...$event, attachment_uuid: document.uuid })"
|
@draw="$emit('draw', {...$event, attachment_uuid: document.uuid })"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,7 +44,7 @@ export default {
|
|||||||
default: false
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['draw', 'drop-field'],
|
emits: ['draw', 'drop-field', 'remove-area'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
pageRefs: []
|
pageRefs: []
|
||||||
@@ -59,7 +60,7 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
scrollToArea (area) {
|
scrollToArea (area) {
|
||||||
this.pageRefs[area.page].areaRefs.find((e) => e.bounds === area).$el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
this.pageRefs[area.page].areaRefs.find((e) => e.area === area).$el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||||
},
|
},
|
||||||
setPageRefs (el) {
|
setPageRefs (el) {
|
||||||
if (el) {
|
if (el) {
|
||||||
|
|||||||
@@ -1,68 +1,36 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="group pb-2"
|
class="group pb-2"
|
||||||
@mouseleave="closeDropdown"
|
@click="field.areas?.[0] && $emit('scroll-to', field.areas[0])"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="border border-base-content rounded rounded-tr-none relative group"
|
class="border border-gray-300 rounded rounded-tr-none relative group"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between space-x-1">
|
<div class="flex items-center justify-between space-x-1">
|
||||||
<div class="flex items-center p-1 space-x-1">
|
<div class="flex items-center p-1 space-x-1">
|
||||||
<span class="dropdown">
|
<FieldType
|
||||||
<label
|
v-model="field.type"
|
||||||
tabindex="0"
|
:button-width="20"
|
||||||
title="Type"
|
/>
|
||||||
class="cursor-pointer"
|
|
||||||
>
|
|
||||||
<component
|
|
||||||
:is="fieldIcons[field.type]"
|
|
||||||
width="18"
|
|
||||||
:stroke-width="1.6"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<ul
|
|
||||||
tabindex="0"
|
|
||||||
class="mt-1.5 dropdown-content menu menu-xs p-2 shadow bg-base-100 rounded-box w-52"
|
|
||||||
@click="closeDropdown"
|
|
||||||
>
|
|
||||||
<li
|
|
||||||
v-for="(name, type) in fieldNames"
|
|
||||||
:key="type"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
class="text-sm py-1 px-2"
|
|
||||||
:class="{ 'active': type === field.type }"
|
|
||||||
@click.prevent="field.type = type"
|
|
||||||
>
|
|
||||||
<component
|
|
||||||
:is="fieldIcons[type]"
|
|
||||||
:stroke-width="1.6"
|
|
||||||
:width="20"
|
|
||||||
/>
|
|
||||||
{{ name }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</span>
|
|
||||||
<Contenteditable
|
<Contenteditable
|
||||||
ref="name"
|
ref="name"
|
||||||
:model-value="field.name || defaultName"
|
:model-value="field.name || defaultName"
|
||||||
:icon-inline="true"
|
:icon-inline="true"
|
||||||
:icon-width="19"
|
:icon-width="18"
|
||||||
|
:icon-stroke-width="1.6"
|
||||||
@focus="onNameFocus"
|
@focus="onNameFocus"
|
||||||
@blur="onNameBlur"
|
@blur="onNameBlur"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-1 opacity-0 group-hover:opacity-100">
|
<div class="flex items-center space-x-1">
|
||||||
<span class="dropdown dropdown-end">
|
<span class="dropdown dropdown-end">
|
||||||
<label
|
<label
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
title="Areas"
|
title="Areas"
|
||||||
class="cursor-pointer"
|
class="cursor-pointer text-base-100 group-hover:text-base-content"
|
||||||
>
|
>
|
||||||
<IconShape
|
<IconShape
|
||||||
:width="20"
|
:width="18"
|
||||||
:stroke-width="1.6"
|
:stroke-width="1.6"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
@@ -102,13 +70,17 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</span>
|
</span>
|
||||||
<button @click="$emit('remove', field)">
|
<button
|
||||||
|
class=" text-base-100 group-hover:text-base-content"
|
||||||
|
title="Remove"
|
||||||
|
@click="$emit('remove', field)"
|
||||||
|
>
|
||||||
<IconTrashX
|
<IconTrashX
|
||||||
:width="20"
|
:width="18"
|
||||||
:stroke-width="1.6"
|
:stroke-width="1.6"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<div class="flex flex-col pr-1">
|
<div class="flex flex-col pr-1 text-base-100 group-hover:text-base-content">
|
||||||
<button
|
<button
|
||||||
title="Up"
|
title="Up"
|
||||||
style="font-size: 10px; margin-bottom: -2px"
|
style="font-size: 10px; margin-bottom: -2px"
|
||||||
@@ -135,7 +107,7 @@
|
|||||||
:key="index"
|
:key="index"
|
||||||
class="flex space-x-1.5 items-center"
|
class="flex space-x-1.5 items-center"
|
||||||
>
|
>
|
||||||
<span class="text-sm">
|
<span class="text-sm w-3.5">
|
||||||
{{ index + 1 }}.
|
{{ index + 1 }}.
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
@@ -145,7 +117,7 @@
|
|||||||
required
|
required
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="text-sm"
|
class="text-sm w-3.5"
|
||||||
@click="field.options.splice(index, 1)"
|
@click="field.options.splice(index, 1)"
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
@@ -165,7 +137,8 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Contenteditable from './contenteditable'
|
import Contenteditable from './contenteditable'
|
||||||
import { IconTextSize, IconWriting, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconShape, IconNewSection, IconTrashX } from '@tabler/icons-vue'
|
import FieldType from './field_type'
|
||||||
|
import { IconShape, IconNewSection, IconTrashX } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TemplateField',
|
name: 'TemplateField',
|
||||||
@@ -173,50 +146,25 @@ export default {
|
|||||||
Contenteditable,
|
Contenteditable,
|
||||||
IconShape,
|
IconShape,
|
||||||
IconNewSection,
|
IconNewSection,
|
||||||
IconTrashX
|
IconTrashX,
|
||||||
|
FieldType
|
||||||
},
|
},
|
||||||
|
inject: ['template'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
|
||||||
typeIndex: {
|
|
||||||
type: Number,
|
|
||||||
required: false,
|
|
||||||
default: 0
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['set-draw', 'remove', 'move-up', 'move-down', 'scroll-to'],
|
emits: ['set-draw', 'remove', 'move-up', 'move-down', 'scroll-to'],
|
||||||
computed: {
|
computed: {
|
||||||
defaultName () {
|
defaultName () {
|
||||||
return `${this.fieldNames[this.field.type]} Field ${this.typeIndex + 1}`
|
const typeIndex = this.template.fields.filter((f) => f.type === this.field.type).indexOf(this.field)
|
||||||
|
|
||||||
|
return `${this.$t(this.field.type)} Field ${typeIndex + 1}`
|
||||||
},
|
},
|
||||||
areas () {
|
areas () {
|
||||||
return this.field.areas || []
|
return this.field.areas || []
|
||||||
},
|
|
||||||
fieldNames () {
|
|
||||||
return {
|
|
||||||
text: 'Text',
|
|
||||||
signature: 'Signature',
|
|
||||||
date: 'Date',
|
|
||||||
image: 'Image',
|
|
||||||
attachment: 'File',
|
|
||||||
select: 'Select',
|
|
||||||
checkbox: 'Checkbox',
|
|
||||||
radio: 'Radio'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fieldIcons () {
|
|
||||||
return {
|
|
||||||
text: IconTextSize,
|
|
||||||
signature: IconWriting,
|
|
||||||
date: IconCalendarEvent,
|
|
||||||
image: IconPhoto,
|
|
||||||
attachment: IconPaperclip,
|
|
||||||
select: IconSelect,
|
|
||||||
checkbox: IconCheckbox,
|
|
||||||
radio: IconCircleDot
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<span class="dropdown">
|
||||||
|
<label
|
||||||
|
tabindex="0"
|
||||||
|
title="Type"
|
||||||
|
class="cursor-pointer"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="fieldIcons[modelValue]"
|
||||||
|
:width="buttonWidth"
|
||||||
|
:class="buttonClasses"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<ul
|
||||||
|
tabindex="0"
|
||||||
|
class="dropdown-content menu menu-xs p-2 shadow rounded-box w-52"
|
||||||
|
:class="menuClasses"
|
||||||
|
@click="closeDropdown"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
v-for="(icon, type) in fieldIcons"
|
||||||
|
:key="type"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="text-sm py-1 px-2"
|
||||||
|
:class="{ 'active': type === modelValue }"
|
||||||
|
@click.prevent="$emit('update:model-value', type)"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="icon"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
:width="20"
|
||||||
|
/>
|
||||||
|
{{ $t(type) }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { IconTextSize, IconWriting, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot } from '@tabler/icons-vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'FiledTypeDropdown',
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
menuClasses: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'mt-1.5 bg-base-100'
|
||||||
|
},
|
||||||
|
buttonClasses: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
buttonWidth: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 18
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['update:model-value'],
|
||||||
|
computed: {
|
||||||
|
fieldIcons () {
|
||||||
|
return {
|
||||||
|
text: IconTextSize,
|
||||||
|
signature: IconWriting,
|
||||||
|
date: IconCalendarEvent,
|
||||||
|
image: IconPhoto,
|
||||||
|
file: IconPaperclip,
|
||||||
|
select: IconSelect,
|
||||||
|
checkbox: IconCheckbox,
|
||||||
|
radio: IconCircleDot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
closeDropdown () {
|
||||||
|
document.activeElement.blur()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mb-2">
|
<div class="mb-1">
|
||||||
<Field
|
<Field
|
||||||
v-for="field in fields"
|
v-for="field in fields"
|
||||||
:key="field.uuid"
|
:key="field.uuid"
|
||||||
@@ -14,13 +14,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-3 gap-1">
|
<div class="grid grid-cols-3 gap-1">
|
||||||
<button
|
<button
|
||||||
v-for="item in fieldTypes"
|
v-for="(icon, type) in fieldIcons"
|
||||||
:key="item.type"
|
:key="type"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
class="flex items-center justify-center border border-dashed border-gray-300 bg-base-100 w-full rounded relative"
|
class="flex items-center justify-center border border-dashed border-gray-300 bg-base-100 w-full rounded relative"
|
||||||
@dragstart="onDragstart(item.value)"
|
@dragstart="onDragstart(type)"
|
||||||
@dragend="$emit('drag-end')"
|
@dragend="$emit('drag-end')"
|
||||||
@click="addField(item.value)"
|
@click="addField(type)"
|
||||||
>
|
>
|
||||||
<div class="w-0 absolute left-0">
|
<div class="w-0 absolute left-0">
|
||||||
<svg
|
<svg
|
||||||
@@ -49,9 +49,9 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center flex-col px-2 py-2">
|
<div class="flex items-center flex-col px-2 py-2">
|
||||||
<component :is="item.icon" />
|
<component :is="icon" />
|
||||||
<span class="text-xs mt-1">
|
<span class="text-xs mt-1">
|
||||||
{{ item.label }}
|
{{ $t(type) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import Field from './field'
|
import Field from './field'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
import { IconTextSize, IconWriting, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot } from '@tabler/icons-vue'
|
import FieldType from './field_type'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TemplateFields',
|
name: 'TemplateFields',
|
||||||
@@ -76,18 +76,7 @@ export default {
|
|||||||
},
|
},
|
||||||
emits: ['set-draw', 'set-drag', 'drag-end', 'scroll-to-area'],
|
emits: ['set-draw', 'set-drag', 'drag-end', 'scroll-to-area'],
|
||||||
computed: {
|
computed: {
|
||||||
fieldTypes () {
|
fieldIcons: FieldType.computed.fieldIcons
|
||||||
return [
|
|
||||||
{ label: 'Text', value: 'text', icon: IconTextSize },
|
|
||||||
{ label: 'Signature', value: 'signature', icon: IconWriting },
|
|
||||||
{ label: 'Date', value: 'date', icon: IconCalendarEvent },
|
|
||||||
{ label: 'Image', value: 'image', icon: IconPhoto },
|
|
||||||
{ label: 'File', value: 'attachment', icon: IconPaperclip },
|
|
||||||
{ label: 'Select', value: 'select', icon: IconSelect },
|
|
||||||
{ label: 'Checkbox', value: 'checkbox', icon: IconCheckbox },
|
|
||||||
{ label: 'Radio', value: 'radio', icon: IconCircleDot }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onDragstart (fieldType) {
|
onDragstart (fieldType) {
|
||||||
@@ -108,7 +97,7 @@ export default {
|
|||||||
},
|
},
|
||||||
addField (type, area = null) {
|
addField (type, area = null) {
|
||||||
const field = {
|
const field = {
|
||||||
name: type === 'signature' ? 'Signature' : '',
|
name: '',
|
||||||
uuid: v4(),
|
uuid: v4(),
|
||||||
required: true,
|
required: true,
|
||||||
type
|
type
|
||||||
@@ -118,10 +107,6 @@ export default {
|
|||||||
field.options = ['']
|
field.options = ['']
|
||||||
}
|
}
|
||||||
|
|
||||||
if (area) {
|
|
||||||
field.areas = [area]
|
|
||||||
}
|
|
||||||
|
|
||||||
this.fields.push(field)
|
this.fields.push(field)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export default {
|
||||||
|
text: 'Text',
|
||||||
|
signature: 'Signature',
|
||||||
|
date: 'Date',
|
||||||
|
image: 'Image',
|
||||||
|
file: 'File',
|
||||||
|
select: 'Select',
|
||||||
|
checkbox: 'Checkbox',
|
||||||
|
radio: 'Radio'
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative cursor-crosshair">
|
<div class="relative cursor-crosshair select-none">
|
||||||
<img
|
<img
|
||||||
ref="image"
|
ref="image"
|
||||||
:src="image.url"
|
:src="image.url"
|
||||||
@@ -16,16 +16,17 @@
|
|||||||
v-for="(item, i) in areas"
|
v-for="(item, i) in areas"
|
||||||
:key="i"
|
:key="i"
|
||||||
:ref="setAreaRefs"
|
:ref="setAreaRefs"
|
||||||
:bounds="item.area"
|
:area="item.area"
|
||||||
:field="item.field"
|
:field="item.field"
|
||||||
@start-resize="showMask = true"
|
@start-resize="showMask = true"
|
||||||
@stop-resize="showMask = false"
|
@stop-resize="showMask = false"
|
||||||
@start-drag="showMask = true"
|
@start-drag="showMask = true"
|
||||||
@stop-drag="showMask = false"
|
@stop-drag="showMask = false"
|
||||||
|
@remove="$emit('remove-area', item)"
|
||||||
/>
|
/>
|
||||||
<FieldArea
|
<FieldArea
|
||||||
v-if="newArea"
|
v-if="newArea"
|
||||||
:bounds="newArea"
|
:area="newArea"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -59,6 +60,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
selectedArea: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
isDrag: {
|
isDrag: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -69,7 +75,7 @@ export default {
|
|||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['draw', 'drop-field'],
|
emits: ['draw', 'drop-field', 'remove-area'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
areaRefs: [],
|
areaRefs: [],
|
||||||
@@ -96,10 +102,10 @@ export default {
|
|||||||
},
|
},
|
||||||
onDrop (e) {
|
onDrop (e) {
|
||||||
this.$emit('drop-field', {
|
this.$emit('drop-field', {
|
||||||
x: e.layerX / this.$refs.mask.clientWidth,
|
x: e.layerX,
|
||||||
y: e.layerY / this.$refs.mask.clientHeight - (this.$refs.mask.clientWidth / 30 / this.$refs.mask.clientWidth) / 2,
|
y: e.layerY,
|
||||||
w: this.$refs.mask.clientWidth / 5 / this.$refs.mask.clientWidth,
|
maskW: this.$refs.mask.clientWidth,
|
||||||
h: this.$refs.mask.clientWidth / 30 / this.$refs.mask.clientWidth,
|
maskH: this.$refs.mask.clientHeight,
|
||||||
page: this.number
|
page: this.number
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<label tabindex="0" class="cursor-pointer bg-neutral-focus text-neutral-content rounded-full w-8 p-2">
|
<label tabindex="0" class="cursor-pointer bg-neutral-focus text-neutral-content rounded-full w-8 p-2">
|
||||||
<span class="text-sm align-text-top"><%= current_user.initials %></span>
|
<span class="text-sm align-text-top"><%= current_user.initials %></span>
|
||||||
</label>
|
</label>
|
||||||
<ul tabindex="0" class="dropdown-content p-2 mt-2 shadow-2xl menu bg-white rounded-box whitespace-nowrap">
|
<ul tabindex="0" class="dropdown-content p-2 mt-2 shadow menu bg-base-100 rounded-box whitespace-nowrap">
|
||||||
<li class>
|
<li class>
|
||||||
<%= link_to 'Profile', settings_profile_index_path, class: 'text-right' %>
|
<%= link_to 'Profile', settings_profile_index_path, class: 'text-right' %>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ module Submissions
|
|||||||
(((attachment.metadata['height'] * scale) + (area['h'] * height)) / 2)],
|
(((attachment.metadata['height'] * scale) + (area['h'] * height)) / 2)],
|
||||||
width: attachment.metadata['width'] * scale,
|
width: attachment.metadata['width'] * scale,
|
||||||
height: attachment.metadata['height'] * scale)
|
height: attachment.metadata['height'] * scale)
|
||||||
when 'attachment'
|
when 'file'
|
||||||
Array.wrap(value).each_with_index do |uuid, index|
|
Array.wrap(value).each_with_index do |uuid, index|
|
||||||
attachment = submission.attachments.find { |a| a.uuid == uuid }
|
attachment = submission.attachments.find { |a| a.uuid == uuid }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user