different names in different dynamics
This commit is contained in:
parent
3c414e1ef1
commit
0fee3c1972
@ -54,7 +54,7 @@ class DynamicController extends Controller
|
|||||||
|
|
||||||
$dynamic->load([
|
$dynamic->load([
|
||||||
'ledgers.media',
|
'ledgers.media',
|
||||||
'participants',
|
'participants' => fn($query) => $query->withPivot('display_name'),
|
||||||
'chat.messages.user',
|
'chat.messages.user',
|
||||||
'chat.messages.media'
|
'chat.messages.media'
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -64,7 +64,7 @@ class LedgerController extends Controller
|
|||||||
|
|
||||||
$activityService->updateCursor($request->user(), $ledger);
|
$activityService->updateCursor($request->user(), $ledger);
|
||||||
|
|
||||||
$dynamic->load('chat', 'participants');
|
$dynamic->load(['chat', 'participants' => fn($query) => $query->withPivot('display_name')]);
|
||||||
|
|
||||||
$ledger->load([
|
$ledger->load([
|
||||||
'media',
|
'media',
|
||||||
|
|||||||
24
app/Http/Controllers/ParticipantController.php
Normal file
24
app/Http/Controllers/ParticipantController.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Dynamic;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class ParticipantController extends Controller
|
||||||
|
{
|
||||||
|
public function update(Request $request, Dynamic $dynamic)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'display_name' => ['required', 'string', 'max:255'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$participant = $dynamic->participants()->where('user_id', $request->user()->id)->firstOrFail();
|
||||||
|
|
||||||
|
$dynamic->participants()->updateExistingPivot($participant->id, [
|
||||||
|
'display_name' => $request->input('display_name'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', 'Display name updated successfully!');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -21,7 +21,7 @@ class Dynamic extends Model
|
|||||||
|
|
||||||
public function participants(): BelongsToMany
|
public function participants(): BelongsToMany
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(User::class, 'participants')->withPivot('role');
|
return $this->belongsToMany(User::class, 'participants')->withPivot('role', 'display_name');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function ledgers(): HasMany
|
public function ledgers(): HasMany
|
||||||
|
|||||||
@ -12,5 +12,6 @@ class Participant extends Pivot
|
|||||||
'user_id',
|
'user_id',
|
||||||
'dynamic_id',
|
'dynamic_id',
|
||||||
'role',
|
'role',
|
||||||
|
'display_name',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -49,6 +49,13 @@ class User extends Authenticatable implements PasskeyUser
|
|||||||
return $this->hasMany(ReadCursor::class);
|
return $this->hasMany(ReadCursor::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function displayNameFor(Dynamic $dynamic): string
|
||||||
|
{
|
||||||
|
$participant = $dynamic->participants()->where('user_id', $this->id)->first();
|
||||||
|
|
||||||
|
return $participant?->pivot?->display_name ?? $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the attributes that should be cast.
|
* Get the attributes that should be cast.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('participants', function (Blueprint $table) {
|
||||||
|
$table->string('display_name')->nullable()->after('role');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('participants', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('display_name');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -56,9 +56,9 @@ class DatabaseSeeder extends Seeder
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// Add participants (Test User is owner, Alice is owner, Bob is submissive/participant)
|
// Add participants (Test User is owner, Alice is owner, Bob is submissive/participant)
|
||||||
$velvetSanctuary->participants()->attach($testUser->id, ['role' => 'owner']);
|
$velvetSanctuary->participants()->attach($testUser->id, ['role' => 'owner', 'display_name' => 'The Master']);
|
||||||
$velvetSanctuary->participants()->attach($alice->id, ['role' => 'owner']);
|
$velvetSanctuary->participants()->attach($alice->id, ['role' => 'owner']);
|
||||||
$velvetSanctuary->participants()->attach($bob->id, ['role' => 'participant']);
|
$velvetSanctuary->participants()->attach($bob->id, ['role' => 'participant', 'display_name' => 'Bitch Boi']);
|
||||||
|
|
||||||
// Chat has been auto-created by the booted hook on Dynamic
|
// Chat has been auto-created by the booted hook on Dynamic
|
||||||
$velvetChat = $velvetSanctuary->chat;
|
$velvetChat = $velvetSanctuary->chat;
|
||||||
|
|||||||
@ -13,5 +13,6 @@
|
|||||||
@import './components/password-input.css';
|
@import './components/password-input.css';
|
||||||
@import './components/auth-layout.css';
|
@import './components/auth-layout.css';
|
||||||
@import './components/chat.css';
|
@import './components/chat.css';
|
||||||
@import './components/lightbox.css';
|
@import "./components/lightbox.css";
|
||||||
@import './components/invite-form.css';
|
@import "./components/invite-form.css";
|
||||||
|
/*@import "./components/display-name-form.css";*/
|
||||||
|
|||||||
113
resources/js/components/DisplayNameForm.vue
Normal file
113
resources/js/components/DisplayNameForm.vue
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useForm } from '@inertiajs/vue3';
|
||||||
|
import { route } from 'ziggy-js';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
dynamicId: number;
|
||||||
|
participant: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
display_name: string | null;
|
||||||
|
};
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
display_name: props.participant.display_name ?? '',
|
||||||
|
});
|
||||||
|
|
||||||
|
function submit() {
|
||||||
|
form.put(route('dynamics.participant.update', props.dynamicId));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="c-display-name-form">
|
||||||
|
<h4 class="c-display-name-form__title">
|
||||||
|
Display Name for {{ participant.name }}
|
||||||
|
</h4>
|
||||||
|
<form @submit.prevent="submit" class="c-display-name-form__form">
|
||||||
|
<div class="c-display-name-form__field">
|
||||||
|
<label
|
||||||
|
for="display_name"
|
||||||
|
class="c-display-name-form__label"
|
||||||
|
>
|
||||||
|
Dynamic-specific Display Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="form.display_name"
|
||||||
|
id="display_name"
|
||||||
|
type="text"
|
||||||
|
class="c-display-name-form__input"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="form.errors.display_name"
|
||||||
|
class="c-display-name-form__error"
|
||||||
|
>
|
||||||
|
{{ form.errors.display_name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="c-display-name-form__actions">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
:disabled="form.processing"
|
||||||
|
class="c-display-name-form__submit-btn"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference '../../css/app.css';
|
||||||
|
|
||||||
|
.c-display-name-form {
|
||||||
|
@apply mt-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c-display-name-form__title {
|
||||||
|
@apply text-lg font-medium;
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.c-display-name-form__form {
|
||||||
|
@apply mt-4 space-y-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c-display-name-form__field {
|
||||||
|
@apply block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c-display-name-form__label {
|
||||||
|
@apply block text-sm font-medium;
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.c-display-name-form__input {
|
||||||
|
@apply mt-1 block w-full rounded-md border shadow-sm focus:border-indigo-500 focus:ring-indigo-500;
|
||||||
|
border-color: var(--border);
|
||||||
|
background-color: var(--background);
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.c-display-name-form__error {
|
||||||
|
@apply text-sm;
|
||||||
|
color: var(--destructive);
|
||||||
|
}
|
||||||
|
|
||||||
|
.c-display-name-form__actions {
|
||||||
|
@apply flex items-center gap-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c-display-name-form__submit-btn {
|
||||||
|
@apply inline-flex items-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-xs font-semibold tracking-widest text-white uppercase transition duration-150 ease-in-out hover:bg-gray-700 focus:bg-gray-700 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none active:bg-gray-900;
|
||||||
|
background-color: var(--primary);
|
||||||
|
color: var(--primary-foreground);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -3,6 +3,9 @@ defineProps<{
|
|||||||
participants: Array<{
|
participants: Array<{
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
pivot: {
|
||||||
|
display_name: string | null;
|
||||||
|
};
|
||||||
}>;
|
}>;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
@ -16,7 +19,7 @@ defineProps<{
|
|||||||
:key="participant.id"
|
:key="participant.id"
|
||||||
class="c-participants-list__item"
|
class="c-participants-list__item"
|
||||||
>
|
>
|
||||||
{{ participant.name }}
|
{{ participant.pivot.display_name ?? participant.name }}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,8 +2,10 @@
|
|||||||
import Chat from '@/components/Chat.vue';
|
import Chat from '@/components/Chat.vue';
|
||||||
import ParticipantsList from '@/components/ParticipantsList.vue';
|
import ParticipantsList from '@/components/ParticipantsList.vue';
|
||||||
import LedgerList from '@/components/LedgerList.vue';
|
import LedgerList from '@/components/LedgerList.vue';
|
||||||
import { Head, Link as InertiaLink } from '@inertiajs/vue3';
|
import DisplayNameForm from '@/components/DisplayNameForm.vue';
|
||||||
|
import { Head, Link as InertiaLink, usePage } from '@inertiajs/vue3';
|
||||||
import { route } from 'ziggy-js';
|
import { route } from 'ziggy-js';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
dynamic: {
|
dynamic: {
|
||||||
@ -11,7 +13,7 @@ const props = defineProps<{
|
|||||||
name: string;
|
name: string;
|
||||||
rules: string;
|
rules: string;
|
||||||
chat: any;
|
chat: any;
|
||||||
participants: Array<{ id: number; name: string }>;
|
participants: Array<{ id: number; name: string, pivot: { display_name: string | null } }>;
|
||||||
ledgers: Array<{
|
ledgers: Array<{
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@ -23,6 +25,18 @@ const props = defineProps<{
|
|||||||
isOwner: boolean;
|
isOwner: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const currentUser = computed(() => {
|
||||||
|
const page = usePage();
|
||||||
|
const authUser = page.props.auth.user;
|
||||||
|
const participant = props.dynamic.participants.find(p => p.id === authUser.id);
|
||||||
|
return {
|
||||||
|
id: authUser.id,
|
||||||
|
name: authUser.name,
|
||||||
|
display_name: participant?.pivot?.display_name ?? null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
const breadcrumbs = [
|
const breadcrumbs = [
|
||||||
{
|
{
|
||||||
name: 'Dynamics',
|
name: 'Dynamics',
|
||||||
@ -65,6 +79,9 @@ const breadcrumbs = [
|
|||||||
<!-- Ledgers List Component -->
|
<!-- Ledgers List Component -->
|
||||||
<LedgerList :dynamic-id="dynamic.id" :ledgers="dynamic.ledgers" />
|
<LedgerList :dynamic-id="dynamic.id" :ledgers="dynamic.ledgers" />
|
||||||
|
|
||||||
|
<!-- Display Name Form -->
|
||||||
|
<DisplayNameForm :dynamic-id="dynamic.id" :participant="currentUser" />
|
||||||
|
|
||||||
<div v-if="isOwner" class="mt-8 flex gap-4">
|
<div v-if="isOwner" class="mt-8 flex gap-4">
|
||||||
<InertiaLink :href="route('dynamics.invitations.create', dynamic.id)" class="c-dynamic-show__action-btn">
|
<InertiaLink :href="route('dynamics.invitations.create', dynamic.id)" class="c-dynamic-show__action-btn">
|
||||||
Invite User
|
Invite User
|
||||||
|
|||||||
@ -25,6 +25,8 @@ Route::middleware(['auth', 'verified'])->group(function () {
|
|||||||
Route::post('/dynamics/{dynamic}/invitations', [\App\Http\Controllers\DynamicInvitationController::class, 'store'])->name('dynamics.invitations.store');
|
Route::post('/dynamics/{dynamic}/invitations', [\App\Http\Controllers\DynamicInvitationController::class, 'store'])->name('dynamics.invitations.store');
|
||||||
|
|
||||||
Route::post('/chats/{chat}/messages', [MessageController::class, 'store'])->name('chats.messages.store');
|
Route::post('/chats/{chat}/messages', [MessageController::class, 'store'])->name('chats.messages.store');
|
||||||
|
|
||||||
|
Route::put('/dynamics/{dynamic}/participant', [\App\Http\Controllers\ParticipantController::class, 'update'])->name('dynamics.participant.update');
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::get('/invitations/accept/{token}', [\App\Http\Controllers\DynamicInvitationController::class, 'accept'])
|
Route::get('/invitations/accept/{token}', [\App\Http\Controllers\DynamicInvitationController::class, 'accept'])
|
||||||
|
|||||||
76
tests/Feature/ParticipantTest.php
Normal file
76
tests/Feature/ParticipantTest.php
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Dynamic;
|
||||||
|
|
||||||
|
test('user displayNameFor returns user name by default', function () {
|
||||||
|
$user = User::factory()->create(['name' => 'Alice']);
|
||||||
|
$dynamic = Dynamic::factory()->create();
|
||||||
|
$dynamic->participants()->attach($user->id, ['role' => 'participant']);
|
||||||
|
|
||||||
|
expect($user->displayNameFor($dynamic))->toBe('Alice');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('user displayNameFor returns custom display name when set', function () {
|
||||||
|
$user = User::factory()->create(['name' => 'Alice']);
|
||||||
|
$dynamic = Dynamic::factory()->create();
|
||||||
|
$dynamic->participants()->attach($user->id, [
|
||||||
|
'role' => 'participant',
|
||||||
|
'display_name' => 'Ally',
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($user->displayNameFor($dynamic))->toBe('Ally');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('participant can update their display name', function () {
|
||||||
|
$user = User::factory()->create(['name' => 'Alice']);
|
||||||
|
$dynamic = Dynamic::factory()->create();
|
||||||
|
$dynamic->participants()->attach($user->id, ['role' => 'participant']);
|
||||||
|
|
||||||
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
$response = $this->put(route('dynamics.participant.update', $dynamic->id), [
|
||||||
|
'display_name' => 'Ally',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertRedirect();
|
||||||
|
|
||||||
|
// Check database
|
||||||
|
$this->assertDatabaseHas('participants', [
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'dynamic_id' => $dynamic->id,
|
||||||
|
'display_name' => 'Ally',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Check display name method
|
||||||
|
expect($user->displayNameFor($dynamic))->toBe('Ally');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('display name update requires display_name parameter', function () {
|
||||||
|
$user = User::factory()->create(['name' => 'Alice']);
|
||||||
|
$dynamic = Dynamic::factory()->create();
|
||||||
|
$dynamic->participants()->attach($user->id, ['role' => 'participant']);
|
||||||
|
|
||||||
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
$response = $this->put(route('dynamics.participant.update', $dynamic->id), [
|
||||||
|
'display_name' => '',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertSessionHasErrors(['display_name']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('non participant cannot update display name', function () {
|
||||||
|
$user = User::factory()->create(['name' => 'Alice']);
|
||||||
|
$nonParticipant = User::factory()->create(['name' => 'Bob']);
|
||||||
|
$dynamic = Dynamic::factory()->create();
|
||||||
|
$dynamic->participants()->attach($user->id, ['role' => 'participant']);
|
||||||
|
|
||||||
|
$this->actingAs($nonParticipant);
|
||||||
|
|
||||||
|
$response = $this->put(route('dynamics.participant.update', $dynamic->id), [
|
||||||
|
'display_name' => 'Bobby',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(404);
|
||||||
|
});
|
||||||
@ -16,7 +16,7 @@ use Tests\TestCase;
|
|||||||
|
|
||||||
pest()->extend(TestCase::class)
|
pest()->extend(TestCase::class)
|
||||||
->use(RefreshDatabase::class)
|
->use(RefreshDatabase::class)
|
||||||
->in('Feature');
|
->in('Feature', 'Browser');
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user