Skip to content
Open
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
2 changes: 1 addition & 1 deletion lang/en/messages.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@
'forgot_password_sent' => 'If an account with that email address exists, a password reset link has been sent.',
'form_configure_blueprint_instructions' => 'Choose from existing blueprints or create a new one.',
'form_configure_email_attachments_instructions' => 'Attach files uploaded with the submission.',
'form_configure_email_body_instructions' => 'You can include submitted values using [Antlers](https://statamic.dev/frontend/antlers). Leave blank to list all submitted fields instead.',
'form_configure_email_body_instructions' => 'Type @ to include submitted values. Leave blank to list all submitted fields instead.',
'form_configure_email_from_instructions' => 'Leave blank to fall back to the site default.',
'form_configure_email_views_instructions' => 'Leave both blank to use the default layout.',
'form_configure_email_description' => 'Be sure your email settings are configured to send and receive notifications when forms are submitted.',
Expand Down
2 changes: 1 addition & 1 deletion resources/views/forms/automagic-email.antlers.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{{ if email_config:body }}
{{ email_config:body | sanitize | nl2br }}
{{ email_config:body }}
{{ else }}
{{ fields }}
<b>{{ display }}:</b>
Expand Down
2 changes: 2 additions & 0 deletions src/Facades/Form.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
* @method static array extraConfigFor(string $handle)
* @method static array extraConfigPositionsFor(string $handle)
* @method static self redirect(string $form, \Closure $callback)
* @method static self registerMention(string $handle, string $label, \Closure $value)
* @method static array mentions()
* @method static \Closure getSubmissionRedirect(Submission $submission)
* @method static ExporterRepository exporters()
*
Expand Down
6 changes: 6 additions & 0 deletions src/Forms/Connectors/Connector.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Statamic\Fields\Blueprint;
use Statamic\Fields\Fields;
use Statamic\Fields\Validator;
use Statamic\Forms\SubmissionMentions;
use Statamic\Statamic;
use Statamic\Support\Arr;
use Statamic\Support\Str;
Expand Down Expand Up @@ -232,6 +233,11 @@ protected function processConnection(array $connection): array
return $this->blueprint()->fields()->addValues($connection)->process()->values()->all();
}

protected function mentionOptions(): array
{
return SubmissionMentions::options($this->form());
}

public function routes(Router $router): void
{
//
Expand Down
8 changes: 5 additions & 3 deletions src/Forms/Connectors/Email.php
Original file line number Diff line number Diff line change
Expand Up @@ -169,16 +169,18 @@ public function blueprint(): \Statamic\Fields\Blueprint
[
'handle' => 'subject',
'field' => [
'type' => 'text',
'type' => 'autocomplete_editor',
'display' => __('Subject'),
'markdown' => false,
'options' => $this->mentionOptions(),
],
],
// TODO: Replace with autocomplete editor once its merged (https://github.com/statamic/cms/pull/15055)
[
'handle' => 'body',
'field' => [
'type' => 'textarea',
'type' => 'autocomplete_editor',
'display' => __('Message'),
'options' => $this->mentionOptions(),
'instructions' => __('statamic::messages.form_configure_email_body_instructions'),
],
],
Expand Down
37 changes: 32 additions & 5 deletions src/Forms/Email.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
use Statamic\Facades\Form;
use Statamic\Facades\GlobalSet;
use Statamic\Facades\Parse;
use Statamic\Fieldtypes\AutocompleteEditor;
use Statamic\Fieldtypes\AutocompleteEditor\Mentions;
use Statamic\Forms\Fields\FormField;
use Statamic\Sites\Site;
use Statamic\Support\Arr;
Expand All @@ -30,6 +32,7 @@ class Email extends Mailable
protected $submissionData;
protected $config;
protected $site;
protected $mentionValues = [];
private $globalData;

public function __construct(Submission $submission, array $config, Site $site)
Expand Down Expand Up @@ -71,14 +74,30 @@ public function build()
$this->submissionData = $this->submission->toAugmentedArray();
$this->config = $this->parseConfig($this->config);

$subject = __((string) ($this->config['subject'] ?? ''));
$body = (string) ($this->config['body'] ?? '');

$this->mentionValues = SubmissionMentions::values(
$this->submission,
SubmissionMentions::handlesIn($subject, $body),
$this->submissionData
);

$this
->subject(isset($this->config['subject']) ? __($this->config['subject']) : __('Form Submission'))
->subject($this->renderSubject($subject))
->addAddresses()
->addViews()
->addAttachments()
->addData();
}

protected function renderSubject(string $subject): string
{
$subject = Mentions::replace($subject, $this->mentionValues, using: fn ($text) => $this->parseConfigValue($text));

return trim(preg_replace('/\s+/', ' ', $subject)) ?: __('Form Submission');
}

protected function addAddresses()
{
$this->to($this->addresses(Arr::get($this->config, 'to')));
Expand Down Expand Up @@ -192,9 +211,15 @@ protected function addData()
? Blueprint::makeFromTabs($configFields)->fields()->addValues($form->data()->all())->augment()->values()->all()
: [];

$emailConfig = $this->config;

if (filled($body = $emailConfig->get('body'))) {
$emailConfig = $emailConfig->merge(['body' => AutocompleteEditor::renderHtml($body, $this->mentionValues)]);
}

$data = array_merge($augmented, $this->getGlobalsData(), [
'form_config' => $formConfig,
'email_config' => $this->config,
'email_config' => $emailConfig,
'config' => Cascade::config(),
'fields' => $fields,
'sections' => $sections->all(),
Expand Down Expand Up @@ -313,9 +338,11 @@ protected function parseConfig(array $config): Collection
{
return collect($config)
->except('conditions')
->map(fn ($value) => is_array($value)
? array_map(fn ($item) => Str::startsWith($item, 'field:') ? $item : $this->parseConfigValue($item), $value)
: $this->parseConfigValue($value));
->map(fn ($value, $key) => match (true) {
in_array($key, ['subject', 'body']) => $value,
is_array($value) => array_map(fn ($item) => Str::startsWith($item, 'field:') ? $item : $this->parseConfigValue($item), $value),
default => $this->parseConfigValue($value),
});
}

private function parseConfigValue($value): string
Expand Down
22 changes: 22 additions & 0 deletions src/Forms/FormRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Statamic\Exceptions\FormNotFoundException;
use Statamic\Facades\File;
use Statamic\Facades\Folder;
use Statamic\Fieldtypes\AutocompleteEditor\Mentions;
use Statamic\Forms\Exporters\ExporterRepository;
use Statamic\Support\Arr;
use Statamic\Support\Str;
Expand All @@ -18,6 +19,7 @@ class FormRepository implements Contract
{
private $configs = [];
private $redirects = [];
private $mentions = [];

/**
* Find a form.
Expand Down Expand Up @@ -143,6 +145,26 @@ public function redirect(string $form, Closure $callback)
return $this;
}

public function registerMention(string $handle, string $label, Closure $value)
{
if (! str_contains($handle, '.') || ! Mentions::isValidValue($handle)) {
throw new InvalidArgumentException("Invalid mention handle [{$handle}]. It must contain a dot and only use letters, numbers, underscores, dashes and dots.");
}

if (str_starts_with($handle, 'form.') || str_starts_with($handle, 'site.') || str_starts_with($handle, 'globals.')) {
throw new InvalidArgumentException("Invalid mention handle [{$handle}]. The [form.], [site.] and [globals.] prefixes are reserved.");
}

$this->mentions[$handle] = ['label' => $label, 'value' => $value];

return $this;
}

public function mentions(): array
{
return $this->mentions;
}

public function getSubmissionRedirect(SubmissionContract $submission)
{
$callback = $this->redirects[$submission->form()->handle()] ?? fn () => null;
Expand Down
189 changes: 189 additions & 0 deletions src/Forms/SubmissionMentions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
<?php

namespace Statamic\Forms;

use Statamic\Contracts\Assets\Asset;
use Statamic\Contracts\Forms\Form;
use Statamic\Contracts\Forms\Submission;
use Statamic\Contracts\Query\Builder;
use Statamic\Facades\Form as FormFacade;
use Statamic\Facades\GlobalSet;
use Statamic\Fields\LabeledValue;
use Statamic\Fields\Value;
use Statamic\Fieldtypes\AutocompleteEditor\Mentions;
use Statamic\Fieldtypes\FormUpload;
use Statamic\Fieldtypes\Markdown;
use Statamic\Fieldtypes\Relationship;
use Statamic\Support\Arr;
use Stringable;

use function Statamic\trans as __;

class SubmissionMentions
{
private const EXCLUDED_GLOBAL_FIELDTYPES = [
'bard', 'replicator', 'grid', 'group', 'table', 'array', 'yaml', 'code',
'section', 'spacer', 'revealer', 'html', 'hidden', 'icon',
];

private const EXCLUDED_FORM_VALUES = ['id', 'date', 'form'];

public static function options(Form $form): array
{
$options = [];

foreach ($form->formFields()->fields() as $handle => $field) {
if ($field->fieldtype()->collectsValue()) {
$options[$handle] = __($field->display());
}
}

$others = [
'form.title' => __('Form Title'),
'site.name' => __('Site Name'),
'site.url' => __('Site URL'),
];

foreach (FormFacade::mentions() as $handle => $mention) {
$others[$handle] = __($mention['label']);
}

foreach (GlobalSet::all() as $set) {
foreach ($set->blueprint()?->fields()->all() ?? [] as $handle => $field) {
if (static::isGlobalFieldOffered($field->fieldtype(), $field->type())) {
$others["globals.{$set->handle()}.{$handle}"] = __($set->title()).': '.__($field->display());
}
}
}

asort($others, SORT_NATURAL | SORT_FLAG_CASE);

return $options + $others;
}

public static function handlesIn(string ...$contents): array
{
preg_match_all(Mentions::TOKEN, implode("\n", $contents), $matches);

return array_values(array_unique($matches[1]));
}

public static function values(Submission $submission, array $handles, ?array $submissionData = null): array
{
$registered = FormFacade::mentions();
$globals = [];
$values = [];

foreach ($handles as $handle) {
if ($handle === 'form.title') {
$value = $submission->form()->title();
} elseif ($handle === 'site.name') {
$value = $submission->site()->name();
} elseif ($handle === 'site.url') {
$value = $submission->site()->absoluteUrl();
} elseif (isset($registered[$handle])) {
$value = $registered[$handle]['value']($submission);
} elseif (str_starts_with($handle, 'globals.')) {
$value = static::globalValue($submission, $handle, $globals);
} elseif (in_array($handle, self::EXCLUDED_FORM_VALUES) || ! $submission->form()->fields()->has($handle)) {
continue;
} else {
$submissionData ??= $submission->toAugmentedArray();
$value = static::formFieldValue($submission, $handle, $submissionData[$handle] ?? null);
}

$values[$handle] = static::mentionValue($value);
}

return $values;
}

public static function replace(Submission $submission, string $content, ?callable $using = null, ?array $submissionData = null): string
{
$values = static::values($submission, static::handlesIn($content), $submissionData);

return Mentions::replace($content, $values, using: $using);
}

public static function mentionValue(mixed $value): string
{
if ($value instanceof Value) {
$value = $value->value();
}

if ($value instanceof Builder) {
$value = $value->get();
}

if ($value instanceof LabeledValue) {
return (string) ($value->label() ?? $value->value());
}

if ($value instanceof Asset) {
return (string) $value->absoluteUrl();
}

if (is_bool($value)) {
return $value ? __('Yes') : __('No');
}

if (is_iterable($value)) {
return collect($value)
->map(fn ($item) => static::mentionValue($item))
->filter(fn ($item) => $item !== '')
->implode(', ');
}

if (is_scalar($value) || $value instanceof Stringable) {
return (string) $value;
}

return '';
}

private static function formFieldValue(Submission $submission, string $handle, mixed $value): mixed
{
$fieldtype = $submission->form()->fields()->get($handle)->fieldtype();

if (! $fieldtype instanceof FormUpload || $fieldtype->config('store')) {
return $value;
}

if ($value instanceof Value) {
$value = $value->value();
}

return collect(Arr::wrap($value))->map(fn ($path) => basename($path))->all();
}

private static function isGlobalFieldOffered($fieldtype, string $type): bool
{
return ! in_array($type, self::EXCLUDED_GLOBAL_FIELDTYPES) && ! $fieldtype instanceof Relationship;
}

private static function globalValue(Submission $submission, string $handle, array &$globals): mixed
{
[, $setHandle, $fieldHandle] = array_pad(explode('.', $handle, 3), 3, null);

if (! $setHandle || ! $fieldHandle) {
return null;
}

if (! array_key_exists($setHandle, $globals)) {
$set = GlobalSet::findByHandle($setHandle);
$site = $submission->site()->handle();

$globals[$setHandle] = $set && $set->existsIn($site)
? $set->in($site)->toAugmentedArray()
: [];
}

$value = $globals[$setHandle][$fieldHandle] ?? null;

if ($value instanceof Value && $value->fieldtype() instanceof Markdown) {
return $value->raw();
}

return $value;
}
}
2 changes: 2 additions & 0 deletions src/Providers/AppServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ public function boot()
return ! $route || $request->is($route.'/*');
});

Facades\Form::registerMention('app.name', 'App Name', fn () => config('app.name'));

$this->addAboutCommandInfo();

$this->registerElevatedSessionMacros();
Expand Down
Loading
Loading