├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── demo.gif ├── dist ├── js │ ├── field.js │ └── field.js.LICENSE.txt └── mix-manifest.json ├── nova.mix.js ├── package.json ├── postcss.config.js ├── resources └── js │ ├── components │ ├── DetailField.vue │ ├── FormField.vue │ ├── IndexField.vue │ └── PanelItem.vue │ ├── field.js │ └── storage │ └── MorphToFieldStorage.js ├── routes └── api.php ├── src ├── FieldServiceProvider.php ├── Http │ └── Controllers │ │ └── MorphableController.php └── InlineMorphTo.php ├── webpack.mix.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /vendor 3 | /node_modules 4 | composer.phar 5 | composer.lock 6 | phpunit.xml 7 | .phpunit.result.cache 8 | .DS_Store 9 | Thumbs.db 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Digital Creative 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nova Inline MorphTo Field 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/digital-creative/nova-inline-morph-to.svg)](https://packagist.org/packages/digital-creative/nova-inline-morph-to) 4 | [![Total Downloads](https://img.shields.io/packagist/dt/digital-creative/nova-inline-morph-to.svg)](https://packagist.org/packages/digital-creative/nova-inline-morph-to) 5 | [![License](https://img.shields.io/packagist/l/digital-creative/nova-inline-morph-to.svg)](https://raw.githubusercontent.com/dcasia/nova-inline-morph-to/master/LICENSE) 6 | 7 | ![Laravel Nova Inline MorphTo Field in action](https://raw.githubusercontent.com/dcasia/nova-inline-morph-to/master/demo.gif) 8 | 9 | ### Install 10 | 11 | ``` 12 | composer require digital-creative/nova-inline-morph-to 13 | ``` 14 | 15 | ### Usage 16 | 17 | The signature is the same as the default `MorphTo` field that ships with Nova. 18 | 19 | ```php 20 | use DigitalCreative\InlineMorphTo\InlineMorphTo; 21 | 22 | class Article extends Resource 23 | { 24 | public function fields(Request $request) 25 | { 26 | return [ 27 | ... 28 | InlineMorphTo::make('Template')->types([ 29 | \App\Nova\Video::class, 30 | \App\Nova\Image::class, 31 | \App\Nova\Text::class, 32 | \App\Nova\Gallery::class, 33 | ]), 34 | ... 35 | ]; 36 | 37 | } 38 | } 39 | ``` 40 | 41 | ## License 42 | 43 | The MIT License (MIT). Please see [License File](https://raw.githubusercontent.com/dcasia/nova-inline-morph-to/master/LICENSE) for more information. 44 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "digital-creative/nova-inline-morph-to", 3 | "description": "A Laravel Nova field for displaying morphTo relationship inline.", 4 | "keywords": [ 5 | "laravel", 6 | "nova", 7 | "field", 8 | "morph", 9 | "morphTo", 10 | "inline" 11 | ], 12 | "authors": [ 13 | { 14 | "name": "Rafael Milewski" 15 | } 16 | ], 17 | "license": "MIT", 18 | "require": { 19 | "php": "^7.3|^8.0", 20 | "laravel/nova": "^4.0" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "DigitalCreative\\InlineMorphTo\\": "src/" 25 | } 26 | }, 27 | "extra": { 28 | "laravel": { 29 | "providers": [ 30 | "DigitalCreative\\InlineMorphTo\\FieldServiceProvider" 31 | ] 32 | } 33 | }, 34 | "config": { 35 | "sort-packages": true 36 | }, 37 | "minimum-stability": "dev", 38 | "prefer-stable": true 39 | } 40 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dcasia/nova-inline-morph-to/23d2cc99012e16fcf797e94967c082d5d24f322c/demo.gif -------------------------------------------------------------------------------- /dist/js/field.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * The buffer module from node.js, for the browser. 3 | * 4 | * @author Feross Aboukhadijeh 5 | * @license MIT 6 | */ 7 | 8 | /*! 9 | * vuex v4.1.0 10 | * (c) 2022 Evan You 11 | * @license MIT 12 | */ 13 | 14 | /*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh */ 15 | 16 | /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ 17 | -------------------------------------------------------------------------------- /dist/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/js/field.js": "/js/field.js" 3 | } 4 | -------------------------------------------------------------------------------- /nova.mix.js: -------------------------------------------------------------------------------- 1 | const mix = require('laravel-mix') 2 | const webpack = require('webpack') 3 | const path = require('path') 4 | 5 | class NovaExtension { 6 | name() { 7 | return 'nova-extension' 8 | } 9 | 10 | register(name) { 11 | this.name = name 12 | } 13 | 14 | webpackConfig(webpackConfig) { 15 | webpackConfig.externals = { 16 | vue: 'Vue', 17 | } 18 | 19 | webpackConfig.resolve.alias = { 20 | ...(webpackConfig.resolve.alias || {}), 21 | 'laravel-nova': path.join(__dirname, '../../vendor/laravel/nova/resources/js/mixins/packages.js'), 22 | '@/storage': path.resolve(__dirname, './resources/js/storage'), 23 | '@': path.resolve(__dirname, '../../vendor/laravel/nova/resources/js/'), 24 | } 25 | 26 | webpackConfig.output = { 27 | uniqueName: this.name, 28 | } 29 | } 30 | } 31 | 32 | mix.extend('nova', new NovaExtension()) 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "npm run development", 5 | "development": "mix", 6 | "watch": "mix watch", 7 | "watch-poll": "mix watch -- --watch-options-poll=1000", 8 | "hot": "mix watch --hot", 9 | "prod": "npm run production", 10 | "production": "mix --production", 11 | "nova:install": "npm --prefix='../../vendor/laravel/nova' ci" 12 | }, 13 | "devDependencies": { 14 | "@vue/compiler-sfc": "^3.2.22", 15 | "laravel-mix": "^6.0.41", 16 | "postcss": "^8.3.11", 17 | "vue-loader": "^16.8.3" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {} 2 | -------------------------------------------------------------------------------- /resources/js/components/DetailField.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 50 | -------------------------------------------------------------------------------- /resources/js/components/FormField.vue: -------------------------------------------------------------------------------- 1 | 84 | 85 | 137 | -------------------------------------------------------------------------------- /resources/js/components/IndexField.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 20 | -------------------------------------------------------------------------------- /resources/js/components/PanelItem.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 63 | -------------------------------------------------------------------------------- /resources/js/field.js: -------------------------------------------------------------------------------- 1 | import IndexField from './components/IndexField' 2 | import DetailField from './components/DetailField' 3 | import FormField from './components/FormField' 4 | 5 | Nova.booting((app, store) => { 6 | app.component('index-inline-morph-to', IndexField) 7 | app.component('detail-inline-morph-to', DetailField) 8 | app.component('form-inline-morph-to', FormField) 9 | }) 10 | -------------------------------------------------------------------------------- /resources/js/storage/MorphToFieldStorage.js: -------------------------------------------------------------------------------- 1 | export default { 2 | fetchAvailableResources(resourceName, fieldAttribute, options) { 3 | if (resourceName === undefined || fieldAttribute == undefined || options == undefined) { 4 | throw new Error('please pass the right things') 5 | } 6 | 7 | return Nova.request().get(`/nova-vendor/inline-morph-to/${ resourceName }/morphable/${ fieldAttribute }`, options) 8 | }, 9 | 10 | determineIfSoftDeletes(resourceType) { 11 | return Nova.request().get(`/nova-api/${ resourceType }/soft-deletes`) 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | app->booted(function () { 17 | $this->routes(); 18 | }); 19 | 20 | Nova::serving(function (ServingNova $event) { 21 | Nova::script('inline-morph-to', __DIR__ . '/../dist/js/field.js'); 22 | }); 23 | } 24 | 25 | protected function routes(): void 26 | { 27 | if ($this->app->routesAreCached()) { 28 | return; 29 | } 30 | 31 | Route::middleware([ 'nova' ]) 32 | ->prefix('nova-vendor/inline-morph-to') 33 | ->group(__DIR__ . '/../routes/api.php'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Http/Controllers/MorphableController.php: -------------------------------------------------------------------------------- 1 | type); 18 | 19 | abort_if(is_null($relatedResource), 403); 20 | 21 | $relationKey = Str::after($request->field, '__'); 22 | 23 | $request->newResource() 24 | ->availableFieldsOnIndexOrDetail($request) 25 | ->whereInstanceOf(RelatableField::class) 26 | ->findFieldByAttribute($relationKey, fn () => abort(404)) 27 | ->applyDependsOn($request); 28 | 29 | $resource = Nova::resourceInstanceForKey($request->type); 30 | $fields = []; 31 | 32 | if ($request->isUpdateOrUpdateAttachedRequest()) { 33 | 34 | $fields = $resource->updateFieldsWithinPanels($request)->applyDependsOnWithDefaultValues($request); 35 | 36 | if ($key = $request->current) { 37 | 38 | $instance = $relatedResource::newModel()->newQuery()->whereKey($key)->firstOrFail(); 39 | 40 | $fields->resolve( 41 | resource: $relatedResource::make($instance), 42 | ); 43 | 44 | } 45 | 46 | } 47 | 48 | if ($request->isCreateOrAttachRequest()) { 49 | 50 | $fields = $resource->creationFieldsWithinPanels($request)->applyDependsOnWithDefaultValues($request); 51 | 52 | } 53 | 54 | foreach ($fields as $field) { 55 | $field->attribute = $request->field . '__' . $field->attribute; 56 | } 57 | 58 | return [ 59 | 'resources' => [ 60 | 'fields' => $fields, 61 | 'panels' => $resource->availablePanelsForCreate($request, $fields), 62 | ], 63 | ]; 64 | } 65 | } -------------------------------------------------------------------------------- /src/InlineMorphTo.php: -------------------------------------------------------------------------------- 1 | morphToTypes)->map->value->values(); 26 | 27 | $attribute = $this->attribute . '_type'; 28 | $resource = Nova::resourceInstanceForKey($request->{$attribute}); 29 | $rules = []; 30 | 31 | if ($resource) { 32 | 33 | $rules = $resource 34 | ->creationFields($request) 35 | ->flatMap(fn (Field $field) => $field->getCreationRules($request)) 36 | ->mapWithKeys(fn (array $rules, string $attribute) => [ 37 | $this->attribute . '__' . $attribute => $rules, 38 | ]); 39 | 40 | } 41 | 42 | return [ 43 | $attribute => [ $this->nullable ? 'nullable' : 'required', 'in:' . $possibleTypes->implode(',') ], 44 | ...$rules, 45 | ]; 46 | } 47 | 48 | public function fill(NovaRequest $request, $model): void 49 | { 50 | $resource = Nova::resourceInstanceForKey($request->{$this->attribute . '_type'}); 51 | 52 | $model::saving(function (Model $parent) use ($request, $resource) { 53 | 54 | if (blank($resource)) { 55 | return $parent->{$this->attribute}()->disassociate(); 56 | } 57 | 58 | $prefix = sprintf('%s__', $this->attribute); 59 | 60 | $attributes = $request 61 | ->collect() 62 | ->filter(fn (mixed $value, string $key) => Str::startsWith($key, $prefix)) 63 | ->mapWithKeys(fn (mixed $value, string $key) => [ 64 | Str::after($key, $prefix) => $value, 65 | ]); 66 | 67 | $request = $request->duplicate(request: $attributes->toArray()); 68 | 69 | if ($key = $attributes->get('key')) { 70 | 71 | $model = $resource->newModelQuery()->whereKey($key)->firstOrFail(); 72 | 73 | } else { 74 | 75 | $model = $resource->model(); 76 | 77 | } 78 | 79 | $resource->fill(request: $request, model: $model); 80 | $model->save(); 81 | 82 | $parent->{$this->attribute}()->associate($model); 83 | 84 | }); 85 | 86 | $foreignKey = $this->getRelationForeignKeyName($model->{$this->attribute}()); 87 | 88 | parent::fillInto($request, $model, $foreignKey); 89 | } 90 | 91 | public function resolve($resource, $attribute = null): void 92 | { 93 | $request = resolve(NovaRequest::class); 94 | 95 | if ($request->isResourceDetailRequest() || $request->isUpdateOrUpdateAttachedRequest()) { 96 | 97 | $value = null; 98 | 99 | if ($resource->relationLoaded($this->attribute)) { 100 | $value = $resource->getRelation($this->attribute); 101 | } 102 | 103 | if (!$value) { 104 | $value = $resource->{$this->attribute}()->withoutGlobalScopes()->getResults(); 105 | } 106 | 107 | /** 108 | * @var NovaRequest $request 109 | */ 110 | $request = $request->duplicate(); 111 | $resolver = $request->getRouteResolver(); 112 | 113 | $request->setRouteResolver(function () use ($resolver, $resource, $value) { 114 | 115 | /** 116 | * @var Route $route 117 | */ 118 | $route = $resolver(); 119 | $route->setParameter('resource', $this->resolveMorphType($resource)); 120 | $route->setParameter('resourceId', $value?->getKey()); 121 | 122 | return $route; 123 | 124 | }); 125 | 126 | 127 | if ($request instanceof ResourceDetailRequest) { 128 | $this->value = DetailViewResource::make()->toArray($request); 129 | } 130 | 131 | if ($request instanceof ResourceUpdateOrUpdateAttachedRequest) { 132 | 133 | $this->value = UpdateViewResource::make()->toArray($request); 134 | 135 | foreach ($this->value[ 'fields' ] as $field) { 136 | $field->attribute = $this->attribute . '__' . $field->attribute; 137 | } 138 | 139 | } 140 | 141 | $this->morphToId = optional($value)->getKey(); 142 | $this->morphToType = $this->resolveMorphType($resource); 143 | 144 | if ($resourceClass = $this->resolveResourceClass($value)) { 145 | $this->resourceName = $resourceClass::uriKey(); 146 | } 147 | 148 | } else { 149 | 150 | parent::resolve($resource, $attribute); 151 | 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | let mix = require('laravel-mix') 2 | 3 | require('./nova.mix') 4 | 5 | mix 6 | .setPublicPath('dist') 7 | .js('resources/js/field.js', 'js') 8 | .vue({ version: 3 }) 9 | .nova('digital-creative/nova-inline-morph-to') 10 | --------------------------------------------------------------------------------