├── .github ├── pr-labeler.yml ├── workflows │ ├── label-pr.yml │ ├── create-draft-release.yml │ └── release.yml └── release-drafter.yml ├── resources └── views │ └── edit.blade.php ├── src ├── Events │ └── ConfigSaved.php ├── ServiceProvider.php ├── Forma.php ├── Addons.php ├── FormaAddon.php └── ConfigController.php ├── LICENSE ├── composer.json └── README.md /.github/pr-labeler.yml: -------------------------------------------------------------------------------- 1 | chore: 'chore/*' 2 | feature: ['feature/*', 'feat/*'] 3 | fix: 'fix/*' 4 | improvement: ['improvement/*', 'improve/*'] 5 | -------------------------------------------------------------------------------- /.github/workflows/label-pr.yml: -------------------------------------------------------------------------------- 1 | name: PR Labeler 2 | on: 3 | pull_request: 4 | types: [opened] 5 | 6 | jobs: 7 | pr-labeler: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: TimonVS/pr-labeler-action@v4 11 | env: 12 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 13 | -------------------------------------------------------------------------------- /resources/views/edit.blade.php: -------------------------------------------------------------------------------- 1 | @extends('statamic::layout') 2 | 3 | @section('title', $title) 4 | 5 | @section('content') 6 | 13 | @stop 14 | -------------------------------------------------------------------------------- /src/Events/ConfigSaved.php: -------------------------------------------------------------------------------- 1 | app->singleton(Forma::class, fn () => new Forma); 12 | } 13 | 14 | public function bootAddon() 15 | { 16 | Forma::all()->each->boot(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/create-draft-release.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | pull_request: 5 | types: [closed] 6 | 7 | jobs: 8 | update_release_draft: 9 | if: github.event.pull_request.merged == true 10 | permissions: write-all 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: release-drafter/release-drafter@v6 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /src/Forma.php: -------------------------------------------------------------------------------- 1 | addons = collect(); 14 | } 15 | 16 | public function add(string $package, ?string $controller = null, ?string $handle = null): void 17 | { 18 | $this->addons->push(new FormaAddon($package, $controller, $handle)); 19 | } 20 | 21 | public function findBySlug(string $slug): FormaAddon 22 | { 23 | return $this->all()->first(fn (FormaAddon $addon) => $addon->statamicAddon()->slug() === $slug); 24 | } 25 | 26 | public function all(): Collection 27 | { 28 | return $this->addons; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$RESOLVED_VERSION' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | categories: 4 | - title: '🚀 New' 5 | labels: 6 | - 'feature' 7 | - 'enhancement' 8 | - title: '🐛 Fixed' 9 | labels: 10 | - 'fix' 11 | - 'bugfix' 12 | - 'bug' 13 | - title: '🔧 Improved' 14 | labels: 15 | - 'change' 16 | - 'improve' 17 | - 'improvement' 18 | - 'sync' 19 | - title: '🧰 Maintenance' 20 | label: 'chore' 21 | change-template: '- $TITLE [@$AUTHOR](https://github.com/$AUTHOR) (#$NUMBER)' 22 | change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. 23 | version-resolver: 24 | major: 25 | labels: 26 | - 'major' 27 | minor: 28 | labels: 29 | - 'feature' 30 | - 'enhancement' 31 | - 'change' 32 | - 'improve' 33 | - 'improvement' 34 | patch: 35 | labels: 36 | - 'fix' 37 | - 'bugfix' 38 | - 'bug' 39 | - 'sync' 40 | default: patch 41 | template: | 42 | $CHANGES 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Erin Dalzell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "edalzell/forma", 3 | "license": "mit", 4 | "type": "statamic-addon", 5 | "require": { 6 | "php": "^8.0", 7 | "laravel/framework": "^10.0 || ^11.0 || ^12.0", 8 | "statamic/cms": "^4.0 || ^5.0", 9 | "stillat/proteus": "^4.0" 10 | }, 11 | "require-dev": { 12 | "mockery/mockery": "^1.3.1", 13 | "nunomaduro/collision": "^7.0 || ^8.0", 14 | "phpunit/phpunit": "^10.0 || ^11.0", 15 | "orchestra/testbench": "^8.0 || ^9.0 || ^10.0" 16 | }, 17 | "description": "Give control panel access to your addon's config", 18 | "authors": [ 19 | { 20 | "name": "Erin Dalzell", 21 | "email": "erin@silentz.co", 22 | "homepage": "https://silentz.co", 23 | "role": "Founder" 24 | } 25 | ], 26 | "autoload": { 27 | "psr-4": { 28 | "Edalzell\\Forma\\": "src" 29 | } 30 | }, 31 | "extra": { 32 | "statamic": { 33 | "name": "Forma", 34 | "description": "Create addon config page in the CP" 35 | }, 36 | "laravel": { 37 | "providers": [ 38 | "Edalzell\\Forma\\ServiceProvider" 39 | ] 40 | } 41 | }, 42 | "config": { 43 | "allow-plugins": { 44 | "pixelfear/composer-dist-plugin": true 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | upload_assets: 7 | default: false 8 | description: 'Upload assets to release' 9 | type: boolean 10 | jobs: 11 | get_draft_release: 12 | runs-on: ubuntu-latest 13 | outputs: 14 | release_body: ${{ steps.latest_draft_release.outputs.body }} 15 | release_id: ${{ steps.latest_draft_release.outputs.id }} 16 | release_tag: ${{ steps.latest_draft_release.outputs.tag_name }} 17 | release_upload_url: ${{ steps.latest_draft_release.outputs.upload_url }} 18 | steps: 19 | - name: Get Draft Release 20 | uses: cardinalby/git-get-release-action@v1 21 | id: latest_draft_release 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | with: 25 | draft: true 26 | latest: true 27 | 28 | upload_assets: 29 | if: ${{ inputs.upload_assets }} 30 | needs: get_draft_release 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: Checkout code 34 | uses: actions/checkout@v3 35 | - name: Install dependencies 36 | run: npm install 37 | - name: Compile assets 38 | run: npm run production 39 | - name: Create zip 40 | run: tar -czvf dist.tar.gz dist 41 | - name: Upload zip to release 42 | uses: actions/upload-release-asset@v1 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | with: 46 | upload_url: ${{ needs.get_draft_release.outputs.release_upload_url }} 47 | asset_path: ./dist.tar.gz 48 | asset_name: dist.tar.gz 49 | asset_content_type: application/tar+gz 50 | 51 | release: 52 | runs-on: ubuntu-latest 53 | needs: get_draft_release 54 | steps: 55 | - name: Checkout code 56 | uses: actions/checkout@v3 57 | - name: Update Changelog 58 | id: update_changelog 59 | uses: stefanzweifel/changelog-updater-action@v1 60 | with: 61 | latest-version: ${{ needs.get_draft_release.outputs.release_tag }} 62 | release-notes: ${{ needs.get_draft_release.outputs.release_body }} 63 | - name: Commit updated CHANGELOG 64 | uses: stefanzweifel/git-auto-commit-action@v4 65 | with: 66 | branch: main 67 | commit_message: Update CHANGELOG 68 | file_pattern: CHANGELOG.md 69 | - uses: eregon/publish-release@v1 70 | env: 71 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 72 | with: 73 | release_id: ${{ needs.get_draft_release.outputs.release_id }} 74 | -------------------------------------------------------------------------------- /src/FormaAddon.php: -------------------------------------------------------------------------------- 1 | controller = $controller ?? ConfigController::class; 24 | } 25 | 26 | public function boot(): void 27 | { 28 | $this 29 | ->bootNav() 30 | ->bootPermissions() 31 | ->registerRoutes(); 32 | } 33 | 34 | public function configHandle(): string 35 | { 36 | return $this->config ?? $this->statamicAddon()->slug(); 37 | } 38 | 39 | public function statamicAddon(): ?Addon 40 | { 41 | return Blink::once($this->package, fn () => AddonFacade::get($this->package)); 42 | } 43 | 44 | private function bootNav(): self 45 | { 46 | if (! $addon = $this->statamicAddon()) { 47 | return $this; 48 | } 49 | 50 | $controllerInstance = app($this->controller); 51 | 52 | NavFacade::extend(fn (Nav $nav) => $nav 53 | ->content($addon->name()) 54 | ->section($controllerInstance::cpSection()) 55 | ->can('manage '.$addon->slug().' settings') 56 | ->route($addon->slug().'.config.edit') 57 | ->icon($controllerInstance::cpIcon()) 58 | ); 59 | 60 | return $this; 61 | } 62 | 63 | private function bootPermissions(): self 64 | { 65 | if (! $addon = $this->statamicAddon()) { 66 | return $this; 67 | } 68 | 69 | Permission::register('manage '.$addon->slug().' settings') 70 | ->label('Manage '.$addon->name().' Settings'); 71 | 72 | return $this; 73 | } 74 | 75 | private function registerRoutes(): self 76 | { 77 | if (is_null($addon = $this->statamicAddon())) { 78 | return $this; 79 | } 80 | 81 | Statamic::pushCpRoutes(fn () => Route::name($addon->slug().'.')->prefix($addon->slug())->group(function () { 82 | Route::name('config.')->prefix('config')->group(function () { 83 | Route::get('edit', [$this->controller, 'edit'])->name('edit'); 84 | Route::post('update', [$this->controller, 'update'])->name('update'); 85 | }); 86 | })); 87 | 88 | return $this; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/ConfigController.php: -------------------------------------------------------------------------------- 1 | segment(2); 21 | 22 | $blueprint = $this->getBlueprint($addon = Forma::findBySlug($slug)); 23 | 24 | $fields = $blueprint 25 | ->fields() 26 | ->addValues($this->preProcess($addon->statamicAddon()->slug())) 27 | ->preProcess(); 28 | 29 | return view('forma::edit', [ 30 | 'blueprint' => $blueprint->toPublishArray(), 31 | 'meta' => $fields->meta(), 32 | 'route' => cp_route("{$slug}.config.update", ['handle' => $slug]), 33 | 'title' => $this->cpTitle($addon), 34 | 'values' => $fields->values(), 35 | ]); 36 | } 37 | 38 | public function update(Request $request): void 39 | { 40 | $slug = $request->segment(2); 41 | 42 | $blueprint = $this->getBlueprint($addon = Forma::findBySlug($slug)); 43 | 44 | // Get a Fields object, and populate it with the submitted values. 45 | $fields = $blueprint->fields()->addValues($request->all()); 46 | 47 | // Perform validation. Like Laravel's standard validation, if it fails, 48 | // a 422 response will be sent back with all the validation errors. 49 | $fields->validate(); 50 | 51 | $data = $this->postProcess($fields->process()->values()->toArray()); 52 | 53 | ConfigWriter::writeMany($addon->configHandle(), $data); 54 | 55 | ConfigSaved::dispatch($data, $addon->statamicAddon()); 56 | } 57 | 58 | private function getBlueprint(FormaAddon $addon): Blueprint 59 | { 60 | $path = Path::assemble($addon->statamicAddon()->directory(), 'resources', 'blueprints', 'config.yaml'); 61 | 62 | $yaml = YAML::file($path)->parse(); 63 | 64 | if ($yaml['tabs'] ?? false) { 65 | return BlueprintAPI::make()->setContents($yaml); 66 | } 67 | 68 | return BlueprintAPI::makeFromFields($yaml); 69 | } 70 | 71 | protected function postProcess(array $values): array 72 | { 73 | return $values; 74 | } 75 | 76 | protected function preProcess(string $handle): array 77 | { 78 | return config(Forma::findBySlug($handle)->configHandle()); 79 | } 80 | 81 | public static function cpIcon(): string 82 | { 83 | return 'settings-horizontal'; 84 | } 85 | 86 | public static function cpSection(): string 87 | { 88 | return __('Settings'); 89 | } 90 | 91 | private function cpTitle(FormaAddon $addon): string 92 | { 93 | return __(':name Settings', ['name' => $addon->statamicAddon()->name()]); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Give your Statamic addons a beautiful configuration page in the control panel 2 | [![Latest Version](https://img.shields.io/github/release/edalzell/statamic-forma.svg?style=flat-square)](https://github.com/edalzell/statamic-forma/releases) 3 | 4 | This package provides an easy way to let users configure your addon. 5 | 6 | ## Requirements 7 | 8 | * PHP 8.2+ 9 | * Laravel 10.0+ 10 | * Statamic 4.0+ 11 | 12 | ## Installation 13 | 14 | You can install this package via composer using: 15 | 16 | ```bash 17 | composer require edalzell/forma 18 | ``` 19 | 20 | The package will automatically register itself. 21 | 22 | ## Usage 23 | 24 | First, create a `config.yaml` file in `resources\blueprints` that contains the blueprint for your configuration. As an example, see Mailchimp's, [here](https://github.com/statamic-rad-pack/mailchimp/blob/main/resources/blueprints/config.yaml). 25 | 26 | Then, in the `boot` method of your addon's Service Provider add: 27 | ```php 28 | parent::boot(); 29 | \Edalzell\Forma\Forma::add('statamic-rad-pack/mailchimp', ConfigController::class); 30 | ``` 31 | 32 | The second parameter is optional and only needed if you need custom config handling (see Extending below) 33 | 34 | There is a 3rd parameter `handle` you can use if the config file is NOT the addon's handle. 35 | 36 | Once you do that, you get a menu item in the cp that your users can access and use. All data is saved into your `addon_handle.php` (or `$handle` as per above) in the `config` folder. 37 | 38 | ![menu item](https://raw.githubusercontent.com/edalzell/statamic-forma/main/images/mailchimp-menu.png) 39 | 40 | ### Permissions 41 | 42 | There is a `Manage Addon Settings` permission that must be enabled to allow a user to update the settings of any Forma-enabled addons. 43 | 44 | ### Extending 45 | 46 | If your addon needs to wangjangle the config before loading and after saving, create your own controller that `extends \Edalzell\Forma\ConfigController` and use the `preProcess` and `postProcess` methods. 47 | 48 | For example, the Mailchimp addon stores a config like this: 49 | ```php 50 | 'user' => [ 51 | 'check_consent' => true, 52 | 'consent_field' => 'permission', 53 | 'merge_fields' => [ 54 | [ 55 | 'field_name' => 'first_name', 56 | ], 57 | ], 58 | 'disable_opt_in' => true, 59 | 'interests_field' => 'interests', 60 | ], 61 | ``` 62 | 63 | But there is no Blueprint that supports that, so it uses a grid, which expects the data to look like: 64 | ```php 65 | 'user' => [ 66 | [ 67 | 'check_consent' => true, 68 | 'consent_field' => 'permission', 69 | 'merge_fields' => [ 70 | [ 71 | 'field_name' => 'first_name', 72 | ], 73 | ], 74 | 'disable_opt_in' => true, 75 | 'interests_field' => 'interests', 76 | ] 77 | ], 78 | ``` 79 | 80 | Therefore in its `ConfigController`: 81 | ```php 82 | protected function postProcess(array $values): array 83 | { 84 | $userConfig = Arr::get($values, 'user'); 85 | 86 | return array_merge( 87 | $values, 88 | ['user' => $userConfig[0]] 89 | ); 90 | } 91 | 92 | protected function preProcess(string $handle): array 93 | { 94 | $config = config($handle); 95 | 96 | return array_merge( 97 | $config, 98 | ['user' => [Arr::get($config, 'user', [])]] 99 | ); 100 | } 101 | ``` 102 | 103 | 104 | ## Changelog 105 | 106 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 107 | 108 | ## Contributing 109 | 110 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 111 | 112 | ## Security 113 | 114 | If you discover any security related issues, please email [addon-security@silentz.co](mailto:addon-security@silentz.co) instead of using the issue tracker. 115 | 116 | ## License 117 | 118 | MIT License 119 | --------------------------------------------------------------------------------