this.selectedAreaRef),
- fieldsDragFieldRef: computed(() => this.fieldsDragFieldRef)
+ selectedAreasRef: computed(() => this.selectedAreasRef),
+ fieldsDragFieldRef: computed(() => this.fieldsDragFieldRef),
+ isSelectModeRef: computed(() => this.isSelectModeRef),
+ isCmdKeyRef: computed(() => this.isCmdKeyRef)
}
},
props: {
@@ -848,11 +894,13 @@ export default {
isDownloading: false,
isLoadingBlankPage: false,
isSaving: false,
+ isDetectingPageFields: false,
+ detectingAnalyzingProgress: null,
+ detectingFieldsAddedCount: null,
selectedSubmitter: null,
showDrawField: false,
pendingFieldAttachmentUuids: [],
drawField: null,
- copiedArea: null,
drawFieldType: null,
drawOption: null,
dragField: null,
@@ -861,8 +909,10 @@ export default {
},
computed: {
submitterDefaultNames: FieldSubmitter.computed.names,
- selectedAreaRef: () => ref(),
+ isSelectModeRef: () => ref(false),
+ isCmdKeyRef: () => ref(false),
fieldsDragFieldRef: () => ref(),
+ selectedAreasRef: () => ref([]),
language () {
return this.locale.split('-')[0].toLowerCase()
},
@@ -876,6 +926,14 @@ export default {
isInlineSize () {
return CSS.supports('container-type: size')
},
+ lowestSelectedArea () {
+ return this.selectedAreasRef.value.reduce((acc, area) => {
+ return area.y + area.h < acc.y + acc.h ? acc : area
+ }, this.selectedAreasRef.value[0])
+ },
+ lastSelectedArea () {
+ return this.selectedAreasRef.value[this.selectedAreasRef.value.length - 1]
+ },
isMobile () {
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
@@ -917,7 +975,7 @@ export default {
})
},
selectedField () {
- return this.template.fields.find((f) => f.areas?.includes(this.selectedAreaRef.value))
+ return this.template.fields.find((f) => f.areas?.includes(this.lastSelectedArea))
},
sortedDocuments () {
return this.template.schema.map((item) => {
@@ -1004,6 +1062,81 @@ export default {
},
methods: {
toRaw,
+ toggleSelectMode () {
+ this.isSelectModeRef.value = !this.isSelectModeRef.value
+
+ if (!this.isSelectModeRef.value && this.selectedAreasRef.value.length > 1) {
+ this.selectedAreasRef.value = []
+ }
+ },
+ deleteSelectedAreas () {
+ [...this.selectedAreasRef.value].forEach((area) => {
+ this.removeArea(area, false)
+ })
+
+ this.save()
+ },
+ moveSelectedAreas (dx, dy) {
+ let clampedDx = dx
+ let clampedDy = dy
+
+ const rectIndex = {}
+
+ this.selectedAreasRef.value.map((area) => {
+ const key = `${area.attachment_uuid}-${area.page}`
+
+ let rect = rectIndex[key]
+
+ if (!rect) {
+ const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid)
+ const page = documentRef.pageRefs[area.page].$refs.image
+ rect = page.getBoundingClientRect()
+
+ rectIndex[key] = rect
+ }
+
+ const normalizedDx = dx / rect.width
+ const normalizedDy = dy / rect.height
+
+ const maxDxLeft = -area.x
+ const maxDxRight = 1 - area.w - area.x
+ const maxDyTop = -area.y
+ const maxDyBottom = 1 - area.h - area.y
+
+ if (normalizedDx < maxDxLeft) clampedDx = Math.max(clampedDx, maxDxLeft * rect.width)
+ if (normalizedDx > maxDxRight) clampedDx = Math.min(clampedDx, maxDxRight * rect.width)
+ if (normalizedDy < maxDyTop) clampedDy = Math.max(clampedDy, maxDyTop * rect.height)
+ if (normalizedDy > maxDyBottom) clampedDy = Math.min(clampedDy, maxDyBottom * rect.height)
+
+ return [area, rect]
+ }).forEach(([area, rect]) => {
+ area.x += clampedDx / rect.width
+ area.y += clampedDy / rect.height
+ })
+
+ this.debouncedSave()
+ },
+ alignSelectedAreas (direction) {
+ const areas = this.selectedAreasRef.value
+
+ let targetValue
+
+ if (direction === 'left') {
+ targetValue = Math.min(...areas.map(a => a.x))
+ areas.forEach((area) => { area.x = targetValue })
+ } else if (direction === 'right') {
+ targetValue = Math.max(...areas.map(a => a.x + a.w))
+ areas.forEach((area) => { area.x = targetValue - area.w })
+ } else if (direction === 'top') {
+ targetValue = Math.min(...areas.map(a => a.y))
+ areas.forEach((area) => { area.y = targetValue })
+ } else if (direction === 'bottom') {
+ targetValue = Math.max(...areas.map(a => a.y + a.h))
+ areas.forEach((area) => { area.y = targetValue - area.h })
+ }
+
+ this.save()
+ },
download () {
this.isDownloading = true
@@ -1388,49 +1521,92 @@ export default {
}
},
onKeyUp (e) {
+ this.isCmdKeyRef.value = false
+
if (e.code === 'Escape') {
+ this.selectedAreasRef.value = []
this.clearDrawField()
-
- this.selectedAreaRef.value = null
}
- if (this.editable && ['Backspace', 'Delete'].includes(e.key) && this.selectedAreaRef.value && document.activeElement === document.body) {
- this.removeArea(this.selectedAreaRef.value)
-
- this.selectedAreaRef.value = null
+ if (this.editable && ['Backspace', 'Delete'].includes(e.key) && document.activeElement === document.body) {
+ if (this.selectedAreasRef.value.length > 1) {
+ this.deleteSelectedAreas()
+ } else if (this.selectedAreasRef.value.length) {
+ this.removeArea(this.lastSelectedArea)
+ }
}
},
onKeyDown (event) {
- if ((event.metaKey && event.shiftKey && event.key === 'z') || (event.ctrlKey && event.key === 'Z')) {
+ if (event.key === 'Tab' && document.activeElement === document.body) {
event.stopImmediatePropagation()
event.preventDefault()
+ this.toggleSelectMode()
+ } else if ((event.metaKey && event.shiftKey && event.key === 'z') || (event.ctrlKey && event.key === 'Z')) {
+ event.stopImmediatePropagation()
+ event.preventDefault()
+
+ this.selectedAreasRef.value = []
+
this.redo()
} else if ((event.ctrlKey || event.metaKey) && event.key === 'z') {
event.stopImmediatePropagation()
event.preventDefault()
+ this.selectedAreasRef.value = []
+
this.undo()
} else if ((event.ctrlKey || event.metaKey) && event.key === 'c' && document.activeElement === document.body) {
- event.preventDefault()
-
- this.copiedArea = this.selectedAreaRef?.value
- } else if ((event.ctrlKey || event.metaKey) && event.key === 'v' && this.copiedArea && document.activeElement === document.body) {
+ if (this.selectedAreasRef.value.length > 1) {
+ event.preventDefault()
+ this.copySelectedAreas()
+ } else if (this.selectedAreasRef.value.length) {
+ event.preventDefault()
+ this.copyField()
+ }
+ } else if ((event.ctrlKey || event.metaKey) && event.key === 'v' && this.hasClipboardData() && document.activeElement === document.body) {
event.preventDefault()
this.pasteField()
- } else if (this.selectedAreaRef.value && ['ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown'].includes(event.key) && document.activeElement === document.body) {
- event.preventDefault()
+ } else if (['ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown'].includes(event.key) && document.activeElement === document.body) {
+ if (this.selectedAreasRef.value.length > 1) {
+ event.preventDefault()
+ this.handleSelectedAreasArrows(event)
+ } else if (this.selectedAreasRef.value.length) {
+ event.preventDefault()
+ this.handleAreaArrows(event)
+ }
+ } else if (event.metaKey || event.ctrlKey) {
+ this.isCmdKeyRef.value = true
+ }
+ },
+ handleSelectedAreasArrows (event) {
+ if (!this.editable) {
+ return
+ }
- this.handleAreaArrows(event)
+ const diff = (event.shiftKey ? 5.0 : 1.0)
+ let dx = 0
+ let dy = 0
+
+ if (event.key === 'ArrowRight') {
+ dx = diff
+ } else if (event.key === 'ArrowLeft') {
+ dx = -diff
+ } else if (event.key === 'ArrowUp') {
+ dy = -diff
+ } else if (event.key === 'ArrowDown') {
+ dy = diff
}
+
+ this.moveSelectedAreas(dx, dy)
},
handleAreaArrows (event) {
if (!this.editable) {
return
}
- const area = this.selectedAreaRef.value
+ const area = this.lastSelectedArea
const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid)
const page = documentRef.pageRefs[area.page].$refs.image
const rect = page.getBoundingClientRect()
@@ -1463,7 +1639,7 @@ export default {
this.save()
}, 700)
},
- removeArea (area) {
+ removeArea (area, save = true) {
const field = this.template.fields.find((f) => f.areas?.includes(area))
field.areas.splice(field.areas.indexOf(area), 1)
@@ -1474,7 +1650,11 @@ export default {
this.removeFieldConditions(field)
}
- this.save()
+ this.selectedAreasRef.value.splice(this.selectedAreasRef.value.indexOf(area), 1)
+
+ if (save) {
+ this.save()
+ }
},
removeFieldConditions (field) {
this.template.fields.forEach((f) => {
@@ -1497,45 +1677,242 @@ export default {
}
})
},
- pasteField () {
- const field = this.template.fields.find((f) => f.areas?.includes(this.copiedArea))
- const currentArea = this.selectedAreaRef?.value || this.copiedArea
-
- if (field && currentArea) {
- const area = {
- ...JSON.parse(JSON.stringify(this.copiedArea)),
- attachment_uuid: currentArea.attachment_uuid,
- page: currentArea.page,
- x: currentArea.x,
- y: currentArea.y + currentArea.h * 1.3
+ copyField () {
+ const area = this.lastSelectedArea
+
+ if (!area) return
+
+ const field = this.template.fields.find((f) => f.areas?.includes(area))
+
+ if (!field) return
+
+ const clipboardData = {
+ field: JSON.parse(JSON.stringify(field)),
+ area: JSON.parse(JSON.stringify(area)),
+ templateId: this.template.id,
+ timestamp: Date.now()
+ }
+
+ delete clipboardData.field.areas
+ delete clipboardData.field.uuid
+ delete clipboardData.field.submitter_uuid
+
+ try {
+ localStorage.setItem('docuseal_clipboard', JSON.stringify(clipboardData))
+ } catch (e) {
+ console.error('Failed to save clipboard:', e)
+ }
+ },
+ copySelectedAreas () {
+ const items = []
+
+ const areas = this.selectedAreasRef.value
+
+ const minX = Math.min(...areas.map(a => a.x))
+ const minY = Math.min(...areas.map(a => a.y))
+
+ areas.forEach((area) => {
+ const field = this.template.fields.find((f) => f.areas?.includes(area))
+
+ if (!field) return
+
+ const fieldCopy = JSON.parse(JSON.stringify(field))
+ const areaCopy = JSON.parse(JSON.stringify(area))
+
+ delete fieldCopy.areas
+ delete fieldCopy.submitter_uuid
+
+ areaCopy.relativeX = area.x - minX
+ areaCopy.relativeY = area.y - minY
+
+ items.push({ field: fieldCopy, area: areaCopy })
+ })
+
+ const clipboardData = {
+ items,
+ templateId: this.template.id,
+ timestamp: Date.now(),
+ isGroup: true
+ }
+
+ try {
+ localStorage.setItem('docuseal_clipboard', JSON.stringify(clipboardData))
+ } catch (e) {
+ console.error('Failed to save clipboard:', e)
+ }
+ },
+ pasteField (targetPosition = null) {
+ const clipboard = localStorage.getItem('docuseal_clipboard')
+
+ if (!clipboard) return
+
+ const data = JSON.parse(clipboard)
+
+ if (Date.now() - data.timestamp >= 3600000) {
+ localStorage.removeItem('docuseal_clipboard')
+
+ return
+ }
+
+ if (data.isGroup && data.items?.length) {
+ this.pasteFieldGroup(data, targetPosition)
+
+ return
+ }
+
+ const field = data.field
+ const area = data.area
+ const isSameTemplate = data.templateId === this.template.id
+
+ if (!field || !area) return
+
+ if (!isSameTemplate) {
+ delete field.conditions
+ delete field.preferences?.formula
+ }
+
+ const defaultAttachmentUuid = this.template.schema[0]?.attachment_uuid
+
+ if (field && (this.lowestSelectedArea || targetPosition)) {
+ const attachmentUuid = targetPosition?.attachment_uuid ||
+ (this.template.documents.find((d) => d.uuid === this.lowestSelectedArea.attachment_uuid) ? this.lowestSelectedArea.attachment_uuid : null) ||
+ defaultAttachmentUuid
+
+ const newArea = {
+ ...JSON.parse(JSON.stringify(area)),
+ attachment_uuid: attachmentUuid,
+ page: targetPosition?.page ?? (attachmentUuid === this.lowestSelectedArea.attachment_uuid ? this.lowestSelectedArea.page : 0),
+ x: targetPosition ? (targetPosition.x - area.w / 2) : Math.min(...this.selectedAreasRef.value.map((area) => area.x)),
+ y: targetPosition ? (targetPosition.y - area.h / 2) : (this.lowestSelectedArea.y + this.lowestSelectedArea.h * 1.3)
}
- if (['radio', 'multiple'].includes(field.type)) {
- this.copiedArea.option_uuid ||= field.options[0].uuid
- area.option_uuid = v4()
+ const newField = {
+ ...JSON.parse(JSON.stringify(field)),
+ uuid: v4(),
+ submitter_uuid: this.selectedSubmitter.uuid,
+ areas: [newArea]
+ }
- const lastOption = field.options[field.options.length - 1]
+ if (['radio', 'multiple'].includes(field.type) && field.options?.length) {
+ const oldOptionUuid = area.option_uuid
+ const optionsMap = {}
- if (!field.areas.find((a) => lastOption.uuid === a.option_uuid)) {
- area.option_uuid = lastOption.uuid
- } else {
- field.options.push({ uuid: area.option_uuid })
- }
+ newField.options = field.options.map((opt) => {
+ const newUuid = v4()
+ optionsMap[opt.uuid] = newUuid
+ return { ...opt, uuid: newUuid }
+ })
- field.areas.push(area)
- } else {
- const newField = {
- ...JSON.parse(JSON.stringify(field)),
- uuid: v4(),
- areas: [area]
+ newArea.option_uuid = optionsMap[oldOptionUuid] || newField.options[0].uuid
+ }
+
+ this.insertField(newField)
+
+ this.selectedAreasRef.value = [newArea]
+
+ this.save()
+ }
+ },
+ pasteFieldGroup (data, targetPosition) {
+ const isSameTemplate = data.templateId === this.template.id
+ const defaultAttachmentUuid = this.template.schema[0]?.attachment_uuid
+
+ const attachmentUuid = targetPosition?.attachment_uuid ||
+ (this.lowestSelectedArea && this.template.documents.find((d) => d.uuid === this.lowestSelectedArea.attachment_uuid) ? this.lowestSelectedArea.attachment_uuid : null) ||
+ defaultAttachmentUuid
+
+ const page = targetPosition?.page ?? (this.lowestSelectedArea && attachmentUuid === this.lowestSelectedArea.attachment_uuid ? this.lowestSelectedArea.page : 0)
+
+ let baseX, baseY
+
+ if (targetPosition) {
+ baseX = targetPosition.x
+ baseY = targetPosition.y
+ } else if (this.lowestSelectedArea) {
+ baseX = Math.min(...this.selectedAreasRef.value.map((area) => area.x))
+ baseY = this.lowestSelectedArea.y + this.lowestSelectedArea.h * 1.3
+ } else {
+ baseX = 0.1
+ baseY = 0.1
+ }
+
+ const newAreas = []
+
+ const fieldUuidIndex = {}
+ const fieldOptionsMap = {}
+
+ data.items.forEach((item) => {
+ const field = JSON.parse(JSON.stringify(item.field))
+ const area = JSON.parse(JSON.stringify(item.area))
+
+ if (!isSameTemplate) {
+ delete field.conditions
+ delete field.preferences?.formula
+ }
+
+ const newArea = {
+ ...area,
+ attachment_uuid: attachmentUuid,
+ page,
+ x: baseX + (area.relativeX || 0),
+ y: baseY + (area.relativeY || 0)
+ }
+
+ delete newArea.relativeX
+ delete newArea.relativeY
+
+ const newField = fieldUuidIndex[field.uuid] || {
+ ...field,
+ uuid: v4(),
+ submitter_uuid: this.selectedSubmitter.uuid,
+ areas: []
+ }
+
+ fieldUuidIndex[field.uuid] = newField
+
+ newField.areas.push(newArea)
+ newAreas.push(newArea)
+
+ if (['radio', 'multiple'].includes(field.type) && field.options?.length) {
+ const oldOptionUuid = area.option_uuid
+
+ if (!fieldOptionsMap[field.uuid]) {
+ fieldOptionsMap[field.uuid] = {}
+
+ newField.options = field.options.map((opt) => {
+ const newUuid = v4()
+
+ fieldOptionsMap[field.uuid][opt.uuid] = newUuid
+
+ return { ...opt, uuid: newUuid }
+ })
}
- this.insertField(newField)
+ newArea.option_uuid = fieldOptionsMap[field.uuid][oldOptionUuid] || newField.options[0].uuid
}
+ })
+
+ Object.values(fieldUuidIndex).forEach((field) => {
+ this.insertField(field)
+ })
- this.selectedAreaRef.value = area
+ this.selectedAreasRef.value = [...newAreas]
- this.save()
+ this.save()
+ },
+ hasClipboardData () {
+ try {
+ const clipboard = localStorage.getItem('docuseal_clipboard')
+
+ if (clipboard) {
+ const data = JSON.parse(clipboard)
+
+ return Date.now() - data.timestamp < 3600000
+ }
+
+ return false
+ } catch {
+ return false
}
},
pushUndo () {
@@ -1589,8 +1966,8 @@ export default {
const previousArea = this.drawField.areas?.[this.drawField.areas.length - 1]
if (this.selectedField?.type === this.drawField.type) {
- area.w = this.selectedAreaRef.value.w
- area.h = this.selectedAreaRef.value.h
+ area.w = this.lastSelectedArea.w
+ area.h = this.lastSelectedArea.h
} else if (previousArea) {
area.w = previousArea.w
area.h = previousArea.h
@@ -1621,7 +1998,7 @@ export default {
this.drawField = null
this.drawOption = null
- this.selectedAreaRef.value = area
+ this.selectedAreasRef.value = [area]
this.save()
} else {
@@ -1658,8 +2035,8 @@ export default {
if (this.drawFieldType && (area.w === 0 || area.h === 0)) {
if (this.selectedField?.type === this.drawFieldType) {
- area.w = this.selectedAreaRef.value.w
- area.h = this.selectedAreaRef.value.h
+ area.w = this.lastSelectedArea.w
+ area.h = this.lastSelectedArea.h
} else {
this.setDefaultAreaSize(area, this.drawFieldType)
}
@@ -1671,7 +2048,7 @@ export default {
if (area.w && (type !== 'checkbox' || this.drawFieldType || !isTooSmall)) {
this.addField(type, area)
- this.selectedAreaRef.value = area
+ this.selectedAreasRef.value = [area]
}
}
},
@@ -1751,7 +2128,11 @@ export default {
field.areas.push(fieldArea)
- this.selectedAreaRef.value = fieldArea
+ if (this.selectedAreasRef.value.length < 2) {
+ this.selectedAreasRef.value = [fieldArea]
+ } else {
+ this.selectedAreasRef.value.push(fieldArea)
+ }
if (this.template.fields.indexOf(field) === -1) {
this.insertField(field)
@@ -1764,7 +2145,7 @@ export default {
if (field.type === 'heading') {
this.$nextTick(() => {
const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid)
- const areaRef = documentRef.pageRefs[area.page].areaRefs.find((ref) => ref.area === this.selectedAreaRef.value)
+ const areaRef = documentRef.pageRefs[area.page].areaRefs.find((ref) => ref.area === fieldArea)
areaRef.isHeadingSelected = true
@@ -1780,7 +2161,7 @@ export default {
let baseArea
if (this.selectedField?.type === fieldType) {
- baseArea = this.selectedAreaRef.value
+ baseArea = this.lastSelectedArea
} else if (previousField?.areas?.length) {
baseArea = previousField.areas[previousField.areas.length - 1]
} else {
@@ -1957,6 +2338,7 @@ export default {
},
onDocumentReplace (data) {
const { replaceSchemaItem, schema, documents } = data
+ // eslint-disable-next-line camelcase
const { google_drive_file_id, ...cleanedReplaceSchemaItem } = replaceSchemaItem
this.template.schema.splice(this.template.schema.indexOf(replaceSchemaItem), 1, { ...cleanedReplaceSchemaItem, ...schema[0] })
@@ -2104,7 +2486,7 @@ export default {
documentRef.scrollToArea(area)
- this.selectedAreaRef.value = area
+ this.selectedAreasRef.value = [area]
},
baseFetch (path, options = {}) {
return fetch(this.baseUrl + path, {
@@ -2116,6 +2498,194 @@ export default {
}
})
},
+ detectFieldsForPage ({ page, attachmentUuid }) {
+ this.isDetectingPageFields = true
+ this.detectingAnalyzingProgress = null
+ this.detectingFieldsAddedCount = null
+
+ let totalFieldsAdded = 0
+ const hadFieldsBeforeDetection = this.template.fields.length > 0
+
+ const calculateIoU = (area1, area2) => {
+ const x1 = Math.max(area1.x, area2.x)
+ const y1 = Math.max(area1.y, area2.y)
+ const x2 = Math.min(area1.x + area1.w, area2.x + area2.w)
+ const y2 = Math.min(area1.y + area1.h, area2.y + area2.h)
+
+ const intersectionArea = Math.max(0, x2 - x1) * Math.max(0, y2 - y1)
+ const area1Size = area1.w * area1.h
+ const area2Size = area2.w * area2.h
+ const unionArea = area1Size + area2Size - intersectionArea
+
+ return unionArea > 0 ? intersectionArea / unionArea : 0
+ }
+
+ const hasOverlappingField = (newArea) => {
+ const pageAreas = this.fieldAreasIndex[newArea.attachment_uuid]?.[newArea.page] || []
+
+ return pageAreas.some(({ area: existingArea }) => {
+ return calculateIoU(existingArea, newArea) >= 0.1
+ })
+ }
+
+ const filterNonOverlappingFields = (detectedFields) => {
+ return detectedFields.filter((field) => {
+ return (field.areas || []).every((area) => !hasOverlappingField(area))
+ })
+ }
+
+ this.baseFetch(`/templates/${this.template.id}/detect_fields`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ attachment_uuid: attachmentUuid, page })
+ }).then(async (response) => {
+ const reader = response.body.getReader()
+ const decoder = new TextDecoder('utf-8')
+ let buffer = ''
+ const fields = []
+
+ while (true) {
+ const { value, done } = await reader.read()
+
+ buffer += decoder.decode(value, { stream: true })
+
+ const lines = buffer.split('\n\n')
+
+ buffer = lines.pop()
+
+ for (const line of lines) {
+ if (line.startsWith('data: ')) {
+ const jsonStr = line.replace(/^data: /, '')
+ const data = JSON.parse(jsonStr)
+
+ if (data.error) {
+ const errorFields = filterNonOverlappingFields(data.fields || fields)
+
+ if (errorFields.length) {
+ errorFields.forEach((f) => {
+ if (!f.submitter_uuid) {
+ f.submitter_uuid = this.template.submitters[0].uuid
+ }
+ this.insertField(f)
+ })
+
+ totalFieldsAdded += errorFields.length
+
+ this.save()
+ } else if (!(data.fields || fields).length) {
+ alert(data.error)
+ }
+
+ break
+ } else if (data.analyzing) {
+ this.detectingAnalyzingProgress = data.progress
+ } else if (data.completed) {
+ if (data.submitters) {
+ if (!hadFieldsBeforeDetection) {
+ this.template.submitters = data.submitters
+ this.selectedSubmitter = this.template.submitters[0]
+
+ const finalFields = data.fields || fields
+
+ finalFields.forEach((f) => {
+ if (!f.submitter_uuid) {
+ f.submitter_uuid = this.template.submitters[0].uuid
+ }
+ })
+
+ const nonOverlappingFields = filterNonOverlappingFields(finalFields)
+
+ nonOverlappingFields.forEach((f) => this.insertField(f))
+ totalFieldsAdded += nonOverlappingFields.length
+
+ if (nonOverlappingFields.length) {
+ this.save()
+ }
+ } else {
+ const existingSubmitters = this.template.submitters
+ const submitterUuidMap = {}
+
+ data.submitters.forEach((newSubmitter) => {
+ const existingMatch = existingSubmitters.find(
+ (s) => s.name.toLowerCase() === newSubmitter.name.toLowerCase()
+ )
+
+ if (existingMatch) {
+ submitterUuidMap[newSubmitter.uuid] = existingMatch.uuid
+ } else {
+ submitterUuidMap[newSubmitter.uuid] = newSubmitter.uuid
+
+ if (!existingSubmitters.find((s) => s.uuid === newSubmitter.uuid)) {
+ this.template.submitters.push(newSubmitter)
+ }
+ }
+ })
+
+ const finalFields = data.fields || fields
+
+ finalFields.forEach((f) => {
+ if (f.submitter_uuid && submitterUuidMap[f.submitter_uuid]) {
+ f.submitter_uuid = submitterUuidMap[f.submitter_uuid]
+ } else if (!f.submitter_uuid) {
+ f.submitter_uuid = this.template.submitters[0].uuid
+ }
+ })
+
+ const nonOverlappingFields = filterNonOverlappingFields(finalFields)
+
+ nonOverlappingFields.forEach((f) => this.insertField(f))
+ totalFieldsAdded += nonOverlappingFields.length
+
+ if (nonOverlappingFields.length) {
+ this.save()
+ }
+ }
+ } else {
+ const finalFields = data.fields || fields
+
+ finalFields.forEach((f) => {
+ if (!f.submitter_uuid) {
+ f.submitter_uuid = this.template.submitters[0].uuid
+ }
+ })
+
+ const nonOverlappingFields = filterNonOverlappingFields(finalFields)
+
+ nonOverlappingFields.forEach((f) => this.insertField(f))
+ totalFieldsAdded += nonOverlappingFields.length
+
+ if (nonOverlappingFields.length) {
+ this.save()
+ }
+ }
+
+ break
+ } else if (data.fields) {
+ data.fields.forEach((f) => {
+ if (!f.submitter_uuid) {
+ f.submitter_uuid = this.template.submitters[0].uuid
+ }
+ })
+
+ fields.push(...data.fields)
+ }
+ }
+ }
+
+ if (done) break
+ }
+ }).catch(error => {
+ console.error('Error in streaming message: ', error)
+ }).finally(() => {
+ this.isDetectingPageFields = false
+ this.detectingAnalyzingProgress = null
+ this.detectingFieldsAddedCount = totalFieldsAdded
+
+ setTimeout(() => {
+ this.detectingFieldsAddedCount = null
+ }, 1000)
+ })
+ },
save ({ force } = { force: false }) {
this.pendingFieldAttachmentUuids = []
diff --git a/app/javascript/template_builder/conditions_modal.vue b/app/javascript/template_builder/conditions_modal.vue
index ebb44c2e..81e54407 100644
--- a/app/javascript/template_builder/conditions_modal.vue
+++ b/app/javascript/template_builder/conditions_modal.vue
@@ -168,9 +168,19 @@ export default {
buildDefaultName: {
type: Function,
required: true
+ },
+ withClickSaveEvent: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
+ excludeFieldUuids: {
+ type: Array,
+ required: false,
+ default: () => []
}
},
- emits: ['close'],
+ emits: ['close', 'click-save'],
data () {
return {
conditions: this.item.conditions?.[0] ? JSON.parse(JSON.stringify(this.item.conditions)) : [{}]
@@ -183,14 +193,14 @@ export default {
fields () {
if (this.item.submitter_uuid) {
return this.template.fields.reduce((acc, f) => {
- if (f !== this.item && !this.excludeTypes.includes(f.type) && (!f.conditions?.length || !f.conditions.find((c) => c.field_uuid === this.item.uuid))) {
+ if (f !== this.item && !this.excludeTypes.includes(f.type) && !this.excludeFieldUuids.includes(f.uuid) && (!f.conditions?.length || !f.conditions.find((c) => c.field_uuid === this.item.uuid))) {
acc.push(f)
}
return acc
}, [])
} else {
- return this.template.fields
+ return this.template.fields.filter((f) => !this.excludeFieldUuids.includes(f.uuid))
}
}
},
@@ -234,7 +244,12 @@ export default {
delete this.item.conditions
}
- this.save()
+ if (this.withClickSaveEvent) {
+ this.$emit('click-save')
+ } else {
+ this.save()
+ }
+
this.$emit('close')
}
}
diff --git a/app/javascript/template_builder/context_menu.vue b/app/javascript/template_builder/context_menu.vue
new file mode 100644
index 00000000..4ad6972b
--- /dev/null
+++ b/app/javascript/template_builder/context_menu.vue
@@ -0,0 +1,546 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/template_builder/document.vue b/app/javascript/template_builder/document.vue
index e512fb2c..5d5135c5 100644
--- a/app/javascript/template_builder/document.vue
+++ b/app/javascript/template_builder/document.vue
@@ -22,8 +22,16 @@
:selected-submitter="selectedSubmitter"
:total-pages="sortedPreviewImages.length"
:image="image"
- @drop-field="$emit('drop-field', {...$event, attachment_uuid: document.uuid })"
+ :attachment-uuid="document.uuid"
+ :with-fields-detection="withFieldsDetection"
+ @drop-field="$emit('drop-field', { ...$event, attachment_uuid: document.uuid })"
@remove-area="$emit('remove-area', $event)"
+ @copy-field="$emit('copy-field', $event)"
+ @paste-field="$emit('paste-field', { ...$event, attachment_uuid: document.uuid })"
+ @copy-selected-areas="$emit('copy-selected-areas')"
+ @delete-selected-areas="$emit('delete-selected-areas')"
+ @align-selected-areas="$emit('align-selected-areas', $event)"
+ @autodetect-fields="$emit('autodetect-fields', $event)"
@scroll-to="scrollToArea"
@draw="$emit('draw', { area: {...$event.area, attachment_uuid: document.uuid }, isTooSmall: $event.isTooSmall })"
/>
@@ -116,9 +124,14 @@ export default {
type: Boolean,
required: false,
default: false
+ },
+ withFieldsDetection: {
+ type: Boolean,
+ required: false,
+ default: false
}
},
- emits: ['draw', 'drop-field', 'remove-area'],
+ emits: ['draw', 'drop-field', 'remove-area', 'paste-field', 'copy-field', 'copy-selected-areas', 'delete-selected-areas', 'align-selected-areas', 'autodetect-fields'],
data () {
return {
pageRefs: []
diff --git a/app/javascript/template_builder/field.vue b/app/javascript/template_builder/field.vue
index c01d4fe1..7af7cee5 100644
--- a/app/javascript/template_builder/field.vue
+++ b/app/javascript/template_builder/field.vue
@@ -330,7 +330,7 @@ export default {
IconMathFunction,
FieldType
},
- inject: ['template', 'save', 'backgroundColor', 'selectedAreaRef', 't', 'locale'],
+ inject: ['template', 'save', 'backgroundColor', 'selectedAreasRef', 't', 'locale'],
props: {
field: {
type: Object,
@@ -451,7 +451,7 @@ export default {
const area = this.field.areas.find((a) => a.option_uuid === option.uuid)
if (area) {
- this.selectedAreaRef.value = area
+ this.selectedAreasRef.value = [area]
}
},
scrollToFirstArea () {
diff --git a/app/javascript/template_builder/fields.vue b/app/javascript/template_builder/fields.vue
index db008c15..9f015106 100644
--- a/app/javascript/template_builder/fields.vue
+++ b/app/javascript/template_builder/fields.vue
@@ -283,7 +283,7 @@ export default {
IconDrag,
IconLock
},
- inject: ['save', 'backgroundColor', 'withPhone', 'withVerification', 'withKba', 'withPayment', 't', 'fieldsDragFieldRef', 'baseFetch'],
+ inject: ['save', 'backgroundColor', 'withPhone', 'withVerification', 'withKba', 'withPayment', 't', 'fieldsDragFieldRef', 'baseFetch', 'selectedAreasRef'],
props: {
fields: {
type: Array,
@@ -610,6 +610,10 @@ export default {
})
})
+ field.areas?.forEach((area) => {
+ this.selectedAreasRef.value.splice(this.selectedAreasRef.value.indexOf(area), 1)
+ })
+
if (save) {
this.save()
}
diff --git a/app/javascript/template_builder/font_modal.vue b/app/javascript/template_builder/font_modal.vue
index 026a7aff..9f43e5ff 100644
--- a/app/javascript/template_builder/font_modal.vue
+++ b/app/javascript/template_builder/font_modal.vue
@@ -212,12 +212,17 @@ export default {
required: false,
default: true
},
+ withClickSaveEvent: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
buildDefaultName: {
type: Function,
required: true
}
},
- emits: ['close'],
+ emits: ['close', 'click-save'],
data () {
return {
preferences: {}
@@ -322,7 +327,11 @@ export default {
Object.assign(this.field.preferences, this.preferences)
- this.save()
+ if (this.withClickSaveEvent) {
+ this.$emit('click-save')
+ } else {
+ this.save()
+ }
this.$emit('close')
}
diff --git a/app/javascript/template_builder/i18n.js b/app/javascript/template_builder/i18n.js
index 5cbc6a19..4fd1ab4b 100644
--- a/app/javascript/template_builder/i18n.js
+++ b/app/javascript/template_builder/i18n.js
@@ -185,7 +185,18 @@ const en = {
start_tour: 'Start Tour',
or_add_from: 'Or add from',
sync: 'Sync',
- syncing: 'Syncing...'
+ syncing: 'Syncing...',
+ copy: 'Copy',
+ paste: 'Paste',
+ select_fields: 'Select Fields',
+ draw_fields: 'Draw Fields',
+ align_left: 'Align Left',
+ align_right: 'Align Right',
+ align_top: 'Align Top',
+ align_bottom: 'Align Bottom',
+ fields_selected: '{count} Fields Selected',
+ field_added: '{count} Field Added',
+ fields_added: '{count} Fields Added'
}
const es = {
@@ -375,7 +386,18 @@ const es = {
start_tour: 'Iniciar guía',
or_add_from: 'O agregar desde',
sync: 'Sincronizar',
- syncing: 'Sincronizando...'
+ syncing: 'Sincronizando...',
+ copy: 'Copiar',
+ paste: 'Pegar',
+ select_fields: 'Seleccionar Campos',
+ draw_fields: 'Dibujar Campos',
+ align_left: 'Alinear a la izquierda',
+ align_right: 'Alinear a la derecha',
+ align_top: 'Alinear arriba',
+ align_bottom: 'Alinear abajo',
+ fields_selected: '{count} Campos Seleccionados',
+ field_added: '{count} Campo Añadido',
+ fields_added: '{count} Campos Añadidos'
}
const it = {
@@ -565,7 +587,18 @@ const it = {
start_tour: 'Inizia il tour',
or_add_from: 'O aggiungi da',
sync: 'Sincronizza',
- syncing: 'Sincronizzazione...'
+ syncing: 'Sincronizzazione...',
+ copy: 'Copia',
+ paste: 'Incolla',
+ select_fields: 'Seleziona Campi',
+ draw_fields: 'Disegna Campi',
+ align_left: 'Allinea a sinistra',
+ align_right: 'Allinea a destra',
+ align_top: 'Allinea in alto',
+ align_bottom: 'Allinea in basso',
+ fields_selected: '{count} Campi Selezionati',
+ field_added: '{count} Campo Aggiunto',
+ fields_added: '{count} Campi Aggiunti'
}
const pt = {
@@ -755,7 +788,18 @@ const pt = {
start_tour: 'Iniciar tour',
or_add_from: 'Ou adicionar de',
sync: 'Sincronizar',
- syncing: 'Sincronizando...'
+ syncing: 'Sincronizando...',
+ copy: 'Copiar',
+ paste: 'Colar',
+ select_fields: 'Selecionar Campos',
+ draw_fields: 'Desenhar Campos',
+ align_left: 'Alinhar à esquerda',
+ align_right: 'Alinhar à direita',
+ align_top: 'Alinhar ao topo',
+ align_bottom: 'Alinhar à parte inferior',
+ fields_selected: '{count} Campos Selecionados',
+ field_added: '{count} Campo Adicionado',
+ fields_added: '{count} Campos Adicionados'
}
const fr = {
@@ -945,7 +989,18 @@ const fr = {
start_tour: 'Démarrer',
or_add_from: 'Ou ajouter depuis',
sync: 'Synchroniser',
- syncing: 'Synchronisation...'
+ syncing: 'Synchronisation...',
+ copy: 'Copier',
+ paste: 'Coller',
+ select_fields: 'Sélectionner Champs',
+ draw_fields: 'Dessiner Champs',
+ align_left: 'Aligner à gauche',
+ align_right: 'Aligner à droite',
+ align_top: 'Aligner en haut',
+ align_bottom: 'Aligner en bas',
+ fields_selected: '{count} Champs Sélectionnés',
+ field_added: '{count} Champ Ajouté',
+ fields_added: '{count} Champs Ajoutés'
}
const de = {
@@ -1135,7 +1190,18 @@ const de = {
start_tour: 'Tour starten',
or_add_from: 'Oder hinzufügen aus',
sync: 'Synchronisieren',
- syncing: 'Synchronisiere...'
+ syncing: 'Synchronisiere...',
+ copy: 'Kopieren',
+ paste: 'Einfügen',
+ select_fields: 'Felder Auswählen',
+ draw_fields: 'Felder Zeichnen',
+ align_left: 'Links ausrichten',
+ align_right: 'Rechts ausrichten',
+ align_top: 'Oben ausrichten',
+ align_bottom: 'Unten ausrichten',
+ fields_selected: '{count} Felder Ausgewählt',
+ field_added: '{count} Feld Hinzugefügt',
+ fields_added: '{count} Felder Hinzugefügt'
}
const nl = {
@@ -1325,7 +1391,18 @@ const nl = {
start_tour: 'Rondleiding starten',
or_add_from: 'Of toevoegen van',
sync: 'Synchroniseren',
- syncing: 'Synchroniseren...'
+ syncing: 'Synchroniseren...',
+ copy: 'Kopiëren',
+ paste: 'Plakken',
+ select_fields: 'Velden Selecteren',
+ draw_fields: 'Velden Tekenen',
+ align_left: 'Links uitlijnen',
+ align_right: 'Rechts uitlijnen',
+ align_top: 'Boven uitlijnen',
+ align_bottom: 'Onder uitlijnen',
+ fields_selected: '{count} Velden Geselecteerd',
+ field_added: '{count} Veld Toegevoegd',
+ fields_added: '{count} Velden Toegevoegd'
}
export { en, es, it, pt, fr, de, nl }
diff --git a/app/javascript/template_builder/page.vue b/app/javascript/template_builder/page.vue
index 75ddf023..0c464c0f 100644
--- a/app/javascript/template_builder/page.vue
+++ b/app/javascript/template_builder/page.vue
@@ -1,7 +1,7 @@
@@ -17,7 +17,20 @@
import FieldArea from './area'
+import ContextMenu from './context_menu'
+import SelectionBox from './selection_box'
export default {
name: 'TemplatePage',
components: {
- FieldArea
+ FieldArea,
+ ContextMenu,
+ SelectionBox
},
- inject: ['fieldTypes', 'defaultDrawFieldType', 'fieldsDragFieldRef', 'assignDropAreaSize'],
+ inject: ['fieldTypes', 'defaultDrawFieldType', 'fieldsDragFieldRef', 'assignDropAreaSize', 'selectedAreasRef', 'template', 'isSelectModeRef'],
props: {
image: {
type: Object,
@@ -155,18 +204,69 @@ export default {
number: {
type: Number,
required: true
+ },
+ attachmentUuid: {
+ type: String,
+ required: false,
+ default: ''
+ },
+ withFieldsDetection: {
+ type: Boolean,
+ required: false,
+ default: false
}
},
- emits: ['draw', 'drop-field', 'remove-area', 'scroll-to'],
+ emits: ['draw', 'drop-field', 'remove-area', 'copy-field', 'paste-field', 'scroll-to', 'copy-selected-areas', 'delete-selected-areas', 'align-selected-areas', 'autodetect-fields'],
data () {
return {
areaRefs: [],
showMask: false,
resizeDirection: null,
- newArea: null
+ newArea: null,
+ contextMenu: null,
+ selectionRect: null,
+ selectionContextMenu: null
}
},
computed: {
+ isSelectMode () {
+ return this.isSelectModeRef.value && !this.drawFieldType && this.editable && !this.drawField
+ },
+ pageSelectedAreas () {
+ if (!this.selectedAreasRef.value) return []
+
+ return this.selectedAreasRef.value.filter((a) =>
+ a.attachment_uuid === this.attachmentUuid && a.page === this.number
+ )
+ },
+ showSelectionBox () {
+ return this.pageSelectedAreas.length >= 2 && this.editable
+ },
+ minSelectionBoxHeight () {
+ const ys = this.pageSelectedAreas.map((a) => a.y)
+
+ return Math.max(...ys) - Math.min(...ys)
+ },
+ minSelectionBoxWidth () {
+ const xs = this.pageSelectedAreas.map((a) => a.x)
+
+ return Math.max(...xs) - Math.min(...xs)
+ },
+ selectionBox () {
+ if (!this.pageSelectedAreas.length) return null
+
+ const minX = Math.min(...this.pageSelectedAreas.map((a) => a.x))
+ const minY = Math.min(...this.pageSelectedAreas.map((a) => a.y))
+ const maxX = Math.max(...this.pageSelectedAreas.map((a) => a.x + a.w))
+ const maxY = Math.max(...this.pageSelectedAreas.map((a) => a.y + a.h))
+
+ return {
+ x: minX,
+ y: minY,
+ w: Math.max(maxX - minX, this.minSelectionBoxWidth),
+ h: Math.max(maxY - minY, this.minSelectionBoxHeight)
+ }
+ },
defaultFieldsIndex () {
return this.defaultFields.reduce((acc, field) => {
acc[field.name] = field
@@ -201,6 +301,16 @@ export default {
},
height () {
return this.image.metadata.height
+ },
+ selectionRectStyle () {
+ if (!this.selectionRect) return {}
+
+ return {
+ left: this.selectionRect.x * 100 + '%',
+ top: this.selectionRect.y * 100 + '%',
+ width: this.selectionRect.w * 100 + '%',
+ height: this.selectionRect.h * 100 + '%'
+ }
}
},
beforeUpdate () {
@@ -211,6 +321,114 @@ export default {
this.image.metadata.width = e.target.naturalWidth
this.image.metadata.height = e.target.naturalHeight
},
+ openContextMenu (event) {
+ if (!this.editable) {
+ return
+ }
+
+ event.preventDefault()
+ event.stopPropagation()
+
+ const rect = this.$refs.image.getBoundingClientRect()
+
+ this.newArea = null
+ this.showMask = false
+
+ this.contextMenu = {
+ x: event.clientX,
+ y: event.clientY,
+ relativeX: (event.clientX - rect.left) / rect.width,
+ relativeY: (event.clientY - rect.top) / rect.height
+ }
+ },
+ openAreaContextMenu (event, area, field) {
+ if (!this.editable) {
+ return
+ }
+
+ event.preventDefault()
+ event.stopPropagation()
+
+ const rect = this.$refs.image.getBoundingClientRect()
+
+ this.newArea = null
+ this.showMask = false
+
+ this.contextMenu = {
+ x: event.clientX,
+ y: event.clientY,
+ relativeX: (event.clientX - rect.left) / rect.width,
+ relativeY: (event.clientY - rect.top) / rect.height,
+ area,
+ field
+ }
+ },
+ openSelectionContextMenu (event) {
+ const rect = this.$el.getBoundingClientRect()
+
+ this.selectionContextMenu = {
+ x: event.clientX,
+ y: event.clientY,
+ relativeX: (event.clientX - rect.left) / rect.width,
+ relativeY: (event.clientY - rect.top) / rect.height
+ }
+ },
+ closeSelectionContextMenu () {
+ this.selectionContextMenu = null
+ },
+ handleSelectionCopy () {
+ this.$emit('copy-selected-areas')
+ this.closeSelectionContextMenu()
+ },
+ handleSelectionDelete () {
+ this.$emit('delete-selected-areas')
+ this.closeSelectionContextMenu()
+ },
+ handleSelectionAlign (direction) {
+ this.$emit('align-selected-areas', direction)
+ this.closeSelectionContextMenu()
+ },
+ closeContextMenu () {
+ this.contextMenu = null
+ this.newArea = null
+ this.showMask = false
+ },
+ handleCopy () {
+ if (this.contextMenu.area) {
+ this.selectedAreasRef.value = [this.contextMenu.area]
+
+ this.$emit('copy-field')
+ }
+
+ this.closeContextMenu()
+ },
+ handleDelete () {
+ if (this.contextMenu.area) {
+ this.$emit('remove-area', this.contextMenu.area)
+ }
+
+ this.closeContextMenu()
+ },
+ handlePaste () {
+ this.newArea = null
+ this.showMask = false
+
+ this.$emit('paste-field', {
+ page: this.number,
+ x: this.contextMenu.relativeX,
+ y: this.contextMenu.relativeY
+ })
+
+ this.closeContextMenu()
+ },
+ handleAutodetectFields () {
+ this.$emit('autodetect-fields', {
+ page: this.number,
+ attachmentUuid: this.attachmentUuid
+ })
+
+ this.closeContextMenu()
+ },
setAreaRefs (el) {
if (el) {
this.areaRefs.push(el)
@@ -243,6 +461,20 @@ export default {
})
},
onStartDraw (e) {
+ if (e.button === 2) {
+ return
+ }
+
+ if (this.selectedAreasRef.value.length >= 2) {
+ this.selectedAreasRef.value = []
+ }
+
+ if (this.isSelectMode) {
+ this.startSelectionRect(e)
+
+ return
+ }
+
if (!this.allowDraw) {
return
}
@@ -268,7 +500,69 @@ export default {
}
})
},
+ startSelectionRect (e) {
+ this.selectedAreasRef.value = []
+
+ this.showMask = true
+
+ this.$nextTick(() => {
+ const x = e.offsetX / this.$refs.mask.clientWidth
+ const y = e.offsetY / this.$refs.mask.clientHeight
+
+ this.selectionRect = {
+ initialX: x,
+ initialY: y,
+ x,
+ y,
+ w: 0,
+ h: 0
+ }
+ })
+ },
+ onSelectionBoxMove (dx, dy) {
+ let clampedDx = dx
+ let clampedDy = dy
+
+ this.pageSelectedAreas.forEach((area) => {
+ const maxDxLeft = -area.x
+ const maxDxRight = 1 - area.w - area.x
+ const maxDyTop = -area.y
+ const maxDyBottom = 1 - area.h - area.y
+
+ if (dx < maxDxLeft) clampedDx = Math.max(clampedDx, maxDxLeft)
+ if (dx > maxDxRight) clampedDx = Math.min(clampedDx, maxDxRight)
+ if (dy < maxDyTop) clampedDy = Math.max(clampedDy, maxDyTop)
+ if (dy > maxDyBottom) clampedDy = Math.min(clampedDy, maxDyBottom)
+ })
+
+ this.pageSelectedAreas.forEach((area) => {
+ area.x += clampedDx
+ area.y += clampedDy
+ })
+ },
onPointermove (e) {
+ if (this.selectionRect) {
+ const dx = e.offsetX / this.$refs.mask.clientWidth - this.selectionRect.initialX
+ const dy = e.offsetY / this.$refs.mask.clientHeight - this.selectionRect.initialY
+
+ if (dx > 0) {
+ this.selectionRect.x = this.selectionRect.initialX
+ } else {
+ this.selectionRect.x = e.offsetX / this.$refs.mask.clientWidth
+ }
+
+ if (dy > 0) {
+ this.selectionRect.y = this.selectionRect.initialY
+ } else {
+ this.selectionRect.y = e.offsetY / this.$refs.mask.clientHeight
+ }
+
+ this.selectionRect.w = Math.abs(dx)
+ this.selectionRect.h = Math.abs(dy)
+
+ return
+ }
+
if (this.newArea) {
const dx = e.offsetX / this.$refs.mask.clientWidth - this.newArea.initialX
const dy = e.offsetY / this.$refs.mask.clientHeight - this.newArea.initialY
@@ -294,7 +588,20 @@ export default {
}
},
onPointerup (e) {
- if (this.newArea) {
+ if (this.selectionRect) {
+ const selRect = this.selectionRect
+ const areasToSelect = this.areas || []
+
+ areasToSelect.forEach((item) => {
+ const area = item.area
+
+ if (this.rectsOverlap(selRect, area)) {
+ this.selectedAreasRef.value.push(area)
+ }
+ })
+
+ this.selectionRect = null
+ } else if (this.newArea) {
const area = {
x: this.newArea.x,
y: this.newArea.y,
@@ -317,6 +624,14 @@ export default {
this.showMask = false
this.newArea = null
+ },
+ rectsOverlap (r1, r2) {
+ return !(
+ r1.x + r1.w < r2.x ||
+ r2.x + r2.w < r1.x ||
+ r1.y + r1.h < r2.y ||
+ r2.y + r2.h < r1.y
+ )
}
}
}
diff --git a/app/javascript/template_builder/selection_box.vue b/app/javascript/template_builder/selection_box.vue
new file mode 100644
index 00000000..932a28eb
--- /dev/null
+++ b/app/javascript/template_builder/selection_box.vue
@@ -0,0 +1,100 @@
+
+
+
+
+
diff --git a/app/models/submission.rb b/app/models/submission.rb
index 5630af76..d57ae5f8 100644
--- a/app/models/submission.rb
+++ b/app/models/submission.rb
@@ -78,8 +78,9 @@ class Submission < ApplicationRecord
scope :active, -> { where(archived_at: nil) }
scope :archived, -> { where.not(archived_at: nil) }
scope :pending, lambda {
- where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
- .and(Submitter.arel_table[:completed_at].eq(nil))).select(1).arel.exists)
+ where(expire_at: nil).or(where(expire_at: Time.current..))
+ .where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
+ .and(Submitter.arel_table[:completed_at].eq(nil))).select(1).arel.exists)
}
scope :completed, lambda {
where.not(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
diff --git a/app/views/shared/_settings_nav.html.erb b/app/views/shared/_settings_nav.html.erb
index 8e26a8fb..ca074009 100644
--- a/app/views/shared/_settings_nav.html.erb
+++ b/app/views/shared/_settings_nav.html.erb
@@ -135,7 +135,7 @@
<%= Docuseal::SUPPORT_EMAIL %>
- <% if Docuseal.version.present? %>
+ <% if Docuseal.version.present? && !Docuseal.multitenant? && can?(:manage, EncryptedConfig) %>
v<%= Docuseal.version %>
diff --git a/app/views/submissions/_detailed_form.html.erb b/app/views/submissions/_detailed_form.html.erb
index 44e87732..ac4c2607 100644
--- a/app/views/submissions/_detailed_form.html.erb
+++ b/app/views/submissions/_detailed_form.html.erb
@@ -30,14 +30,14 @@
">
- <%= tag.input type: 'email', multiple: true, name: 'submission[1][submitters][][email]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: (local_assigns[:require_email_2fa] == true || local_assigns[:prefillable_fields].present? ? t(:email) : "#{t('email')} (#{t('optional')})"), value: item['email'].presence || ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.email : ''), id: "detailed_email_#{item['uuid']}", required: local_assigns[:require_email_2fa] == true %>
+ <%= tag.input type: 'email', multiple: true, name: 'submission[1][submitters][][email]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: (local_assigns[:require_email_2fa] == true || local_assigns[:prefillable_fields].present? ? t(:email) : "#{t('email')} (#{t('optional')})"), value: item['email'].presence || ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.email : ''), id: "detailed_email_#{item['uuid']}", required: local_assigns[:require_email_2fa] == true && index.zero? %>
<% has_phone_field = true %>
">
- <%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: local_assigns[:require_phone_2fa] == true || local_assigns[:prefillable_fields].present? ? t(:phone) : "#{t('phone')} (#{t('optional')})", id: "detailed_phone_#{item['uuid']}", required: local_assigns[:require_phone_2fa] == true %>
+ <%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: local_assigns[:require_phone_2fa] == true || local_assigns[:prefillable_fields].present? ? t(:phone) : "#{t('phone')} (#{t('optional')})", id: "detailed_phone_#{item['uuid']}", required: local_assigns[:require_phone_2fa] == true && index.zero? %>
@@ -99,6 +99,7 @@
<% if has_phone_field %>
<%= render 'send_sms', f: %>
<% end %>
+ <%= render 'extra_fields', f: %>