Repository: statamic-rad-pack/runway (php)

Issue:
HasMany relationship: unlink deletes related resource
### Description

I have 2 models: ArticleCategory and Article. Article category has many articles. In the article category blueprint I added HasMany fieldtype for that relationship. Adding article to article category works well, as expected, but when I try to unlink the article from article category the article gets deleted instead of just 'unlink'.

By 'unlink' I mean that article_category_id column should have value NULL. Also, it is worth mentioning that I implemented soft deletion for article so articles get soft deleted, not permanently deleted. (just in case it helps in detecting the problem)

ArticleCategory model has 'articles' eloquent relationship.

I provide you with both blueprints and migrations:

article blueprint

```
tabs:
  main:
    display: Hauptteil
    sections:
      -
        fields:
          -
            handle: unit_number
            field:
              type: text
              display: 'Unit no.'
              visibility: read_only
              width: 25
              listable: true
          -
            handle: article_number
            field:
              type: text
              display: 'Artikel Nr.'
              visibility: read_only
              width: 25
              listable: true
title: Article
```

article category blueprint

```
tabs:
  main:
    display: Hauptteil
    sections:
      -
        fields:
          -
            handle: title_de
            field:
              type: text
              display: 'Title (de)'
              validate:
                - required
                - 'new App\Domains\Articles\Rules\UniqueArticleCategoryValue'
          -
            handle: title_fr
            field:
              type: text
              display: 'Title (fr)'
              validate:
                - 'new App\Domains\Articles\Rules\UniqueArticleCategoryValue'
          -
            handle: articles
            field:
              resource: article
              create: false
              type: has_many
              display: Articles
title: 'Article category'

```

articles migration

```
Schema::create('articles', function (Blueprint $table) {
            $table->id();
            $table->bigInteger('article_number')->unique();
            $table->string('unit_number');
            $table->foreignId('article_category_id')->nullable()->constrained('article_categories');
            $table->timestamps();
            $table->softDeletes();
        });
```

article categories migration

```
Schema::create('article_categories', function (Blueprint $table) {
            $table->id();
            $table->string('title_de')->unique();
            $table->string('title_fr')->nullable()->unique();
            $table->timestamps();
        });
```



### Steps to reproduce

1. create migrations and models: Article and ArticleCategory
2. make them runway resources (add in runway config, in resources array), implement HasRunwayResource trait in models
3. create blueprints (given above) for those resources
4. add article to article category
5. try to unlink the article from article category

### Environment

Environment
Application Name: Statamic
Laravel Version: 11.19.0
PHP Version: 8.3.10
Composer Version: 2.7.7
Environment: local
Debug Mode: ENABLED
URL: localhost:8080
Maintenance Mode: OFF
Timezone: Europe/Zurich
Locale: de

Cache
Config: NOT CACHED
Events: NOT CACHED
Routes: NOT CACHED
Views: CACHED

Drivers
Broadcasting: null
Cache: file
Database: sqlite
Logs: stack / daily
Mail: log
Queue: sync
Scout: collection
Session: file

Locales
Installed: de, en, fr
LaravelLang\Lang\Plugin: 15.5.6
Locales Version: 2.9.2
Protected: de
Publisher Version: 16.4.0

Sentry
Enabled: MISSING DSN
Environment: local
Laravel SDK Version: 4.7.1
PHP SDK Version: 4.8.1
Release: NOT SET
Sample Rate Errors: 100%
Sample Rate Performance Monitoring: NOT SET
Sample Rate Profiling: NOT SET
Send Default PII: DISABLED

Statamic
Addons: 3
Sites: 1
Stache Watcher: Disabled
Static Caching: Disabled
Version: 5.19.0 PRO

Statamic Addons
statamic-rad-pack/runway: 7.7.3
statamic/eloquent-driver: 4.12.0
tv2regionerne/statamic-safeguard: 1.4.0

Statamic Eloquent Driver
Asset Containers: file
Assets: eloquent
Blueprints: file
Collection Trees: eloquent
Collections: file
Entries: eloquent
Forms: file
Global Sets: file
Global Variables: eloquent
Navigation Trees: eloquent
Navigations: file
Revisions: file
Sites: file
Taxonomies: file
Terms: eloquent
Tokens: eloquent


Pull request description:
[7.x] Improvements around unlinking relationships
Right now, when you "unlink" a related model using the Has Many fieldtype, it'll permanently delete that model. It has worked this way for as long as I can remember, for no good reason. 

This pull request makes some changes to how "unlinking" relationships work in the Has Many fieldtype.

* When the `relationship_id` column is nullable, instead of deleting the related model, it'll simply set the column to `null`.
* When the `relationship_id` column is not nullable, it will continue to delete the related model, however it'll show a confirmation prompt before the model can be unlinked.

    ![CleanShot 2024-09-05 at 11 29 31](https://github.com/user-attachments/assets/a0e99b1c-21d3-4c70-975f-18e71ac3bf27)

* When the Has Many fieldtype is used for `BelongsToMany` relationships, it'll always "unlink" the relationship by deletng the row in the pivot table.

Closes #582.

Patch:
diff --git a/resources/js/components/inputs/relationship/Item.vue b/resources/js/components/inputs/relationship/Item.vue
new file mode 100644
index 0000000..d8d89cf
--- /dev/null
+++ b/resources/js/components/inputs/relationship/Item.vue
@@ -0,0 +1,136 @@
+<template>
+
+    <div
+        class="item select-none"
+        :class="{ 'invalid': item.invalid }"
+    >
+        <div class="item-move" v-if="sortable">&nbsp;</div>
+        <div class="item-inner">
+            <div v-if="statusIcon" class="little-dot rtl:ml-2 ltr:mr-2 hidden @sm:block" :class="item.status" />
+
+            <div
+                v-if="item.invalid"
+                v-tooltip.top="__('An item with this ID could not be found')"
+                v-text="__(item.title)" />
+
+            <a v-if="!item.invalid && editable" @click.prevent="edit" v-text="__(item.title)" class="truncate" v-tooltip="item.title" :href="item.edit_url" />
+
+            <div v-if="!item.invalid && !editable" v-text="__(item.title)" />
+
+            <inline-edit-form
+                v-if="isEditing"
+                :item="item"
+                :component="formComponent"
+                :component-props="formComponentProps"
+                @updated="itemUpdated"
+                @closed="isEditing = false"
+            />
+
+            <div class="flex items-center flex-1 justify-end">
+                <div v-if="item.hint" v-text="item.hint" class="text-4xs text-gray-600 uppercase whitespace-nowrap rtl:ml-2 ltr:mr-2 hidden @sm:block" />
+
+                <div class="flex items-center" v-if="!readOnly">
+                    <dropdown-list>
+                        <dropdown-item :text="__('Edit')" @click="edit" v-if="editable" />
+                        <dropdown-item :text="__('Unlink')" class="warning" @click="remove" />
+                    </dropdown-list>
+                </div>
+            </div>
+
+        </div>
+
+        <confirmation-modal
+            v-if="showDeletionConfirmationModel"
+            :title="__('Unlink')"
+            :danger="true"
+            :buttonText="__('Delete')"
+            @confirm="showDeletionConfirmationModel = false; $emit('removed')"
+            @cancel="showDeletionConfirmationModel = false"
+        >
+            <div>{{ __('Unlinking this model will result in it being deleted. Are you sure you want to do this?') }}</div>
+        </confirmation-modal>
+
+    </div>
+
+</template>
+
+<script>
+/**
+ * This component is a copy of Statamic's RelatedItem component, but it adds a
+ * confirmation modal when deleting models.
+ */
+
+import InlineEditForm from '../../../../../vendor/statamic/cms/resources/js/components/inputs/relationship/InlineEditForm.vue';
+
+export default {
+
+    components: {
+        InlineEditForm
+    },
+
+    inject: {
+        storeName: {
+            default: null
+        }
+    },
+
+    props: {
+        item: Object,
+        config: Object,
+        statusIcon: Boolean,
+        editable: Boolean,
+        sortable: Boolean,
+        readOnly: Boolean,
+        formComponent: String,
+        formComponentProps: Object,
+    },
+
+    data() {
+        return {
+            isEditing: false,
+            showDeletionConfirmationModel: false,
+        }
+    },
+
+    computed: {
+        unlinkBehavior() {
+            return this.$parent.$parent.meta.unlinkBehavior;
+        }
+    },
+
+    methods: {
+
+        edit() {
+            if (! this.editable) return;
+            if (this.item.invalid) return;
+
+            if (this.item.reference && Object.entries(this.$store.state.publish).find(([key, value]) => value.reference === this.item.reference)) {
+                this.$toast.error(__("You're already editing this item."));
+                return;
+            }
+
+            this.isEditing = true;
+        },
+
+        itemUpdated(responseData) {
+            this.item.title = responseData.title;
+            this.item.published = responseData.published;
+            this.item.private = responseData.private;
+            this.item.status = responseData.status;
+
+            this.$events.$emit(`live-preview.${this.storeName}.refresh`);
+        },
+
+        remove() {
+            if (this.unlinkBehavior === 'delete') {
+                this.showDeletionConfirmationModel = true;
+                return;
+            }
+
+            this.$emit('removed');
+        },
+
+    }
+
+}
+</script>
diff --git a/resources/js/cp.js b/resources/js/cp.js
index 354a793..0242814 100644
--- a/resources/js/cp.js
+++ b/resources/js/cp.js
@@ -1,5 +1,7 @@
-import PublishForm from './components/resources/PublishForm.vue'
-import ResourceView from './components/resources/View.vue'
+import PublishForm from './components/resources/PublishForm.vue';
+import ResourceView from './components/resources/View.vue';
+import RelatedItem from './components/inputs/relationship/Item.vue';
 
-Statamic.$components.register('runway-publish-form', PublishForm)
-Statamic.$components.register('resource-view', ResourceView)
+Statamic.$components.register('runway-publish-form', PublishForm);
+Statamic.$components.register('resource-view', ResourceView);
+Statamic.$components.register('runway-related-item', RelatedItem);
diff --git a/src/Fieldtypes/BaseFieldtype.php b/src/Fieldtypes/BaseFieldtype.php
index 6821bee..5e0f3e3 100644
--- a/src/Fieldtypes/BaseFieldtype.php
+++ b/src/Fieldtypes/BaseFieldtype.php
@@ -4,9 +4,11 @@ namespace StatamicRadPack\Runway\Fieldtypes;
 
 use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\HasMany;
 use Illuminate\Support\Arr;
 use Illuminate\Support\Collection;
 use Illuminate\Support\Facades\File;
+use Illuminate\Support\Facades\Schema;
 use Statamic\CP\Column;
 use Statamic\Facades\Blink;
 use Statamic\Fieldtypes\Relationship;
@@ -25,6 +27,7 @@ class BaseFieldtype extends Relationship
     protected $canSearch = true;
     protected $categories = ['relationship'];
     protected $formComponent = 'runway-publish-form';
+    protected $itemComponent = 'runway-related-item';
 
     protected $formComponentProps = [
         'initialReference' => 'reference',
@@ -102,6 +105,37 @@ class BaseFieldtype extends Relationship
         return File::get(__DIR__.'/../../resources/svg/database.svg');
     }
 
+    public function preload()
+    {
+        return array_merge(parent::preload(), [
+            'unlinkBehavior' => $this->getUnlinkBehavior(),
+        ]);
+    }
+
+    private function getUnlinkBehavior(): string
+    {
+        if ($this instanceof BelongsToFieldtype) {
+            return 'unlink';
+        }
+
+        $relationshipName = $this->config('relationship_name') ?? $this->field->handle();
+        $relationship = $this->field->parent()->{$relationshipName}();
+
+        if ($relationship instanceof HasMany) {
+            $foreignKey = $relationship->getQualifiedForeignKeyName();
+
+            $foreignTable = explode('.', $foreignKey)[0];
+            $foreignColumn = explode('.', $foreignKey)[1];
+
+            $column = collect(Schema::getColumns($foreignTable))
+                ->first(fn (array $column) => $column['name'] === $foreignColumn);
+
+            return Arr::get($column, 'nullable', false) ? 'unlink' : 'delete';
+        }
+
+        return 'unlink';
+    }
+
     public function getIndexItems($request)
     {
         $resource = Runway::findResource($this->config('resource'));
diff --git a/src/Relationships.php b/src/Relationships.php
index a2b9624..a24e6da 100644
--- a/src/Relationships.php
+++ b/src/Relationships.php
@@ -6,6 +6,8 @@ use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 use Illuminate\Database\Eloquent\Relations\HasMany;
 use Illuminate\Database\Eloquent\Relations\Relation;
+use Illuminate\Support\Arr;
+use Illuminate\Support\Facades\Schema;
 use Statamic\Fields\Field;
 use StatamicRadPack\Runway\Fieldtypes\HasManyFieldtype;
 
@@ -45,7 +47,10 @@ class Relationships
         $relatedResource = Runway::findResource($field->fieldtype()->config('resource'));
 
         $deleted = $relationship->whereNotIn($relatedResource->primaryKey(), $values)->get()
-            ->each->delete()
+            ->each(fn (Model $model) => match ($this->getUnlinkBehaviorForHasManyRelationship($relationship)) {
+                'unlink' => $model->update([$relationship->getForeignKeyName() => null]),
+                'delete' => $model->delete(),
+            })
             ->map->getKey()
             ->all();
 
@@ -66,6 +71,19 @@ class Relationships
         }
     }
 
+    private function getUnlinkBehaviorForHasManyRelationship(HasMany $relationship): string
+    {
+        $foreignKey = $relationship->getQualifiedForeignKeyName();
+
+        $foreignTable = explode('.', $foreignKey)[0];
+        $foreignColumn = explode('.', $foreignKey)[1];
+
+        $column = collect(Schema::getColumns($foreignTable))
+            ->first(fn (array $column) => $column['name'] === $foreignColumn);
+
+        return Arr::get($column, 'nullable') ? 'unlink' : 'delete';
+    }
+
     protected function saveBelongsToManyRelationship(Field $field, Relation $relationship, array $values): void
     {
         if ($field->fieldtype()->config('reorderable') && $orderColumn = $field->fieldtype()->config('order_column')) {


Test patch:
diff --git a/tests/Fieldtypes/BelongsToFieldtypeTest.php b/tests/Fieldtypes/BelongsToFieldtypeTest.php
index 8f205c3..52926fd 100644
--- a/tests/Fieldtypes/BelongsToFieldtypeTest.php
+++ b/tests/Fieldtypes/BelongsToFieldtypeTest.php
@@ -35,6 +35,12 @@ class BelongsToFieldtypeTest extends TestCase
             ]));
     }
 
+    #[Test]
+    public function unlink_behavior_is_unlink()
+    {
+        $this->assertEquals('unlink', $this->fieldtype->preload()['unlinkBehavior']);
+    }
+
     #[Test]
     public function can_get_index_items()
     {
diff --git a/tests/Fieldtypes/HasManyFieldtypeTest.php b/tests/Fieldtypes/HasManyFieldtypeTest.php
index 1ceb47d..0f4e806 100644
--- a/tests/Fieldtypes/HasManyFieldtypeTest.php
+++ b/tests/Fieldtypes/HasManyFieldtypeTest.php
@@ -6,6 +6,7 @@ use Illuminate\Contracts\Pagination\Paginator;
 use Illuminate\Foundation\Testing\WithFaker;
 use Illuminate\Support\Collection;
 use Illuminate\Support\Facades\Config;
+use Illuminate\Support\Facades\Schema;
 use PHPUnit\Framework\Attributes\Test;
 use Statamic\Facades\Blink;
 use Statamic\Fields\Field;
@@ -35,6 +36,58 @@ class HasManyFieldtypeTest extends TestCase
             ]));
     }
 
+    #[Test]
+    public function unlink_behavior_is_unlink_when_relationship_column_is_nullable()
+    {
+        Schema::shouldReceive('getColumns')->with('posts')->andReturn([
+            ['name' => 'author_id', 'type' => 'integer', 'nullable' => true],
+        ]);
+
+        Schema::shouldIgnoreMissing();
+
+        $author = Author::factory()->create();
+
+        $field = new Field('posts', [
+            'mode' => 'stack',
+            'resource' => 'post',
+            'display' => 'Posts',
+            'type' => 'has_many',
+        ]);
+
+        $field->setParent($author);
+
+        $fieldtype = new HasManyFieldtype;
+        $fieldtype->setField($field);
+
+        $this->assertEquals('unlink', $fieldtype->preload()['unlinkBehavior']);
+    }
+
+    #[Test]
+    public function unlink_behavior_is_delete_when_relationship_column_is_not_nullable()
+    {
+        Schema::shouldReceive('getColumns')->with('posts')->andReturn([
+            ['name' => 'author_id', 'type' => 'integer', 'nullable' => false],
+        ]);
+
+        Schema::shouldIgnoreMissing();
+
+        $author = Author::factory()->create();
+
+        $field = new Field('posts', [
+            'mode' => 'stack',
+            'resource' => 'post',
+            'display' => 'Posts',
+            'type' => 'has_many',
+        ]);
+
+        $field->setParent($author);
+
+        $fieldtype = new HasManyFieldtype;
+        $fieldtype->setField($field);
+
+        $this->assertEquals('delete', $fieldtype->preload()['unlinkBehavior']);
+    }
+
     #[Test]
     public function can_get_index_items()
     {
diff --git a/tests/RelationshipsTest.php b/tests/RelationshipsTest.php
index a8dd902..7c4ad90 100644
--- a/tests/RelationshipsTest.php
+++ b/tests/RelationshipsTest.php
@@ -2,6 +2,7 @@
 
 namespace StatamicRadPack\Runway\Tests;
 
+use Illuminate\Support\Facades\Schema;
 use PHPUnit\Framework\Attributes\Test;
 use Statamic\Facades\Blueprint;
 use Statamic\Fields\Blueprint as FieldsBlueprint;
@@ -25,7 +26,7 @@ class RelationshipsTest extends TestCase
                 'tabs' => [
                     'main' => [
                         'fields' => [
-                            ['handle' => 'posts', 'field' => ['type' => 'has_many', 'mode' => 'stack', 'resource' => 'post']],
+                            ['handle' => 'posts', 'field' => ['type' => 'has_many', 'resource' => 'post']],
                         ],
                     ],
                 ],
@@ -39,11 +40,58 @@ class RelationshipsTest extends TestCase
     }
 
     #[Test]
-    public function can_delete_models_when_saving_has_many_relationship()
+    public function can_unlink_models_when_saving_has_many_relationship_when_relationship_column_is_nullable()
     {
         $author = Author::factory()->create();
         $posts = Post::factory()->count(3)->create(['author_id' => $author->id]);
 
+        Schema::shouldReceive('getColumns')->with('posts')->andReturn([
+            ['name' => 'author_id', 'type' => 'integer', 'nullable' => true],
+        ]);
+
+        // Without this, Schema::getColumnListing() will return null, even though shouldIgnoreMissing() is called. But whatever 🤷‍♂️
+        Schema::shouldReceive('getColumnListing')->with('posts')->andReturn(['published']);
+
+        Schema::shouldIgnoreMissing();
+
+        Blueprint::shouldReceive('find')->with('runway::post')->andReturn(new FieldsBlueprint);
+
+        Blueprint::shouldReceive('find')
+            ->with('runway::author')
+            ->andReturn((new FieldsBlueprint)->setContents([
+                'tabs' => [
+                    'main' => [
+                        'fields' => [
+                            ['handle' => 'posts', 'field' => ['type' => 'has_many', 'resource' => 'post']],
+                        ],
+                    ],
+                ],
+            ]));
+
+        Relationships::for($author)
+            ->with(['posts' => [$posts[1]->id, $posts[2]->id]])
+            ->save();
+
+        $this->assertDatabaseHas('posts', ['id' => $posts[0]->id, 'author_id' => null]);
+        $this->assertDatabaseHas('posts', ['id' => $posts[1]->id, 'author_id' => $author->id]);
+        $this->assertDatabaseHas('posts', ['id' => $posts[2]->id, 'author_id' => $author->id]);
+    }
+
+    #[Test]
+    public function can_delete_models_when_saving_has_many_relationship_when_relationship_column_is_not_nullable()
+    {
+        $author = Author::factory()->create();
+        $posts = Post::factory()->count(3)->create(['author_id' => $author->id]);
+
+        Schema::shouldReceive('getColumns')->with('posts')->andReturn([
+            ['name' => 'author_id', 'type' => 'integer', 'nullable' => false],
+        ]);
+
+        // Without this, Schema::getColumnListing() will return null, even though shouldIgnoreMissing() is called. But whatever 🤷‍♂️
+        Schema::shouldReceive('getColumnListing')->with('posts')->andReturn(['published']);
+
+        Schema::shouldIgnoreMissing();
+
         Blueprint::shouldReceive('find')->with('runway::post')->andReturn(new FieldsBlueprint);
 
         Blueprint::shouldReceive('find')
@@ -52,16 +100,15 @@ class RelationshipsTest extends TestCase
                 'tabs' => [
                     'main' => [
                         'fields' => [
-                            ['handle' => 'posts', 'field' => ['type' => 'has_many', 'mode' => 'stack', 'resource' => 'post']],
+                            ['handle' => 'posts', 'field' => ['type' => 'has_many', 'resource' => 'post']],
                         ],
                     ],
                 ],
             ]));
 
-        Relationships::for($author)->with(['posts' => [
-            $posts[1]->id,
-            $posts[2]->id,
-        ]])->save();
+        Relationships::for($author)
+            ->with(['posts' => [$posts[1]->id, $posts[2]->id]])
+            ->save();
 
         $this->assertDatabaseMissing('posts', ['id' => $posts[0]->id]);
         $this->assertDatabaseHas('posts', ['id' => $posts[1]->id, 'author_id' => $author->id]);
@@ -82,17 +129,15 @@ class RelationshipsTest extends TestCase
                 'tabs' => [
                     'main' => [
                         'fields' => [
-                            ['handle' => 'posts', 'field' => ['type' => 'has_many', 'mode' => 'stack', 'resource' => 'post', 'reorderable' => true, 'order_column' => 'sort_order']],
+                            ['handle' => 'posts', 'field' => ['type' => 'has_many', 'resource' => 'post', 'reorderable' => true, 'order_column' => 'sort_order']],
                         ],
                     ],
                 ],
             ]));
 
-        Relationships::for($author)->with(['posts' => [
-            $posts[1]->id,
-            $posts[2]->id,
-            $posts[0]->id,
-        ]])->save();
+        Relationships::for($author)
+            ->with(['posts' => [$posts[1]->id, $posts[2]->id, $posts[0]->id]])
+            ->save();
 
         $this->assertDatabaseHas('posts', ['id' => $posts[0]->id, 'author_id' => $author->id, 'sort_order' => 2]);
         $this->assertDatabaseHas('posts', ['id' => $posts[1]->id, 'author_id' => $author->id, 'sort_order' => 0]);
@@ -111,7 +156,7 @@ class RelationshipsTest extends TestCase
                 'tabs' => [
                     'main' => [
                         'fields' => [
-                            ['handle' => 'pivottedPosts', 'field' => ['type' => 'has_many', 'mode' => 'stack', 'resource' => 'post']],
+                            ['handle' => 'pivottedPosts', 'field' => ['type' => 'has_many', 'resource' => 'post']],
                         ],
                     ],
                 ],
@@ -136,16 +181,15 @@ class RelationshipsTest extends TestCase
                 'tabs' => [
                     'main' => [
                         'fields' => [
-                            ['handle' => 'pivottedPosts', 'field' => ['type' => 'has_many', 'mode' => 'stack', 'resource' => 'post']],
+                            ['handle' => 'pivottedPosts', 'field' => ['type' => 'has_many', 'resource' => 'post']],
                         ],
                     ],
                 ],
             ]));
 
-        Relationships::for($author)->with(['pivottedPosts' => [
-            $posts[1]->id,
-            $posts[2]->id,
-        ]])->save();
+        Relationships::for($author)
+            ->with(['pivottedPosts' => [$posts[1]->id, $posts[2]->id]])
+            ->save();
 
         $this->assertDatabaseMissing('post_author', ['post_id' => $posts[0]->id, 'author_id' => $author->id]);
         $this->assertDatabaseHas('post_author', ['post_id' => $posts[1]->id, 'author_id' => $author->id]);
@@ -153,7 +197,7 @@ class RelationshipsTest extends TestCase
     }
 
     #[Test]
-    public function can_update_sort_orders_when_saving_belongs_to_relationship()
+    public function can_update_sort_orders_when_saving_belongs_to_many_relationship()
     {
         $author = Author::factory()->create();
         $posts = Post::factory()->count(3)->create();
@@ -164,17 +208,15 @@ class RelationshipsTest extends TestCase
                 'tabs' => [
                     'main' => [
                         'fields' => [
-                            ['handle' => 'pivottedPosts', 'field' => ['type' => 'has_many', 'mode' => 'stack', 'resource' => 'post', 'reorderable' => true, 'order_column' => 'pivot_sort_order']],
+                            ['handle' => 'pivottedPosts', 'field' => ['type' => 'has_many', 'resource' => 'post', 'reorderable' => true, 'order_column' => 'pivot_sort_order']],
                         ],
                     ],
                 ],
             ]));
 
-        Relationships::for($author)->with(['pivottedPosts' => [
-            $posts[0]->id,
-            $posts[2]->id,
-            $posts[1]->id,
-        ]])->save();
+        Relationships::for($author)
+            ->with(['pivottedPosts' => [$posts[0]->id, $posts[2]->id, $posts[1]->id]])
+            ->save();
 
         $this->assertDatabaseHas('post_author', ['post_id' => $posts[0]->id, 'author_id' => $author->id, 'pivot_sort_order' => 0]);
         $this->assertDatabaseHas('post_author', ['post_id' => $posts[1]->id, 'author_id' => $author->id, 'pivot_sort_order' => 2]);
@@ -195,7 +237,7 @@ class RelationshipsTest extends TestCase
                 'tabs' => [
                     'main' => [
                         'fields' => [
-                            ['handle' => 'posts', 'field' => ['type' => 'has_many', 'mode' => 'stack', 'resource' => 'post', 'visibility' => 'computed', 'save' => false]],
+                            ['handle' => 'posts', 'field' => ['type' => 'has_many', 'resource' => 'post', 'visibility' => 'computed', 'save' => false]],
                         ],
                     ],
                 ],
diff --git a/tests/__fixtures__/database/migrations/2020_12_12_220453_create_posts_table.php b/tests/__fixtures__/database/migrations/2020_12_12_220453_create_posts_table.php
index 604c889..f1faa88 100644
--- a/tests/__fixtures__/database/migrations/2020_12_12_220453_create_posts_table.php
+++ b/tests/__fixtures__/database/migrations/2020_12_12_220453_create_posts_table.php
@@ -20,7 +20,7 @@ class CreatePostsTable extends Migration
             $table->longText('body');
             $table->json('values')->nullable();
             $table->json('external_links')->nullable();
-            $table->integer('author_id');
+            $table->integer('author_id')->nullable();
             $table->integer('sort_order')->nullable();
             $table->datetime('start_date')->nullable();
             $table->datetime('end_date')->nullable();