Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[6.x] "Duplicate" action for models #374

Merged
merged 3 commits into from
Nov 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 25 additions & 26 deletions resources/js/components/Publish/PublishForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -180,33 +180,8 @@ export default {
},

created() {
// If we're creating a resource through the 'Create' on a HasMany field somewhere, fill any fields...
if (this.publishContainer.includes('relate-fieldtype-inline')) {
this.values['from_inline_publish_form'] = true

this.initialBlueprint.tabs.forEach((tab) => {
tab.sections.forEach((section) => {
section.fields.forEach((field) => {
if (
field.type === 'belongs_to' &&
field.resource === window.Runway.currentResource
) {
let alreadyExists = this.values[field.handle].includes(
window.Runway.currentRecord.id
)

if (!alreadyExists) {
this.values[field.handle].push(
window.Runway.currentRecord.id
)
this.meta[field.handle].data = [
window.Runway.currentRecord,
]
}
}
})
})
})
this.prefillBelongsToField()
}
},

Expand Down Expand Up @@ -295,6 +270,30 @@ export default {
}
)
},

/**
* When creating a new model via the HasMany fieldtype, pre-fill the belongs_to field to the current record.
*/
prefillBelongsToField() {
this.values['from_inline_publish_form'] = true

this.initialBlueprint.tabs.forEach((tab) => {
tab.sections.forEach((section) => {
section.fields
.filter((field) => {
return field.type === 'belongs_to' || field.resource === window.Runway.currentResource;
})
.forEach((field) => {
let alreadyExists = this.values[field.handle].includes(window.Runway.currentRecord.id)

if (!alreadyExists) {
this.values[field.handle].push(window.Runway.currentRecord.id)
this.meta[field.handle].data = [window.Runway.currentRecord]
}
})
})
})
},
},

watch: {
Expand Down
63 changes: 63 additions & 0 deletions src/Actions/DuplicateModel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

namespace DoubleThreeDigital\Runway\Actions;

use DoubleThreeDigital\Runway\Exceptions\ResourceNotFound;
use DoubleThreeDigital\Runway\Runway;
use Illuminate\Database\Eloquent\Model;
use Statamic\Actions\Action;

class DuplicateModel extends Action
{
public static function title()
{
return __('Duplicate');
}

public function visibleTo($item)
{
try {
$resource = Runway::findResourceByModel($item);
} catch (ResourceNotFound $e) {
return false;
}

return $item instanceof Model && $resource->readOnly() !== true;
}

public function visibleToBulk($items)
{
return $items
->map(fn ($item) => $this->visibleTo($item))
->filter(fn ($isVisible) => $isVisible === true)
->count() === $items->count();
}

public function authorize($user, $item)
{
$resource = Runway::findResourceByModel($item);

return $user->can('create', $resource);
}

public function buttonText()
{
/* @translation */
return 'Duplicate|Duplicate :count items?';
}

public function run($items, $values)
{
$resource = Runway::findResourceByModel($items->first());

$items->each(function (Model $item) use ($resource) {
$duplicateModel = $item->replicate();

if ($resource->titleField()) {
$duplicateModel->{$resource->titleField()} = $duplicateModel->{$resource->titleField()}.' (Duplicate)';
}

$duplicateModel->save();
});
}
}
1 change: 1 addition & 0 deletions src/ServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class ServiceProvider extends AddonServiceProvider

protected $actions = [
Actions\DeleteModel::class,
Actions\DuplicateModel::class,
];

protected $commands = [
Expand Down
139 changes: 139 additions & 0 deletions tests/Actions/DuplicateModelTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
<?php

namespace DoubleThreeDigital\Runway\Tests\Actions;

use DoubleThreeDigital\Runway\Actions\DuplicateModel;
use DoubleThreeDigital\Runway\Runway;
use DoubleThreeDigital\Runway\Tests\Fixtures\Models\Post;
use DoubleThreeDigital\Runway\Tests\TestCase;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Config;
use Statamic\Facades\Collection;
use Statamic\Facades\Entry;
use Statamic\Facades\Role;
use Statamic\Facades\User;

class DuplicateModelTest extends TestCase
{
/** @test */
public function it_returns_title()
{
$this->assertEquals('Duplicate', DuplicateModel::title());
}

/** @test */
public function is_visible_to_eloquent_model()
{
$visibleTo = (new DuplicateModel())->visibleTo(Post::factory()->create());

$this->assertTrue($visibleTo);
}

/** @test */
public function is_not_visible_to_eloquent_model_when_resource_is_read_only()
{
Config::set('runway.resources.DoubleThreeDigital\Runway\Tests\Fixtures\Models\Post.read_only', true);
Runway::discoverResources();

$visibleTo = (new DuplicateModel())->visibleTo(Post::factory()->create());

$this->assertFalse($visibleTo);
}

/** @test */
public function is_not_visible_to_eloquent_model_without_a_runway_resource()
{
$model = new class extends Model
{
protected $table = 'posts';
};

$visibleTo = (new DuplicateModel())->visibleTo(new $model);

$this->assertFalse($visibleTo);
}

/** @test */
public function is_not_visible_to_entry()
{
Collection::make('posts')->save();

$visibleTo = (new DuplicateModel())->visibleTo(
tap(Entry::make()->collection('posts')->slug('hello-world'))->save()
);

$this->assertFalse($visibleTo);
}

/** @test */
public function is_visible_to_eloquent_models_in_bulk()
{
$posts = Post::factory()->count(3)->create();

$visibleToBulk = (new DuplicateModel())->visibleToBulk($posts);

$this->assertTrue($visibleToBulk);
}

/** @test */
public function is_not_visible_to_entries_in_bulk()
{
Collection::make('posts')->save();

$entries = collect([
tap(Entry::make()->collection('posts')->slug('hello-world'))->save(),
tap(Entry::make()->collection('posts')->slug('foo-bar'))->save(),
tap(Entry::make()->collection('posts')->slug('bye-bye'))->save(),
]);

$visibleToBulk = (new DuplicateModel())->visibleToBulk($entries);

$this->assertFalse($visibleToBulk);
}

/** @test */
public function super_user_is_authorized()
{
$user = User::make()->makeSuper()->save();

$authorize = (new DuplicateModel())->authorize($user, Post::factory()->create());

$this->assertTrue($authorize);
}

/** @test */
public function user_with_permission_is_authorized()
{
Role::make('editor')->addPermission('create post')->save();

$user = User::make()->assignRole('editor')->save();

$authorize = (new DuplicateModel())->authorize($user, Post::factory()->create());

$this->assertTrue($authorize);

Role::find('editor')->delete();
}

/** @test */
public function user_without_permission_is_not_authorized()
{
$user = User::make()->save();

$authorize = (new DuplicateModel())->authorize($user, Post::factory()->create());

$this->assertFalse($authorize);
}

/** @test */
public function it_duplicates_models()
{
$post = Post::factory()->create(['title' => 'Hello World']);

$this->assertCount(1, Post::where('title', 'like', 'Hello World%')->get());

(new DuplicateModel)->run(collect([$post]), []);

$this->assertCount(2, Post::where('title', 'like', 'Hello World%')->get());
}
}
Loading