group form checkboxes into single step

pull/105/head
Alex Turchyn 2 years ago
parent 7978b3ebe8
commit bfbf3d7ddb

@ -1,11 +1,11 @@
<template> <template>
<div <div
class="flex cursor-pointer bg-red-100 absolute border" class="flex cursor-pointer bg-red-100 absolute border text-[1.5vw] lg:text-base"
:style="computedStyle" :style="computedStyle"
:class="{ 'border-red-100 bg-opacity-70': !isActive, 'border-red-500 border-dashed bg-opacity-30 z-10': isActive }" :class="{ 'border-red-100': !isActive, 'bg-opacity-70': !isActive && !isValue, 'border-red-500 border-dashed z-10': isActive, 'bg-opacity-30': isActive || isValue }"
> >
<div <div
v-if="!isActive && !modelValue" v-if="!isActive && !isValue && field.type !== 'checkbox'"
class="absolute top-0 bottom-0 right-0 left-0 items-center justify-center h-full w-full" class="absolute top-0 bottom-0 right-0 left-0 items-center justify-center h-full w-full"
> >
<span <span
@ -25,6 +25,9 @@
class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap" class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap"
> >
{{ field.name || fieldNames[field.type] }} {{ field.name || fieldNames[field.type] }}
<template v-if="field.type === 'checkbox'">
{{ fieldIndex + 1 }}
</template>
</div> </div>
<div <div
v-if="isActive" v-if="isActive"
@ -33,39 +36,83 @@
/> />
<img <img
v-if="field.type === 'image' && image" v-if="field.type === 'image' && image"
class="object-contain" class="object-contain mx-auto"
:src="image.url" :src="image.url"
> >
<img <img
v-else-if="field.type === 'signature' && signature" v-else-if="field.type === 'signature' && signature"
class="object-contain" class="object-contain mx-auto"
:src="signature.url" :src="signature.url"
> >
<div v-else-if="field.type === 'attachment'"> <div
v-else-if="field.type === 'file'"
class="px-0.5 flex items-center"
>
<a <a
v-for="(attachment, index) in attachments" v-for="(attachment, index) in attachments"
:key="index" :key="index"
target="_blank"
:href="attachment.url" :href="attachment.url"
> >
<IconPaperclip
class="inline w-[1.5vw] h-[1.5vw] lg:w-4 lg:h-4"
/>
{{ attachment.filename }} {{ attachment.filename }}
</a> </a>
</div> </div>
<span v-else> <div
{{ modelValue }} v-else-if="field.type === 'checkbox'"
</span> class="w-full p-[0.2vw] flex items-center justify-center"
>
<input
type="checkbox"
:value="false"
class="aspect-square base-checkbox"
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
:checked="!!modelValue"
@click="$emit('update:model-value', !modelValue)"
>
</div>
<div
v-else
class="flex items-center px-0.5"
>
<span v-if="Array.isArray(modelValue)">
{{ modelValue.join(', ') }}
</span>
<span v-else-if="field.type === 'date'">
{{ formattedDate }}
</span>
<span v-else>
{{ modelValue }}
</span>
</div>
</div> </div>
</template> </template>
<script> <script>
import { IconTextSize, IconWriting, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot } from '@tabler/icons-vue' import { IconTextSize, IconWriting, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks } from '@tabler/icons-vue'
export default { export default {
name: 'FieldArea', name: 'FieldArea',
components: {
IconPaperclip
},
props: { props: {
field: { field: {
type: Object, type: Object,
required: true required: true
}, },
step: {
type: Array,
required: false,
default: () => []
},
values: {
type: Object,
required: false,
default: () => ({})
},
modelValue: { modelValue: {
type: [Array, String, Number, Object, Boolean], type: [Array, String, Number, Object, Boolean],
required: false, required: false,
@ -76,6 +123,11 @@ export default {
required: false, required: false,
default: false default: false
}, },
fieldIndex: {
type: Number,
required: false,
default: 0
},
attachmentsIndex: { attachmentsIndex: {
type: Object, type: Object,
required: false, required: false,
@ -97,9 +149,13 @@ export default {
file: 'File', file: 'File',
select: 'Select', select: 'Select',
checkbox: 'Checkbox', checkbox: 'Checkbox',
radio: 'Radio' radio: 'Radio',
multiple: 'Multiple Select'
} }
}, },
isValue () {
return this.step.some((f) => ![null, undefined, ''].includes(this.values[f.uuid]))
},
fieldIcons () { fieldIcons () {
return { return {
text: IconTextSize, text: IconTextSize,
@ -109,7 +165,8 @@ export default {
file: IconPaperclip, file: IconPaperclip,
select: IconSelect, select: IconSelect,
checkbox: IconCheckbox, checkbox: IconCheckbox,
radio: IconCircleDot radio: IconCircleDot,
multiple: IconChecks
} }
}, },
image () { image () {
@ -126,8 +183,15 @@ export default {
return null return null
} }
}, },
formattedDate () {
if (this.field.type === 'date' && this.modelValue) {
return new Intl.DateTimeFormat({ year: 'numeric', month: 'numeric', day: 'numeric' }).format(new Date(this.modelValue))
} else {
return ''
}
},
attachments () { attachments () {
if (this.field.type === 'attachment') { if (this.field.type === 'file') {
return (this.modelValue || []).map((uuid) => this.attachmentsIndex[uuid]) return (this.modelValue || []).map((uuid) => this.attachmentsIndex[uuid])
} else { } else {
return [] return []

@ -1,23 +1,31 @@
<template> <template>
<template <template
v-for="field in fields" v-for="step in steps"
:key="field.uuid" :key="step[0].uuid"
> >
<template <template
v-for="(area, index) in field.areas" v-for="(field, fieldIndex) in step"
:key="index" :key="field.uuid"
> >
<Teleport :to="`#page-${area.attachment_uuid}-${area.page}`"> <template
<FieldArea v-for="(area, areaIndex) in field.areas"
:ref="setAreaRef" :key="areaIndex"
v-model="values[field.uuid]" >
:field="field" <Teleport :to="`#page-${area.attachment_uuid}-${area.page}`">
:area="area" <FieldArea
:is-active="currentField === field" :ref="setAreaRef"
:attachments-index="attachmentsIndex" v-model="values[field.uuid]"
@click="$emit('focus-field', field)" :field="field"
/> :values="values"
</Teleport> :area="area"
:field-index="fieldIndex"
:step="step"
:is-active="currentStep === step"
:attachments-index="attachmentsIndex"
@click="$emit('focus-step', step)"
/>
</Teleport>
</template>
</template> </template>
</template> </template>
</template> </template>
@ -31,7 +39,7 @@ export default {
FieldArea FieldArea
}, },
props: { props: {
fields: { steps: {
type: Array, type: Array,
required: false, required: false,
default: () => [] default: () => []
@ -46,13 +54,13 @@ export default {
required: false, required: false,
default: () => ({}) default: () => ({})
}, },
currentField: { currentStep: {
type: Object, type: Array,
required: false, required: false,
default: () => ({}) default: () => []
} }
}, },
emits: ['focus-field'], emits: ['focus-step'],
data () { data () {
return { return {
areaRefs: [] areaRefs: []

@ -1,11 +1,11 @@
<template> <template>
<FieldAreas <FieldAreas
ref="areas" ref="areas"
:fields="submitterFields" :steps="stepFields"
:values="values" :values="values"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:current-field="currentField" :current-step="currentStepFields"
@focus-field="goToField($event, false, true)" @focus-step="goToStep($event, false, true)"
/> />
<form <form
v-if="!isCompleted" v-if="!isCompleted"
@ -21,7 +21,7 @@
:value="authenticityToken" :value="authenticityToken"
> >
<input <input
v-if="currentStep === submitterFields.length - 1" v-if="currentStep === stepFields.length - 1"
type="hidden" type="hidden"
name="completed" name="completed"
value="true" value="true"
@ -42,12 +42,12 @@
<input <input
:id="currentField.uuid" :id="currentField.uuid"
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
autofocus
class="base-input !text-2xl w-full" class="base-input !text-2xl w-full"
:required="currentField.required" :required="currentField.required"
placeholder="Type here..." placeholder="Type here..."
type="text" type="text"
:name="`values[${currentField.uuid}]`" :name="`values[${currentField.uuid}]`"
@focus="$refs.areas.scrollIntoField(currentField)"
> >
</div> </div>
</div> </div>
@ -57,15 +57,15 @@
:for="currentField.uuid" :for="currentField.uuid"
class="label text-2xl mb-2" class="label text-2xl mb-2"
>{{ currentField.name }}</label> >{{ currentField.name }}</label>
<div> <div class="text-center">
<input <input
:id="currentField.uuid" :id="currentField.uuid"
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
class="base-input !text-2xl w-full text-center" class="base-input !text-2xl text-center w-full"
autofocus
:required="currentField.required" :required="currentField.required"
type="date" type="date"
:name="`values[${currentField.uuid}]`" :name="`values[${currentField.uuid}]`"
@focus="$refs.areas.scrollIntoField(currentField)"
> >
</div> </div>
</div> </div>
@ -81,6 +81,7 @@
class="select base-input !text-2xl w-full text-center font-normal" class="select base-input !text-2xl w-full text-center font-normal"
:name="`values[${currentField.uuid}]`" :name="`values[${currentField.uuid}]`"
@change="values[currentField.uuid] = $event.target.value" @change="values[currentField.uuid] = $event.target.value"
@focus="$refs.areas.scrollIntoField(currentField)"
> >
<option <option
value="" value=""
@ -98,65 +99,75 @@
</option> </option>
</select> </select>
</div> </div>
<div v-else-if="currentField.type === 'radio' && currentField.options?.length"> <div v-else-if="currentField.type === 'radio'">
<label <label
v-if="currentField.name" v-if="currentField.name"
:for="currentField.uuid" :for="currentField.uuid"
class="label text-2xl mb-2" class="label text-2xl mb-2"
>{{ currentField.name }}</label> >{{ currentField.name }}</label>
<div class="space-y-3.5"> <div class="flex w-full">
<div class="space-y-3.5 mx-auto">
<div
v-for="(option, index) in currentField.options"
:key="index"
>
<label
:for="currentField.uuid + option"
class="flex items-center space-x-3"
>
<input
:id="currentField.uuid + option"
v-model="values[currentField.uuid]"
type="radio"
class="base-radio !h-7 !w-7"
:name="`values[${currentField.uuid}]`"
:value="option"
required
>
<span class="text-xl">
{{ option }}
</span>
</label>
</div>
</div>
</div>
</div>
<MultiSelectStep
v-else-if="currentField.type === 'multiple'"
v-model="values[currentField.uuid]"
:field="currentField"
/>
<div
v-else-if="currentField.type === 'checkbox'"
class="flex w-full"
>
<div
class="space-y-3.5 mx-auto"
>
<div <div
v-for="(option, index) in currentField.options" v-for="(field, index) in currentStepFields"
:key="index" :key="field.uuid"
> >
<label <label
:for="currentField.uuid + option" :for="field.uuid"
class="flex items-center space-x-3" class="flex items-center space-x-3"
> >
<input <input
:id="currentField.uuid + option" :id="field.uuid"
v-model="values[currentField.uuid]" type="checkbox"
type="radio" :name="`values[${field.uuid}]`"
class="base-radio !h-7 !w-7" :value="false"
:name="`values[${currentField.uuid}]`" class="base-checkbox !h-7 !w-7"
:value="option" :checked="!!values[field.uuid]"
required @click="values[field.uuid] = !values[field.uuid]"
> >
<span class="text-xl"> <span class="text-xl">
{{ option }} {{ currentField.name || currentField.type + ' ' + (index + 1) }}
</span> </span>
</label> </label>
</div> </div>
</div> </div>
</div> </div>
<CheckboxStep
v-else-if="currentField.type === 'checkbox' && currentField.options?.length"
v-model="values[currentField.uuid]"
:field="currentField"
/>
<div v-else-if="['radio', 'checkbox'].includes(currentField.type)">
<div class="flex justify-center">
<label
:for="currentField.uuid"
class="flex items-center space-x-3"
>
<input
:id="currentField.uuid"
:model-value="values[currentField.uuid]"
:type="currentField.type"
:name="`values[${currentField.uuid}]`"
:value="true"
class="!h-7 !w-7"
:class="{'base-radio' : currentField.type === 'radio', 'base-checkbox': currentField.type === 'checkbox'}"
:checked="!!values[currentField.uuid]"
@click="values[currentField.uuid] = !values[currentField.uuid]"
>
<span class="text-xl">
{{ currentField.name || currentField.type }}
</span>
</label>
</div>
</div>
<ImageStep <ImageStep
v-else-if="currentField.type === 'image'" v-else-if="currentField.type === 'image'"
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
@ -199,12 +210,12 @@
<div class="flex justify-center"> <div class="flex justify-center">
<div class="flex items-center mt-5 mb-1"> <div class="flex items-center mt-5 mb-1">
<a <a
v-for="(field, index) in submitterFields" v-for="(step, index) in stepFields"
:key="field.uuid" :key="step[0].uuid"
href="#" href="#"
class="inline border border-base-300 h-3 w-3 rounded-full mx-1" class="inline border border-base-300 h-3 w-3 rounded-full mx-1"
:class="{ 'bg-base-200': index === currentStep, 'bg-base-content': index < currentStep, 'bg-white': index > currentStep }" :class="{ 'bg-base-200': index === currentStep, 'bg-base-content': index < currentStep, 'bg-white': index > currentStep }"
@click.prevent="goToField(field, true)" @click.prevent="goToStep(step, true)"
/> />
</div> </div>
</div> </div>
@ -220,7 +231,7 @@ import FieldAreas from './areas'
import ImageStep from './image_step' import ImageStep from './image_step'
import SignatureStep from './signature_step' import SignatureStep from './signature_step'
import AttachmentStep from './attachment_step' import AttachmentStep from './attachment_step'
import CheckboxStep from './checkbox_step' import MultiSelectStep from './multi_select_step'
import FormCompleted from './completed' import FormCompleted from './completed'
export default { export default {
@ -230,7 +241,7 @@ export default {
ImageStep, ImageStep,
SignatureStep, SignatureStep,
AttachmentStep, AttachmentStep,
CheckboxStep, MultiSelectStep,
FormCompleted FormCompleted
}, },
props: { props: {
@ -270,12 +281,28 @@ export default {
} }
}, },
computed: { computed: {
currentStepFields () {
return this.stepFields[this.currentStep]
},
currentField () { currentField () {
return this.submitterFields[this.currentStep] return this.currentStepFields[0]
}, },
submitterFields () { submitterFields () {
return this.fields.filter((f) => f.submitter_uuid === this.submitterUuid) return this.fields.filter((f) => f.submitter_uuid === this.submitterUuid)
}, },
stepFields () {
return this.submitterFields.reduce((acc, f) => {
const prevStep = acc[acc.length - 1]
if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox') {
prevStep.push(f)
} else {
acc.push([f])
}
return acc
}, [])
},
attachmentsIndex () { attachmentsIndex () {
return this.attachments.reduce((acc, a) => { return this.attachments.reduce((acc, a) => {
acc[a.uuid] = a acc[a.uuid] = a
@ -289,17 +316,17 @@ export default {
}, },
mounted () { mounted () {
this.currentStep = Math.min( this.currentStep = Math.min(
this.submitterFields.indexOf([...this.submitterFields].reverse().find((field) => !!this.values[field.uuid])) + 1, this.stepFields.indexOf([...this.stepFields].reverse().find((fields) => fields.some((f) => !!this.values[f.uuid]))) + 1,
this.submitterFields.length - 1 this.stepFields.length - 1
) )
}, },
methods: { methods: {
goToField (field, scrollToArea = false, clickUpload = false) { goToStep (step, scrollToArea = false, clickUpload = false) {
this.currentStep = this.submitterFields.indexOf(field) this.currentStep = this.stepFields.indexOf(step)
this.$nextTick(() => { this.$nextTick(() => {
if (scrollToArea) { if (scrollToArea) {
this.$refs.areas.scrollIntoField(field) this.$refs.areas.scrollIntoField(step[0])
} }
this.$refs.form.querySelector('input[type="date"], input[type="text"], select')?.focus() this.$refs.form.querySelector('input[type="date"], input[type="text"], select')?.focus()
@ -322,10 +349,10 @@ export default {
method: 'POST', method: 'POST',
body: new FormData(this.$refs.form) body: new FormData(this.$refs.form)
}).then(response => { }).then(response => {
const nextField = this.submitterFields[this.currentStep + 1] const nextStep = this.stepFields[this.currentStep + 1]
if (nextField) { if (nextStep) {
this.goToField(this.submitterFields[this.currentStep + 1], true) this.goToStep(this.stepFields[this.currentStep + 1], true)
} else { } else {
this.isCompleted = true this.isCompleted = true
} }

@ -4,36 +4,38 @@
:for="field.uuid" :for="field.uuid"
class="label text-2xl mb-2" class="label text-2xl mb-2"
>{{ field.name }}</label> >{{ field.name }}</label>
<div class="space-y-3.5"> <div class="flex w-full">
<div <div class="space-y-3.5 mx-auto">
v-for="(option, index) in field.options" <div
:key="index" v-for="(option, index) in field.options"
> :key="index"
<label
:for="field.uuid + option"
class="flex items-center space-x-3"
> >
<input <label
:id="field.uuid + option" :for="field.uuid + option"
:ref="setInputRef" class="flex items-center space-x-3"
type="checkbox"
:name="`values[${field.uuid}][]`"
:value="option"
class="base-checkbox !h-7 !w-7"
:checked="modelValue.includes(option)"
@change="onChange"
> >
<span class="text-xl"> <input
{{ option }} :id="field.uuid + option"
</span> :ref="setInputRef"
</label> type="checkbox"
:name="`values[${field.uuid}][]`"
:value="option"
class="base-checkbox !h-7 !w-7"
:checked="modelValue.includes(option)"
@change="onChange"
>
<span class="text-xl">
{{ option }}
</span>
</label>
</div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'SheckboxStep', name: 'MultiSelectStep',
props: { props: {
field: { field: {
type: Object, type: Object,

@ -35,7 +35,7 @@
@click="selectedAreaRef.value = area" @click="selectedAreaRef.value = area"
/> />
<span <span
v-if="withName" v-if="field.type !== 'checkbox' || field.name"
ref="name" ref="name"
contenteditable contenteditable
class="pr-1 cursor-text outline-none block" class="pr-1 cursor-text outline-none block"
@ -138,9 +138,6 @@ export default {
'bg-purple-100' 'bg-purple-100'
] ]
}, },
withName () {
return !['checkbox', 'radio'].includes(this.field.type) || this.field.options
},
isSelected () { isSelected () {
return this.selectedAreaRef.value === this.area return this.selectedAreaRef.value === this.area
}, },
@ -169,11 +166,11 @@ export default {
} }
}, },
maybeUpdateOptions () { maybeUpdateOptions () {
if (!['radio', 'checkbox', 'select'].includes(this.field.type)) { if (!['radio', 'multiple', 'select'].includes(this.field.type)) {
delete this.field.options delete this.field.options
} }
if (this.field.type === 'select') { if (['select', 'multiple', 'radio'].includes(this.field.type)) {
this.field.options ||= [''] this.field.options ||= ['']
} }
}, },

@ -291,7 +291,7 @@ export default {
required: true required: true
} }
if (this.dragFieldType === 'select') { if (['select', 'multiple', 'radio'].includes(this.dragFieldType)) {
field.options = [''] field.options = ['']
} }
@ -311,7 +311,7 @@ export default {
} else if (previousField?.areas) { } else if (previousField?.areas) {
baseArea = previousField.areas[previousField.areas.length - 1] baseArea = previousField.areas[previousField.areas.length - 1]
} else { } else {
if (['checkbox', 'radio'].includes(this.dragFieldType)) { if (['checkbox'].includes(this.dragFieldType)) {
baseArea = { baseArea = {
w: area.maskW / 30 / area.maskW, w: area.maskW / 30 / area.maskW,
h: area.maskW / 30 / area.maskW * (area.maskW / area.maskH) h: area.maskW / 30 / area.maskW * (area.maskW / area.maskH)

@ -182,7 +182,9 @@ export default {
defaultName () { defaultName () {
const typeIndex = this.template.fields.filter((f) => f.type === this.field.type).indexOf(this.field) const typeIndex = this.template.fields.filter((f) => f.type === this.field.type).indexOf(this.field)
return `${this.$t(this.field.type)} Field ${typeIndex + 1}` const suffix = { multiple: 'Select', radio: 'Group' }[this.field.type] || 'Field'
return `${this.$t(this.field.type)} ${suffix} ${typeIndex + 1}`
}, },
areas () { areas () {
return this.field.areas || [] return this.field.areas || []
@ -203,11 +205,11 @@ export default {
document.activeElement.blur() document.activeElement.blur()
}, },
maybeUpdateOptions () { maybeUpdateOptions () {
if (!['radio', 'checkbox', 'select'].includes(this.field.type)) { if (!['radio', 'multiple', 'select'].includes(this.field.type)) {
delete this.field.options delete this.field.options
} }
if (this.field.type === 'select') { if (['radio', 'multiple', 'select'].includes(this.field.type)) {
this.field.options ||= [''] this.field.options ||= ['']
} }
}, },

@ -41,7 +41,7 @@
</template> </template>
<script> <script>
import { IconTextSize, IconWriting, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot } from '@tabler/icons-vue' import { IconTextSize, IconWriting, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks } from '@tabler/icons-vue'
export default { export default {
name: 'FiledTypeDropdown', name: 'FiledTypeDropdown',
@ -77,6 +77,7 @@ export default {
file: IconPaperclip, file: IconPaperclip,
select: IconSelect, select: IconSelect,
checkbox: IconCheckbox, checkbox: IconCheckbox,
multiple: IconChecks,
radio: IconCircleDot radio: IconCircleDot
} }
} }

@ -1,11 +1,13 @@
<template> <template>
<FieldSubmitter <div class="sticky -top-1 bg-base-100 pt-1 -mt-1 z-10">
:model-value="selectedSubmitter.uuid" <FieldSubmitter
class="w-full" :model-value="selectedSubmitter.uuid"
:submitters="submitters" class="w-full bg-base-100"
@remove="removeSubmitter" :submitters="submitters"
@update:model-value="$emit('change-submitter', submitters.find((s) => s.uuid === $event))" @remove="removeSubmitter"
/> @update:model-value="$emit('change-submitter', submitters.find((s) => s.uuid === $event))"
/>
</div>
<div class="mb-1 mt-2"> <div class="mb-1 mt-2">
<Field <Field
v-for="field in submitterFields" v-for="field in submitterFields"
@ -19,7 +21,7 @@
@set-draw="$emit('set-draw', $event)" @set-draw="$emit('set-draw', $event)"
/> />
</div> </div>
<div class="grid grid-cols-3 gap-1"> <div class="grid grid-cols-3 gap-1 pb-2">
<button <button
v-for="(icon, type) in fieldIcons" v-for="(icon, type) in fieldIcons"
:key="type" :key="type"
@ -145,7 +147,7 @@ export default {
type type
} }
if (['select', 'checkbox', 'radio'].includes(type)) { if (['select', 'multiple', 'radio'].includes(type)) {
field.options = [''] field.options = ['']
} }

@ -6,5 +6,6 @@ export default {
file: 'File', file: 'File',
select: 'Select', select: 'Select',
checkbox: 'Checkbox', checkbox: 'Checkbox',
multiple: 'Multiple',
radio: 'Radio' radio: 'Radio'
} }

@ -18,7 +18,7 @@
<% end %> <% end %>
<div class="text-center px-2"> <div class="text-center px-2">
Powered by Powered by
<a href="https://www.docuseal.co" class="underline">DocuSeal</a> - An open source documents software <a href="https://www.docuseal.co" class="underline">DocuSeal</a> - open source documents software
</div> </div>
</div> </div>
<div class="fixed bottom-0 w-full h-0 z-20"> <div class="fixed bottom-0 w-full h-0 z-20">

Loading…
Cancel
Save