Merge branch 'main' into new-ui
This commit is contained in:
commit
8068ed1129
|
@ -34,7 +34,7 @@ class FormController extends Controller
|
||||||
$this->authorize('viewAny', Form::class);
|
$this->authorize('viewAny', Form::class);
|
||||||
|
|
||||||
$workspaceIsPro = $workspace->is_pro;
|
$workspaceIsPro = $workspace->is_pro;
|
||||||
$forms = $workspace->forms()->with(['creator','views','submissions'])->get()->map(function (Form $form) use ($workspace, $workspaceIsPro){
|
$forms = $workspace->forms()->with(['creator','views','submissions'])->paginate(10)->through(function (Form $form) use ($workspace, $workspaceIsPro){
|
||||||
// Add attributes for faster loading
|
// Add attributes for faster loading
|
||||||
$form->extra = (object) [
|
$form->extra = (object) [
|
||||||
'loadedWorkspace' => $workspace,
|
'loadedWorkspace' => $workspace,
|
||||||
|
|
|
@ -35,7 +35,8 @@ class FormSubmissionController extends Controller
|
||||||
foreach ($form->submissions->toArray() as $row) {
|
foreach ($form->submissions->toArray() as $row) {
|
||||||
$formatter = (new FormSubmissionFormatter($form, $row['data']))
|
$formatter = (new FormSubmissionFormatter($form, $row['data']))
|
||||||
->outputStringsOnly()
|
->outputStringsOnly()
|
||||||
->setEmptyForNoValue();
|
->setEmptyForNoValue()
|
||||||
|
->showRemovedFields();
|
||||||
$tmp = $formatter->getCleanKeyValue();
|
$tmp = $formatter->getCleanKeyValue();
|
||||||
$tmp['Create Date'] = date("Y-m-d H:i", strtotime($row['created_at']));
|
$tmp['Create Date'] = date("Y-m-d H:i", strtotime($row['created_at']));
|
||||||
$allRows[] = $tmp;
|
$allRows[] = $tmp;
|
||||||
|
|
|
@ -21,7 +21,7 @@ class PublicFormController extends Controller
|
||||||
|
|
||||||
public function show(Request $request, string $slug)
|
public function show(Request $request, string $slug)
|
||||||
{
|
{
|
||||||
$form = Form::whereSlug($slug)->firstOrFail();
|
$form = Form::whereSlug($slug)->whereVisibility('public')->firstOrFail();
|
||||||
if ($form->workspace == null) {
|
if ($form->workspace == null) {
|
||||||
// Workspace deleted
|
// Workspace deleted
|
||||||
return $this->error([
|
return $this->error([
|
||||||
|
|
|
@ -40,7 +40,7 @@ class AnswerFormRequest extends FormRequest
|
||||||
*/
|
*/
|
||||||
public function authorize()
|
public function authorize()
|
||||||
{
|
{
|
||||||
return !$this->form->is_closed && !$this->form->max_number_of_submissions_reached;
|
return !$this->form->is_closed && !$this->form->max_number_of_submissions_reached && $this->form->visibility === 'public';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -29,6 +29,7 @@ abstract class UserFormRequest extends \Illuminate\Foundation\Http\FormRequest
|
||||||
'title' => 'required|string|max:60',
|
'title' => 'required|string|max:60',
|
||||||
'description' => 'nullable|string|max:2000',
|
'description' => 'nullable|string|max:2000',
|
||||||
'tags' => 'nullable|array',
|
'tags' => 'nullable|array',
|
||||||
|
'visibility' => ['required',Rule::in(Form::VISIBILITY)],
|
||||||
|
|
||||||
// Notifications
|
// Notifications
|
||||||
'notifies' => 'boolean',
|
'notifies' => 'boolean',
|
||||||
|
@ -83,6 +84,7 @@ abstract class UserFormRequest extends \Illuminate\Foundation\Http\FormRequest
|
||||||
'properties.*.timezone' => 'sometimes|nullable',
|
'properties.*.timezone' => 'sometimes|nullable',
|
||||||
'properties.*.width' => ['sometimes', Rule::in(['full','1/2','1/3','2/3','1/3','3/4','1/4'])],
|
'properties.*.width' => ['sometimes', Rule::in(['full','1/2','1/3','2/3','1/3','3/4','1/4'])],
|
||||||
'properties.*.allowed_file_types' => 'sometimes|nullable',
|
'properties.*.allowed_file_types' => 'sometimes|nullable',
|
||||||
|
'properties.*.use_toggle_switch' => 'boolean|nullable',
|
||||||
|
|
||||||
// Logic
|
// Logic
|
||||||
'properties.*.logic' => ['array', 'nullable', new FormPropertyLogicRule()],
|
'properties.*.logic' => ['array', 'nullable', new FormPropertyLogicRule()],
|
||||||
|
|
|
@ -42,8 +42,10 @@ class FormResource extends JsonResource
|
||||||
'can_be_indexed' => $this->can_be_indexed,
|
'can_be_indexed' => $this->can_be_indexed,
|
||||||
'password' => $this->password,
|
'password' => $this->password,
|
||||||
'tags' => $this->tags,
|
'tags' => $this->tags,
|
||||||
|
'visibility' => $this->visibility,
|
||||||
'notification_emails' => $this->notification_emails,
|
'notification_emails' => $this->notification_emails,
|
||||||
'slack_webhook_url' => $this->slack_webhook_url,
|
'slack_webhook_url' => $this->slack_webhook_url,
|
||||||
|
'removed_properties' => $this->removed_properties
|
||||||
] : [];
|
] : [];
|
||||||
|
|
||||||
$baseData = $this->getFilteredFormData(parent::toArray($request), $this->userIsFormOwner());
|
$baseData = $this->getFilteredFormData(parent::toArray($request), $this->userIsFormOwner());
|
||||||
|
|
|
@ -20,6 +20,7 @@ class Form extends Model
|
||||||
const DARK_MODE_VALUES = ['auto', 'light', 'dark'];
|
const DARK_MODE_VALUES = ['auto', 'light', 'dark'];
|
||||||
const THEMES = ['default', 'simple', 'notion'];
|
const THEMES = ['default', 'simple', 'notion'];
|
||||||
const WIDTHS = ['centered', 'full'];
|
const WIDTHS = ['centered', 'full'];
|
||||||
|
const VISIBILITY = ['public', 'draft'];
|
||||||
|
|
||||||
use HasFactory, HasSlug, SoftDeletes;
|
use HasFactory, HasSlug, SoftDeletes;
|
||||||
|
|
||||||
|
@ -45,6 +46,7 @@ class Form extends Model
|
||||||
'title',
|
'title',
|
||||||
'description',
|
'description',
|
||||||
'tags',
|
'tags',
|
||||||
|
'visibility',
|
||||||
|
|
||||||
// Customization
|
// Customization
|
||||||
'theme',
|
'theme',
|
||||||
|
|
|
@ -27,6 +27,8 @@ class FormSubmissionFormatter
|
||||||
|
|
||||||
private $setEmptyForNoValue = false;
|
private $setEmptyForNoValue = false;
|
||||||
|
|
||||||
|
private $showRemovedFields = false;
|
||||||
|
|
||||||
public function __construct(private Form $form, private array $formData)
|
public function __construct(private Form $form, private array $formData)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -55,6 +57,12 @@ class FormSubmissionFormatter
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function showRemovedFields()
|
||||||
|
{
|
||||||
|
$this->showRemovedFields = true;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a nice "FieldName": "Field Response" array
|
* Return a nice "FieldName": "Field Response" array
|
||||||
* - If createLink enabled, returns html link for emails and links
|
* - If createLink enabled, returns html link for emails and links
|
||||||
|
@ -63,10 +71,15 @@ class FormSubmissionFormatter
|
||||||
public function getCleanKeyValue()
|
public function getCleanKeyValue()
|
||||||
{
|
{
|
||||||
$data = $this->formData;
|
$data = $this->formData;
|
||||||
$fields = $this->form->properties;
|
$fields = ($this->showRemovedFields) ? array_merge($this->form->properties, $this->form->removed_properties) : $this->form->properties;
|
||||||
|
|
||||||
$returnArray = [];
|
$returnArray = [];
|
||||||
foreach ($fields as &$field) {
|
foreach ($fields as &$field) {
|
||||||
|
$isRemoved = in_array($field['id'], array_column($this->form->removed_properties, 'id')) ?? false;
|
||||||
|
if($isRemoved){
|
||||||
|
$field['name'] = $field['name']." (deleted)";
|
||||||
|
}
|
||||||
|
|
||||||
// If not present skip
|
// If not present skip
|
||||||
if (!isset($data[$field['id']])) {
|
if (!isset($data[$field['id']])) {
|
||||||
if ($this->setEmptyForNoValue) {
|
if ($this->setEmptyForNoValue) {
|
||||||
|
|
|
@ -58,6 +58,7 @@ class FormFactory extends Factory
|
||||||
return [
|
return [
|
||||||
'title' => $this->faker->text(30),
|
'title' => $this->faker->text(30),
|
||||||
'description' => $this->faker->randomHtml(1),
|
'description' => $this->faker->randomHtml(1),
|
||||||
|
'visibility' => 'public',
|
||||||
'notifies' => false,
|
'notifies' => false,
|
||||||
'send_submission_confirmation' => false,
|
'send_submission_confirmation' => false,
|
||||||
'webhook_url' => null,
|
'webhook_url' => null,
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('forms', function (Blueprint $table) {
|
||||||
|
$table->string('visibility')->default('public');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table('forms', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('visibility');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="wrapperClass">
|
<div :class="wrapperClass">
|
||||||
<v-checkbox :id="id?id:name" v-model="compVal" :disabled="disabled" :name="name" @input="$emit('input',$event)">
|
<v-checkbox :id="id?id:name" v-model="compVal" :disabled="disabled" :name="name" @input="$emit('input',$event)">
|
||||||
{{ label }}
|
{{ label }} <span v-if="required" class="text-red-500 required-dot">*</span>
|
||||||
</v-checkbox>
|
</v-checkbox>
|
||||||
<small v-if="help" :class="theme.default.help">
|
<small v-if="help" :class="theme.default.help">
|
||||||
<slot name="help">{{ help }}</slot>
|
<slot name="help">{{ help }}</slot>
|
||||||
|
|
|
@ -6,6 +6,9 @@
|
||||||
{{ label }}
|
{{ label }}
|
||||||
<span v-if="required" class="text-red-500 required-dot">*</span>
|
<span v-if="required" class="text-red-500 required-dot">*</span>
|
||||||
</label>
|
</label>
|
||||||
|
<small v-if="help" :class="theme.SelectInput.help" class="block mb-2">
|
||||||
|
<slot name="help">{{ help }}</slot>
|
||||||
|
</small>
|
||||||
|
|
||||||
<loader v-if="loading" key="loader" class="h-6 w-6 text-nt-blue mx-auto" />
|
<loader v-if="loading" key="loader" class="h-6 w-6 text-nt-blue mx-auto" />
|
||||||
<div v-for="(option, index) in options" v-else :key="option[optionKey]"
|
<div v-for="(option, index) in options" v-else :key="option[optionKey]"
|
||||||
|
@ -22,9 +25,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<small v-if="help" :class="theme.SelectInput.help">
|
|
||||||
<slot name="help">{{ help }}</slot>
|
|
||||||
</small>
|
|
||||||
<has-error v-if="hasValidation" :form="form" :field="name" />
|
<has-error v-if="hasValidation" :form="form" :field="name" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
<template>
|
||||||
|
<div :class="wrapperClass">
|
||||||
|
<div class="flex">
|
||||||
|
<v-switch :id="id?id:name" v-model="compVal" class="inline-block mr-2" :disabled="disabled" :name="name" @input="$emit('input',$event)" />
|
||||||
|
<span>{{ label }} <span v-if="required" class="text-red-500 required-dot">*</span></span>
|
||||||
|
</div>
|
||||||
|
<small v-if="help" :class="theme.default.help">
|
||||||
|
<slot name="help">{{ help }}</slot>
|
||||||
|
</small>
|
||||||
|
<has-error v-if="hasValidation" :form="form" :field="name" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import inputMixin from '~/mixins/forms/input'
|
||||||
|
|
||||||
|
import VSwitch from './components/VSwitch'
|
||||||
|
export default {
|
||||||
|
name: 'ToggleSwitchInput',
|
||||||
|
|
||||||
|
components: { VSwitch },
|
||||||
|
mixins: [inputMixin],
|
||||||
|
props: {},
|
||||||
|
|
||||||
|
mounted () {
|
||||||
|
this.compVal = !!this.compVal
|
||||||
|
this.$emit('input', !!this.compVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
|
@ -1,42 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div @click="onClick">
|
||||||
<Motion
|
<div class="inline-flex items-center h-6 w-12 p-1 bg-gray-300 border rounded-full cursor-pointer focus:outline-none transition-all transform ease-in-out duration-100" :class="{'bg-nt-blue': internalValue}">
|
||||||
v-model="value"
|
<div class="inline-block h-4 w-4 rounded-full bg-white shadow transition-all transform ease-in-out duration-150 rounded-2xl scale-100" :class="{'translate-x-5.5': internalValue}" />
|
||||||
:options="{
|
</div>
|
||||||
duration: 150,
|
|
||||||
}"
|
|
||||||
:trigger="[
|
|
||||||
'bg-gray-200 border-gray-300 duration-100 dark:bg-gray-700 dark:border-gray-600',
|
|
||||||
'bg-gray-200 dark:bg-gray-700',
|
|
||||||
'bg-nt-blue border-nt-blue',
|
|
||||||
'bg-nt-blue duration-100',
|
|
||||||
]"
|
|
||||||
class="inline-flex items-center h-6 w-12 p-1 border rounded-full cursor-pointer focus:outline-none"
|
|
||||||
@click="$emit('input',!internalValue)"
|
|
||||||
>
|
|
||||||
<Motion
|
|
||||||
v-model="internalValue"
|
|
||||||
tag="span"
|
|
||||||
:options="{
|
|
||||||
duration: 150,
|
|
||||||
}"
|
|
||||||
:trigger="[
|
|
||||||
'translate-x-0 duration-150',
|
|
||||||
'rounded-2xl scale-75 duration-100',
|
|
||||||
'translate-x-6 duration-100',
|
|
||||||
'scale-100 duration-150',
|
|
||||||
]"
|
|
||||||
class="inline-block h-4 w-4 rounded-full bg-white dark:bg-gray-500 shadow"
|
|
||||||
/>
|
|
||||||
</Motion>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Motion from 'tinymotion'
|
|
||||||
export default {
|
export default {
|
||||||
name: 'VSwitch',
|
name: 'VSwitch',
|
||||||
components: { Motion },
|
components: { },
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
value: { type: Boolean, default: false }
|
value: { type: Boolean, default: false }
|
||||||
|
@ -48,14 +21,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {},
|
||||||
sizeClasses () {
|
|
||||||
if (this.size === 'small') {
|
|
||||||
return 'w-3 h-3'
|
|
||||||
}
|
|
||||||
return 'w-5 h-5'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
value (val) {
|
value (val) {
|
||||||
|
@ -68,12 +34,10 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
onClick () {
|
||||||
|
this.$emit('input', !this.internalValue)
|
||||||
|
this.internalValue = !this.internalValue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.translate-x-6 {
|
|
||||||
--tw-translate-x: 1.4rem !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ import ImageInput from './ImageInput'
|
||||||
import DateInput from './DateInput';
|
import DateInput from './DateInput';
|
||||||
import RatingInput from './RatingInput';
|
import RatingInput from './RatingInput';
|
||||||
import FlatSelectInput from './FlatSelectInput';
|
import FlatSelectInput from './FlatSelectInput';
|
||||||
|
import ToggleSwitchInput from './ToggleSwitchInput';
|
||||||
|
|
||||||
// Components that are registered globaly.
|
// Components that are registered globaly.
|
||||||
[
|
[
|
||||||
|
@ -34,7 +35,8 @@ import FlatSelectInput from './FlatSelectInput';
|
||||||
RichTextAreaInput,
|
RichTextAreaInput,
|
||||||
DateInput,
|
DateInput,
|
||||||
RatingInput,
|
RatingInput,
|
||||||
FlatSelectInput
|
FlatSelectInput,
|
||||||
|
ToggleSwitchInput
|
||||||
].forEach(Component => {
|
].forEach(Component => {
|
||||||
Vue.component(Component.name, Component)
|
Vue.component(Component.name, Component)
|
||||||
})
|
})
|
||||||
|
|
|
@ -317,6 +317,9 @@ export default {
|
||||||
if (['select', 'multi_select'].includes(field.type) && field.without_dropdown) {
|
if (['select', 'multi_select'].includes(field.type) && field.without_dropdown) {
|
||||||
return 'FlatSelectInput'
|
return 'FlatSelectInput'
|
||||||
}
|
}
|
||||||
|
if (field.type === 'checkbox' && field.use_toggle_switch) {
|
||||||
|
return 'ToggleSwitchInput'
|
||||||
|
}
|
||||||
return this.fieldComponents[field.type]
|
return this.fieldComponents[field.type]
|
||||||
},
|
},
|
||||||
getFieldClasses (field) {
|
getFieldClasses (field) {
|
||||||
|
|
|
@ -2,11 +2,41 @@
|
||||||
<div
|
<div
|
||||||
class="my-4 w-full mx-auto">
|
class="my-4 w-full mx-auto">
|
||||||
<h3 class="font-semibold mb-4">
|
<h3 class="font-semibold mb-4">
|
||||||
Form Submissions <span v-if="form && !isLoading && tableData.length > 0"
|
Form Submissions
|
||||||
class="text-right text-xs uppercase mb-2">
|
<span v-if="form && !isLoading && tableData.length > 0" class="text-right text-xs uppercase mb-2"> - <a :href="exportUrl" target="_blank">Export as CSV</a></span>
|
||||||
- <a :href="exportUrl" target="_blank">Export as CSV</a>
|
<span v-if="form && !isLoading && formInitDone" class="float-right text-xs uppercase mb-2"> <a href="javascript:void(0);" @click="showColumnsModal=true">Display columns</a></span>
|
||||||
</span>
|
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
|
<!-- Table columns modal -->
|
||||||
|
<modal :show="showColumnsModal" @close="showColumnsModal=false">
|
||||||
|
<div class="-m-6">
|
||||||
|
<div class="px-6 py-3">
|
||||||
|
<h2 class="text-nt-blue text-3xl font-bold">
|
||||||
|
Display columns
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="border-t py-4 px-6">
|
||||||
|
<template v-if="properties.length > 0">
|
||||||
|
<h4 class="font-bold mb-2">Form Fields</h4>
|
||||||
|
<div v-for="field in properties" :key="field.id" class="p-2 border">
|
||||||
|
{{ field.name }}
|
||||||
|
<v-switch v-model="displayColumns[field.id]" @input="onChangeDisplayColumns" class="float-right" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-if="removed_properties.length > 0">
|
||||||
|
<h4 class="font-bold mb-2 mt-4">Removed Fields</h4>
|
||||||
|
<div v-for="field in removed_properties" :key="field.id" class="p-2 border">
|
||||||
|
{{ field.name }}
|
||||||
|
<v-switch v-model="displayColumns[field.id]" @input="onChangeDisplayColumns" class="float-right" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end mt-4 pb-5 px-6">
|
||||||
|
<v-button color="gray" shade="light" @click="showColumnsModal=false">Close</v-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</modal>
|
||||||
|
|
||||||
<loader v-if="!form || isLoading" class="h-6 w-6 text-nt-blue mx-auto"/>
|
<loader v-if="!form || isLoading" class="h-6 w-6 text-nt-blue mx-auto"/>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<scroll-shadow
|
<scroll-shadow
|
||||||
|
@ -33,10 +63,11 @@ import axios from 'axios'
|
||||||
import ScrollShadow from '../../../common/ScrollShadow'
|
import ScrollShadow from '../../../common/ScrollShadow'
|
||||||
import OpenTable from '../../tables/OpenTable'
|
import OpenTable from '../../tables/OpenTable'
|
||||||
import clonedeep from "clone-deep";
|
import clonedeep from "clone-deep";
|
||||||
|
import VSwitch from '../../../forms/components/VSwitch'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FormSubmissions',
|
name: 'FormSubmissions',
|
||||||
components: {ScrollShadow, OpenTable},
|
components: {ScrollShadow, OpenTable, VSwitch},
|
||||||
props: {},
|
props: {},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -45,6 +76,10 @@ export default {
|
||||||
tableData: [],
|
tableData: [],
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
fullyLoaded: false,
|
fullyLoaded: false,
|
||||||
|
showColumnsModal: false,
|
||||||
|
properties: [],
|
||||||
|
removed_properties: [],
|
||||||
|
displayColumns: {},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
@ -69,18 +104,6 @@ export default {
|
||||||
this.$store.commit('open/working_form/set', value)
|
this.$store.commit('open/working_form/set', value)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tableStructure() {
|
|
||||||
if (!this.form) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
let tmp = this.form.properties.filter(property => !property.hasOwnProperty('hidden') || !property.hidden)
|
|
||||||
tmp.push({
|
|
||||||
"name": "Create Date",
|
|
||||||
"id": "create_date",
|
|
||||||
"type": "date"
|
|
||||||
});
|
|
||||||
return tmp
|
|
||||||
},
|
|
||||||
exportUrl() {
|
exportUrl() {
|
||||||
if (!this.form) {
|
if (!this.form) {
|
||||||
return ''
|
return ''
|
||||||
|
@ -104,6 +127,20 @@ export default {
|
||||||
})
|
})
|
||||||
this.$set(this.form, 'properties', columns)
|
this.$set(this.form, 'properties', columns)
|
||||||
this.formInitDone = true
|
this.formInitDone = true
|
||||||
|
|
||||||
|
this.properties = clonedeep(this.form.properties)
|
||||||
|
this.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.form.properties.forEach((field) => {
|
||||||
|
this.displayColumns[field.id] = true
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
getSubmissionsData() {
|
getSubmissionsData() {
|
||||||
if (!this.form || this.fullyLoaded) {
|
if (!this.form || this.fullyLoaded) {
|
||||||
|
@ -131,6 +168,13 @@ export default {
|
||||||
this.$refs.shadows.toggleShadow()
|
this.$refs.shadows.toggleShadow()
|
||||||
this.$refs.shadows.calcDimensions()
|
this.$refs.shadows.calcDimensions()
|
||||||
},
|
},
|
||||||
|
onChangeDisplayColumns(){
|
||||||
|
window.localStorage.setItem('display-columns-formid-'+this.form.id, JSON.stringify(this.displayColumns))
|
||||||
|
const final_properties = this.properties.concat(this.removed_properties).filter((field) => {
|
||||||
|
return this.displayColumns[field.id] === true
|
||||||
|
})
|
||||||
|
this.$set(this.form, 'properties', final_properties)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -46,7 +46,7 @@
|
||||||
>
|
>
|
||||||
<img alt="Logo Picture" :src="coverPictureSrc(form.logo_picture)"
|
<img alt="Logo Picture" :src="coverPictureSrc(form.logo_picture)"
|
||||||
:class="{'top-5':!form.cover_picture, '-top-10':form.cover_picture}"
|
:class="{'top-5':!form.cover_picture, '-top-10':form.cover_picture}"
|
||||||
class="w-20 h-20 absolute left-5 transition-all"
|
class="w-20 h-20 object-contain absolute left-5 transition-all"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -27,6 +27,11 @@
|
||||||
placeholder="Select Tag(s)" :multiple="true" :allowCreation="true"
|
placeholder="Select Tag(s)" :multiple="true" :allowCreation="true"
|
||||||
:options="allTagsOptions"
|
:options="allTagsOptions"
|
||||||
/>
|
/>
|
||||||
|
<select-input name="visibility" label="Visibility" :form="form" class="mt-3 mb-6"
|
||||||
|
help="Only public form will be accessible"
|
||||||
|
placeholder="Select Visibility" :required="true"
|
||||||
|
:options="visibilityOptions"
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
v-if="copyFormOptions.length > 0"
|
v-if="copyFormOptions.length > 0"
|
||||||
class="group mt-3 cursor-pointer relative w-full rounded-lg border-transparent flex-1 appearance-none border border-gray-300 dark:border-gray-600 w-full py-2 px-4 bg-white text-gray-700 dark:bg-notion-dark-light dark:text-gray-300 dark:placeholder-gray-500 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:border-transparent focus:ring-opacity-100"
|
class="group mt-3 cursor-pointer relative w-full rounded-lg border-transparent flex-1 appearance-none border border-gray-300 dark:border-gray-600 w-full py-2 px-4 bg-white text-gray-700 dark:bg-notion-dark-light dark:text-gray-300 dark:placeholder-gray-500 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:border-transparent focus:ring-opacity-100"
|
||||||
|
@ -86,7 +91,17 @@ export default {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
showCopyFormSettingsModal: false,
|
showCopyFormSettingsModal: false,
|
||||||
copyFormId: null
|
copyFormId: null,
|
||||||
|
visibilityOptions: [
|
||||||
|
{
|
||||||
|
name: "Public",
|
||||||
|
value: "public"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Draft (form won't be accessible)",
|
||||||
|
value: "draft"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,24 @@
|
||||||
</v-checkbox>
|
</v-checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Checkbox -->
|
||||||
|
<div v-if="field.type === 'checkbox'" class="-mx-4 sm:-mx-6 p-5 border-b">
|
||||||
|
<h3 class="font-semibold block text-lg">
|
||||||
|
Checkbox
|
||||||
|
</h3>
|
||||||
|
<p class="text-gray-400 mb-5">
|
||||||
|
Advanced options for checkbox.
|
||||||
|
</p>
|
||||||
|
<v-checkbox v-model="field.use_toggle_switch" class="mt-4"
|
||||||
|
name="use_toggle_switch" help=""
|
||||||
|
>
|
||||||
|
Use toggle switch
|
||||||
|
</v-checkbox>
|
||||||
|
<p class="text-gray-400 mb-5">
|
||||||
|
If enabled, checkbox will be replaced with a toggle switch
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- File Uploads -->
|
<!-- File Uploads -->
|
||||||
<div v-if="field.type === 'files'" class="-mx-4 sm:-mx-6 p-5 border-b">
|
<div v-if="field.type === 'files'" class="-mx-4 sm:-mx-6 p-5 border-b">
|
||||||
<h3 class="font-semibold block text-lg">
|
<h3 class="font-semibold block text-lg">
|
||||||
|
|
|
@ -171,6 +171,7 @@ export default {
|
||||||
this.form = new Form({
|
this.form = new Form({
|
||||||
title: 'My Form',
|
title: 'My Form',
|
||||||
description: null,
|
description: null,
|
||||||
|
visibility: 'public',
|
||||||
workspace_id: this.workspace?.id,
|
workspace_id: this.workspace?.id,
|
||||||
properties: [],
|
properties: [],
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
>
|
>
|
||||||
<img alt="Logo Picture" :src="form.logo_picture"
|
<img alt="Logo Picture" :src="form.logo_picture"
|
||||||
:class="{'top-5':!form.cover_picture, '-top-10':form.cover_picture}"
|
:class="{'top-5':!form.cover_picture, '-top-10':form.cover_picture}"
|
||||||
class="w-20 h-20 absolute left-5 transition-all"
|
class="w-20 h-20 object-contain absolute left-5 transition-all"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
<div v-if="form" class="sm:px-6">
|
<div v-if="form" class="sm:px-6">
|
||||||
<h2 class="text-nt-blue text-3xl font-bold z-10 mt-6 mb-3">
|
<h2 class="text-nt-blue text-3xl font-bold z-10 mt-6 mb-3">
|
||||||
{{ form.title }}
|
{{ form.title }}
|
||||||
|
<span v-if="form.visibility=='draft'" class="float-right text-white p-2 text-xs inline rounded-lg font-semibold mr-2 bg-gray-400 dark:bg-gray-700">Draft (not public)</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p class="mb-3">
|
<p class="mb-3">
|
||||||
|
@ -305,7 +306,7 @@ import FormSubmissions from '../../components/open/forms/components/FormSubmissi
|
||||||
const loadForms = function () {
|
const loadForms = function () {
|
||||||
store.commit('open/forms/startLoading')
|
store.commit('open/forms/startLoading')
|
||||||
store.dispatch('open/workspaces/loadIfEmpty').then(() => {
|
store.dispatch('open/workspaces/loadIfEmpty').then(() => {
|
||||||
store.dispatch('open/forms/load', store.state['open/workspaces'].currentId)
|
store.dispatch('open/forms/loadIfEmpty', store.state['open/workspaces'].currentId)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,13 +10,11 @@
|
||||||
Create a new form
|
Create a new form
|
||||||
</v-button>
|
</v-button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="formsLoading" class="text-center">
|
|
||||||
<loader class="h-6 w-6 text-nt-blue mx-auto" />
|
<p v-if="!formsLoading && enrichedForms.length === 0 && !isFilteringForms">
|
||||||
</div>
|
|
||||||
<p v-else-if="enrichedForms.length === 0 && !isFilteringForms">
|
|
||||||
You don't have any form yet.
|
You don't have any form yet.
|
||||||
</p>
|
</p>
|
||||||
<div v-else class="mb-10">
|
<div v-else-if="forms.length > 0" class="mb-10">
|
||||||
<text-input v-if="forms.length > 5" class="mb-6" :form="searchForm" name="search" label="Search a form"
|
<text-input v-if="forms.length > 5" class="mb-6" :form="searchForm" name="search" label="Search a form"
|
||||||
placeholder="Name of form to search"
|
placeholder="Name of form to search"
|
||||||
/>
|
/>
|
||||||
|
@ -31,7 +29,8 @@
|
||||||
</div>
|
</div>
|
||||||
<div v-if="enrichedForms && enrichedForms.length" class="border border border-gray-300 dark:bg-notion-dark-light rounded-md w-full">
|
<div v-if="enrichedForms && enrichedForms.length" class="border border border-gray-300 dark:bg-notion-dark-light rounded-md w-full">
|
||||||
<div v-for="(form, index) in enrichedForms" :key="form.id"
|
<div v-for="(form, index) in enrichedForms" :key="form.id"
|
||||||
class="p-4 w-full mx-auto border-gray-300 hover:bg-blue-100 dark:hover:bg-blue-900 transition-colors cursor-pointer relative" :class="{'border-t':index!==0}"
|
class="p-4 w-full mx-auto border-gray-300 hover:bg-blue-100 dark:hover:bg-blue-900 transition-colors cursor-pointer relative"
|
||||||
|
:class="{'border-t':index!==0, 'bg-gray-50 dark:bg-gray-400':form.visibility=='draft'}"
|
||||||
>
|
>
|
||||||
<div class="items-center space-x-4 truncate">
|
<div class="items-center space-x-4 truncate">
|
||||||
<p class="truncate float-left">
|
<p class="truncate float-left">
|
||||||
|
@ -65,6 +64,9 @@
|
||||||
</template>.
|
</template>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="formsLoading" class="text-center">
|
||||||
|
<loader class="h-6 w-6 text-nt-blue mx-auto" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<open-form-footer class="mt-8 border-t" />
|
<open-form-footer class="mt-8 border-t" />
|
||||||
|
@ -82,7 +84,7 @@ import OpenFormFooter from '../components/pages/OpenFormFooter'
|
||||||
const loadForms = function () {
|
const loadForms = function () {
|
||||||
store.commit('open/forms/startLoading')
|
store.commit('open/forms/startLoading')
|
||||||
store.dispatch('open/workspaces/loadIfEmpty').then(() => {
|
store.dispatch('open/workspaces/loadIfEmpty').then(() => {
|
||||||
store.dispatch('open/forms/load', store.state['open/workspaces'].currentId)
|
store.dispatch('open/forms/loadIfEmpty', store.state['open/workspaces'].currentId)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import axios from 'axios'
|
||||||
|
|
||||||
export const formsEndpoint = '/api/open/workspaces/{workspaceId}/forms'
|
export const formsEndpoint = '/api/open/workspaces/{workspaceId}/forms'
|
||||||
export const namespaced = true
|
export const namespaced = true
|
||||||
|
export let currentPage = 1
|
||||||
|
|
||||||
// state
|
// state
|
||||||
export const state = {
|
export const state = {
|
||||||
|
@ -36,6 +37,9 @@ export const mutations = {
|
||||||
set (state, items) {
|
set (state, items) {
|
||||||
state.content = items
|
state.content = items
|
||||||
},
|
},
|
||||||
|
append (state, items) {
|
||||||
|
state.content = state.content.concat(items)
|
||||||
|
},
|
||||||
addOrUpdate (state, item) {
|
addOrUpdate (state, item) {
|
||||||
state.content = state.content.filter((val) => val.id !== item.id)
|
state.content = state.content.filter((val) => val.id !== item.id)
|
||||||
state.content.push(item)
|
state.content.push(item)
|
||||||
|
@ -56,12 +60,26 @@ export const actions = {
|
||||||
resetState (context) {
|
resetState (context) {
|
||||||
context.commit('set', [])
|
context.commit('set', [])
|
||||||
context.commit('stopLoading')
|
context.commit('stopLoading')
|
||||||
|
currentPage = 1
|
||||||
},
|
},
|
||||||
load (context, workspaceId) {
|
load (context, workspaceId) {
|
||||||
context.commit('startLoading')
|
context.commit('startLoading')
|
||||||
return axios.get(formsEndpoint.replace('{workspaceId}', workspaceId)).then((response) => {
|
return axios.get(formsEndpoint.replace('{workspaceId}', workspaceId)+'?page='+currentPage).then((response) => {
|
||||||
context.commit('set', response.data)
|
context.commit((currentPage == 1) ? 'set' : 'append', response.data.data)
|
||||||
|
if (currentPage < response.data.meta.last_page) {
|
||||||
|
currentPage += 1
|
||||||
|
context.dispatch('load', workspaceId)
|
||||||
|
} else {
|
||||||
context.commit('stopLoading')
|
context.commit('stopLoading')
|
||||||
|
currentPage = 1
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
loadIfEmpty (context, workspaceId) {
|
||||||
|
if (context.state.content.length === 0) {
|
||||||
|
return context.dispatch('load', workspaceId)
|
||||||
|
}
|
||||||
|
context.commit('stopLoading')
|
||||||
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,9 @@ module.exports = {
|
||||||
10: '10rem',
|
10: '10rem',
|
||||||
8: '2rem'
|
8: '2rem'
|
||||||
},
|
},
|
||||||
|
translate: {
|
||||||
|
5.5: '1.4rem'
|
||||||
|
},
|
||||||
boxShadow: {
|
boxShadow: {
|
||||||
'inner-notion': '#0f0f0f1a 0px 0px 0px 1px inset',
|
'inner-notion': '#0f0f0f1a 0px 0px 0px 1px inset',
|
||||||
'focus-notion': '#2eaadcb3 0px 0px 0px 1px inset, #2eaadc66 0px 0px 0px 2px !important'
|
'focus-notion': '#2eaadcb3 0px 0px 0px 1px inset, #2eaadc66 0px 0px 0px 2px !important'
|
||||||
|
|
|
@ -61,3 +61,26 @@ it('can submit form till max submissions count is not reached at limit', functio
|
||||||
$this->postJson(route('forms.answer', $form->slug), $formData)
|
$this->postJson(route('forms.answer', $form->slug), $formData)
|
||||||
->assertStatus(403);
|
->assertStatus(403);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can not open draft form', function () {
|
||||||
|
$user = $this->actingAsUser();
|
||||||
|
$workspace = $this->createUserWorkspace($user);
|
||||||
|
$form = $this->createForm($user, $workspace, [
|
||||||
|
'visibility' => 'draft'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->getJson(route('forms.show', $form->slug))
|
||||||
|
->assertStatus(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can not submit draft form', function () {
|
||||||
|
$user = $this->actingAsUser();
|
||||||
|
$workspace = $this->createUserWorkspace($user);
|
||||||
|
$form = $this->createForm($user, $workspace, [
|
||||||
|
'visibility' => 'draft'
|
||||||
|
]);
|
||||||
|
$formData = FormSubmissionDataFactory::generateSubmissionData($form);
|
||||||
|
|
||||||
|
$this->postJson(route('forms.answer', $form->slug), $formData)
|
||||||
|
->assertStatus(403);
|
||||||
|
});
|
|
@ -28,12 +28,10 @@ it('can fetch forms', function () {
|
||||||
|
|
||||||
$this->getJson(route('open.workspaces.forms.index', $workspace->id))
|
$this->getJson(route('open.workspaces.forms.index', $workspace->id))
|
||||||
->assertSuccessful()
|
->assertSuccessful()
|
||||||
->assertJsonCount(1)
|
->assertJsonCount(3)
|
||||||
->assertJson(function (AssertableJson $json) use ($form) {
|
->assertSuccessful()
|
||||||
return $json->where('0.id', $form->id)
|
->assertJsonPath('data.0.id', $form->id)
|
||||||
->whereType('0.title', 'string')
|
->assertJsonPath('data.0.title', $form->title);
|
||||||
->whereType('0.properties', 'array');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can update a form', function () {
|
it('can update a form', function () {
|
||||||
|
|
Loading…
Reference in New Issue