Better submission table
This commit is contained in:
parent
062223fab2
commit
e64d0d5da2
|
@ -1,84 +1,89 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div id="table-page"
|
||||||
class="my-4 w-full mx-auto"
|
class="w-full flex flex-col"
|
||||||
>
|
>
|
||||||
<h3 class="font-semibold mb-4 text-xl">
|
<div class="w-full md:w-4/5 lg:w-3/5 md:mx-auto md:max-w-4xl px-4 pt-4">
|
||||||
Form Submissions
|
<h3 class="font-semibold mb-4 text-xl">
|
||||||
</h3>
|
Form Submissions
|
||||||
|
</h3>
|
||||||
|
|
||||||
<!-- Table columns modal -->
|
<!-- Table columns modal -->
|
||||||
<modal :show="showColumnsModal" @close="showColumnsModal=false">
|
<modal :show="showColumnsModal" @close="showColumnsModal=false">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<svg class="w-8 h-8" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg class="w-8 h-8" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path
|
<path
|
||||||
d="M16 5H8C4.13401 5 1 8.13401 1 12C1 15.866 4.13401 19 8 19H16C19.866 19 23 15.866 23 12C23 8.13401 19.866 5 16 5Z"
|
d="M16 5H8C4.13401 5 1 8.13401 1 12C1 15.866 4.13401 19 8 19H16C19.866 19 23 15.866 23 12C23 8.13401 19.866 5 16 5Z"
|
||||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path
|
<path
|
||||||
d="M8 15C9.65685 15 11 13.6569 11 12C11 10.3431 9.65685 9 8 9C6.34315 9 5 10.3431 5 12C5 13.6569 6.34315 15 8 15Z"
|
d="M8 15C9.65685 15 11 13.6569 11 12C11 10.3431 9.65685 9 8 9C6.34315 9 5 10.3431 5 12C5 13.6569 6.34315 15 8 15Z"
|
||||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
|
||||||
<template #title>
|
|
||||||
Display columns
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="px-4">
|
|
||||||
<template v-if="form.properties.length > 0">
|
|
||||||
<h4 class="font-bold mb-2">
|
|
||||||
Form Fields
|
|
||||||
</h4>
|
|
||||||
<div class="border border-gray-300 rounded-md">
|
|
||||||
<div v-for="(field,index) in form.properties" :key="field.id" class="p-2 border-gray-300 flex items-center"
|
|
||||||
:class="{'border-t':index!=0}">
|
|
||||||
<p class="flex-grow truncate">
|
|
||||||
{{ field.name }}
|
|
||||||
</p>
|
|
||||||
<v-switch v-model="displayColumns[field.id]" class="float-right"
|
|
||||||
@update:model-value="onChangeDisplayColumns"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<template v-if="removed_properties.length > 0">
|
<template #title>
|
||||||
<h4 class="font-bold mb-2 mt-4">
|
Display columns
|
||||||
Removed Fields
|
|
||||||
</h4>
|
|
||||||
<div class="border border-gray-300 rounded-md">
|
|
||||||
<div v-for="(field,index) in removed_properties" :key="field.id"
|
|
||||||
class="p-2 border-gray-300 flex items-center" :class="{'border-t':index!=0}">
|
|
||||||
<p class="flex-grow truncate">
|
|
||||||
{{ field.name }}
|
|
||||||
</p>
|
|
||||||
<v-switch v-model="displayColumns[field.id]" class="float-right"
|
|
||||||
@update:model-value="onChangeDisplayColumns"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
|
||||||
</modal>
|
|
||||||
|
|
||||||
<Loader v-if="!form" class="h-6 w-6 text-nt-blue mx-auto"/>
|
<div class="px-4">
|
||||||
<div v-else>
|
<template v-if="form.properties.length > 0">
|
||||||
<div v-if="form && tableData.length > 0" class="flex flex-wrap items-end">
|
<h4 class="font-bold mb-2">
|
||||||
<div class="flex-grow">
|
Form Fields
|
||||||
<text-input class="w-64" :form="searchForm" name="search" placeholder="Search..."/>
|
</h4>
|
||||||
|
<div class="border border-gray-300 rounded-md">
|
||||||
|
<div v-for="(field,index) in candidatesProperties" :key="field.id"
|
||||||
|
class="p-2 border-gray-300 flex items-center"
|
||||||
|
:class="{'border-t':index!=0}">
|
||||||
|
<p class="flex-grow truncate">
|
||||||
|
{{ field.name }}
|
||||||
|
</p>
|
||||||
|
<v-switch v-model="displayColumns[field.id]" class="float-right"
|
||||||
|
@update:model-value="onChangeDisplayColumns"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-if="removed_properties.length > 0">
|
||||||
|
<h4 class="font-bold mb-2 mt-4">
|
||||||
|
Removed Fields
|
||||||
|
</h4>
|
||||||
|
<div class="border border-gray-300 rounded-md">
|
||||||
|
<div v-for="(field,index) in removed_properties" :key="field.id"
|
||||||
|
class="p-2 border-gray-300 flex items-center" :class="{'border-t':index!=0}">
|
||||||
|
<p class="flex-grow truncate">
|
||||||
|
{{ field.name }}
|
||||||
|
</p>
|
||||||
|
<v-switch v-model="displayColumns[field.id]" class="float-right"
|
||||||
|
@update:model-value="onChangeDisplayColumns"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="font-semibold flex gap-4">
|
</modal>
|
||||||
<p class="float-right text-xs uppercase mb-2">
|
|
||||||
<a
|
|
||||||
href="javascript:void(0);" class="text-gray-500" @click="showColumnsModal=true"
|
|
||||||
>Display columns</a>
|
|
||||||
</p>
|
|
||||||
<p class="text-right cursor-pointer text-xs uppercase">
|
|
||||||
<a
|
|
||||||
@click.prevent="downloadAsCsv" href="#"
|
|
||||||
>Export as CSV</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<Loader v-if="!form" class="h-6 w-6 text-nt-blue mx-auto"/>
|
||||||
|
<div v-else>
|
||||||
|
<div v-if="form && tableData.length > 0" class="flex flex-wrap items-end">
|
||||||
|
<div class="flex-grow">
|
||||||
|
<text-input class="w-64" :form="searchForm" name="search" placeholder="Search..."/>
|
||||||
|
</div>
|
||||||
|
<div class="font-semibold flex gap-4">
|
||||||
|
<p class="float-right text-xs uppercase mb-2">
|
||||||
|
<a
|
||||||
|
href="javascript:void(0);" class="text-gray-500" @click="showColumnsModal=true"
|
||||||
|
>Display columns</a>
|
||||||
|
</p>
|
||||||
|
<p class="text-right cursor-pointer text-xs uppercase">
|
||||||
|
<a
|
||||||
|
@click.prevent="downloadAsCsv" href="#"
|
||||||
|
>Export as CSV</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="px-4 pb-4">
|
||||||
<scroll-shadow
|
<scroll-shadow
|
||||||
ref="shadows"
|
ref="shadows"
|
||||||
class="border max-h-full h-full notion-database-renderer"
|
class="border h-full notion-database-renderer"
|
||||||
:shadow-top-offset="0"
|
:shadow-top-offset="0"
|
||||||
:hide-scrollbar="true"
|
:hide-scrollbar="true"
|
||||||
>
|
>
|
||||||
|
@ -88,6 +93,7 @@
|
||||||
:columns="properties"
|
:columns="properties"
|
||||||
:data="filteredData"
|
:data="filteredData"
|
||||||
:loading="isLoading"
|
:loading="isLoading"
|
||||||
|
:scroll-parent="parentPage"
|
||||||
@resize="dataChanged()"
|
@resize="dataChanged()"
|
||||||
@deleted="onDeleteRecord"
|
@deleted="onDeleteRecord"
|
||||||
@updated="(submission)=>onUpdateRecord(submission)"
|
@updated="(submission)=>onUpdateRecord(submission)"
|
||||||
|
@ -116,7 +122,7 @@ export default {
|
||||||
workingFormStore,
|
workingFormStore,
|
||||||
recordStore,
|
recordStore,
|
||||||
form: storeToRefs(workingFormStore).content,
|
form: storeToRefs(workingFormStore).content,
|
||||||
tableData:storeToRefs(recordStore).getAll,
|
tableData: storeToRefs(recordStore).getAll,
|
||||||
runtimeConfig: useRuntimeConfig(),
|
runtimeConfig: useRuntimeConfig(),
|
||||||
slug: useRoute().params.slug
|
slug: useRoute().params.slug
|
||||||
}
|
}
|
||||||
|
@ -132,17 +138,28 @@ export default {
|
||||||
displayColumns: {},
|
displayColumns: {},
|
||||||
searchForm: useForm({
|
searchForm: useForm({
|
||||||
search: ''
|
search: ''
|
||||||
})
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
parentPage() {
|
||||||
|
if (process.server) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return window
|
||||||
|
},
|
||||||
|
candidatesProperties() {
|
||||||
|
return clonedeep(this.form.properties).filter((field) => {
|
||||||
|
return !['nf-text', 'nf-code', 'nf-page-break', 'nf-divider', 'nf-image'].includes(field.type)
|
||||||
|
})
|
||||||
|
},
|
||||||
exportUrl() {
|
exportUrl() {
|
||||||
if (!this.form) {
|
if (!this.form) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
return this.runtimeConfig.public.apiBase + '/open/forms/' + this.form.id + '/submissions/export'
|
return this.runtimeConfig.public.apiBase + '/open/forms/' + this.form.id + '/submissions/export'
|
||||||
},
|
},
|
||||||
isLoading(){
|
isLoading() {
|
||||||
return this.recordStore.loading
|
return this.recordStore.loading
|
||||||
},
|
},
|
||||||
filteredData() {
|
filteredData() {
|
||||||
|
@ -183,7 +200,7 @@ export default {
|
||||||
},
|
},
|
||||||
initFormStructure() {
|
initFormStructure() {
|
||||||
// check if form properties already has a created_at column
|
// check if form properties already has a created_at column
|
||||||
this.properties = clonedeep(this.form.properties)
|
this.properties = this.candidatesProperties
|
||||||
if (!this.properties.find((property) => {
|
if (!this.properties.find((property) => {
|
||||||
if (property.id === 'created_at') {
|
if (property.id === 'created_at') {
|
||||||
return true
|
return true
|
||||||
|
@ -245,7 +262,7 @@ export default {
|
||||||
return this.displayColumns[field.id] === true
|
return this.displayColumns[field.id] === true
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onUpdateRecord(submission){
|
onUpdateRecord(submission) {
|
||||||
this.recordStore.save(submission);
|
this.recordStore.save(submission);
|
||||||
this.dataChanged()
|
this.dataChanged()
|
||||||
},
|
},
|
||||||
|
|
|
@ -91,7 +91,7 @@ import {hash} from "~/lib/utils.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {ResizableTh, RecordOperations},
|
components: {ResizableTh, RecordOperations},
|
||||||
emits: ["updated", "deleted", "resize"],
|
emits: ["updated", "deleted", "resize", "update-columns"],
|
||||||
props: {
|
props: {
|
||||||
columns: {
|
columns: {
|
||||||
type: Array,
|
type: Array,
|
||||||
|
@ -109,7 +109,8 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
default: true,
|
default: true,
|
||||||
type: Boolean
|
type: Boolean
|
||||||
}
|
},
|
||||||
|
scrollParent: {},
|
||||||
},
|
},
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
|
@ -126,6 +127,7 @@ export default {
|
||||||
skip: false,
|
skip: false,
|
||||||
hasActions: true,
|
hasActions: true,
|
||||||
internalColumns: [],
|
internalColumns: [],
|
||||||
|
rafId: null,
|
||||||
fieldComponents: {
|
fieldComponents: {
|
||||||
text: shallowRef(OpenText),
|
text: shallowRef(OpenText),
|
||||||
number: shallowRef(OpenText),
|
number: shallowRef(OpenText),
|
||||||
|
@ -159,10 +161,10 @@ export default {
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.internalColumns = clonedeep(this.columns)
|
this.internalColumns = clonedeep(this.columns)
|
||||||
const parent = document.getElementById('table-page')
|
const parent = this.scrollParent ?? document.getElementById('table-page')
|
||||||
this.tableHash = hash(JSON.stringify(this.form.properties))
|
this.tableHash = hash(JSON.stringify(this.form.properties))
|
||||||
if (parent) {
|
if (parent) {
|
||||||
parent.addEventListener('scroll', this.handleScroll, {passive: true})
|
parent.addEventListener('scroll', this.handleScroll, {passive: false})
|
||||||
}
|
}
|
||||||
window.addEventListener('resize', this.handleScroll)
|
window.addEventListener('resize', this.handleScroll)
|
||||||
this.onStructureChange()
|
this.onStructureChange()
|
||||||
|
@ -170,7 +172,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
const parent = document.getElementById('table-page')
|
const parent = this.scrollParent ?? document.getElementById('table-page')
|
||||||
if (parent) {
|
if (parent) {
|
||||||
parent.removeEventListener('scroll', this.handleScroll)
|
parent.removeEventListener('scroll', this.handleScroll)
|
||||||
}
|
}
|
||||||
|
@ -227,33 +229,44 @@ export default {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
handleScroll() {
|
handleScroll() {
|
||||||
const parent = document.getElementById('table-page')
|
|
||||||
const posTop = parent.getBoundingClientRect().top
|
|
||||||
const tablePosition = Math.max(0, posTop - this.$refs.table.getBoundingClientRect().top)
|
|
||||||
const tableHeader = document.getElementById('table-header-' + this.tableHash)
|
|
||||||
|
|
||||||
// Set position of table header
|
if (this.rafId) {
|
||||||
if (tableHeader) {
|
cancelAnimationFrame(this.rafId);
|
||||||
tableHeader.style.transform = `translate3d(0px, ${tablePosition}px, 0px)`
|
}
|
||||||
if (tablePosition > 0) {
|
|
||||||
tableHeader.classList.add('border-t')
|
this.rafId = requestAnimationFrame(() => {
|
||||||
|
const table = this.$refs.table;
|
||||||
|
const tableHeader = document.getElementById('table-header-' + this.tableHash);
|
||||||
|
const tableActionsRow = document.getElementById('table-actions-' + this.tableHash);
|
||||||
|
|
||||||
|
if (!table || !tableHeader) return;
|
||||||
|
|
||||||
|
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
||||||
|
const tableRect = table.getBoundingClientRect();
|
||||||
|
|
||||||
|
// The starting point of the table relative to the viewport
|
||||||
|
const tableStart = tableRect.top + scrollTop;
|
||||||
|
// The end point of the table relative to the viewport
|
||||||
|
const tableEnd = tableStart + tableRect.height;
|
||||||
|
|
||||||
|
let headerY = scrollTop - tableStart;
|
||||||
|
let actionsY = scrollTop + window.innerHeight - tableEnd;
|
||||||
|
|
||||||
|
if (headerY < 0) headerY = 0;
|
||||||
|
if (scrollTop + window.innerHeight > tableEnd) {
|
||||||
|
actionsY = tableRect.height - (scrollTop + window.innerHeight - tableEnd);
|
||||||
} else {
|
} else {
|
||||||
tableHeader.classList.remove('border-t')
|
actionsY = tableRect.height;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Set position of actions row
|
if (tableHeader) {
|
||||||
if (this.$slots.hasOwnProperty('actions')) {
|
tableHeader.style.transform = `translate3d(0px, ${headerY}px, 0px)`;
|
||||||
const tableActionsRow = document.getElementById('table-actions-' + this.tableHash)
|
|
||||||
if (tableActionsRow) {
|
|
||||||
if (tablePosition > 100) {
|
|
||||||
tableActionsRow.style.transform = `translate3d(0px, ${tablePosition + 33}px, 0px)`
|
|
||||||
} else {
|
|
||||||
const parentContainer = document.getElementById('table-page')
|
|
||||||
tableActionsRow.style.transform = `translate3d(0px, ${parentContainer.offsetHeight + (posTop - this.$refs.table.getBoundingClientRect().top) - 35}px, 0px)`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
if (tableActionsRow) {
|
||||||
|
tableActionsRow.style.transform = `translate3d(0px, ${actionsY}px, 0px)`;
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
setColumns(val) {
|
setColumns(val) {
|
||||||
this.$emit('update-columns', val)
|
this.$emit('update-columns', val)
|
||||||
|
|
|
@ -124,12 +124,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex bg-white">
|
<div class="flex flex-col bg-white">
|
||||||
<div class="w-full md:w-4/5 lg:w-3/5 md:mx-auto md:max-w-4xl px-4">
|
<NuxtPage :form="form"/>
|
||||||
<div class="py-4">
|
|
||||||
<NuxtPage :form="form"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-else-if="loading" class="text-center w-full p-5">
|
<div v-else-if="loading" class="text-center w-full p-5">
|
||||||
|
|
|
@ -1,22 +1,24 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="mb-20">
|
<div class="w-full md:w-4/5 lg:w-3/5 md:mx-auto md:max-w-4xl p-4">
|
||||||
|
<div class="mb-20">
|
||||||
|
|
||||||
<div class="mb-6 pb-6 border-b w-full flex flex-col sm:flex-row gap-2">
|
<div class="mb-6 pb-6 border-b w-full flex flex-col sm:flex-row gap-2">
|
||||||
<regenerate-form-link class="sm:w-1/2 flex" :form="props.form"/>
|
<regenerate-form-link class="sm:w-1/2 flex" :form="props.form"/>
|
||||||
|
|
||||||
<url-form-prefill class="sm:w-1/2" :form="props.form" :extra-query-param="shareUrlForQueryParams"/>
|
<url-form-prefill class="sm:w-1/2" :form="props.form" :extra-query-param="shareUrlForQueryParams"/>
|
||||||
|
|
||||||
|
<embed-form-as-popup-modal class="sm:w-1/2 flex" :form="props.form"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<share-link class="mt-4" :form="props.form" :extra-query-param="shareUrlForQueryParams"/>
|
||||||
|
|
||||||
|
<embed-code class="mt-6" :form="props.form" :extra-query-param="shareUrlForQueryParams"/>
|
||||||
|
|
||||||
|
<form-qr-code class="mt-6" :form="props.form" :extra-query-param="shareUrlForQueryParams"/>
|
||||||
|
|
||||||
|
<advanced-form-url-settings :form="props.form" v-model="shareFormConfig"/>
|
||||||
|
|
||||||
<embed-form-as-popup-modal class="sm:w-1/2 flex" :form="props.form"/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<share-link class="mt-4" :form="props.form" :extra-query-param="shareUrlForQueryParams"/>
|
|
||||||
|
|
||||||
<embed-code class="mt-6" :form="props.form" :extra-query-param="shareUrlForQueryParams"/>
|
|
||||||
|
|
||||||
<form-qr-code class="mt-6" :form="props.form" :extra-query-param="shareUrlForQueryParams"/>
|
|
||||||
|
|
||||||
<advanced-form-url-settings :form="props.form" v-model="shareFormConfig"/>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="w-full md:w-4/5 lg:w-3/5 md:mx-auto md:max-w-4xl p-4">
|
||||||
<h3 class="font-semibold mt-4 text-xl">
|
<h3 class="font-semibold mt-4 text-xl">
|
||||||
Form Analytics (last 30 days)
|
Form Analytics (last 30 days)
|
||||||
</h3>
|
</h3>
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="table-page">
|
<form-submissions/>
|
||||||
<form-submissions/>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
@ -18,7 +16,7 @@ useOpnSeoMeta({
|
||||||
title: (props.form) ? 'Form Submissions - ' + props.form.title : 'Form Submissions'
|
title: (props.form) ? 'Form Submissions - ' + props.form.title : 'Form Submissions'
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeRouteLeave(()=>{
|
onBeforeRouteLeave(() => {
|
||||||
console.log('Clearing store state')
|
console.log('Clearing store state')
|
||||||
useRecordsStore().resetState()
|
useRecordsStore().resetState()
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue