refactor and fixes
This commit is contained in:
@@ -17,7 +17,7 @@ class TemplatesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def edit
|
def edit
|
||||||
@template = current_account.templates.preload(documents_attachments: { preview_images_attachments: :blob })
|
@template = current_account.templates.preload(schema_documents: { preview_images_attachments: :blob })
|
||||||
.find(params[:id])
|
.find(params[:id])
|
||||||
|
|
||||||
render :edit, layout: 'plain'
|
render :edit, layout: 'plain'
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestB
|
|||||||
window.customElements.define('template-builder', class extends HTMLElement {
|
window.customElements.define('template-builder', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
this.appElem = document.createElement('div')
|
this.appElem = document.createElement('div')
|
||||||
|
this.appElem.classList.add('max-h-screen')
|
||||||
|
|
||||||
this.app = createApp(TemplateBuilder, {
|
this.app = createApp(TemplateBuilder, {
|
||||||
template: reactive(JSON.parse(this.dataset.template)),
|
template: reactive(JSON.parse(this.dataset.template)),
|
||||||
|
|||||||
@@ -226,14 +226,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
startResizeCell (e) {
|
startResizeCell (e) {
|
||||||
document.addEventListener('mousemove', this.onResizeCell)
|
this.$el.getRootNode().addEventListener('mousemove', this.onResizeCell)
|
||||||
document.addEventListener('mouseup', this.stopResizeCell)
|
this.$el.getRootNode().addEventListener('mouseup', this.stopResizeCell)
|
||||||
|
|
||||||
this.$emit('start-resize', 'ew')
|
this.$emit('start-resize', 'ew')
|
||||||
},
|
},
|
||||||
stopResizeCell (e) {
|
stopResizeCell (e) {
|
||||||
document.removeEventListener('mousemove', this.onResizeCell)
|
this.$el.getRootNode().removeEventListener('mousemove', this.onResizeCell)
|
||||||
document.removeEventListener('mouseup', this.stopResizeCell)
|
this.$el.getRootNode().removeEventListener('mouseup', this.stopResizeCell)
|
||||||
|
|
||||||
this.$emit('stop-resize')
|
this.$emit('stop-resize')
|
||||||
|
|
||||||
@@ -266,11 +266,13 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
onNameBlur (e) {
|
onNameBlur (e) {
|
||||||
|
const text = this.$refs.name.innerText.trim()
|
||||||
|
|
||||||
this.isNameFocus = false
|
this.isNameFocus = false
|
||||||
this.$refs.name.style.minWidth = ''
|
this.$refs.name.style.minWidth = ''
|
||||||
|
|
||||||
if (e.target.innerText.trim()) {
|
if (text) {
|
||||||
this.field.name = e.target.innerText.trim()
|
this.field.name = text
|
||||||
} else {
|
} else {
|
||||||
this.field.name = ''
|
this.field.name = ''
|
||||||
this.$refs.name.innerText = this.defaultName
|
this.$refs.name.innerText = this.defaultName
|
||||||
@@ -302,14 +304,14 @@ export default {
|
|||||||
|
|
||||||
this.dragFrom = { x: e.clientX - rect.left, y: e.clientY - rect.top }
|
this.dragFrom = { x: e.clientX - rect.left, y: e.clientY - rect.top }
|
||||||
|
|
||||||
document.addEventListener('mousemove', this.drag)
|
this.$el.getRootNode().addEventListener('mousemove', this.drag)
|
||||||
document.addEventListener('mouseup', this.stopDrag)
|
this.$el.getRootNode().addEventListener('mouseup', this.stopDrag)
|
||||||
|
|
||||||
this.$emit('start-drag')
|
this.$emit('start-drag')
|
||||||
},
|
},
|
||||||
stopDrag () {
|
stopDrag () {
|
||||||
document.removeEventListener('mousemove', this.drag)
|
this.$el.getRootNode().removeEventListener('mousemove', this.drag)
|
||||||
document.removeEventListener('mouseup', this.stopDrag)
|
this.$el.getRootNode().removeEventListener('mouseup', this.stopDrag)
|
||||||
|
|
||||||
if (this.isDragged) {
|
if (this.isDragged) {
|
||||||
this.save()
|
this.save()
|
||||||
@@ -322,14 +324,14 @@ export default {
|
|||||||
startResize () {
|
startResize () {
|
||||||
this.selectedAreaRef.value = this.area
|
this.selectedAreaRef.value = this.area
|
||||||
|
|
||||||
document.addEventListener('mousemove', this.resize)
|
this.$el.getRootNode().addEventListener('mousemove', this.resize)
|
||||||
document.addEventListener('mouseup', this.stopResize)
|
this.$el.getRootNode().addEventListener('mouseup', this.stopResize)
|
||||||
|
|
||||||
this.$emit('start-resize', 'nwse')
|
this.$emit('start-resize', 'nwse')
|
||||||
},
|
},
|
||||||
stopResize () {
|
stopResize () {
|
||||||
document.removeEventListener('mousemove', this.resize)
|
this.$el.getRootNode().removeEventListener('mousemove', this.resize)
|
||||||
document.removeEventListener('mouseup', this.stopResize)
|
this.$el.getRootNode().removeEventListener('mouseup', this.stopResize)
|
||||||
|
|
||||||
this.$emit('stop-resize')
|
this.$emit('stop-resize')
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
style="max-width: 1600px"
|
style="max-width: 1600px"
|
||||||
class="mx-auto pl-4"
|
class="mx-auto pl-4 h-full"
|
||||||
>
|
>
|
||||||
<div class="flex justify-between py-1.5 items-center pr-4">
|
<div class="flex justify-between py-1.5 items-center pr-4">
|
||||||
<div class="flex space-x-3">
|
<div class="flex space-x-3">
|
||||||
<a href="/">
|
<a
|
||||||
|
v-if="withLogoLink"
|
||||||
|
href="/"
|
||||||
|
>
|
||||||
<Logo />
|
<Logo />
|
||||||
</a>
|
</a>
|
||||||
|
<Logo v-else />
|
||||||
<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"
|
||||||
@@ -16,46 +20,53 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-x-3 flex items-center">
|
<div class="space-x-3 flex items-center">
|
||||||
<a
|
<slot
|
||||||
:href="`/templates/${template.id}/submissions/new`"
|
v-if="$slots.buttons"
|
||||||
data-turbo-frame="modal"
|
name="buttons"
|
||||||
class="btn btn-primary"
|
/>
|
||||||
>
|
<template v-else>
|
||||||
<IconUsersPlus
|
<a
|
||||||
width="20"
|
:href="`/templates/${template.id}/submissions/new`"
|
||||||
class="inline"
|
data-turbo-frame="modal"
|
||||||
/>
|
class="btn btn-primary"
|
||||||
<span class="hidden md:inline">
|
>
|
||||||
Recipients
|
<IconUsersPlus
|
||||||
</span>
|
width="20"
|
||||||
</a>
|
class="inline"
|
||||||
<button
|
/>
|
||||||
class="base-button"
|
<span class="hidden md:inline">
|
||||||
:class="{ disabled: isSaving }"
|
Recipients
|
||||||
v-bind="isSaving ? { disabled: true } : {}"
|
</span>
|
||||||
@click.prevent="onSaveClick"
|
</a>
|
||||||
>
|
<button
|
||||||
<IconInnerShadowTop
|
class="base-button"
|
||||||
v-if="isSaving"
|
:class="{ disabled: isSaving }"
|
||||||
width="20"
|
v-bind="isSaving ? { disabled: true } : {}"
|
||||||
class="animate-spin"
|
@click.prevent="onSaveClick"
|
||||||
/>
|
>
|
||||||
<IconDeviceFloppy
|
<IconInnerShadowTop
|
||||||
v-else
|
v-if="isSaving"
|
||||||
width="20"
|
width="20"
|
||||||
/>
|
class="animate-spin"
|
||||||
<span class="hidden md:inline">
|
/>
|
||||||
Save
|
<IconDeviceFloppy
|
||||||
</span>
|
v-else
|
||||||
</button>
|
width="20"
|
||||||
|
/>
|
||||||
|
<span class="hidden md:inline">
|
||||||
|
Save
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex"
|
class="flex"
|
||||||
style="max-height: calc(100vh - 60px)"
|
style="max-height: calc(100% - 60px)"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref="previews"
|
ref="previews"
|
||||||
|
:style="{ 'display': isBreakpointLg ? 'none' : 'initial' }"
|
||||||
class="overflow-y-auto overflow-x-hidden w-52 flex-none pr-3 mt-0.5 pt-0.5 hidden lg:block"
|
class="overflow-y-auto overflow-x-hidden w-52 flex-none pr-3 mt-0.5 pt-0.5 hidden lg:block"
|
||||||
>
|
>
|
||||||
<DocumentPreview
|
<DocumentPreview
|
||||||
@@ -94,19 +105,46 @@
|
|||||||
@success="updateFromUpload"
|
@success="updateFromUpload"
|
||||||
/>
|
/>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<Document
|
<template
|
||||||
v-for="document in sortedDocuments"
|
v-for="document in sortedDocuments"
|
||||||
:key="document.uuid"
|
:key="document.uuid"
|
||||||
:ref="setDocumentRefs"
|
>
|
||||||
:areas-index="fieldAreasIndex[document.uuid]"
|
<Document
|
||||||
:selected-submitter="selectedSubmitter"
|
:ref="setDocumentRefs"
|
||||||
:document="document"
|
:areas-index="fieldAreasIndex[document.uuid]"
|
||||||
:is-drag="!!dragFieldType"
|
:selected-submitter="selectedSubmitter"
|
||||||
:draw-field="drawField"
|
:document="document"
|
||||||
@draw="onDraw"
|
:is-drag="!!dragFieldType"
|
||||||
@drop-field="onDropfield"
|
:draw-field="drawField"
|
||||||
@remove-area="removeArea"
|
@draw="onDraw"
|
||||||
/>
|
@drop-field="onDropfield"
|
||||||
|
@remove-area="removeArea"
|
||||||
|
/>
|
||||||
|
<DocumentControls
|
||||||
|
v-if="isBreakpointLg"
|
||||||
|
:with-arrows="template.schema.length > 1"
|
||||||
|
:item="template.schema.find((item) => item.attachment_uuid === document.uuid)"
|
||||||
|
:document="document"
|
||||||
|
:template="template"
|
||||||
|
:is-direct-upload="isDirectUpload"
|
||||||
|
class="pb-2 mb-2 border-b border-base-300 border-dashed"
|
||||||
|
@remove="onDocumentRemove"
|
||||||
|
@replace="onDocumentReplace"
|
||||||
|
@up="moveDocument(template.schema.find((item) => item.attachment_uuid === document.uuid), -1)"
|
||||||
|
@down="moveDocument(template.schema.find((item) => item.attachment_uuid === document.uuid), 1)"
|
||||||
|
@change="save"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<div
|
||||||
|
v-if="sortedDocuments.length && isBreakpointLg"
|
||||||
|
class="pb-4"
|
||||||
|
>
|
||||||
|
<Upload
|
||||||
|
:template-id="template.id"
|
||||||
|
:is-direct-upload="isDirectUpload"
|
||||||
|
@success="updateFromUpload"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -122,7 +160,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="relative w-80 flex-none pt-0.5 pr-4 pl-0.5 hidden md:block"
|
class="relative w-80 flex-none mt-1 pr-4 pl-0.5 hidden md:block"
|
||||||
:class="drawField ? 'overflow-hidden' : 'overflow-auto'"
|
:class="drawField ? 'overflow-hidden' : 'overflow-auto'"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -149,6 +187,7 @@
|
|||||||
:fields="template.fields"
|
:fields="template.fields"
|
||||||
:submitters="template.submitters"
|
:submitters="template.submitters"
|
||||||
:selected-submitter="selectedSubmitter"
|
:selected-submitter="selectedSubmitter"
|
||||||
|
:with-sticky-submitters="withStickySubmitters"
|
||||||
@set-draw="drawField = $event"
|
@set-draw="drawField = $event"
|
||||||
@set-drag="dragFieldType = $event"
|
@set-drag="dragFieldType = $event"
|
||||||
@change-submitter="selectedSubmitter = $event"
|
@change-submitter="selectedSubmitter = $event"
|
||||||
@@ -169,6 +208,7 @@ import Document from './document'
|
|||||||
import Logo from './logo'
|
import Logo from './logo'
|
||||||
import Contenteditable from './contenteditable'
|
import Contenteditable from './contenteditable'
|
||||||
import DocumentPreview from './preview'
|
import DocumentPreview from './preview'
|
||||||
|
import DocumentControls from './controls'
|
||||||
import { IconUsersPlus, IconDeviceFloppy, IconInnerShadowTop } from '@tabler/icons-vue'
|
import { IconUsersPlus, IconDeviceFloppy, IconInnerShadowTop } from '@tabler/icons-vue'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
@@ -182,6 +222,7 @@ export default {
|
|||||||
Logo,
|
Logo,
|
||||||
Dropzone,
|
Dropzone,
|
||||||
DocumentPreview,
|
DocumentPreview,
|
||||||
|
DocumentControls,
|
||||||
IconInnerShadowTop,
|
IconInnerShadowTop,
|
||||||
Contenteditable,
|
Contenteditable,
|
||||||
IconUsersPlus,
|
IconUsersPlus,
|
||||||
@@ -191,6 +232,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
template: this.template,
|
template: this.template,
|
||||||
save: this.save,
|
save: this.save,
|
||||||
|
baseFetch: this.baseFetch,
|
||||||
selectedAreaRef: computed(() => this.selectedAreaRef)
|
selectedAreaRef: computed(() => this.selectedAreaRef)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -201,13 +243,34 @@ export default {
|
|||||||
},
|
},
|
||||||
isDirectUpload: {
|
isDirectUpload: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
baseUrl: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
withLogoLink: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
withStickySubmitters: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
fetchOptions: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({ headers: {} })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
documentRefs: [],
|
documentRefs: [],
|
||||||
|
isBreakpointLg: false,
|
||||||
isSaving: false,
|
isSaving: false,
|
||||||
selectedSubmitter: null,
|
selectedSubmitter: null,
|
||||||
drawField: null,
|
drawField: null,
|
||||||
@@ -244,15 +307,28 @@ export default {
|
|||||||
this.selectedSubmitter = this.template.submitters[0]
|
this.selectedSubmitter = this.template.submitters[0]
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.onWindowResize()
|
||||||
|
})
|
||||||
|
|
||||||
document.addEventListener('keyup', this.onKeyUp)
|
document.addEventListener('keyup', this.onKeyUp)
|
||||||
|
|
||||||
|
window.addEventListener('resize', this.onWindowResize)
|
||||||
},
|
},
|
||||||
unmounted () {
|
unmounted () {
|
||||||
document.removeEventListener('keyup', this.onKeyUp)
|
document.removeEventListener('keyup', this.onKeyUp)
|
||||||
|
|
||||||
|
window.removeEventListener('resize', this.onWindowResize)
|
||||||
},
|
},
|
||||||
beforeUpdate () {
|
beforeUpdate () {
|
||||||
this.documentRefs = []
|
this.documentRefs = []
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
onWindowResize (e) {
|
||||||
|
const breakpointLg = 1024
|
||||||
|
|
||||||
|
this.isBreakpointLg = this.$el.getRootNode().children[0].offsetWidth < breakpointLg
|
||||||
|
},
|
||||||
setDocumentRefs (el) {
|
setDocumentRefs (el) {
|
||||||
if (el) {
|
if (el) {
|
||||||
this.documentRefs.push(el)
|
this.documentRefs.push(el)
|
||||||
@@ -474,10 +550,18 @@ export default {
|
|||||||
|
|
||||||
this.selectedAreaRef.value = area
|
this.selectedAreaRef.value = area
|
||||||
},
|
},
|
||||||
|
baseFetch (path, options = {}) {
|
||||||
|
return fetch(this.baseUrl + path, {
|
||||||
|
...options,
|
||||||
|
headers: { ...this.fetchOptions.headers, ...options.headers }
|
||||||
|
})
|
||||||
|
},
|
||||||
save () {
|
save () {
|
||||||
this.$el.closest('template-builder').dataset.template = JSON.stringify(this.template)
|
if (this.$el.closest('template-builder')) {
|
||||||
|
this.$el.closest('template-builder').dataset.template = JSON.stringify(this.template)
|
||||||
|
}
|
||||||
|
|
||||||
return fetch(`/api/templates/${this.template.id}`, {
|
return this.baseFetch(`/api/templates/${this.template.id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
template: {
|
template: {
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<Contenteditable
|
||||||
|
class="w-full block mr-6"
|
||||||
|
:model-value="item.name"
|
||||||
|
:icon-width="16"
|
||||||
|
@update:model-value="onUpdateName"
|
||||||
|
/>
|
||||||
|
<ReplaceButton
|
||||||
|
:is-direct-upload="isDirectUpload"
|
||||||
|
:template-id="template.id"
|
||||||
|
@click.stop
|
||||||
|
@success="$emit('replace', { replaceSchemaItem: item, ...$event })"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-if="withArrows"
|
||||||
|
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
|
||||||
|
style="width: 24px; height: 24px"
|
||||||
|
@click.stop="$emit('up', item)"
|
||||||
|
>
|
||||||
|
↑
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="withArrows"
|
||||||
|
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
|
||||||
|
style="width: 24px; height: 24px"
|
||||||
|
@click.stop="$emit('down', item)"
|
||||||
|
>
|
||||||
|
↓
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
|
||||||
|
style="width: 24px; height: 24px"
|
||||||
|
@click.stop="$emit('remove', item)"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Contenteditable from './contenteditable'
|
||||||
|
import Upload from './upload'
|
||||||
|
import ReplaceButton from './replace'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DocumentControls',
|
||||||
|
components: {
|
||||||
|
Contenteditable,
|
||||||
|
ReplaceButton
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
item: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
template: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
document: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
isDirectUpload: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
withArrows: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['change', 'remove', 'up', 'down', 'replace'],
|
||||||
|
mounted () {
|
||||||
|
if (this.isDirectUpload) {
|
||||||
|
import('@rails/activestorage')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
upload: Upload.methods.upload,
|
||||||
|
onUpdateName (value) {
|
||||||
|
this.item.name = value
|
||||||
|
|
||||||
|
this.$emit('change')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -61,6 +61,7 @@ export default {
|
|||||||
IconCloudUpload,
|
IconCloudUpload,
|
||||||
IconInnerShadowTop
|
IconInnerShadowTop
|
||||||
},
|
},
|
||||||
|
inject: ['baseFetch'],
|
||||||
props: {
|
props: {
|
||||||
templateId: {
|
templateId: {
|
||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
<label
|
<label
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
title="Areas"
|
title="Areas"
|
||||||
class="cursor-pointer text-base-100 group-hover:text-base-content"
|
class="cursor-pointer text-transparent group-hover:text-base-content"
|
||||||
>
|
>
|
||||||
<IconShape
|
<IconShape
|
||||||
:width="18"
|
:width="18"
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
<button
|
<button
|
||||||
v-else
|
v-else
|
||||||
title="Areas"
|
title="Areas"
|
||||||
class="relative cursor-pointer text-base-100 group-hover:text-base-content"
|
class="relative cursor-pointer text-transparent group-hover:text-base-content"
|
||||||
@click="$emit('set-draw', field)"
|
@click="$emit('set-draw', field)"
|
||||||
>
|
>
|
||||||
<IconShape
|
<IconShape
|
||||||
@@ -113,7 +113,7 @@
|
|||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="relative text-base-100 group-hover:text-base-content"
|
class="relative text-transparent group-hover:text-base-content"
|
||||||
title="Remove"
|
title="Remove"
|
||||||
@click="$emit('remove', field)"
|
@click="$emit('remove', field)"
|
||||||
>
|
>
|
||||||
@@ -122,7 +122,7 @@
|
|||||||
:stroke-width="1.6"
|
:stroke-width="1.6"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<div class="flex flex-col pr-1 text-base-100 group-hover:text-base-content">
|
<div class="flex flex-col pr-1 text-transparent group-hover:text-base-content">
|
||||||
<button
|
<button
|
||||||
title="Up"
|
title="Up"
|
||||||
class="relative"
|
class="relative"
|
||||||
@@ -156,7 +156,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
v-model="field.options[index]"
|
v-model="field.options[index]"
|
||||||
class="w-full input input-primary input-xs text-sm"
|
class="w-full input input-primary input-xs text-sm bg-transparent"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
@blur="save"
|
@blur="save"
|
||||||
@@ -254,8 +254,10 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
onNameBlur (e) {
|
onNameBlur (e) {
|
||||||
if (e.target.innerText.trim()) {
|
const text = this.$refs.name.$refs.contenteditable.innerText.trim()
|
||||||
this.field.name = e.target.innerText.trim()
|
|
||||||
|
if (text) {
|
||||||
|
this.field.name = text
|
||||||
} else {
|
} else {
|
||||||
this.field.name = ''
|
this.field.name = ''
|
||||||
this.$refs.name.$refs.contenteditable.innerText = this.defaultName
|
this.$refs.name.$refs.contenteditable.innerText = this.defaultName
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="sticky -top-1 bg-base-100 pt-1 -mt-1 z-10">
|
<div :class="withStickySubmitters ? 'sticky top-0 z-10' : ''">
|
||||||
<FieldSubmitter
|
<FieldSubmitter
|
||||||
:model-value="selectedSubmitter.uuid"
|
:model-value="selectedSubmitter.uuid"
|
||||||
class="w-full bg-base-100"
|
class="w-full rounded-lg"
|
||||||
|
:class="{ 'bg-base-100': withStickySubmitters }"
|
||||||
:submitters="submitters"
|
:submitters="submitters"
|
||||||
@new-submitter="save"
|
@new-submitter="save"
|
||||||
@remove="removeSubmitter"
|
@remove="removeSubmitter"
|
||||||
@@ -28,7 +29,7 @@
|
|||||||
v-for="(icon, type) in fieldIcons"
|
v-for="(icon, type) in fieldIcons"
|
||||||
:key="type"
|
:key="type"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
class="flex items-center justify-center border border-dashed border-base-300 bg-base-100 w-full rounded relative"
|
class="flex items-center justify-center border border-dashed border-base-300 w-full rounded relative"
|
||||||
@dragstart="onDragstart(type)"
|
@dragstart="onDragstart(type)"
|
||||||
@dragend="$emit('drag-end')"
|
@dragend="$emit('drag-end')"
|
||||||
@click="addField(type)"
|
@click="addField(type)"
|
||||||
@@ -106,6 +107,11 @@ export default {
|
|||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
withStickySubmitters: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
submitters: {
|
submitters: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
ref="image"
|
ref="image"
|
||||||
:src="image.url"
|
:src="image.url"
|
||||||
:width="width"
|
:width="width"
|
||||||
class="shadow-md mb-4"
|
class="border rounded mb-4"
|
||||||
:height="height"
|
:height="height"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import Upload from './upload'
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ReplaceDocument',
|
name: 'ReplaceDocument',
|
||||||
|
inject: ['baseFetch'],
|
||||||
props: {
|
props: {
|
||||||
templateId: {
|
templateId: {
|
||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export default {
|
|||||||
IconUpload,
|
IconUpload,
|
||||||
IconInnerShadowTop
|
IconInnerShadowTop
|
||||||
},
|
},
|
||||||
|
inject: ['baseFetch'],
|
||||||
props: {
|
props: {
|
||||||
templateId: {
|
templateId: {
|
||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
@@ -113,7 +114,7 @@ export default {
|
|||||||
|
|
||||||
this.isProcessing = true
|
this.isProcessing = true
|
||||||
|
|
||||||
fetch(`/api/templates/${this.templateId}/documents`, {
|
this.baseFetch(`/api/templates/${this.templateId}/documents`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ blobs }),
|
body: JSON.stringify({ blobs }),
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
@@ -124,7 +125,7 @@ export default {
|
|||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
fetch(`/api/templates/${this.templateId}/documents`, {
|
this.baseFetch(`/api/templates/${this.templateId}/documents`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: new FormData(this.$refs.form)
|
body: new FormData(this.$refs.form)
|
||||||
}).then(resp => resp.json()).then((data) => {
|
}).then(resp => resp.json()).then((data) => {
|
||||||
|
|||||||
+10
-5
@@ -11,8 +11,8 @@
|
|||||||
# email :string not null
|
# email :string not null
|
||||||
# encrypted_password :string not null
|
# encrypted_password :string not null
|
||||||
# failed_attempts :integer default(0), not null
|
# failed_attempts :integer default(0), not null
|
||||||
# first_name :string not null
|
# first_name :string
|
||||||
# last_name :string not null
|
# last_name :string
|
||||||
# last_sign_in_at :datetime
|
# last_sign_in_at :datetime
|
||||||
# last_sign_in_ip :string
|
# last_sign_in_ip :string
|
||||||
# locked_at :datetime
|
# locked_at :datetime
|
||||||
@@ -46,6 +46,7 @@ class User < ApplicationRecord
|
|||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
has_one :access_token, dependent: :destroy
|
has_one :access_token, dependent: :destroy
|
||||||
|
has_many :templates, dependent: :destroy, foreign_key: :author_id, inverse_of: :author
|
||||||
|
|
||||||
devise :database_authenticatable, :recoverable, :rememberable, :validatable, :trackable
|
devise :database_authenticatable, :recoverable, :rememberable, :validatable, :trackable
|
||||||
devise :registerable, :omniauthable, omniauth_providers: [:google_oauth2] if Docuseal.multitenant?
|
devise :registerable, :omniauthable, omniauth_providers: [:google_oauth2] if Docuseal.multitenant?
|
||||||
@@ -68,14 +69,18 @@ class User < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def initials
|
def initials
|
||||||
[first_name.first, last_name.first].join.upcase
|
[first_name&.first, last_name&.first].compact_blank.join.upcase
|
||||||
end
|
end
|
||||||
|
|
||||||
def full_name
|
def full_name
|
||||||
[first_name, last_name].join(' ')
|
[first_name, last_name].compact_blank.join(' ')
|
||||||
end
|
end
|
||||||
|
|
||||||
def friendly_name
|
def friendly_name
|
||||||
"#{full_name} <#{email}>"
|
if full_name.present?
|
||||||
|
"#{full_name} <#{email}>"
|
||||||
|
else
|
||||||
|
email
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<div class="pb-6 pt-1 space-y-1">
|
<div class="pb-6 pt-1 space-y-1">
|
||||||
<p class="flex items-center space-x-1 text-xs text-base-content/60">
|
<p class="flex items-center space-x-1 text-xs text-base-content/60">
|
||||||
<%= svg_icon('user', class: 'w-4 h-4') %>
|
<%= svg_icon('user', class: 'w-4 h-4') %>
|
||||||
<span><%= template.author.full_name %></span>
|
<span><%= template.author.full_name.presence || template.author.email %></span>
|
||||||
</p>
|
</p>
|
||||||
<p class="flex items-center space-x-1 text-xs text-base-content/60">
|
<p class="flex items-center space-x-1 text-xs text-base-content/60">
|
||||||
<%= svg_icon('calendar', class: 'w-4 h-4') %>
|
<%= svg_icon('calendar', class: 'w-4 h-4') %>
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
<template-builder data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-template="<%= @template.as_json.merge(documents: @template.schema_documents.as_json(include: { preview_images: { methods: %i[url metadata filename] } })).to_json %>"></template-builder>
|
<template-builder class="grid" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-template="<%= @template.as_json.merge(documents: @template.schema_documents.as_json(include: { preview_images: { methods: %i[url metadata filename] } })).to_json %>"></template-builder>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
<% @submissions.each do |submission| %>
|
<% @submissions.each do |submission| %>
|
||||||
<a href="<%= submission_path(submission) %>" class="bg-base-200 w-full flex flex-col md:flex-row space-y-4 md:space-y-0 md:justify-between rounded-2xl px-6 py-5 md:items-center">
|
<a href="<%= submission_path(submission) %>" class="bg-base-200 w-full flex flex-col md:flex-row space-y-4 md:space-y-0 md:justify-between rounded-2xl px-6 py-5 md:items-center">
|
||||||
<% submitters = (submission.template_submitters || submission.template.submitters).filter_map { |item| submission.submitters.find { |e| e.uuid == item['uuid'] } } %>
|
<% submitters = (submission.template_submitters || submission.template.submitters).filter_map { |item| submission.submitters.find { |e| e.uuid == item['uuid'] } } %>
|
||||||
<% is_submission_complated = submitters.all?(&:completed_at?) %>
|
<% is_submission_completed = submitters.all?(&:completed_at?) %>
|
||||||
<% if submitters.size == 1 %>
|
<% if submitters.size == 1 %>
|
||||||
<div>
|
<div>
|
||||||
<% submitter = submitters.first %>
|
<% submitter = submitters.first %>
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
<% else %>
|
<% else %>
|
||||||
<div class="space-y-1 w-full md:mr-2">
|
<div class="space-y-1 w-full md:mr-2">
|
||||||
<div class="flex items-center space-x-3">
|
<div class="flex items-center space-x-3">
|
||||||
<% if is_submission_complated %>
|
<% if is_submission_completed %>
|
||||||
<% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %>
|
<% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %>
|
||||||
<div class="tooltip flex" data-tip="<%= l(latest_submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
|
<div class="tooltip flex" data-tip="<%= l(latest_submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
|
||||||
<span class="badge <%= status_badges[latest_submitter.status] %> md:w-32 bg-opacity-50 badge-lg uppercase text-sm font-semibold">
|
<span class="badge <%= status_badges[latest_submitter.status] %> md:w-32 bg-opacity-50 badge-lg uppercase text-sm font-semibold">
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
<% submitters.each_with_index do |submitter, index| %>
|
<% submitters.each_with_index do |submitter, index| %>
|
||||||
<div class="flex justify-between items-center space-x-3">
|
<div class="flex justify-between items-center space-x-3">
|
||||||
<span class="flex items-center space-x-3">
|
<span class="flex items-center space-x-3">
|
||||||
<% unless is_submission_complated %>
|
<% unless is_submission_completed %>
|
||||||
<div class="tooltip flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
|
<div class="tooltip flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
|
||||||
<span class="badge md:w-24 <%= status_badges[submitter.status] %> bg-opacity-50 uppercase text-xs font-semibold">
|
<span class="badge md:w-24 <%= status_badges[submitter.status] %> bg-opacity-50 uppercase text-xs font-semibold">
|
||||||
<%= submitter.status %>
|
<%= submitter.status %>
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
<%= submitter.email %>
|
<%= submitter.email %>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<% if submitter.completed_at? && !is_submission_complated %>
|
<% if submitter.completed_at? && !is_submission_completed %>
|
||||||
<form onsubmit="event.preventDefault()">
|
<form onsubmit="event.preventDefault()">
|
||||||
<button onclick="event.stopPropagation()">
|
<button onclick="event.stopPropagation()">
|
||||||
<download-button data-src="<%= submitter_download_index_path(submitter.slug) %>" class="btn btn-xs btn-neutral text-white md:w-36">
|
<download-button data-src="<%= submitter_download_index_path(submitter.slug) %>" class="btn btn-xs btn-neutral text-white md:w-36">
|
||||||
@@ -118,7 +118,7 @@
|
|||||||
</download-button>
|
</download-button>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<% elsif !is_submission_complated %>
|
<% elsif !is_submission_completed %>
|
||||||
<div class="flex items-center space-x-3">
|
<div class="flex items-center space-x-3">
|
||||||
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'btn btn-xs text-xs btn-neutral text-white md:w-36 flex', icon_class: 'w-4 h-4 text-white', copy_title: 'Copy Link' %>
|
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'btn btn-xs text-xs btn-neutral text-white md:w-36 flex', icon_class: 'w-4 h-4 text-white', copy_title: 'Copy Link' %>
|
||||||
</div>
|
</div>
|
||||||
@@ -129,7 +129,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex space-x-2 items-center">
|
<div class="flex space-x-2 items-center">
|
||||||
<% if is_submission_complated %>
|
<% if is_submission_completed %>
|
||||||
<% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %>
|
<% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %>
|
||||||
<form onsubmit="event.preventDefault()">
|
<form onsubmit="event.preventDefault()">
|
||||||
<button onclick="event.stopPropagation()">
|
<button onclick="event.stopPropagation()">
|
||||||
|
|||||||
+1
-1
@@ -32,7 +32,7 @@ Rails.application.routes.draw do
|
|||||||
resources :submissions, only: %i[create]
|
resources :submissions, only: %i[create]
|
||||||
resources :templates, only: %i[update show index] do
|
resources :templates, only: %i[update show index] do
|
||||||
resources :submissions, only: %i[create]
|
resources :submissions, only: %i[create]
|
||||||
resources :documents, only: %i[create destroy], controller: 'templates_documents'
|
resources :documents, only: %i[create], controller: 'templates_documents'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class RemoveUserFirstLastNameNotNull < ActiveRecord::Migration[7.0]
|
||||||
|
def change
|
||||||
|
change_column_null :users, :first_name, true
|
||||||
|
change_column_null :users, :last_name, true
|
||||||
|
end
|
||||||
|
end
|
||||||
+3
-3
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.0].define(version: 2023_08_15_190540) do
|
ActiveRecord::Schema[7.0].define(version: 2023_08_19_113427) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
|
||||||
@@ -140,8 +140,8 @@ ActiveRecord::Schema[7.0].define(version: 2023_08_15_190540) do
|
|||||||
end
|
end
|
||||||
|
|
||||||
create_table "users", force: :cascade do |t|
|
create_table "users", force: :cascade do |t|
|
||||||
t.string "first_name", null: false
|
t.string "first_name"
|
||||||
t.string "last_name", null: false
|
t.string "last_name"
|
||||||
t.string "email", null: false
|
t.string "email", null: false
|
||||||
t.string "role", null: false
|
t.string "role", null: false
|
||||||
t.string "encrypted_password", null: false
|
t.string "encrypted_password", null: false
|
||||||
|
|||||||
Reference in New Issue
Block a user