Merge branch 'main' of https://github.com/JhumanJ/OpnForm
This commit is contained in:
commit
4ae0e7fa68
|
@ -6,7 +6,11 @@ use App\Http\Controllers\Controller;
|
||||||
use App\Http\Resources\FormSubmissionResource;
|
use App\Http\Resources\FormSubmissionResource;
|
||||||
use App\Models\Forms\Form;
|
use App\Models\Forms\Form;
|
||||||
use App\Exports\FormSubmissionExport;
|
use App\Exports\FormSubmissionExport;
|
||||||
|
use App\Http\Requests\AnswerFormRequest;
|
||||||
|
use App\Jobs\Form\StoreFormSubmissionJob;
|
||||||
|
use App\Models\Forms\FormSubmission;
|
||||||
use App\Service\Forms\FormSubmissionFormatter;
|
use App\Service\Forms\FormSubmissionFormatter;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Maatwebsite\Excel\Facades\Excel;
|
use Maatwebsite\Excel\Facades\Excel;
|
||||||
|
@ -27,6 +31,20 @@ class FormSubmissionController extends Controller
|
||||||
return FormSubmissionResource::collection($form->submissions()->paginate(100));
|
return FormSubmissionResource::collection($form->submissions()->paginate(100));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function update(AnswerFormRequest $request, $id, $submissionId)
|
||||||
|
{
|
||||||
|
$form = $request->form;
|
||||||
|
$this->authorize('update', $form);
|
||||||
|
$job = new StoreFormSubmissionJob($request->form, $request->validated());
|
||||||
|
$job->setSubmissionId($submissionId)->handle();
|
||||||
|
|
||||||
|
$data = new FormSubmissionResource(FormSubmission::findOrFail($submissionId));
|
||||||
|
return $this->success([
|
||||||
|
'message' => 'Record successfully updated.',
|
||||||
|
'data' => $data
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function export(string $id)
|
public function export(string $id)
|
||||||
{
|
{
|
||||||
$form = Form::findOrFail((int) $id);
|
$form = Form::findOrFail((int) $id);
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware\Form;
|
||||||
|
|
||||||
|
use App\Models\Forms\Form;
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class ResolveFormMiddleware
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
|
||||||
|
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Closure $next, string $routeParamName = "id")
|
||||||
|
{
|
||||||
|
$form = Form::where($routeParamName,$request->route($routeParamName))->firstOrFail();
|
||||||
|
$request->merge([
|
||||||
|
'form' => $form,
|
||||||
|
]);
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
|
@ -56,6 +56,12 @@ class StoreFormSubmissionJob implements ShouldQueue
|
||||||
return $this->submissionId;
|
return $this->submissionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setSubmissionId(int $id)
|
||||||
|
{
|
||||||
|
$this->submissionId = $id;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
private function storeSubmission(array $formData)
|
private function storeSubmission(array $formData)
|
||||||
{
|
{
|
||||||
// Create or update record
|
// Create or update record
|
||||||
|
@ -76,6 +82,9 @@ class StoreFormSubmissionJob implements ShouldQueue
|
||||||
*/
|
*/
|
||||||
private function submissionToUpdate(): ?FormSubmission
|
private function submissionToUpdate(): ?FormSubmission
|
||||||
{
|
{
|
||||||
|
if($this->submissionId){
|
||||||
|
return $this->form->submissions()->findOrFail($this->submissionId);
|
||||||
|
}
|
||||||
if ($this->form->editable_submissions && isset($this->submissionData['submission_id']) && $this->submissionData['submission_id']) {
|
if ($this->form->editable_submissions && isset($this->submissionData['submission_id']) && $this->submissionData['submission_id']) {
|
||||||
$submissionId = $this->submissionData['submission_id'] ? Hashids::decode($this->submissionData['submission_id']) : false;
|
$submissionId = $this->submissionData['submission_id'] ? Hashids::decode($this->submissionData['submission_id']) : false;
|
||||||
$submissionId = $submissionId[0] ?? null;
|
$submissionId = $submissionId[0] ?? null;
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
<template #label>
|
<template #label>
|
||||||
<slot name="label" />
|
<slot name="label" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<v-select v-model="compVal"
|
<v-select v-model="compVal"
|
||||||
:data="finalOptions"
|
:data="finalOptions"
|
||||||
:label="label"
|
:label="label"
|
||||||
|
@ -26,14 +25,14 @@
|
||||||
:help-position="helpPosition"
|
:help-position="helpPosition"
|
||||||
|
|
||||||
@update-options="updateOptions"
|
@update-options="updateOptions"
|
||||||
|
@update:model-value="updateModelValue"
|
||||||
>
|
>
|
||||||
<template #selected="{option}">
|
<template #selected="{option}">
|
||||||
<slot name="selected" :option="option" :optionName="getOptionName(option)">
|
<slot name="selected" :option="option" :optionName="getOptionName(option)">
|
||||||
<template v-if="multiple">
|
<template v-if="multiple">
|
||||||
<div class="flex items-center truncate mr-6">
|
<div class="flex items-center truncate mr-6">
|
||||||
<span v-for="(item,index) in option" :key="item" class="truncate">
|
<span class="truncate">
|
||||||
<span v-if="index!==0">, </span>
|
{{ selectedValues.join(', ') }}
|
||||||
{{ getOptionName(item) }}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -104,7 +103,8 @@ export default {
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
additionalOptions: []
|
additionalOptions: [],
|
||||||
|
selectedValues:[],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -121,6 +121,9 @@ export default {
|
||||||
if (option) return option[this.displayKey]
|
if (option) return option[this.displayKey]
|
||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
|
updateModelValue(newValues){
|
||||||
|
this.selectedValues = newValues
|
||||||
|
},
|
||||||
updateOptions (newItem) {
|
updateOptions (newItem) {
|
||||||
if (newItem) {
|
if (newItem) {
|
||||||
this.additionalOptions.push(newItem)
|
this.additionalOptions.push(newItem)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<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">
|
<span class="inline-block w-full rounded-md">
|
||||||
<button type="button" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label"
|
<button type="button" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label"
|
||||||
class="cursor-pointer"
|
class="cursor-pointer"
|
||||||
|
|
|
@ -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>
|
|
@ -1,5 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-center justify-center space-x-1">
|
<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
|
<button v-track.delete_record_click
|
||||||
class="border rounded py-1 px-2 text-gray-500 dark:text-gray-400 hover:text-red-700"
|
class="border rounded py-1 px-2 text-gray-500 dark:text-gray-400 hover:text-red-700"
|
||||||
@click="onDeleteClick"
|
@click="onDeleteClick"
|
||||||
|
@ -13,12 +21,15 @@
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<EditSubmissionModal :show="showEditSubmissionModal" :form="form" :submission="submission" @close="showEditSubmissionModal=false" @updated="(submission)=>$emit('updated', submission)"/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import EditSubmissionModal from './EditSubmissionModal.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { },
|
components: { EditSubmissionModal },
|
||||||
|
emits: ["updated", "deleted"],
|
||||||
props: {
|
props: {
|
||||||
form: {
|
form: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -28,8 +39,8 @@ export default {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
rowid: {
|
submission: {
|
||||||
type: Number,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -40,6 +51,7 @@ export default {
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
showEditSubmissionModal:false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -51,9 +63,9 @@ export default {
|
||||||
this.useAlert.confirm('Do you really want to delete this record?', this.deleteRecord)
|
this.useAlert.confirm('Do you really want to delete this record?', this.deleteRecord)
|
||||||
},
|
},
|
||||||
async 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') {
|
if (data.type === 'success') {
|
||||||
this.$emit('deleted')
|
this.$emit('deleted',this.submission)
|
||||||
this.useAlert.success(data.message)
|
this.useAlert.success(data.message)
|
||||||
} else {
|
} else {
|
||||||
this.useAlert.error('Something went wrong!')
|
this.useAlert.error('Something went wrong!')
|
||||||
|
|
|
@ -195,10 +195,7 @@ export default {
|
||||||
window.parent.postMessage(payload, '*')
|
window.parent.postMessage(payload, '*')
|
||||||
}
|
}
|
||||||
window.postMessage(payload, '*')
|
window.postMessage(payload, '*')
|
||||||
|
this.pendingSubmission.remove()
|
||||||
try {
|
|
||||||
this.pendingSubmission.remove()
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
if (data.redirect && data.redirect_url) {
|
if (data.redirect && data.redirect_url) {
|
||||||
window.location.href = data.redirect_url
|
window.location.href = data.redirect_url
|
||||||
|
|
|
@ -93,6 +93,7 @@ export default {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
defaultDataForm:{},
|
||||||
adminPreview: { type: Boolean, default: false } // If used in FormEditorPreview
|
adminPreview: { type: Boolean, default: false } // If used in FormEditorPreview
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -297,12 +298,17 @@ export default {
|
||||||
}
|
}
|
||||||
await this.recordsStore.loadRecord(
|
await this.recordsStore.loadRecord(
|
||||||
opnFetch('/forms/' + this.form.slug + '/submissions/' + this.form.submission_id).then((data) => {
|
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 () {
|
async initForm () {
|
||||||
|
if(this.defaultDataForm){
|
||||||
|
this.dataForm = useForm(this.defaultDataForm)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isPublicFormPage && this.form.editable_submissions) {
|
if (this.isPublicFormPage && this.form.editable_submissions) {
|
||||||
const urlParam = new URLSearchParams(window.location.search)
|
const urlParam = new URLSearchParams(window.location.search)
|
||||||
if (urlParam && urlParam.get('submission_id')) {
|
if (urlParam && urlParam.get('submission_id')) {
|
||||||
|
|
|
@ -56,7 +56,7 @@
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</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-else>
|
||||||
<div v-if="form && tableData.length > 0" class="flex flex-wrap items-end">
|
<div v-if="form && tableData.length > 0" class="flex flex-wrap items-end">
|
||||||
<div class="flex-grow">
|
<div class="flex-grow">
|
||||||
|
@ -89,7 +89,8 @@
|
||||||
:data="filteredData"
|
:data="filteredData"
|
||||||
:loading="isLoading"
|
:loading="isLoading"
|
||||||
@resize="dataChanged()"
|
@resize="dataChanged()"
|
||||||
@deleted="onDeleteRecord()"
|
@deleted="onDeleteRecord"
|
||||||
|
@updated="(submission)=>onUpdateRecord(submission)"
|
||||||
@update-columns="onColumnUpdated"
|
@update-columns="onColumnUpdated"
|
||||||
/>
|
/>
|
||||||
</scroll-shadow>
|
</scroll-shadow>
|
||||||
|
@ -102,7 +103,6 @@ import Fuse from 'fuse.js'
|
||||||
import clonedeep from 'clone-deep'
|
import clonedeep from 'clone-deep'
|
||||||
import VSwitch from '../../../forms/components/VSwitch.vue'
|
import VSwitch from '../../../forms/components/VSwitch.vue'
|
||||||
import OpenTable from '../../tables/OpenTable.vue'
|
import OpenTable from '../../tables/OpenTable.vue'
|
||||||
import {now} from "@vueuse/core";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FormSubmissions',
|
name: 'FormSubmissions',
|
||||||
|
@ -111,17 +111,19 @@ export default {
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
const workingFormStore = useWorkingFormStore()
|
const workingFormStore = useWorkingFormStore()
|
||||||
|
const recordStore = useRecordsStore()
|
||||||
return {
|
return {
|
||||||
workingFormStore,
|
workingFormStore,
|
||||||
runtimeConfig: useRuntimeConfig()
|
recordStore,
|
||||||
|
form: storeToRefs(workingFormStore).content,
|
||||||
|
tableData:storeToRefs(recordStore).getAll,
|
||||||
|
runtimeConfig: useRuntimeConfig(),
|
||||||
|
slug: useRoute().params.slug
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
formInitDone: false,
|
|
||||||
isLoading: false,
|
|
||||||
tableData: [],
|
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
fullyLoaded: false,
|
fullyLoaded: false,
|
||||||
showColumnsModal: false,
|
showColumnsModal: false,
|
||||||
|
@ -134,20 +136,15 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
form: {
|
|
||||||
get() {
|
|
||||||
return this.workingFormStore.content
|
|
||||||
},
|
|
||||||
set(value) {
|
|
||||||
this.workingFormStore.set(value)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
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(){
|
||||||
|
return this.recordStore.loading
|
||||||
|
},
|
||||||
filteredData() {
|
filteredData() {
|
||||||
if (!this.tableData) return []
|
if (!this.tableData) return []
|
||||||
|
|
||||||
|
@ -169,23 +166,22 @@ export default {
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'form.id'() {
|
'form.id'() {
|
||||||
if (this.form === null) {
|
this.onFormChange()
|
||||||
return
|
|
||||||
}
|
|
||||||
this.initFormStructure()
|
|
||||||
this.getSubmissionsData()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.initFormStructure()
|
this.onFormChange()
|
||||||
this.getSubmissionsData()
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
initFormStructure() {
|
onFormChange() {
|
||||||
if (!this.form || !this.form.properties || this.formInitDone) {
|
if (this.form === null || this.form.slug !== this.slug) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
this.fullyLoaded = false
|
||||||
|
this.initFormStructure()
|
||||||
|
this.getSubmissionsData()
|
||||||
|
},
|
||||||
|
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 = clonedeep(this.form.properties)
|
||||||
if (!this.properties.find((property) => {
|
if (!this.properties.find((property) => {
|
||||||
|
@ -201,7 +197,6 @@ export default {
|
||||||
width: 140
|
width: 140
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
this.formInitDone = true
|
|
||||||
this.removed_properties = (this.form.removed_properties) ? clonedeep(this.form.removed_properties) : []
|
this.removed_properties = (this.form.removed_properties) ? clonedeep(this.form.removed_properties) : []
|
||||||
|
|
||||||
// Get display columns from local storage
|
// Get display columns from local storage
|
||||||
|
@ -216,24 +211,22 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getSubmissionsData() {
|
getSubmissionsData() {
|
||||||
if (!this.form || this.fullyLoaded) {
|
if (this.fullyLoaded) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.isLoading = true
|
this.recordStore.startLoading()
|
||||||
opnFetch('/open/forms/' + this.form.id + '/submissions?page=' + this.currentPage).then((resData) => {
|
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()
|
this.dataChanged()
|
||||||
|
|
||||||
if (this.currentPage < resData.meta.last_page) {
|
if (this.currentPage < resData.meta.last_page) {
|
||||||
this.currentPage += 1
|
this.currentPage += 1
|
||||||
this.getSubmissionsData()
|
this.getSubmissionsData()
|
||||||
} else {
|
} else {
|
||||||
this.isLoading = false
|
this.recordStore.stopLoading()
|
||||||
this.fullyLoaded = true
|
this.fullyLoaded = true
|
||||||
}
|
}
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
console.error(error)
|
this.recordStore.startLoading()
|
||||||
this.isLoading = false
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
dataChanged() {
|
dataChanged() {
|
||||||
|
@ -252,10 +245,13 @@ export default {
|
||||||
return this.displayColumns[field.id] === true
|
return this.displayColumns[field.id] === true
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onDeleteRecord() {
|
onUpdateRecord(submission){
|
||||||
this.fullyLoaded = false
|
this.recordStore.save(submission);
|
||||||
this.tableData = []
|
this.dataChanged()
|
||||||
this.getSubmissionsData()
|
},
|
||||||
|
onDeleteRecord(submission) {
|
||||||
|
this.recordStore.remove(submission);
|
||||||
|
this.dataChanged()
|
||||||
},
|
},
|
||||||
downloadAsCsv() {
|
downloadAsCsv() {
|
||||||
opnFetch(this.exportUrl, {responseType: "blob"})
|
opnFetch(this.exportUrl, {responseType: "blob"})
|
||||||
|
|
|
@ -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"
|
<td v-if="hasActions" class="n-table-cell border-gray-100 dark:border-gray-900 text-sm p-2 border-b"
|
||||||
style="width: 100px"
|
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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="loading" class="n-table-row border-t bg-gray-50 dark:bg-gray-900">
|
<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 {
|
export default {
|
||||||
components: {ResizableTh, RecordOperations},
|
components: {ResizableTh, RecordOperations},
|
||||||
|
emits: ["updated", "deleted"],
|
||||||
props: {
|
props: {
|
||||||
columns: {
|
columns: {
|
||||||
type: Array,
|
type: Array,
|
||||||
|
|
|
@ -13,7 +13,7 @@ export const pendingSubmission = (form) => {
|
||||||
|
|
||||||
const set = (value) => {
|
const set = (value) => {
|
||||||
if (process.server || !enabled.value) return
|
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 = () => {
|
const remove = () => {
|
||||||
|
@ -29,6 +29,7 @@ export const pendingSubmission = (form) => {
|
||||||
return {
|
return {
|
||||||
enabled,
|
enabled,
|
||||||
set,
|
set,
|
||||||
get
|
get,
|
||||||
|
remove
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -162,6 +162,7 @@ const workspacesStore = useWorkspacesStore()
|
||||||
|
|
||||||
const slug = useRoute().params.slug
|
const slug = useRoute().params.slug
|
||||||
|
|
||||||
|
formsStore.startLoading()
|
||||||
const user = computed(() => authStore.user)
|
const user = computed(() => authStore.user)
|
||||||
const form = computed(() => formsStore.getByKey(slug))
|
const form = computed(() => formsStore.getByKey(slug))
|
||||||
const workspace = computed(() => workspacesStore.getByKey(form?.value?.workspace_id))
|
const workspace = computed(() => workspacesStore.getByKey(form?.value?.workspace_id))
|
||||||
|
|
|
@ -18,4 +18,9 @@ useOpnSeoMeta({
|
||||||
title: (props.form) ? 'Form Submissions - ' + props.form.title : 'Form Submissions'
|
title: (props.form) ? 'Form Submissions - ' + props.form.title : 'Form Submissions'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onBeforeRouteLeave(()=>{
|
||||||
|
console.log('Clearing store state')
|
||||||
|
useRecordsStore().resetState()
|
||||||
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,49 +1,21 @@
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
|
import { useContentStore } from '~/composables/stores/useContentStore'
|
||||||
export const namespaced = true
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads records from database
|
* Loads records from database
|
||||||
*/
|
*/
|
||||||
export const useRecordsStore = defineStore('records', {
|
export const useRecordsStore = defineStore('records', ()=>{
|
||||||
state: () => ({
|
|
||||||
content: [],
|
const contentStore = useContentStore()
|
||||||
loading: false
|
|
||||||
}),
|
const loadRecord = (request)=> {
|
||||||
getters: {
|
contentStore.resetState()
|
||||||
getById: (state) => (id) => {
|
contentStore.startLoading()
|
||||||
if (state.content.length === 0) return null
|
return request.then((data) => {
|
||||||
return state.content.find(item => item.submission_id === id)
|
contentStore.save(data)
|
||||||
}
|
contentStore.stopLoading()
|
||||||
},
|
})
|
||||||
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()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
return {...contentStore, loadRecord}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
|
@ -18,6 +18,7 @@ use App\Http\Controllers\Forms\RecordController;
|
||||||
use App\Http\Controllers\WorkspaceController;
|
use App\Http\Controllers\WorkspaceController;
|
||||||
use App\Http\Controllers\TemplateController;
|
use App\Http\Controllers\TemplateController;
|
||||||
use App\Http\Controllers\Forms\Integration\FormZapierWebhookController;
|
use App\Http\Controllers\Forms\Integration\FormZapierWebhookController;
|
||||||
|
use App\Http\Middleware\Form\ResolveFormMiddleware;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
@ -85,6 +86,7 @@ Route::group(['middleware' => 'auth:api'], function () {
|
||||||
Route::delete('/{id}', [FormController::class, 'destroy'])->name('destroy');
|
Route::delete('/{id}', [FormController::class, 'destroy'])->name('destroy');
|
||||||
|
|
||||||
Route::get('/{id}/submissions', [FormSubmissionController::class, 'submissions'])->name('submissions');
|
Route::get('/{id}/submissions', [FormSubmissionController::class, 'submissions'])->name('submissions');
|
||||||
|
Route::put('/{id}/submissions/{submission_id}', [FormSubmissionController::class, 'update'])->name('submissions.update')->middleware([ResolveFormMiddleware::class]);
|
||||||
Route::get('/{id}/submissions/export', [FormSubmissionController::class, 'export'])->name('submissions.export');
|
Route::get('/{id}/submissions/export', [FormSubmissionController::class, 'export'])->name('submissions.export');
|
||||||
Route::get('/{id}/submissions/file/{filename}', [FormSubmissionController::class, 'submissionFile'])
|
Route::get('/{id}/submissions/file/{filename}', [FormSubmissionController::class, 'submissionFile'])
|
||||||
->middleware('signed')
|
->middleware('signed')
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Tests\Helpers\FormSubmissionDataFactory;
|
||||||
|
|
||||||
|
it('can update form submission', function () {
|
||||||
|
$user = $this->actingAsUser();
|
||||||
|
$workspace = $this->createUserWorkspace($user);
|
||||||
|
$form = $this->makeForm($user, $workspace);
|
||||||
|
$form = $this->createForm($user, $workspace, [
|
||||||
|
'closes_at' => \Carbon\Carbon::now()->addDays(1)->toDateTimeString(),
|
||||||
|
]);
|
||||||
|
$formData = FormSubmissionDataFactory::generateSubmissionData($form, ['text' => 'John']);
|
||||||
|
$textFieldId = array_keys($formData)[0];
|
||||||
|
$updatedFormData = $formData;
|
||||||
|
$updatedFormTextValue = "Updated text";
|
||||||
|
$updatedFormData[$textFieldId] = $updatedFormTextValue;
|
||||||
|
$this->postJson(route('forms.answer', $form->slug), $formData)
|
||||||
|
->assertSuccessful()
|
||||||
|
->assertJson([
|
||||||
|
'type' => 'success',
|
||||||
|
'message' => 'Form submission saved.'
|
||||||
|
]);
|
||||||
|
$submission = $form->submissions()->first();
|
||||||
|
$updateResponse = $this->putJson(route('open.forms.submissions.update', ['id'=>$form->id, 'submission_id' => $submission->id]), $updatedFormData)
|
||||||
|
->assertSuccessful()
|
||||||
|
->assertJson([
|
||||||
|
'type' => 'success',
|
||||||
|
'message' => 'Record successfully updated.'
|
||||||
|
]);
|
||||||
|
$expectedTextString = $updateResponse->json('data')['data'][$textFieldId];
|
||||||
|
expect($expectedTextString)->toBe($updatedFormTextValue);
|
||||||
|
$updatedSubmission = $form->submissions()->first();
|
||||||
|
expect($updatedSubmission->data[$textFieldId])->toBe($updatedFormTextValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cannot update form submission as non admin', function () {
|
||||||
|
$secondUser =$this->createUser();
|
||||||
|
$user = $this->actingAsUser();
|
||||||
|
$workspace = $this->createUserWorkspace($user);
|
||||||
|
$form = $this->makeForm($user, $workspace);
|
||||||
|
$form = $this->createForm($user, $workspace, [
|
||||||
|
'closes_at' => \Carbon\Carbon::now()->addDays(1)->toDateTimeString(),
|
||||||
|
]);
|
||||||
|
$formData = FormSubmissionDataFactory::generateSubmissionData($form, ['text' => 'John']);
|
||||||
|
$textFieldId = array_keys($formData)[0];
|
||||||
|
$updatedFormData = $formData;
|
||||||
|
$updatedFormTextValue = "Updated text";
|
||||||
|
$updatedFormData[$textFieldId] = $updatedFormTextValue;
|
||||||
|
$this->postJson(route('forms.answer', $form->slug), $formData)
|
||||||
|
->assertSuccessful()
|
||||||
|
->assertJson([
|
||||||
|
'type' => 'success',
|
||||||
|
'message' => 'Form submission saved.'
|
||||||
|
]);
|
||||||
|
$submission = $form->submissions()->first();
|
||||||
|
$this->actingAs($secondUser);
|
||||||
|
$updateResponse = $this->putJson(route('open.forms.submissions.update', ['id'=>$form->id, 'submission_id' => $submission->id]), $updatedFormData)
|
||||||
|
->assertStatus(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue