Add store Action for subactivity
This commit is contained in:
parent
9baf5eac6b
commit
7db9e10200
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
namespace App\Activity\Api;
|
||||
|
||||
use App\Subactivity;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Arr;
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class SubactivityStoreAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
/**
|
||||
* @param array<string, string|array<int, int>> $payload
|
||||
*/
|
||||
public function handle(array $payload): Subactivity
|
||||
{
|
||||
$subactivity = Subactivity::create(Arr::except($payload, 'activities'));
|
||||
$subactivity->activities()->sync($payload['activities']);
|
||||
|
||||
return $subactivity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'required|unique:subactivities,name',
|
||||
'activities' => 'present|array|min:1',
|
||||
'is_filterable' => 'present|boolean',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getValidationAttributes(): array
|
||||
{
|
||||
return [
|
||||
'activities' => 'Tätigkeiten',
|
||||
];
|
||||
}
|
||||
|
||||
public function asController(ActionRequest $request): JsonResponse
|
||||
{
|
||||
return response()->json($this->handle($request->validated()));
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@
|
|||
"query-string": "^7.0.0",
|
||||
"svg-sprite": "^2.0.2",
|
||||
"v-tooltip": "^2.1.3",
|
||||
"vue-toasted": "^1.1.28",
|
||||
"wnumb": "^1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -10318,6 +10319,11 @@
|
|||
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/vue-toasted": {
|
||||
"version": "1.1.28",
|
||||
"resolved": "https://registry.npmjs.org/vue-toasted/-/vue-toasted-1.1.28.tgz",
|
||||
"integrity": "sha512-UUzr5LX51UbbiROSGZ49GOgSzFxaMHK6L00JV8fir/CYNJCpIIvNZ5YmS4Qc8Y2+Z/4VVYRpeQL2UO0G800Raw=="
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz",
|
||||
|
@ -18728,6 +18734,11 @@
|
|||
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
|
||||
"dev": true
|
||||
},
|
||||
"vue-toasted": {
|
||||
"version": "1.1.28",
|
||||
"resolved": "https://registry.npmjs.org/vue-toasted/-/vue-toasted-1.1.28.tgz",
|
||||
"integrity": "sha512-UUzr5LX51UbbiROSGZ49GOgSzFxaMHK6L00JV8fir/CYNJCpIIvNZ5YmS4Qc8Y2+Z/4VVYRpeQL2UO0G800Raw=="
|
||||
},
|
||||
"watchpack": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz",
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
"query-string": "^7.0.0",
|
||||
"svg-sprite": "^2.0.2",
|
||||
"v-tooltip": "^2.1.3",
|
||||
"vue-toasted": "^1.1.28",
|
||||
"wnumb": "^1.2.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,16 +8,20 @@ import VLabel from './components/VLabel.vue';
|
|||
import VBool from './components/VBool.vue';
|
||||
import Box from './components/Box.vue';
|
||||
import Heading from './components/Heading.vue';
|
||||
import IconButton from './components/Ui/IconButton.vue';
|
||||
import AppLayout from './layouts/AppLayout.vue';
|
||||
import VTooltip from 'v-tooltip';
|
||||
import hasModule from './mixins/hasModule.js';
|
||||
import hasFlash from './mixins/hasFlash.js';
|
||||
import PortalVue from 'portal-vue';
|
||||
import axios from 'axios';
|
||||
import VueAxios from 'vue-axios';
|
||||
import Toasted from 'vue-toasted';
|
||||
|
||||
Vue.use(plugin);
|
||||
Vue.use(PortalVue);
|
||||
Vue.use(VTooltip);
|
||||
Vue.use(Toasted);
|
||||
Vue.use(VueAxios, axios);
|
||||
Vue.component('f-text', () => import(/* webpackChunkName: "form" */ './components/FText'));
|
||||
Vue.component('f-switch', () => import(/* webpackChunkName: "form" */ './components/FSwitch'));
|
||||
|
@ -29,11 +33,13 @@ Vue.component('v-bool', VBool);
|
|||
Vue.component('v-label', VLabel);
|
||||
Vue.component('box', Box);
|
||||
Vue.component('heading', Heading);
|
||||
Vue.component('icon-button', IconButton);
|
||||
Vue.component('save-button', () => import(/* webpackChunkName: "form" */ './components/SaveButton'));
|
||||
|
||||
const el = document.getElementById('app');
|
||||
|
||||
Vue.mixin(hasModule);
|
||||
Vue.mixin(hasFlash);
|
||||
Vue.component('ILink', ILink);
|
||||
|
||||
Inertia.on('start', (event) => window.dispatchEvent(new Event('inertiaStart')));
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
<template>
|
||||
<button v-on="$listeners" type="button" class="btn label btn-primary">
|
||||
<svg-sprite class="w-3 h-3 mr-2" :src="icon"></svg-sprite>
|
||||
<span><slot></slot></span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
icon: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,18 @@
|
|||
export default {
|
||||
methods: {
|
||||
['$success'](message) {
|
||||
this.$toasted.show(message, {
|
||||
position: 'bottom-right',
|
||||
duration: 2000,
|
||||
type: 'success',
|
||||
});
|
||||
},
|
||||
['$error'](message) {
|
||||
this.$toasted.show(message, {
|
||||
position: 'bottom-right',
|
||||
duration: 2000,
|
||||
type: 'error',
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,38 @@
|
|||
<template>
|
||||
<div>
|
||||
<icon-button class="mt-4" icon="plus" v-show="model === null" @click.prevent="model = {name: '', is_filterable: false, activities: [activityId]}">Neue Untertätigkeit</icon-button>
|
||||
<icon-button class="mt-4" icon="close" v-show="model !== null" @click.prevent="model = null">Schließen</icon-button>
|
||||
<div class="mt-2 border border-primary-700 rounded-lg p-5" v-if="model !== null">
|
||||
<div class="flex space-x-3">
|
||||
<f-text size="sm" id="name" v-model="model.name" label="Name" required></f-text>
|
||||
<f-switch size="sm" v-model="model.is_filterable" name="is_filterable" id="is_filterable" label="Filterbar"></f-switch>
|
||||
</div>
|
||||
<icon-button class="mt-3" icon="save" @click.prevent="store">Speichern</icon-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data: function () {
|
||||
return {
|
||||
visible: false,
|
||||
model: null,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
activityId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async store() {
|
||||
var response = await this.axios.post('/subactivity', this.model);
|
||||
this.model = null;
|
||||
this.$emit('stored', response.data);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -1,11 +1,23 @@
|
|||
<template>
|
||||
<form id="actionform" class="grow p-3" @submit.prevent="submit">
|
||||
<form id="actionform" class="grow p-3" @submit.prevent="submit">
|
||||
<f-text id="name" v-model="inner.name" label="Name" required></f-text>
|
||||
<checkboxes-label class="mt-4">Untertätigkeiten</checkboxes-label>
|
||||
<checkboxes-label class="mt-6">Untertätigkeiten</checkboxes-label>
|
||||
<div class="grid gap-2 sm:grid-cols-2 md:grid-cols-4">
|
||||
<f-switch inline size="sm" :key="option.id" v-model="inner.subactivities" name="subactivities[]" :id="`subactivities-${option.id}`" :value="option.id" :label="option.name" v-for="option in meta.subactivities"></f-switch>
|
||||
<f-switch
|
||||
inline
|
||||
size="sm"
|
||||
:key="option.id"
|
||||
v-model="inner.subactivities"
|
||||
name="subactivities[]"
|
||||
:id="`subactivities-${option.id}`"
|
||||
:value="option.id"
|
||||
:label="option.name"
|
||||
v-for="option in subactivities"
|
||||
></f-switch>
|
||||
</div>
|
||||
<save-button form="actionform"></save-button>
|
||||
|
||||
<new-subactivity @stored="reloadSubactivities" :activity-id="inner.id"></new-subactivity>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
|
@ -13,6 +25,7 @@
|
|||
export default {
|
||||
data: function () {
|
||||
return {
|
||||
subactivities: [...this.meta.subactivities],
|
||||
inner: {...this.data},
|
||||
mode: this.data.name === '' ? 'create' : 'edit',
|
||||
};
|
||||
|
@ -25,15 +38,25 @@ export default {
|
|||
|
||||
components: {
|
||||
'checkboxes-label': () => import('../../components/Form/CheckboxesLabel'),
|
||||
'new-subactivity': () => import('./NewSubactivity.vue'),
|
||||
},
|
||||
|
||||
methods: {
|
||||
submit() {
|
||||
this.mode === 'create'
|
||||
? this.$inertia.post('/activity', this.inner)
|
||||
: this.$inertia.patch(`/activity/${this.inner.id}`, this.inner);
|
||||
this.mode === 'create' ? this.$inertia.post('/activity', this.inner) : this.$inertia.patch(`/activity/${this.inner.id}`, this.inner);
|
||||
},
|
||||
}
|
||||
|
||||
reloadSubactivities(model) {
|
||||
var _self = this;
|
||||
|
||||
this.$inertia.reload({
|
||||
onSuccess(page) {
|
||||
_self.subactivities = page.props.meta.subactivities;
|
||||
_self.inner.subactivities.push(model.id);
|
||||
_self.$success('Untertätigkeit gespeichert.');
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -2,10 +2,11 @@
|
|||
|
||||
use App\Activity\Actions\ActivityStoreAction;
|
||||
use App\Activity\Actions\ActivityUpdateAction;
|
||||
use App\Activity\Actions\IndexAction as ActivityIndexAction;
|
||||
use App\Activity\Actions\CreateAction as ActivityCreateAction;
|
||||
use App\Activity\Actions\EditAction as ActivityEditAction;
|
||||
use App\Activity\Actions\DestroyAction as ActivityDestroyAction;
|
||||
use App\Activity\Actions\EditAction as ActivityEditAction;
|
||||
use App\Activity\Actions\IndexAction as ActivityIndexAction;
|
||||
use App\Activity\Api\SubactivityStoreAction;
|
||||
use App\Contribution\Actions\FormAction as ContributionFormAction;
|
||||
use App\Contribution\ContributionController;
|
||||
use App\Course\Controllers\CourseController;
|
||||
|
@ -60,4 +61,5 @@ Route::group(['middleware' => 'auth:web'], function (): void {
|
|||
Route::post('/activity', ActivityStoreAction::class)->name('activity.store');
|
||||
Route::patch('/activity/{activity}', ActivityUpdateAction::class)->name('activity.update');
|
||||
Route::delete('/activity/{activity}', ActivityDestroyAction::class)->name('activity.destroy');
|
||||
Route::post('/subactivity', SubactivityStoreAction::class)->name('api.subactivity.store');
|
||||
});
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\Activity;
|
||||
|
||||
use App\Activity;
|
||||
use App\Subactivity;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SubactivityTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
public function testItStoresASubactivity(): void
|
||||
{
|
||||
$this->login()->loginNami()->withoutExceptionHandling();
|
||||
$activity = Activity::factory()->create();
|
||||
|
||||
$response = $this->postJson(route('api.subactivity.store'), [
|
||||
'name' => 'Leiter Lost',
|
||||
'activities' => [$activity->id],
|
||||
'is_filterable' => true,
|
||||
]);
|
||||
|
||||
$subactivity = Subactivity::where('name', 'Leiter Lost')->firstOrFail();
|
||||
$response->assertJsonPath('id', $subactivity->id);
|
||||
|
||||
$this->assertDatabaseHas('subactivities', [
|
||||
'name' => 'Leiter Lost',
|
||||
'nami_id' => null,
|
||||
'is_age_group' => 0,
|
||||
'is_filterable' => 1,
|
||||
]);
|
||||
$this->assertDatabaseHas('activity_subactivity', ['activity_id' => $activity->id, 'subactivity_id' => $subactivity->id]);
|
||||
}
|
||||
|
||||
public function testNameIsRequired(): void
|
||||
{
|
||||
$this->login()->loginNami();
|
||||
$activity = Activity::factory()->create();
|
||||
|
||||
$response = $this->postJson(route('api.subactivity.store'), [
|
||||
'name' => '',
|
||||
'activities' => [$activity->id],
|
||||
'is_filterable' => true,
|
||||
]);
|
||||
$response->assertJsonValidationErrors(['name' => 'Name ist erforderlich.']);
|
||||
}
|
||||
|
||||
public function testNameIsUnique(): void
|
||||
{
|
||||
$this->login()->loginNami();
|
||||
$activity = Activity::factory()->create();
|
||||
$subactivity = Subactivity::factory()->name('Lott')->create();
|
||||
|
||||
$response = $this->postJson(route('api.subactivity.store'), [
|
||||
'name' => 'Lott',
|
||||
'activities' => [$activity->id],
|
||||
'is_filterable' => true,
|
||||
]);
|
||||
$response->assertJsonValidationErrors(['name' => 'Name ist bereits vergeben.']);
|
||||
}
|
||||
|
||||
public function testItNeedsAtLeasttOneActivity(): void
|
||||
{
|
||||
$this->login()->loginNami();
|
||||
|
||||
$response = $this->postJson(route('api.subactivity.store'), [
|
||||
'name' => '',
|
||||
'activities' => [],
|
||||
'is_filterable' => true,
|
||||
]);
|
||||
$response->assertJsonValidationErrors(['activities' => 'Tätigkeiten muss mindestens 1 Elemente haben.']);
|
||||
}
|
||||
|
||||
public function testNamiIdIsNotSet(): void
|
||||
{
|
||||
$this->login()->loginNami();
|
||||
$activity = Activity::factory()->create();
|
||||
|
||||
$this->postJson(route('api.subactivity.store'), [
|
||||
'name' => 'aaaa',
|
||||
'nami_id' => 556,
|
||||
'activities' => [$activity->id],
|
||||
'is_filterable' => true,
|
||||
]);
|
||||
$this->assertDatabaseMissing('subactivities', ['nami_id' => 556]);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue