diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a7c44dd --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = space +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..9e9519b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,19 @@ +# Path-based git attributes +# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html + +# Ignore all test and documentation with "export-ignore". +/.github export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/phpunit.xml.dist export-ignore +/art export-ignore +/docs export-ignore +/tests export-ignore +/.editorconfig export-ignore +/.php_cs.dist.php export-ignore +/psalm.xml export-ignore +/psalm.xml.dist export-ignore +/testbench.yaml export-ignore +/UPGRADING.md export-ignore +/phpstan.neon.dist export-ignore +/phpstan-baseline.neon export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e779afd --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +.idea +.php_cs +.php_cs.cache +.phpunit.result.cache +build +composer.lock +coverage +docs +phpunit.xml +phpstan.neon +testbench.yaml +vendor +node_modules +.php-cs-fixer.cache +**/.DS_Store \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..af8ef6b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +All notable changes to `filament-page-blocks` will be documented in this file. + +## 1.0.0 - 2023-08-01 + +- initial release diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..7ea0ebb --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Martin Ro + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..58c1ac5 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# Block-Based Page Builder for Filament + +This is basically a lightweight version of [Z3d0X's](https://github.com/z3d0x) excellent [Filament Fabricator](https://filamentphp.com/plugins/fabricator) Plugin. + +It only provides the blocks functionality without layouts, pages, routing, etc. + +## Installation + +```bash +composer require martin-ro/filament-page-blocks +``` + +## Creating a block + +```bash +php artisan make:page-block +``` + +## Using blocks in your template + +```html + +``` \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..26c389a --- /dev/null +++ b/composer.json @@ -0,0 +1,79 @@ +{ + "name": "martin-ro/filament-page-blocks", + "description": "Block-Based Page Builder Skeleton for Filament Apps", + "keywords": [ + "martin-ro", + "laravel", + "filament-page-blocks" + ], + "homepage": "https://github.com/martin-ro/filament-page-blocks", + "license": "MIT", + "authors": [ + { + "name": "Martin Ro", + "email": "mail@martin.ph", + "role": "Developer" + } + ], + "require": { + "php": "^8.2", + "filament/filament": "^3.0", + "illuminate/contracts": "^9.0 | ^10.0", + "spatie/laravel-package-tools": "^1.13.5" + }, + "require-dev": { + "laravel/pint": "^1.0", + "nunomaduro/collision": "^6.0", + "nunomaduro/larastan": "^2.0.1", + "orchestra/testbench": "^7.0", + "pestphp/pest": "^1.21", + "pestphp/pest-plugin-laravel": "^1.1", + "pestphp/pest-plugin-livewire": "^1.0", + "pestphp/pest-plugin-parallel": "^0.3", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^9.5", + "spatie/laravel-ray": "^1.26" + }, + "autoload": { + "psr-4": { + "MartinRo\\FilamentPageBlocks\\": "src/", + "MartinRo\\FilamentPageBlocks\\Database\\Factories\\": "database/factories" + } + }, + "autoload-dev": { + "psr-4": { + "MartinRo\\FilamentPageBlocks\\Tests\\": "tests" + } + }, + "scripts": { + "pint": "vendor/bin/pint", + "test:pest": "vendor/bin/pest --parallel", + "test:phpstan": "vendor/bin/phpstan analyse", + "test": [ + "@test:pest", + "@test:phpstan" + ] + }, + "config": { + "sort-packages": true, + "allow-plugins": { + "composer/package-versions-deprecated": true, + "pestphp/pest-plugin": true, + "phpstan/extension-installer": true + } + }, + "extra": { + "laravel": { + "providers": [ + "MartinRo\\FilamentPageBlocks\\FilamentPageBlocksServiceProvider" + ], + "aliases": { + "FilamentPageBlocks": "MartinRo\\FilamentPageBlocks\\Facades\\FilamentPageBlocks" + } + } + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..e69de29 diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..966cf39 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,16 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: 5 + paths: + - src + - config + - database + - routes + tmpDir: build/phpstan + checkOctaneCompatibility: true + checkModelProperties: true + checkMissingIterableValueType: false + checkGenericClassInNonGenericObjectType: false + diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..d230ac2 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,39 @@ + + + + + tests + + + + + ./src + + + + + + + + + + + diff --git a/pint.json b/pint.json new file mode 100644 index 0000000..304d8f3 --- /dev/null +++ b/pint.json @@ -0,0 +1,11 @@ +{ + "preset": "laravel", + "rules": { + "blank_line_before_statement": true, + "concat_space": { + "spacing": "one" + }, + "method_argument_space": true, + "single_trait_insert_per_statement": true + } +} diff --git a/resources/views/components/page-blocks.blade.php b/resources/views/components/page-blocks.blade.php new file mode 100644 index 0000000..49d2083 --- /dev/null +++ b/resources/views/components/page-blocks.blade.php @@ -0,0 +1,14 @@ +@props(['blocks' => []]) + +@foreach ($blocks as $blockData) + @php + $pageBlock = \MartinRo\FilamentPageBlocks\Facades\FilamentPageBlocks::getPageBlockFromName($blockData['type']) + @endphp + + @isset($pageBlock) + + @endisset +@endforeach diff --git a/src/Commands/MakePageBlockCommand.php b/src/Commands/MakePageBlockCommand.php new file mode 100644 index 0000000..d2c74ae --- /dev/null +++ b/src/Commands/MakePageBlockCommand.php @@ -0,0 +1,85 @@ +argument('name') ?? $this->askRequired('Name (e.g. `HeroBlock`)', 'name')) + ->trim('/') + ->trim('\\') + ->trim(' ') + ->replace('/', '\\'); + + $pageBlockClass = (string) Str::of($pageBlock)->afterLast('\\'); + + $pageBlockNamespace = Str::of($pageBlock)->contains('\\') ? + (string) Str::of($pageBlock)->beforeLast('\\') : + ''; + + $label = Str::of($pageBlock) + ->beforeLast('Block') + ->explode('\\') + ->map(fn ($segment) => Str::title($segment)) + ->implode(': '); + + $shortName = Str::of($pageBlock) + ->beforeLast('Block') + ->explode('\\') + ->map(fn ($segment) => Str::kebab($segment)) + ->implode('.'); + + $view = Str::of($pageBlock) + ->beforeLast('Block') + ->prepend('components\\page-blocks\\') + ->explode('\\') + ->map(fn ($segment) => Str::kebab($segment)) + ->implode('.'); + + $path = app_path( + (string) Str::of($pageBlock) + ->prepend('Filament\\PageBlocks\\') + ->replace('\\', '/') + ->append('.php'), + ); + + $viewPath = resource_path( + (string) Str::of($view) + ->replace('.', '/') + ->prepend('views/') + ->append('.blade.php'), + ); + + $files = [$path, $viewPath]; + + if (! $this->option('force') && $this->checkForCollision($files)) { + return static::INVALID; + } + + $this->copyStubToApp('PageBlock', $path, [ + 'class' => $pageBlockClass, + 'namespace' => 'App\\Filament\\PageBlocks'.($pageBlockNamespace !== '' ? "\\{$pageBlockNamespace}" : ''), + 'label' => $label, + 'shortName' => $shortName, + ]); + + $this->copyStubToApp('PageBlockView', $viewPath); + + $this->info("Successfully created {$pageBlock}!"); + + return static::SUCCESS; + } +} diff --git a/src/Facades/FilamentPageBlocks.php b/src/Facades/FilamentPageBlocks.php new file mode 100644 index 0000000..57b7bcd --- /dev/null +++ b/src/Facades/FilamentPageBlocks.php @@ -0,0 +1,40 @@ + getPageModel() + * @method static string getRoutingPrefix() + * @method static array getPageUrls() + * @method static ?string getPageUrlFromId(int $id, bool $prefixSlash = false) + * + * @see \MartinRo\FilamentPageBlocks\FilamentPageBlocksManager + */ +class FilamentPageBlocks extends Facade +{ + protected static function getFacadeAccessor() + { + return 'filament-page-blocks'; + } +} diff --git a/src/FilamentPageBlocksManager.php b/src/FilamentPageBlocksManager.php new file mode 100644 index 0000000..42708f1 --- /dev/null +++ b/src/FilamentPageBlocksManager.php @@ -0,0 +1,56 @@ + */ + protected Collection $pageBlocks; + + public function __construct() + { + /** @var Collection */ + $pageBlocks = collect([]); + + $this->pageBlocks = $pageBlocks; + } + + /** + * @param class-string $class + * @param class-string $baseClass + */ + public function register(string $class, string $baseClass): void + { + match ($baseClass) { + PageBlock::class => static::registerPageBlock($class), + default => throw new \Exception('Invalid class type'), + }; + } + + /** @param class-string $pageBlock */ + public function registerPageBlock(string $pageBlock): void + { + if (! is_subclass_of($pageBlock, PageBlock::class)) { + throw new \InvalidArgumentException("{$pageBlock} must extend ".PageBlock::class); + } + + $this->pageBlocks->put($pageBlock::getName(), $pageBlock); + } + + public function getPageBlockFromName(string $name): ?string + { + return $this->pageBlocks->get($name); + } + + public function getPageBlocks(): array + { + return $this->pageBlocks->map(fn ($block) => $block::getBlockSchema())->toArray(); + } + + public function getPageBlocksRaw(): array + { + return $this->pageBlocks->toArray(); + } +} diff --git a/src/FilamentPageBlocksServiceProvider.php b/src/FilamentPageBlocksServiceProvider.php new file mode 100644 index 0000000..718bd20 --- /dev/null +++ b/src/FilamentPageBlocksServiceProvider.php @@ -0,0 +1,81 @@ +name(static::$name) + ->hasViews() + ->hasCommands([ + Commands\MakePageBlockCommand::class, + ]); + } + + public function packageRegistered(): void + { + parent::packageRegistered(); + + $this->app->scoped('filament-page-blocks', function () { + return new FilamentPageBlocksManager(); + }); + } + + public function bootingPackage(): void + { + $this->registerComponentsFromDirectory( + PageBlock::class, + [], + app_path('Filament/PageBlocks'), + 'App\\Filament\\PageBlocks' + ); + } + + protected function registerComponentsFromDirectory(string $baseClass, array $register, ?string $directory, ?string $namespace): void + { + if (blank($directory) || blank($namespace)) { + return; + } + + $filesystem = app(Filesystem::class); + + if ((! $filesystem->exists($directory)) && (! Str::of($directory)->contains('*'))) { + return; + } + + $namespace = Str::of($namespace); + + $register = array_merge( + $register, + collect($filesystem->allFiles($directory)) + ->map(function (SplFileInfo $file) use ($namespace): string { + $variableNamespace = $namespace->contains('*') ? str_ireplace( + ['\\'.$namespace->before('*'), $namespace->after('*')], + ['', ''], + Str::of($file->getPath()) + ->after(base_path()) + ->replace(['/'], ['\\']), + ) : null; + + return (string) $namespace + ->append('\\', $file->getRelativePathname()) + ->replace('*', $variableNamespace) + ->replace(['/', '.php'], ['\\', '']); + }) + ->filter(fn (string $class): bool => is_subclass_of($class, $baseClass) && (! (new ReflectionClass($class))->isAbstract())) + ->each(fn (string $class) => FilamentPageBlocks::register($class, $baseClass)) + ->all(), + ); + } +} diff --git a/src/Forms/Components/PageBuilder.php b/src/Forms/Components/PageBuilder.php new file mode 100644 index 0000000..2704731 --- /dev/null +++ b/src/Forms/Components/PageBuilder.php @@ -0,0 +1,34 @@ +blocks(FilamentPageBlocks::getPageBlocks()); + + $this->label(fn () => new HtmlString('

Content Blocks

')); + + $this->addActionLabel('Add Block'); + + $this->mutateDehydratedStateUsing(static function (?array $state): array { + if (! is_array($state)) { + return array_values([]); + } + + $registerPageBlockNames = array_keys(FilamentPageBlocks::getPageBlocksRaw()); + + return collect($state) + ->filter(fn (array $block) => in_array($block['type'], $registerPageBlockNames, true)) + ->values() + ->toArray(); + }); + } +} diff --git a/src/PageBlock.php b/src/PageBlock.php new file mode 100644 index 0000000..37d0321 --- /dev/null +++ b/src/PageBlock.php @@ -0,0 +1,31 @@ +getName(); + } + + public static function mutateData(array $data): array + { + return $data; + } +} diff --git a/stubs/PageBlock.stub b/stubs/PageBlock.stub new file mode 100644 index 0000000..f334aab --- /dev/null +++ b/stubs/PageBlock.stub @@ -0,0 +1,24 @@ +label('{{ label }}') + ->icon('heroicon-o-rectangle-stack') + ->schema([ + // + ]); + } + + public static function mutateData(array $data): array + { + return $data; + } +} \ No newline at end of file diff --git a/stubs/PageBlockView.stub b/stubs/PageBlockView.stub new file mode 100644 index 0000000..4f185a4 --- /dev/null +++ b/stubs/PageBlockView.stub @@ -0,0 +1,5 @@ +@aware(['page']) + +
+ // +
diff --git a/tests/ExampleTest.php b/tests/ExampleTest.php new file mode 100644 index 0000000..5d36321 --- /dev/null +++ b/tests/ExampleTest.php @@ -0,0 +1,5 @@ +toBeTrue(); +}); diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..eb0c66a --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,5 @@ +in(__DIR__); diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..41bc041 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,10 @@ +