├── .github
└── FUNDING.yml
├── .gitignore
├── LICENSE
├── README.md
├── composer.json
├── dist
├── js
│ ├── field.js
│ └── field.js.LICENSE.txt
└── mix-manifest.json
├── nova.mix.js
├── package.json
├── resources
└── js
│ ├── components
│ └── CustomRelationshipField.vue
│ └── field.js
├── screenshots
├── dark.png
└── light.png
├── src
├── CustomRelationshipField.php
├── CustomRelationshipFieldServiceProvider.php
└── CustomRelationshipFieldTrait.php
├── webpack.mix.js
└── yarn.lock
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: milewski
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea
2 | /vendor
3 | /node_modules
4 | package-lock.json
5 | composer.phar
6 | composer.lock
7 | phpunit.xml
8 | .phpunit.result.cache
9 | .DS_Store
10 | Thumbs.db
11 |
--------------------------------------------------------------------------------
/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 | # Custom Relationship Field
2 |
3 | [](https://packagist.org/packages/digital-creative/custom-relationship-field)
4 | [](https://packagist.org/packages/digital-creative/custom-relationship-field)
5 | [](https://github.com/dcasia/custom-relationship-field/blob/main/LICENSE)
6 |
7 |
8 |
9 |
10 |
11 |
12 | This field works just like as the default HasMany relationship field from nova but **without requiring a real relation** with the resource.
13 |
14 | That means you are free to show resource `A` into the details page of resource `B` without having to create a real relation between them.
15 |
16 | # Installation
17 |
18 | You can install the package via composer:
19 |
20 | ```shell
21 | composer require digital-creative/custom-relationship-field
22 | ```
23 |
24 | ```php
25 | use DigitalCreative\CustomRelationshipField\CustomRelationshipField;
26 | use DigitalCreative\CustomRelationshipField\CustomRelationshipFieldTrait;
27 |
28 | trait UserWithSimilarNameTrait
29 | {
30 | public static function similarNameQuery(NovaRequest $request, Builder $query, User $model): Builder
31 | {
32 | return $query->where('last_name', $model->last_name)->whereKeyNot($model->getKey());
33 | }
34 |
35 | public function similarNameFields(NovaRequest $request): array
36 | {
37 | return [
38 | ID::make(),
39 | Text::make('First Name'),
40 | Text::make('Last Name'),
41 | ];
42 | }
43 |
44 | public function similarNameActions(NovaRequest $request): array
45 | {
46 | return [];
47 | }
48 |
49 | public function similarNameFilters(NovaRequest $request): array
50 | {
51 | return [];
52 | }
53 | }
54 |
55 | class User extends Resource
56 | {
57 | use CustomRelationshipFieldTrait;
58 | use UserWithSimilarNameTrait;
59 |
60 | public function fields(NovaRequest $request): array
61 | {
62 | return [
63 | ...
64 | CustomRelationshipField::make('Users with similar name', 'similarName', User::class),
65 | ...
66 | ];
67 | }
68 | }
69 | ```
70 |
71 | ## ⭐️ Show Your Support
72 |
73 | Please give a ⭐️ if this project helped you!
74 |
75 | ### Other Packages You Might Like
76 |
77 | - [Nova Dashboard](https://github.com/dcasia/nova-dashboard) - The missing dashboard for Laravel Nova!
78 | - [Nova Welcome Card](https://github.com/dcasia/nova-welcome-card) - A configurable version of the `Help card` that comes with Nova.
79 | - [Icon Action Toolbar](https://github.com/dcasia/icon-action-toolbar) - Replaces the default boring action menu with an inline row of icon-based actions.
80 | - [Expandable Table Row](https://github.com/dcasia/expandable-table-row) - Provides an easy way to append extra data to each row of your resource tables.
81 | - [Collapsible Resource Manager](https://github.com/dcasia/collapsible-resource-manager) - Provides an easy way to order and group your resources on the sidebar.
82 | - [Resource Navigation Tab](https://github.com/dcasia/resource-navigation-tab) - Organize your resource fields into tabs.
83 | - [Resource Navigation Link](https://github.com/dcasia/resource-navigation-link) - Create links to internal or external resources.
84 | - [Nova Mega Filter](https://github.com/dcasia/nova-mega-filter) - Display all your filters in a card instead of a tiny dropdown!
85 | - [Nova Pill Filter](https://github.com/dcasia/nova-pill-filter) - A Laravel Nova filter that renders into clickable pills.
86 | - [Nova Slider Filter](https://github.com/dcasia/nova-slider-filter) - A Laravel Nova filter for picking range between a min/max value.
87 | - [Nova Range Input Filter](https://github.com/dcasia/nova-range-input-filter) - A Laravel Nova range input filter.
88 | - [Nova FilePond](https://github.com/dcasia/nova-filepond) - A Nova field for uploading File, Image and Video using Filepond.
89 | - [Custom Relationship Field](https://github.com/dcasia/custom-relationship-field) - Emulate HasMany relationship without having a real relationship set between resources.
90 | - [Column Toggler](https://github.com/dcasia/column-toggler) - A Laravel Nova package that allows you to hide/show columns in the index view.
91 | - [Batch Edit Toolbar](https://github.com/dcasia/batch-edit-toolbar) - Allows you to update a single column of a resource all at once directly from the index page.
92 |
93 | ## License
94 |
95 | The MIT License (MIT). Please see [License File](https://raw.githubusercontent.com/dcasia/custom-relationship-field/master/LICENSE) for more information.
96 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "digital-creative/custom-relationship-field",
3 | "description": "Emulate HasMany relationship without having a real relationship set between resources",
4 | "keywords": [
5 | "laravel",
6 | "nova",
7 | "nova4",
8 | "relationship",
9 | "has-many",
10 | "custom"
11 | ],
12 | "license": "MIT",
13 | "homepage": "https://github.com/dcasia/custom-relationship-field",
14 | "authors": [
15 | {
16 | "name": "Rafael Milewski"
17 | }
18 | ],
19 | "require": {
20 | "php": ">=8.0",
21 | "laravel/nova": "^4.0"
22 | },
23 | "autoload": {
24 | "psr-4": {
25 | "DigitalCreative\\CustomRelationshipField\\": "src/"
26 | }
27 | },
28 | "extra": {
29 | "laravel": {
30 | "providers": [
31 | "DigitalCreative\\CustomRelationshipField\\CustomRelationshipFieldServiceProvider"
32 | ]
33 | }
34 | },
35 | "config": {
36 | "sort-packages": true
37 | },
38 | "minimum-stability": "dev",
39 | "prefer-stable": true
40 | }
41 |
--------------------------------------------------------------------------------
/dist/js/field.js:
--------------------------------------------------------------------------------
1 | (()=>{"use strict";var e={744:(e,t)=>{t.Z=(e,t)=>{const i=e.__vccOpts||e;for(const[e,o]of t)i[e]=o;return i}}},t={};function i(o){var r=t[o];if(void 0!==r)return r.exports;var n=t[o]={exports:{}};return e[o](n,n.exports,i),n.exports}(()=>{const e=Vue;const t={emits:["actionExecuted"],props:["resourceName","resourceId","resource","field"],methods:{actionExecuted:function(){this.$emit("actionExecuted")}},data:function(){return{eventCallback:null}},mounted:function(){var e=this;Nova.$on("custom-relationship-field:request-extra-params",this.eventCallback=function(){Nova.$emit("custom-relationship-field:extra-params",{relationshipType:e.relationshipType})})},unmounted:function(){Nova.$off("custom-relationship-field:request-extra-params",this.eventCallback)},computed:{encodedAttribute:function(){return btoa("".concat(this.field.attribute,"|_::_|").concat(this.field.name))},relationshipType:function(){return"CustomRelationshipField:".concat(this.encodedAttribute)}}};const o=(0,i(744).Z)(t,[["render",function(t,i,o,r,n,a){var c=(0,e.resolveComponent)("ResourceIndex");return(0,e.openBlock)(),(0,e.createBlock)(c,{field:o.field,"resource-name":o.field.resourceName,"via-resource":o.resourceName,"via-resource-id":o.resourceId,"via-relationship":null,"relationship-type":a.relationshipType,onActionExecuted:a.actionExecuted,"load-cards":!1,"initial-per-page":o.field.perPage||5,"should-override-meta":!1,"custom-relationship-field-attribute":o.field.attribute,"custom-relationship-field-label":o.field.name},null,8,["field","resource-name","via-resource","via-resource-id","relationship-type","onActionExecuted","initial-per-page","custom-relationship-field-attribute","custom-relationship-field-label"])}]]);Nova.booting((function(e,t){e.component("detail-custom-relationship-field",o)}))})()})();
--------------------------------------------------------------------------------
/dist/js/field.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*!
2 | * vuex v4.1.0
3 | * (c) 2022 Evan You
4 | * @license MIT
5 | */
6 |
--------------------------------------------------------------------------------
/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 | '@': path.resolve(__dirname, '../../vendor/laravel/nova/resources/js/'),
22 | }
23 |
24 | webpackConfig.output = {
25 | uniqueName: this.name,
26 | }
27 | }
28 | }
29 |
30 | mix.extend('nova', new NovaExtension())
31 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "watch": "mix watch",
5 | "production": "mix --production",
6 | "nova:install": "npm --prefix='../../vendor/laravel/nova' ci"
7 | },
8 | "devDependencies": {
9 | "@vue/compiler-sfc": "^3.3.4",
10 | "laravel-mix": "^6.0.49",
11 | "vue-loader": "^17.2.2"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/resources/js/components/CustomRelationshipField.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
17 |
18 |
19 |
20 |
54 |
--------------------------------------------------------------------------------
/resources/js/field.js:
--------------------------------------------------------------------------------
1 | import CustomRelationshipField from './components/CustomRelationshipField'
2 |
3 | Nova.booting((app, store) => {
4 | app.component('detail-custom-relationship-field', CustomRelationshipField)
5 | })
6 |
--------------------------------------------------------------------------------
/screenshots/dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dcasia/custom-relationship-field/a4759e28540f1915b27b2e529a5567edb332112e/screenshots/dark.png
--------------------------------------------------------------------------------
/screenshots/light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dcasia/custom-relationship-field/a4759e28540f1915b27b2e529a5567edb332112e/screenshots/light.png
--------------------------------------------------------------------------------
/src/CustomRelationshipField.php:
--------------------------------------------------------------------------------
1 | app->afterResolving(NovaRequest::class, function (NovaRequest $request): void {
22 |
23 | if ($relationshipType = $request->relationshipType) {
24 |
25 | if (Str::startsWith($relationshipType, 'CustomRelationshipField')) {
26 |
27 | $encoded = Str::after($relationshipType, ':');
28 |
29 | [ $attribute, $label ] = Str::of(base64_decode($encoded))->explode('|_::_|');
30 |
31 | $request->merge([
32 | 'relationshipType' => 'hasMany',
33 | 'customRelationshipFieldAttribute' => $attribute,
34 | 'customRelationshipFieldLabel' => $label,
35 | ]);
36 |
37 | }
38 |
39 | }
40 |
41 | });
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/CustomRelationshipFieldTrait.php:
--------------------------------------------------------------------------------
1 | input('customRelationshipFieldLabel') ?? parent::label();
25 | }
26 |
27 | protected function fieldsMethod(NovaRequest $request): string
28 | {
29 | $prefix = 'fields';
30 |
31 | if ($attribute = $this->extractAttributeFromRequest($request)) {
32 | $prefix = "{$attribute}Fields";
33 | }
34 |
35 | if ($request->isInlineCreateRequest() && method_exists($this, "{$prefix}ForInlineCreate")) {
36 | return "{$prefix}ForInlineCreate";
37 | }
38 |
39 | if ($request->isResourceIndexRequest() && method_exists($this, "{$prefix}ForIndex")) {
40 | return "{$prefix}ForIndex";
41 | }
42 |
43 | if ($request->isResourceDetailRequest() && method_exists($this, "{$prefix}ForDetail")) {
44 | return "{$prefix}ForDetail";
45 | }
46 |
47 | if ($request->isCreateOrAttachRequest() && method_exists($this, "{$prefix}ForCreate")) {
48 | return "{$prefix}ForCreate";
49 | }
50 |
51 | if ($request->isUpdateOrUpdateAttachedRequest() && method_exists($this, "{$prefix}ForUpdate")) {
52 | return "{$prefix}ForUpdate";
53 | }
54 |
55 | return $prefix;
56 | }
57 |
58 | public function buildAvailableFields(NovaRequest $request, array $methods): FieldCollection
59 | {
60 | if ($attribute = $this->extractAttributeFromRequest($request)) {
61 |
62 | $method = "{$attribute}Fields";
63 |
64 | $fields = collect([
65 | method_exists($this, $method) ? $this->{$method}($request) : [],
66 | ]);
67 |
68 | collect($methods)
69 | ->map(fn (string $method) => $attribute . ucfirst($method))
70 | ->filter(fn (string $method) => $method !== "{$attribute}Fields" && method_exists($this, $method))
71 | ->each(function (string $method) use ($request, $fields): void {
72 | $fields->push([ $this->{$method}($request) ]);
73 | });
74 |
75 | return FieldCollection::make(array_values($this->filter($fields->flatten()->all())));
76 |
77 | }
78 |
79 | return parent::buildAvailableFields($request, $methods);
80 | }
81 |
82 | public function resolveActions(NovaRequest $request): Collection
83 | {
84 | if ($method = $this->extractAttributeFromRequest($request)) {
85 |
86 | $method = "{$method}Actions";
87 |
88 | if (method_exists($this, $method)) {
89 |
90 | return ActionCollection::make(
91 | $this->filter($this->{$method}($request)),
92 | );
93 |
94 | }
95 |
96 | }
97 |
98 | return parent::resolveActions($request);
99 | }
100 |
101 | public function resolveCards(NovaRequest $request): Collection
102 | {
103 | if ($method = $this->extractAttributeFromRequest($request)) {
104 |
105 | $method = "{$method}Cards";
106 |
107 | if (method_exists($this, $method)) {
108 | return collect(array_values($this->filter($this->{$method}($request))));
109 | }
110 |
111 | }
112 |
113 | return parent::resolveCards($request);
114 | }
115 |
116 | public function resolveFilters(NovaRequest $request): Collection
117 | {
118 | if ($method = $this->extractAttributeFromRequest($request)) {
119 |
120 | $method = "{$method}Filters";
121 |
122 | if (method_exists($this, $method)) {
123 | return collect(array_values($this->filter($this->{$method}($request))));
124 | }
125 |
126 | }
127 |
128 | return parent::resolveFilters($request);
129 | }
130 |
131 | public static function buildIndexQuery(
132 | NovaRequest $request,
133 | $query,
134 | $search = null,
135 | array $filters = [],
136 | array $orderings = [],
137 | $withTrashed = TrashedStatus::DEFAULT,
138 | ): Builder|BelongsToMany
139 | {
140 | if ($method = static::extractAttributeFromRequest($request)) {
141 |
142 | $method = "{$method}Query";
143 |
144 | if (method_exists(static::class, $method)) {
145 |
146 | return static::applyOrderings(static::applyFilters(
147 | $request, static::initializeQuery($request, $query, $search, $withTrashed), $filters,
148 | ), $orderings)->tap(function ($query) use ($method, $request): void {
149 |
150 | $resource = Nova::modelInstanceForKey($request->viaResource)
151 | ->newQueryWithoutScopes()
152 | ->find($request->viaResourceId);
153 |
154 | static::$method($request, $query->with(static::$with), $resource);
155 |
156 | });
157 |
158 | }
159 |
160 | }
161 |
162 | return parent::buildIndexQuery(...func_get_args());
163 | }
164 |
165 | private static function extractAttributeFromRequest(NovaRequest $request): ?string
166 | {
167 | return $request->input('customRelationshipFieldAttribute');
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/webpack.mix.js:
--------------------------------------------------------------------------------
1 | const 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/custom-relationship-field')
10 |
--------------------------------------------------------------------------------