Compare commits

...

5 Commits

Author SHA1 Message Date
philipp lang d53a6d23d2 Add Frontend for copying events
continuous-integration/drone/push Build was killed Details
2025-06-17 00:08:29 +02:00
philipp lang ccef61352d Add backend for copying forms 2025-06-17 00:01:07 +02:00
philipp lang 5be7987d80 Lint 2025-06-16 23:39:40 +02:00
philipp lang ac592920e7 Mod from and to in FormFactory 2025-06-16 23:38:56 +02:00
philipp lang 68651b6c16 Fix FormFactory Date formatting 2025-06-16 00:34:58 +02:00
8 changed files with 148 additions and 11 deletions

View File

@ -0,0 +1,41 @@
<?php
namespace App\Form\Actions;
use App\Form\Models\Form;
use App\Lib\Events\Succeeded;
use Illuminate\Http\JsonResponse;
use Lorisleiva\Actions\Concerns\AsAction;
class FormCopyAction
{
use AsAction;
/**
* @param array<string, mixed> $attributes
*/
public function handle(Form $form): Form
{
$newForm = $form->replicate();
$newForm->save();
$newForm->update(['name' => $form->name.' - Kopie', 'is_active' => false]);
foreach ($form->getRegisteredMediaCollections() as $collection) {
foreach ($form->getMedia($collection->name) as $media) {
$media->copy($newForm, $collection->name);
}
}
ClearFrontendCacheAction::run();
return $form;
}
public function asController(Form $form): JsonResponse
{
$this->handle($form);
Succeeded::message('Veranstaltung kopiert.')->dispatch();
return response()->json([]);
}
}

View File

@ -56,11 +56,12 @@ class FormResource extends JsonResource
'links' => [
'participant_index' => route('form.participant.index', ['form' => $this->getModel(), 'parent' => null]),
'participant_root_index' => route('form.participant.index', ['form' => $this->getModel(), 'parent' => -1]),
'update' => route('form.update', ['form' => $this->getModel()]),
'destroy' => route('form.destroy', ['form' => $this->getModel()]),
'is_dirty' => route('form.is-dirty', ['form' => $this->getModel()]),
'update' => route('form.update', $this->getModel()),
'destroy' => route('form.destroy', $this->getModel()),
'is_dirty' => route('form.is-dirty', $this->getModel()),
'frontend' => str(app(FormSettings::class)->registerUrl)->replace('{slug}', $this->slug),
'export' => route('form.export', ['form' => $this->getModel()]),
'export' => route('form.export', $this->getModel()),
'copy' => route('form.copy', $this->getModel()),
]
];
}

View File

@ -47,8 +47,8 @@ class FormFactory extends Factory
'description' => EditorRequestFactory::new()->toData(),
'excerpt' => $this->faker->words(10, true),
'config' => ['sections' => []],
'from' => $this->faker->dateTimeBetween('+1 week', '+4 weeks')->format('Y-m-d H:i:s'),
'to' => $this->faker->dateTimeBetween('+1 week', '+4 weeks')->format('Y-m-d H:i:s'),
'from' => $this->faker->dateTimeBetween('+1 week', '+3 weeks')->format('Y-m-d'),
'to' => $this->faker->dateTimeBetween('+4 week', '+6 weeks')->format('Y-m-d'),
'registration_from' => $this->faker->dateTimeBetween(Carbon::parse('-2 weeks'), now())->format('Y-m-d H:i:s'),
'registration_until' => $this->faker->dateTimeBetween(now(), Carbon::parse('+2 weeks'))->format('Y-m-d H:i:s'),
'mail_top' => EditorRequestFactory::new()->toData(),
@ -65,7 +65,7 @@ class FormFactory extends Factory
*/
public function sections(array $sections): self
{
return $this->state(['config' => ['sections' => array_map(fn ($section) => $section->create(), $sections)]]);
return $this->state(['config' => ['sections' => array_map(fn($section) => $section->create(), $sections)]]);
}
/**

View File

@ -200,7 +200,7 @@ import ConditionsForm from './ConditionsForm.vue';
import { useToast } from 'vue-toastification';
const props = defineProps(indexProps);
const { meta, data, reloadPage, create, single, edit, cancel, submit, remove, getFilter, setFilter } = useIndex(props.data, 'form');
const { meta, data, reloadPage, reload, create, single, edit, cancel, submit, remove, getFilter, setFilter } = useIndex(props.data, 'form');
const axios = inject('axios');
const toast = useToast();
@ -228,9 +228,9 @@ const allFields = computed(() => {
return result;
});
function onCopy(form) {
single.value = {...meta.value.default, ...form};
single.value.id = null;
async function onCopy(form) {
await axios.post(form.links.copy, {});
reload(false);
}
function setTemplate(template) {

View File

@ -23,6 +23,7 @@ use App\Fileshare\Actions\FileshareStoreAction;
use App\Fileshare\Actions\FileshareUpdateAction;
use App\Fileshare\Actions\ListFilesAction;
use App\Form\Actions\ExportAction as ActionsExportAction;
use App\Form\Actions\FormCopyAction;
use App\Form\Actions\FormDestroyAction;
use App\Form\Actions\FormIndexAction;
use App\Group\Actions\GroupBulkstoreAction;
@ -179,6 +180,7 @@ Route::group(['middleware' => 'auth:web'], function (): void {
Route::get('/participant/{participant}/fields', ParticipantFieldsAction::class)->name('participant.fields');
Route::patch('/participant/{participant}', ParticipantUpdateAction::class)->name('participant.update');
Route::post('/form/{form}/participant', ParticipantStoreAction::class)->name('form.participant.store');
Route::post('/form/{form}/copy', FormCopyAction::class)->name('form.copy');
// ------------------------------------ fileshare -----------------------------------
Route::post('/fileshare', FileshareStoreAction::class)->name('fileshare.store');

View File

@ -145,6 +145,15 @@ class FormIndexActionTest extends FormTestCase
$this->callFilter('form.index', [])->assertInertiaPath('data.data.0.links.frontend', 'https://example.com/form/zem-2024/register');
}
public function testItDisplaysCopyUrl(): void
{
$this->withoutExceptionHandling()->login()->loginNami();
$form = Form::factory()->create();
sleep(1);
$this->callFilter('form.index', [])->assertInertiaPath('data.data.0.links.copy', route('form.copy', $form));
}
public function testItDoesntReturnInactiveForms(): void
{
$this->withoutExceptionHandling()->login()->loginNami();

View File

@ -0,0 +1,70 @@
<?php
namespace Tests\Feature\Form;
use App\Form\Models\Form;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Arr;
use Tests\Lib\CreatesFormFields;
uses(DatabaseTransactions::class);
uses(CreatesFormFields::class);
beforeEach(function () {
test()->fakeMessages();
test()->setUpForm();
});
dataset('media', fn () => [
['mailattachments'],
['headerImage'],
]);
it('copies a form', function () {
test()->login()->loginNami()->withoutExceptionHandling();
$form = Form::factory()->name('Lager')->create();
test()->post(route('form.copy', ['form' => $form]))
->assertOk();
test()->assertDatabaseCount('forms', 2);
$newForm = Form::where('name', 'Lager - Kopie')->firstOrFail();
test()->assertEquals(
Arr::except($form->fresh()->toArray(), ['id', 'name', 'slug', 'created_at', 'updated_at', 'is_active']),
Arr::except($newForm->fresh()->toArray(), ['id', 'name', 'slug', 'created_at', 'updated_at', 'is_active'])
);
});
it('copies the forms media', function (string $collectionName) {
test()->login()->loginNami()->withoutExceptionHandling();
$form = Form::factory()->withImage($collectionName, 'lala.jpg')->name('Lager')->create();
test()->post(route('form.copy', ['form' => $form]))->assertOk();
test()->assertDatabaseCount('forms', 2);
$newForm = Form::where('name', 'Lager - Kopie')->firstOrFail();
test()->assertEquals($form->getMedia($collectionName)->first()->name, $newForm->getMedia($collectionName)->first()->name);
test()->assertNotEquals($form->getMedia($collectionName)->first()->id, $newForm->getMedia($collectionName)->first()->id);
test()->assertNotEquals($form->getMedia($collectionName)->first()->getFullUrl(), $newForm->getMedia($collectionName)->first()->getFullUrl());
})->with('media');
it('deactivates a copied form', function () {
test()->login()->loginNami()->withoutExceptionHandling();
$form = Form::factory()->name('Lager')->create(['is_active' => true]);
test()->post(route('form.copy', ['form' => $form]))->assertOk();
$newForm = Form::where('name', 'Lager - Kopie')->firstOrFail();
test()->assertEquals(false, $newForm->is_active);
});
it('shows success message', function () {
test()->login()->loginNami()->withoutExceptionHandling();
$form = Form::factory()->create();
test()->post(route('form.copy', ['form' => $form]))->assertOk();
test()->assertSuccessMessage('Veranstaltung kopiert.');
});

View File

@ -3,12 +3,14 @@
namespace Tests;
use App\Group;
use App\Lib\Events\Succeeded;
use App\Member\Member;
use App\Setting\NamiSettings;
use App\User;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Http;
use Illuminate\Testing\AssertableJsonString;
use Illuminate\Testing\TestResponse;
@ -192,4 +194,16 @@ class TestCase extends BaseTestCase
return $this;
});
}
public function fakeMessages() {
Event::fake([Succeeded::class]);
return $this;
}
public function assertSuccessMessage(string $message) {
Event::assertDispatched(Succeeded::class, fn ($event) => $event->message === $message);
return $this;
}
}