Better submission table

This commit is contained in:
Julien Nahum 2024-02-22 16:56:35 +01:00
parent 062223fab2
commit e64d0d5da2
6 changed files with 154 additions and 128 deletions

View File

@ -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()
}, },

View File

@ -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)

View File

@ -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">

View File

@ -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>

View File

@ -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>

View File

@ -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()
}) })