This commit is contained in:
Julien Nahum
2024-02-03 17:41:57 +01:00
17 changed files with 254 additions and 100 deletions

View File

@@ -5,7 +5,6 @@
<template #label>
<slot name="label" />
</template>
<v-select v-model="compVal"
:data="finalOptions"
:label="label"
@@ -26,14 +25,14 @@
:help-position="helpPosition"
@update-options="updateOptions"
@update:model-value="updateModelValue"
>
<template #selected="{option}">
<slot name="selected" :option="option" :optionName="getOptionName(option)">
<template v-if="multiple">
<div class="flex items-center truncate mr-6">
<span v-for="(item,index) in option" :key="item" class="truncate">
<span v-if="index!==0">, </span>
{{ getOptionName(item) }}
<span class="truncate">
{{ selectedValues.join(', ') }}
</span>
</div>
</template>
@@ -104,7 +103,8 @@ export default {
data () {
return {
additionalOptions: []
additionalOptions: [],
selectedValues:[],
}
},
@@ -121,6 +121,9 @@ export default {
if (option) return option[this.displayKey]
return null
},
updateModelValue(newValues){
this.selectedValues = newValues
},
updateOptions (newItem) {
if (newItem) {
this.additionalOptions.push(newItem)

View File

@@ -1,5 +1,5 @@
<template>
<div class="v-select relative" ref="select">
<div class="v-select relative" :class="[{'w-0': multiple, 'min-w-full':multiple}]" ref="select">
<span class="inline-block w-full rounded-md">
<button type="button" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label"
class="cursor-pointer"

View File

@@ -0,0 +1,42 @@
<template>
<modal :show="show" max-width="lg" @close="emit('close')">
<open-form :theme="theme" :loading="false" :show-hidden="true" :form="form" :fields="form.properties" @submit="updateForm" :default-data-form="submission">
<template #submit-btn="{submitForm}">
<v-button :loading="loading" class="mt-2 px-8 mx-1" @click.prevent="submitForm">
Update Submission
</v-button>
</template>
</open-form>
</modal>
</template>
<script setup>
import {ref, defineProps, defineEmits, onMounted } from 'vue'
import OpenForm from '../forms/OpenForm.vue';
import { themes } from '~/lib/forms/form-themes.js'
const props = defineProps({
show: { type: Boolean, required: true },
form: { type: Object, required: true },
theme:{type:Object, default:themes.default},
submission:{type:Object}
})
let loading = ref(false)
const emit = defineEmits(['close', 'updated'])
const updateForm = (form, onFailure) =>{
loading.value = true
form.put('/open/forms/' + props.form.id + '/submissions/'+props.submission.id).then((res) => {
useAlert().success(res.message)
loading.value = false
emit('close')
emit('updated', res.data.data)
}).catch((error) => {
console.error(error)
loading.value = false
onFailure()
})
}
</script>

View File

@@ -1,5 +1,13 @@
<template>
<div class="flex items-center justify-center space-x-1">
<button v-track.delete_record_click
class="border rounded py-1 px-2 text-gray-500 dark:text-gray-400 hover:text-blue-700"
@click="showEditSubmissionModal=true"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" />
</svg>
</button>
<button v-track.delete_record_click
class="border rounded py-1 px-2 text-gray-500 dark:text-gray-400 hover:text-red-700"
@click="onDeleteClick"
@@ -13,12 +21,15 @@
</svg>
</button>
</div>
<EditSubmissionModal :show="showEditSubmissionModal" :form="form" :submission="submission" @close="showEditSubmissionModal=false" @updated="(submission)=>$emit('updated', submission)"/>
</template>
<script>
import EditSubmissionModal from './EditSubmissionModal.vue'
export default {
components: { },
components: { EditSubmissionModal },
emits: ["updated", "deleted"],
props: {
form: {
type: Object,
@@ -28,8 +39,8 @@ export default {
type: Array,
default: () => []
},
rowid: {
type: Number,
submission: {
type: Object,
default: () => {}
}
},
@@ -40,6 +51,7 @@ export default {
},
data () {
return {
showEditSubmissionModal:false,
}
},
computed: {
@@ -51,9 +63,9 @@ export default {
this.useAlert.confirm('Do you really want to delete this record?', this.deleteRecord)
},
async deleteRecord () {
opnFetch('/open/forms/' + this.form.id + '/records/' + this.rowid + '/delete', {method:'DELETE'}).then(async (data) => {
opnFetch('/open/forms/' + this.form.id + '/records/' + this.submission.id + '/delete', {method:'DELETE'}).then(async (data) => {
if (data.type === 'success') {
this.$emit('deleted')
this.$emit('deleted',this.submission)
this.useAlert.success(data.message)
} else {
this.useAlert.error('Something went wrong!')

View File

@@ -195,10 +195,7 @@ export default {
window.parent.postMessage(payload, '*')
}
window.postMessage(payload, '*')
try {
this.pendingSubmission.remove()
} catch (e) {}
this.pendingSubmission.remove()
if (data.redirect && data.redirect_url) {
window.location.href = data.redirect_url

View File

@@ -93,6 +93,7 @@ export default {
type: Array,
required: true
},
defaultDataForm:{},
adminPreview: { type: Boolean, default: false } // If used in FormEditorPreview
},
@@ -297,12 +298,17 @@ export default {
}
await this.recordsStore.loadRecord(
opnFetch('/forms/' + this.form.slug + '/submissions/' + this.form.submission_id).then((data) => {
return { submission_id: this.form.submission_id, ...data.data }
return { submission_id: this.form.submission_id, id: this.form.submission_id,...data.data }
})
)
return this.recordsStore.getById(this.form.submission_id)
return this.recordsStore.getByKey(this.form.submission_id)
},
async initForm () {
if(this.defaultDataForm){
this.dataForm = useForm(this.defaultDataForm)
return;
}
if (this.isPublicFormPage && this.form.editable_submissions) {
const urlParam = new URLSearchParams(window.location.search)
if (urlParam && urlParam.get('submission_id')) {

View File

@@ -56,7 +56,7 @@
</div>
</modal>
<Loader v-if="!form || !formInitDone" class="h-6 w-6 text-nt-blue mx-auto"/>
<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">
@@ -89,7 +89,8 @@
:data="filteredData"
:loading="isLoading"
@resize="dataChanged()"
@deleted="onDeleteRecord()"
@deleted="onDeleteRecord"
@updated="(submission)=>onUpdateRecord(submission)"
@update-columns="onColumnUpdated"
/>
</scroll-shadow>
@@ -102,7 +103,6 @@ import Fuse from 'fuse.js'
import clonedeep from 'clone-deep'
import VSwitch from '../../../forms/components/VSwitch.vue'
import OpenTable from '../../tables/OpenTable.vue'
import {now} from "@vueuse/core";
export default {
name: 'FormSubmissions',
@@ -111,17 +111,19 @@ export default {
setup() {
const workingFormStore = useWorkingFormStore()
const recordStore = useRecordsStore()
return {
workingFormStore,
runtimeConfig: useRuntimeConfig()
recordStore,
form: storeToRefs(workingFormStore).content,
tableData:storeToRefs(recordStore).getAll,
runtimeConfig: useRuntimeConfig(),
slug: useRoute().params.slug
}
},
data() {
return {
formInitDone: false,
isLoading: false,
tableData: [],
currentPage: 1,
fullyLoaded: false,
showColumnsModal: false,
@@ -134,20 +136,15 @@ export default {
}
},
computed: {
form: {
get() {
return this.workingFormStore.content
},
set(value) {
this.workingFormStore.set(value)
}
},
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 []
@@ -169,23 +166,22 @@ export default {
},
watch: {
'form.id'() {
if (this.form === null) {
return
}
this.initFormStructure()
this.getSubmissionsData()
this.onFormChange()
}
},
mounted() {
this.initFormStructure()
this.getSubmissionsData()
this.onFormChange()
},
methods: {
initFormStructure() {
if (!this.form || !this.form.properties || this.formInitDone) {
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 = clonedeep(this.form.properties)
if (!this.properties.find((property) => {
@@ -201,7 +197,6 @@ export default {
width: 140
})
}
this.formInitDone = true
this.removed_properties = (this.form.removed_properties) ? clonedeep(this.form.removed_properties) : []
// Get display columns from local storage
@@ -216,24 +211,22 @@ export default {
}
},
getSubmissionsData() {
if (!this.form || this.fullyLoaded) {
if (this.fullyLoaded) {
return
}
this.isLoading = true
this.recordStore.startLoading()
opnFetch('/open/forms/' + this.form.id + '/submissions?page=' + this.currentPage).then((resData) => {
this.tableData = this.tableData.concat(resData.data.map((record) => record.data))
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.isLoading = false
this.recordStore.stopLoading()
this.fullyLoaded = true
}
}).catch((error) => {
console.error(error)
this.isLoading = false
this.recordStore.startLoading()
})
},
dataChanged() {
@@ -252,10 +245,13 @@ export default {
return this.displayColumns[field.id] === true
})
},
onDeleteRecord() {
this.fullyLoaded = false
this.tableData = []
this.getSubmissionsData()
onUpdateRecord(submission){
this.recordStore.save(submission);
this.dataChanged()
},
onDeleteRecord(submission) {
this.recordStore.remove(submission);
this.dataChanged()
},
downloadAsCsv() {
opnFetch(this.exportUrl, {responseType: "blob"})

View File

@@ -53,7 +53,9 @@
<td v-if="hasActions" class="n-table-cell border-gray-100 dark:border-gray-900 text-sm p-2 border-b"
style="width: 100px"
>
<record-operations :form="form" :structure="columns" :rowid="row.id" @deleted="$emit('deleted')"/>
<record-operations :form="form" :structure="columns" :submission="row"
@deleted="(submission)=>$emit('deleted',submission)"
@updated="(submission)=>$emit('updated', submission)"/>
</td>
</tr>
<tr v-if="loading" class="n-table-row border-t bg-gray-50 dark:bg-gray-900">
@@ -89,6 +91,7 @@ import {hash} from "~/lib/utils.js";
export default {
components: {ResizableTh, RecordOperations},
emits: ["updated", "deleted"],
props: {
columns: {
type: Array,

View File

@@ -13,7 +13,7 @@ export const pendingSubmission = (form) => {
const set = (value) => {
if (process.server || !enabled.value) return
useStorage(formPendingSubmissionKey.value).value = JSON.stringify(value)
useStorage(formPendingSubmissionKey.value).value = value === null ? value : JSON.stringify(value)
}
const remove = () => {
@@ -29,6 +29,7 @@ export const pendingSubmission = (form) => {
return {
enabled,
set,
get
get,
remove
}
}

View File

@@ -162,6 +162,7 @@ const workspacesStore = useWorkspacesStore()
const slug = useRoute().params.slug
formsStore.startLoading()
const user = computed(() => authStore.user)
const form = computed(() => formsStore.getByKey(slug))
const workspace = computed(() => workspacesStore.getByKey(form?.value?.workspace_id))

View File

@@ -18,4 +18,9 @@ useOpnSeoMeta({
title: (props.form) ? 'Form Submissions - ' + props.form.title : 'Form Submissions'
})
onBeforeRouteLeave(()=>{
console.log('Clearing store state')
useRecordsStore().resetState()
})
</script>

View File

@@ -1,49 +1,21 @@
import { defineStore } from 'pinia'
export const namespaced = true
import { useContentStore } from '~/composables/stores/useContentStore'
/**
* Loads records from database
*/
export const useRecordsStore = defineStore('records', {
state: () => ({
content: [],
loading: false
}),
getters: {
getById: (state) => (id) => {
if (state.content.length === 0) return null
return state.content.find(item => item.submission_id === id)
}
},
actions: {
set (items) {
this.content = items
},
addOrUpdate (item) {
this.content = this.content.filter((val) => val.id !== item.id)
this.content.push(item)
},
remove (itemId) {
this.content = this.content.filter((val) => val.id !== itemId)
},
startLoading () {
this.loading = true
},
stopLoading () {
this.loading = false
},
resetState () {
this.set([])
this.stopLoading()
},
loadRecord (request) {
this.set([])
this.startLoading()
return request.then((data) => {
this.addOrUpdate(data)
this.stopLoading()
})
}
export const useRecordsStore = defineStore('records', ()=>{
const contentStore = useContentStore()
const loadRecord = (request)=> {
contentStore.resetState()
contentStore.startLoading()
return request.then((data) => {
contentStore.save(data)
contentStore.stopLoading()
})
}
})
return {...contentStore, loadRecord}
})