352 lines
10 KiB
Vue
352 lines
10 KiB
Vue
<template>
|
|
<div
|
|
id="table-page"
|
|
class="w-full flex flex-col"
|
|
>
|
|
<div class="w-full md:w-4/5 lg:w-3/5 md:mx-auto md:max-w-4xl px-4 pt-4">
|
|
<h3 class="font-semibold mb-4 text-xl">
|
|
Form Submissions
|
|
</h3>
|
|
|
|
<!-- Table columns modal -->
|
|
<modal
|
|
:show="showColumnsModal"
|
|
@close="showColumnsModal=false"
|
|
>
|
|
<template #icon>
|
|
<svg
|
|
class="w-8 h-8"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
<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"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
/>
|
|
<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"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
/>
|
|
</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 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>
|
|
</modal>
|
|
|
|
<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
|
|
v-if="!exportLoading"
|
|
class="text-right cursor-pointer text-xs uppercase"
|
|
>
|
|
<a
|
|
href="#"
|
|
@click.prevent="downloadAsCsv"
|
|
>Export as CSV</a>
|
|
</p>
|
|
<p v-else>
|
|
<loader class="w-3 h-3 text-blue-500" />
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="px-4 pb-4 flex justify-center">
|
|
<scroll-shadow
|
|
ref="shadows"
|
|
class="border h-full notion-database-renderer"
|
|
:shadow-top-offset="0"
|
|
:hide-scrollbar="true"
|
|
>
|
|
<open-table
|
|
v-if="form"
|
|
ref="table"
|
|
class="max-h-full"
|
|
:columns="properties"
|
|
:data="filteredData"
|
|
:loading="isLoading"
|
|
:scroll-parent="parentPage"
|
|
@resize="dataChanged()"
|
|
@deleted="onDeleteRecord"
|
|
@updated="(submission)=>onUpdateRecord(submission)"
|
|
@update-columns="onColumnUpdated"
|
|
/>
|
|
</scroll-shadow>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import Fuse from 'fuse.js'
|
|
import clonedeep from 'clone-deep'
|
|
import VSwitch from '../../../forms/components/VSwitch.vue'
|
|
import OpenTable from '../../tables/OpenTable.vue'
|
|
|
|
export default {
|
|
name: 'FormSubmissions',
|
|
components: {OpenTable, VSwitch},
|
|
props: {},
|
|
|
|
setup() {
|
|
const workingFormStore = useWorkingFormStore()
|
|
const recordStore = useRecordsStore()
|
|
return {
|
|
workingFormStore,
|
|
recordStore,
|
|
form: storeToRefs(workingFormStore).content,
|
|
tableData: storeToRefs(recordStore).getAll,
|
|
runtimeConfig: useRuntimeConfig(),
|
|
slug: useRoute().params.slug
|
|
}
|
|
},
|
|
|
|
data() {
|
|
return {
|
|
currentPage: 1,
|
|
fullyLoaded: false,
|
|
showColumnsModal: false,
|
|
properties: [],
|
|
removed_properties: [],
|
|
displayColumns: {},
|
|
exportLoading: false,
|
|
searchForm: useForm({
|
|
search: ''
|
|
}),
|
|
}
|
|
},
|
|
computed: {
|
|
parentPage() {
|
|
if (import.meta.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() {
|
|
if (!this.form) {
|
|
return ''
|
|
}
|
|
return this.runtimeConfig.public.apiBase + '/open/forms/' + this.form.id + '/submissions/export'
|
|
},
|
|
isLoading() {
|
|
return this.recordStore.loading
|
|
},
|
|
filteredData() {
|
|
if (!this.tableData) return []
|
|
|
|
const filteredData = clonedeep(this.tableData)
|
|
|
|
if (this.searchForm.search === '' || this.searchForm.search === null) {
|
|
return filteredData
|
|
}
|
|
|
|
// Fuze search
|
|
const fuzeOptions = {
|
|
keys: this.properties.map((field) => field.id)
|
|
}
|
|
const fuse = new Fuse(filteredData, fuzeOptions)
|
|
return fuse.search(this.searchForm.search).map((res) => {
|
|
return res.item
|
|
})
|
|
}
|
|
},
|
|
watch: {
|
|
'form.id'() {
|
|
this.onFormChange()
|
|
},
|
|
'searchForm.search'() {
|
|
this.dataChanged()
|
|
}
|
|
},
|
|
mounted() {
|
|
this.onFormChange()
|
|
},
|
|
methods: {
|
|
onFormChange() {
|
|
if (this.form === null || this.form.slug !== this.slug) {
|
|
return
|
|
}
|
|
this.fullyLoaded = false
|
|
this.initFormStructure()
|
|
this.getSubmissionsData()
|
|
},
|
|
initFormStructure() {
|
|
// check if form properties already has a created_at column
|
|
this.properties = this.candidatesProperties
|
|
if (!this.properties.find((property) => {
|
|
if (property.id === 'created_at') {
|
|
return true
|
|
}
|
|
})) {
|
|
// Add a "created at" column
|
|
this.properties.push({
|
|
name: 'Created at',
|
|
id: 'created_at',
|
|
type: 'date',
|
|
width: 140
|
|
})
|
|
}
|
|
this.removed_properties = (this.form.removed_properties) ? clonedeep(this.form.removed_properties) : []
|
|
|
|
// Get display columns from local storage
|
|
const tmpColumns = window.localStorage.getItem('display-columns-formid-' + this.form.id)
|
|
if (tmpColumns !== null && tmpColumns) {
|
|
this.displayColumns = JSON.parse(tmpColumns)
|
|
this.onChangeDisplayColumns()
|
|
} else {
|
|
this.properties.forEach((field) => {
|
|
this.displayColumns[field.id] = true
|
|
})
|
|
}
|
|
},
|
|
getSubmissionsData() {
|
|
if (this.fullyLoaded) {
|
|
return
|
|
}
|
|
this.recordStore.startLoading()
|
|
opnFetch('/open/forms/' + this.form.id + '/submissions?page=' + this.currentPage).then((resData) => {
|
|
this.recordStore.save(resData.data.map((record) => record.data))
|
|
this.dataChanged()
|
|
if (this.currentPage < resData.meta.last_page) {
|
|
this.currentPage += 1
|
|
this.getSubmissionsData()
|
|
} else {
|
|
this.recordStore.stopLoading()
|
|
this.fullyLoaded = true
|
|
}
|
|
}).catch(() => {
|
|
this.recordStore.startLoading()
|
|
})
|
|
},
|
|
dataChanged() {
|
|
if (this.$refs.shadows) {
|
|
this.$refs.shadows.toggleShadow()
|
|
this.$refs.shadows.calcDimensions()
|
|
}
|
|
},
|
|
onColumnUpdated(columns) {
|
|
this.properties = columns
|
|
},
|
|
onChangeDisplayColumns() {
|
|
if (!import.meta.client) return
|
|
window.localStorage.setItem('display-columns-formid-' + this.form.id, JSON.stringify(this.displayColumns))
|
|
this.properties = clonedeep(this.form.properties).concat(this.removed_properties).filter((field) => {
|
|
return this.displayColumns[field.id] === true
|
|
})
|
|
},
|
|
onUpdateRecord(submission) {
|
|
this.recordStore.save(submission)
|
|
this.dataChanged()
|
|
},
|
|
onDeleteRecord(submission) {
|
|
this.recordStore.remove(submission)
|
|
this.dataChanged()
|
|
},
|
|
downloadAsCsv() {
|
|
if (this.exportLoading) {
|
|
return
|
|
}
|
|
this.exportLoading = true
|
|
opnFetch(this.exportUrl, {responseType: "blob"})
|
|
.then(blob => {
|
|
const filename = `${this.form.slug}-${Date.now()}-submissions.csv`
|
|
const a = document.createElement("a")
|
|
document.body.appendChild(a)
|
|
a.style = "display: none"
|
|
const url = window.URL.createObjectURL(blob)
|
|
a.href = url
|
|
a.download = filename
|
|
a.click()
|
|
window.URL.revokeObjectURL(url)
|
|
}).catch((error) => {
|
|
console.error(error)
|
|
}).finally(() => {
|
|
this.exportLoading = false
|
|
})
|
|
}
|
|
}
|
|
}
|
|
</script>
|