Form editor v2 (#579)
* Form editor v2 * fix template test * setFormDefaults when save * fix form cleaning dark mode * improvements on open sidebar * UI polish * Fix change type button --------- Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
@@ -48,7 +48,6 @@ class Form extends Model implements CachableAttributes
|
||||
'removed_properties',
|
||||
|
||||
'title',
|
||||
'description',
|
||||
'tags',
|
||||
'visibility',
|
||||
|
||||
@@ -177,12 +176,6 @@ class Form extends Model implements CachableAttributes
|
||||
});
|
||||
}
|
||||
|
||||
public function setDescriptionAttribute($value)
|
||||
{
|
||||
// Strip out unwanted html
|
||||
$this->attributes['description'] = Purify::clean($value);
|
||||
}
|
||||
|
||||
public function setSubmittedTextAttribute($value)
|
||||
{
|
||||
// Strip out unwanted html
|
||||
|
||||
@@ -1,217 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Models\Forms\Form;
|
||||
use App\Models\Template;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Route;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Generates meta per-route matching. This is useful because Google, Twitter and Facebook struggle to load meta tags
|
||||
* injected dynamically by JavaScript. This class allows us to inject meta tags in the HTML head tag.
|
||||
*
|
||||
* Here's how to use this class
|
||||
* - Add a pattern to URL_PATTERNS
|
||||
* - Then choose between a static meta or a dynamic meta:
|
||||
* - If the content is dynamic (ex: needs to retrieve data from the database), then add a method to this class for
|
||||
* the corresponding pattern. The method should be named "getMyPatternName" (where pattern name is
|
||||
* my_pattern_name) and it should return an array of meta tags.
|
||||
* - If the content is static, then add meta tags to the PATTERN_STATIC_META array.
|
||||
*/
|
||||
class SeoMetaResolver
|
||||
{
|
||||
private array $patternData = [];
|
||||
|
||||
public const URL_PATTERNS = [
|
||||
'welcome' => '/',
|
||||
'form_show' => '/forms/{slug}',
|
||||
'login' => '/login',
|
||||
'register' => '/register',
|
||||
'reset_password' => '/password/reset',
|
||||
'privacy_policy' => '/privacy-policy',
|
||||
'terms_conditions' => '/terms-conditions',
|
||||
'integrations' => '/integrations',
|
||||
'templates' => '/form-templates',
|
||||
'templates_show' => '/form-templates/{slug}',
|
||||
'templates_types_show' => '/form-templates/types/{slug}',
|
||||
'templates_industries_show' => '/form-templates/industries/{slug}',
|
||||
];
|
||||
|
||||
/**
|
||||
* Metas for simple route (without needing to access DB)
|
||||
*/
|
||||
public const PATTERN_STATIC_META = [
|
||||
'login' => [
|
||||
'title' => 'Login',
|
||||
],
|
||||
'register' => [
|
||||
'title' => 'Create your account',
|
||||
],
|
||||
'reset_password' => [
|
||||
'title' => 'Reset your password',
|
||||
],
|
||||
'privacy_policy' => [
|
||||
'title' => 'Our Privacy Policy',
|
||||
],
|
||||
'terms_conditions' => [
|
||||
'title' => 'Our Terms & Conditions',
|
||||
],
|
||||
'integrations' => [
|
||||
'title' => 'Our Integrations',
|
||||
],
|
||||
'templates' => [
|
||||
'title' => 'Templates',
|
||||
'description' => 'Our collection of beautiful templates to create your own forms!',
|
||||
],
|
||||
];
|
||||
|
||||
public const META_CACHE_DURATION = 60 * 60 * 12; // 12 hours
|
||||
|
||||
public const META_CACHE_KEY_PREFIX = 'seo_meta_';
|
||||
|
||||
public function __construct(private Request $request)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the right metas for a given route, caches meta for 1 hour.
|
||||
*/
|
||||
public function getMetas(): array
|
||||
{
|
||||
$cacheKey = self::META_CACHE_KEY_PREFIX.urlencode($this->request->path());
|
||||
|
||||
return Cache::remember($cacheKey, now()->addSeconds(self::META_CACHE_DURATION), function () {
|
||||
$pattern = $this->resolvePattern();
|
||||
|
||||
if ($this->hasPatternMetaGetter($pattern)) {
|
||||
// Custom function for pattern
|
||||
try {
|
||||
return array_merge($this->getDefaultMeta(), $this->{'get'.Str::studly($pattern).'Meta'}());
|
||||
} catch (\Exception $e) {
|
||||
return $this->getDefaultMeta();
|
||||
}
|
||||
} elseif (in_array($pattern, array_keys(self::PATTERN_STATIC_META))) {
|
||||
// Simple meta for pattern
|
||||
$meta = self::PATTERN_STATIC_META[$pattern];
|
||||
if (isset($meta['title'])) {
|
||||
$meta['title'] .= $this->titleSuffix();
|
||||
}
|
||||
if (isset($meta['image'])) {
|
||||
$meta['image'] = asset($meta['image']);
|
||||
}
|
||||
|
||||
return array_merge($this->getDefaultMeta(), $meta);
|
||||
}
|
||||
|
||||
return $this->getDefaultMeta();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates the Laravel router to match route with Metas
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function resolvePattern()
|
||||
{
|
||||
foreach (self::URL_PATTERNS as $patternName => $patternData) {
|
||||
$path = rtrim($this->request->getPathInfo(), '/') ?: '/';
|
||||
|
||||
$route = (new Route('GET', $patternData, fn () => ''))->bind($this->request);
|
||||
if (preg_match($route->getCompiled()->getRegex(), rawurldecode($path))) {
|
||||
$this->patternData = $route->parameters();
|
||||
|
||||
return $patternName;
|
||||
}
|
||||
}
|
||||
|
||||
return 'default';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a get mutator exists for a pattern.
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
private function hasPatternMetaGetter($key)
|
||||
{
|
||||
return method_exists($this, 'get'.Str::studly($key).'Meta');
|
||||
}
|
||||
|
||||
private function titleSuffix()
|
||||
{
|
||||
return ' · '.config('app.name');
|
||||
}
|
||||
|
||||
private function getDefaultMeta(): array
|
||||
{
|
||||
return [
|
||||
'title' => 'Create beautiful forms for free'.$this->titleSuffix(),
|
||||
'description' => "Create beautiful forms for free. Unlimited fields, unlimited submissions. It's free and it takes less than 1 minute to create your first form.",
|
||||
'image' => asset('/img/social-preview.jpg'),
|
||||
];
|
||||
}
|
||||
|
||||
private function getFormShowMeta(): array
|
||||
{
|
||||
$form = Form::whereSlug($this->patternData['slug'])->firstOrFail();
|
||||
|
||||
$meta = [];
|
||||
if ($form->is_pro && $form->seo_meta->page_title) {
|
||||
$meta['title'] = $form->seo_meta->page_title;
|
||||
} else {
|
||||
$meta['title'] = $form->title.$this->titleSuffix();
|
||||
}
|
||||
|
||||
if ($form->is_pro && $form->seo_meta->page_description) {
|
||||
$meta['description'] = $form->seo_meta->page_description;
|
||||
} elseif ($form->description) {
|
||||
$meta['description'] = Str::of($form->description)->limit(160);
|
||||
}
|
||||
|
||||
if ($form->is_pro && $form->seo_meta->page_thumbnail) {
|
||||
$meta['image'] = $form->seo_meta->page_thumbnail;
|
||||
} elseif ($form->cover_picture) {
|
||||
$meta['image'] = $form->cover_picture;
|
||||
}
|
||||
|
||||
return $meta;
|
||||
}
|
||||
|
||||
private function getTemplatesShowMeta(): array
|
||||
{
|
||||
$template = Template::whereSlug($this->patternData['slug'])->firstOrFail();
|
||||
|
||||
return [
|
||||
'title' => $template->name.$this->titleSuffix(),
|
||||
'description' => Str::of($template->short_description)->limit(140).' | Customize any template and create your own form in minutes.',
|
||||
'image' => $template->image_url,
|
||||
];
|
||||
}
|
||||
|
||||
private function getTemplatesTypesShowMeta(): array
|
||||
{
|
||||
$types = json_decode(file_get_contents(resource_path('data/forms/templates/types.json')), true);
|
||||
$type = $types[array_search($this->patternData['slug'], array_column($types, 'slug'))];
|
||||
|
||||
return [
|
||||
'title' => $type['meta_title'],
|
||||
'description' => Str::of($type['meta_description'])->limit(140),
|
||||
];
|
||||
}
|
||||
|
||||
private function getTemplatesIndustriesShowMeta(): array
|
||||
{
|
||||
$industries = json_decode(file_get_contents(resource_path('data/forms/templates/industries.json')), true);
|
||||
$industry = $industries[array_search($this->patternData['slug'], array_column($industries, 'slug'))];
|
||||
|
||||
return [
|
||||
'title' => $industry['meta_title'],
|
||||
'description' => Str::of($industry['meta_description'])->limit(140),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Forms\Form;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return new class () extends Migration {
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Form::chunk(100, function ($forms) {
|
||||
foreach ($forms as $form) {
|
||||
$properties = $form->properties;
|
||||
if (!empty($form->description)) {
|
||||
array_unshift($properties, [
|
||||
'type' => 'nf-text',
|
||||
'content' => $form->description,
|
||||
'name' => 'Description',
|
||||
'id' => Str::uuid()
|
||||
]);
|
||||
$form->properties = $properties;
|
||||
$form->timestamps = false;
|
||||
$form->save();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Form::chunk(100, function ($forms) {
|
||||
foreach ($forms as $form) {
|
||||
$properties = $form->properties;
|
||||
if (!empty($properties) && $properties[0]['type'] === 'nf-text' && $properties[0]['name'] === 'Description') {
|
||||
array_shift($properties);
|
||||
$form->properties = $properties;
|
||||
$form->save();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -65,8 +65,7 @@ it('can update a form', function () {
|
||||
|
||||
$this->assertDatabaseHas('forms', [
|
||||
'id' => $form->id,
|
||||
'title' => $form->title,
|
||||
'description' => $form->description,
|
||||
'title' => $form->title
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -125,8 +124,7 @@ it('can duplicate a form', function () {
|
||||
expect($workspace->forms()->count())->toBe(2);
|
||||
$this->assertDatabaseHas('forms', [
|
||||
'id' => $response->json('new_form.id'),
|
||||
'title' => 'Copy of ' . $form->title,
|
||||
'description' => $form->description,
|
||||
'title' => 'Copy of ' . $form->title
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -18,6 +18,5 @@ it('can make a form for a database', function () {
|
||||
$workspace = $this->createUserWorkspace($user);
|
||||
$form = $this->makeForm($user, $workspace);
|
||||
expect($form->title)->not()->toBeNull();
|
||||
expect($form->description)->not()->toBeNull();
|
||||
expect(count($form->properties))->not()->toBe(0);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user