Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions lang/en/messages.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,14 @@
'form_configure_generate_fake_submissions_instructions' => 'Allow generating fake submissions and workflow testing from the submissions screen.',
'form_configure_store_instructions' => 'Disable to stop storing submissions. Events and email notifications will still be sent.',
'form_configure_title_instructions' => 'Use a call to action, such as \'Contact Us\'.',
'form_configure_close_date_instructions' => 'The form will stop accepting submissions after this date. Leave blank to never close.',
'form_configure_submission_limit_instructions' => 'The maximum number of submissions to accept. Leave blank for no limit.',
'form_configure_submission_limit_period_instructions' => 'How often the submission limit resets.',
'form_configure_closed_message_instructions' => 'Shown when the form is closed or the submission limit has been reached. Leave blank to use the default message.',
'form_configure_require_login_instructions' => 'Only allow logged in users to submit this form.',
'form_configure_require_login_message_instructions' => 'Shown when a logged out visitor tries to submit the form. Leave blank to use the default message.',
'form_closed_message' => 'This form is no longer accepting submissions.',
'form_require_login_message' => 'You must be logged in to submit this form.',
'form_create_description' => 'Get started by creating your first form.',
'form_builder' => 'Form Builder',
'form_export_filtered_description' => 'Exports submissions with current filters and visible columns.',
Expand Down
33 changes: 33 additions & 0 deletions resources/js/components/forms/FormStatusIndicator.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<script setup>
import { computed } from 'vue';

const props = defineProps({
status: {
type: String,
required: false,
default: 'open',
validator: (value) => ['open', 'closed', 'limit_reached'].includes(value),
},
showDot: { type: Boolean, default: true },
showLabel: { type: Boolean, default: false },
});

const statusClass = computed(() => {
return props.status === 'open' ? 'bg-green-400' : 'bg-gray-300 dark:bg-gray-200';
});

const label = computed(() => {
return {
open: __('Open'),
closed: __('Closed'),
limit_reached: __('Limit Reached'),
}[props.status];
});
</script>

<template>
<span class="flex items-center gap-2">
<span v-if="showDot" class="size-2 rounded-full" :class="statusClass" v-tooltip="label" />
<span v-if="showLabel" class="status-index-field select-none" :class="`status-${status}`" v-text="label" />
</span>
</template>
5 changes: 3 additions & 2 deletions resources/js/pages/forms/Builder.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ export const [injectBuilderContext, provideBuilderContext] = createContext('Form
</script>

<script setup lang="ts">
import { Button, Header, Icon, StatusIndicator, ToggleGroup, ToggleItem } from '@ui';
import { Button, Header, Icon, ToggleGroup, ToggleItem } from '@ui';
import FormStatusIndicator from '@/components/forms/FormStatusIndicator.vue';
import { computed, nextTick, onMounted, onUnmounted, provide, ref, watch } from 'vue';
import axios from 'axios';
import FormsLayout from './Layout.vue';
Expand Down Expand Up @@ -419,7 +420,7 @@ onUnmounted(() => {
<div class="col-span-full row-start-1 max-[1000px]:pt-14">
<Header class="mx-auto max-w-5xl">
<template #title>
<StatusIndicator status="published" />
<FormStatusIndicator :status="form.status" />
{{ __(form.title) }}
</template>
<template #actions>
Expand Down
5 changes: 3 additions & 2 deletions resources/js/pages/forms/Connect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
import Layout from '@/pages/layout/Layout.vue';
import PanelLayout from '@/pages/layout/PanelLayout.vue';
import FormsLayout from './Layout.vue';
import { Badge, Button, Card, DocsCallout, Header, Heading, Icon, Panel, PanelHeader, StatusIndicator, Table, TableCell, TableColumn, TableColumns, TableRow, TableRows, ToggleGroup, ToggleItem, publishContextKey } from '@ui';
import { Badge, Button, Card, DocsCallout, Header, Heading, Icon, Panel, PanelHeader, Table, TableCell, TableColumn, TableColumns, TableRow, TableRows, ToggleGroup, ToggleItem, publishContextKey } from '@ui';
import FormStatusIndicator from '@/components/forms/FormStatusIndicator.vue';
import { computed, onMounted, provide, ref, watch, watchEffect } from 'vue';
import emailNotificationsLogoRaw from '../../../svg/forms/connect/email-notifications.svg?raw';
import zapierLogoRaw from '../../../svg/forms/connect/zapier.svg?raw';
Expand Down Expand Up @@ -239,7 +240,7 @@ watch(selectedIntegrationName, (integrationName) => {
<div class="mx-auto max-w-5xl">
<Header class="mb-2">
<template #title>
<StatusIndicator status="published" />
<FormStatusIndicator :status="form?.status" />
{{ __(formTitle) }}
</template>
<template #actions>
Expand Down
6 changes: 5 additions & 1 deletion resources/js/pages/forms/Index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { computed } from 'vue';
import Head from '@/pages/layout/Head.vue';
import { Header, Button, Badge, CommandPaletteItem, EmptyStateMenu, EmptyStateItem, DocsCallout, Icon, Listing, DropdownItem } from '@ui';
import FormStatusIndicator from '@/components/forms/FormStatusIndicator.vue';
import { Link, router } from '@inertiajs/vue3';

const props = defineProps([
Expand Down Expand Up @@ -65,7 +66,10 @@ const reloadPage = () => router.reload();

<Listing :items="forms" :columns="initialColumns" :action-url="actionUrl" @refreshing="reloadPage">
<template #cell-title="{ row: form }">
<Link :href="form.show_url">{{ form.title }}</Link>
<div class="flex items-center gap-2">
<FormStatusIndicator :status="form.status" />
<Link :href="form.show_url">{{ form.title }}</Link>
</div>
</template>
<template #cell-submissions="{ row: form, value: submissions }">
<Badge
Expand Down
5 changes: 3 additions & 2 deletions resources/js/pages/forms/Logic.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
import Layout from '@/pages/layout/Layout.vue';
import PanelLayout from '@/pages/layout/PanelLayout.vue';
import FormsLayout from './Layout.vue';
import { Button, Header, Icon, SplitterGroup, SplitterPanel, SplitterResizeHandle, StatusIndicator, ToggleGroup, ToggleItem } from '@ui';
import { Button, Header, Icon, SplitterGroup, SplitterPanel, SplitterResizeHandle, ToggleGroup, ToggleItem } from '@ui';
import FormStatusIndicator from '@/components/forms/FormStatusIndicator.vue';
import FieldNumberingToggle from '@/components/forms/FieldNumberingToggle.vue';
import LogicList from '@/components/forms/logic/LogicList.vue';
import LogicTree, { TreeDensity, SelectionType, type Selection } from '@/components/forms/logic/LogicTree.vue';
Expand Down Expand Up @@ -159,7 +160,7 @@ onUnmounted(() => {
<div class="mx-auto w-full max-w-5xl min-w-0 shrink-0">
<Header class="mb-2 md:py-9">
<template #title>
<StatusIndicator status="published" />
<FormStatusIndicator :status="form.status" />
{{ __(form.title) }}
</template>
<template #actions>
Expand Down
8 changes: 7 additions & 1 deletion resources/js/pages/forms/Submissions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ref, computed } from 'vue';
import axios from 'axios';
import Head from '@/pages/layout/Head.vue';
import { Header, Dropdown, DropdownMenu, DropdownItem, Button, Modal, RadioGroup, Radio, CommandPaletteItem } from '@ui';
import FormStatusIndicator from '@/components/forms/FormStatusIndicator.vue';
import ResourceDeleter from '@/components/ResourceDeleter.vue';
import FormSubmissionListing from '@/components/forms/SubmissionListing.vue';
import Layout from '@/pages/layout/Layout.vue';
Expand Down Expand Up @@ -120,7 +121,12 @@ function exportSubmissions() {
<Head :title="[__('Results'), __(form.title), __('Forms')]" />

<div class="max-w-5xl 3xl:max-w-6xl mx-auto" data-max-width-wrapper>
<Header :title="__(form.title)" icon="forms">
<Header>
<template #title>
<FormStatusIndicator :status="form.status" />
{{ __(form.title) }}
</template>

<Dropdown v-if="form.canEdit || form.canDelete" placement="left-start" class="me-2">
<DropdownMenu>
<DropdownItem v-if="form.canEdit" :text="__('Configure Form')" icon="cog" :href="form.editUrl" />
Expand Down
2 changes: 1 addition & 1 deletion src/Forms/AugmentedForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class AugmentedForm extends AbstractAugmented
{
public function keys()
{
$keys = ['handle', 'title', 'fields', 'api_url'];
$keys = ['handle', 'title', 'fields', 'status', 'api_url'];

if (! Statamic::isApiRoute()) {
$keys[] = 'honeypot';
Expand Down
70 changes: 70 additions & 0 deletions src/Forms/Form.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Statamic\Forms;

use Carbon\Carbon;
use Illuminate\Contracts\Support\Arrayable;
use Statamic\Contracts\Data\Augmentable;
use Statamic\Contracts\Data\Augmented;
Expand All @@ -23,6 +24,7 @@
use Statamic\Facades\File;
use Statamic\Facades\Form as FormFacade;
use Statamic\Facades\FormSubmission;
use Statamic\Facades\User;
use Statamic\Facades\YAML;
use Statamic\Fields\Blueprint;
use Statamic\Forms\Exporters\Exporter;
Expand All @@ -32,6 +34,8 @@
use Statamic\Support\Str;
use Statamic\Support\Traits\FluentlyGetsAndSets;

use function Statamic\trans as __;

class Form implements Arrayable, Augmentable, ContainsQueryableValues, FormContract
{
use ContainsData, FluentlyGetsAndSets, HasAugmentedInstance;
Expand Down Expand Up @@ -491,6 +495,72 @@ public function querySubmissions(): SubmissionQueryBuilder
return FormSubmission::query()->where('form', $this->handle());
}

public function status(): string
{
return Blink::once('form-status-'.$this->handle(), fn () => match (true) {
$this->closingDateHasPassed() => 'closed',
$this->submissionLimitReached() => 'limit_reached',
default => 'open',
});
}

public function restricted(): bool
{
return $this->restrictionMessage() !== null;
}

public function restrictionMessage(): ?string
{
if ($this->closingDateHasPassed() || $this->submissionLimitReached()) {
return ($msg = $this->get('closed_message')) ? __($msg) : __('statamic::messages.form_closed_message');
}

if ($this->get('require_login') && ! User::current()) {
return ($msg = $this->get('require_login_message')) ? __($msg) : __('statamic::messages.form_require_login_message');
}

return null;
}

private function closingDateHasPassed(): bool
{
if (! $date = $this->get('close_date')) {
return false;
}

return Carbon::parse($date, config('app.timezone'))->isPast();
}

private function submissionLimitReached(): bool
{
if (! $limit = (int) $this->get('submission_limit')) {
return false;
}

return $this->submissionCount() >= $limit;
}

private function submissionCount(): int
{
$query = $this->querySubmissions()->whereNull('partial');

if ($start = $this->submissionLimitPeriodStart()) {
$query->where('date', '>=', $start);
}

return $query->count();
}

private function submissionLimitPeriodStart(): ?Carbon
{
return match ($this->get('submission_limit_period', 'total')) {
'day' => now()->startOfDay(),
'week' => now()->startOfWeek(),
'month' => now()->startOfMonth(),
default => null,
};
}

/**
* Get a submission.
*
Expand Down
4 changes: 4 additions & 0 deletions src/Forms/Tags.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ public function create()

$data['honeypot'] = $form->honeypot();

$data['restricted'] = $form->restricted();
$data['restriction_message'] = $form->restrictionMessage();
$data['status'] = $form->status();

if ($jsDriver) {
$data['js_driver'] = $jsDriver->handle();
$data['show_field'] = $jsDriver->copyShowFieldToFormData($data['fields']);
Expand Down
3 changes: 3 additions & 0 deletions src/GraphQL/Types/FormType.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ public function fields(): array
'honeypot' => [
'type' => GraphQL::string(),
],
'status' => [
'type' => GraphQL::nonNull(GraphQL::string()),
],
'fields' => [
'type' => GraphQL::listOf(GraphQL::type(FieldType::NAME)),
'resolve' => function ($form, $args, $context, $info) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public function index(FilteredRequest $request, $form)
'form' => [
'title' => __($form->title()),
'handle' => $form->handle(),
'status' => $form->status(),
'editUrl' => $form->editUrl(),
'deleteUrl' => $form->deleteUrl(),
'canEdit' => User::current()->can('edit', $form),
Expand Down
54 changes: 54 additions & 0 deletions src/Http/Controllers/CP/Forms/FormsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public function index(Request $request)
return [
'id' => $form->handle(),
'title' => __($form->title()),
'status' => $form->status(),
'submissions' => $form->querySubmissions()->whereNull('partial')->count(),
'show_url' => $form->showUrl(),
'submissions_url' => $form->submissionsUrl(),
Expand Down Expand Up @@ -204,6 +205,59 @@ protected function editFormBlueprint($form)
],
],
],
'access' => [
'display' => __('Access'),
'fields' => [
'close_date' => [
'display' => __('Close Date'),
'type' => 'date',
'time_enabled' => true,
'instructions' => __('statamic::messages.form_configure_close_date_instructions'),
],
'submission_limit' => [
'display' => __('Submission Limit'),
'type' => 'integer',
'instructions' => __('statamic::messages.form_configure_submission_limit_instructions'),
],
'submission_limit_period' => [
'display' => __('Submission Limit Period'),
'type' => 'button_group',
'default' => 'total',
'options' => [
'total' => __('Total'),
'day' => __('Per Day'),
'week' => __('Per Week'),
'month' => __('Per Month'),
],
'if' => [
'submission_limit' => 'not empty',
],
'instructions' => __('statamic::messages.form_configure_submission_limit_period_instructions'),
],
'closed_message' => [
'display' => __('Closed Message'),
'type' => 'textarea',
'if_any' => [
'close_date' => 'not empty',
'submission_limit' => 'not empty',
],
'instructions' => __('statamic::messages.form_configure_closed_message_instructions'),
],
'require_login' => [
'display' => __('Require Login'),
'type' => 'toggle',
'instructions' => __('statamic::messages.form_configure_require_login_instructions'),
],
'require_login_message' => [
'display' => __('Require Login Message'),
'type' => 'textarea',
'if' => [
'require_login' => 'equals true',
],
'instructions' => __('statamic::messages.form_configure_require_login_message_instructions'),
],
],
],
'email' => [
'display' => __('Email'),
'fields' => [
Expand Down
4 changes: 4 additions & 0 deletions src/Http/Controllers/FormController.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ public function submit(FrontendFormRequest $request, $form)
return Str::startsWith($key, '_');
})->all();

if ($form->restricted()) {
return $this->formFailure($params, ['*' => [$form->restrictionMessage()]], $form->handle());
}

$fields = $fields->addValues($values);

$submission = $form->makeSubmission()->asPartial()->site($site);
Expand Down
22 changes: 22 additions & 0 deletions tests/Feature/Forms/UpdateFormTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,28 @@ public function it_updates_a_form()
$this->assertFalse($updated->store());
}

#[Test]
public function it_updates_restrictions()
{
$form = tap(Form::make('test'))->save();

$this
->actingAs($this->userWithPermission())
->update($form, [
'submission_limit' => 5,
'submission_limit_period' => 'day',
'closed_message' => 'Sorry, we are isClosed.',
'require_login' => true,
])
->assertOk();

$updated = Form::all()->first();
$this->assertEquals(5, $updated->get('submission_limit'));
$this->assertEquals('day', $updated->get('submission_limit_period'));
$this->assertEquals('Sorry, we are isClosed.', $updated->get('closed_message'));
$this->assertTrue($updated->get('require_login'));
}

#[Test]
public function it_updates_emails()
{
Expand Down
Loading
Loading