Add store Action for subactivity

This commit is contained in:
philipp lang 2023-03-04 12:02:17 +01:00
parent 9baf5eac6b
commit 7db9e10200
10 changed files with 265 additions and 9 deletions

View File

@ -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()));
}
}

11
package-lock.json generated
View File

@ -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",

View File

@ -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"
}
}

6
resources/js/app.js vendored
View File

@ -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')));

View File

@ -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>

18
resources/js/mixins/hasFlash.js vendored Normal file
View File

@ -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',
});
},
},
};

View File

@ -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>

View File

@ -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>

View File

@ -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');
});

View File

@ -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]);
}
}