Compare commits

..

No commits in common. "b2a7e2557ec8e05cca50effc2ef1c0df25c9b9c1" and "371b75193ae164cdac63db3d699cf26f3dff3534" have entirely different histories.

11 changed files with 24 additions and 210 deletions

View File

@ -11,10 +11,10 @@ class DashboardController extends Controller
public function index(Request $request, ActivityService $activityService) public function index(Request $request, ActivityService $activityService)
{ {
$user = $request->user(); $user = $request->user();
$unreadDynamics = $activityService->getUnreadEntitiesGrouped($user); $unreadEntities = $activityService->getUnreadEntitiesGrouped($user);
return Inertia::render('Dashboard', [ return Inertia::render('Dashboard', [
'unreadDynamics' => $unreadDynamics, 'unreadEntities' => $unreadEntities,
]); ]);
} }
} }

View File

@ -30,7 +30,7 @@ class NewActivityNotification extends Notification
*/ */
public function via(object $notifiable): array public function via(object $notifiable): array
{ {
return ['database', WebPushChannel::class]; return [WebPushChannel::class];
} }
/** /**
@ -46,6 +46,15 @@ class NewActivityNotification extends Notification
->action('View', 'view') ->action('View', 'view')
->data(['url' => $this->activity['url']]); ->data(['url' => $this->activity['url']]);
switch (get_class($this->activity)) {
case Message::class:
/** @var Chat $chat */
$chat = $this->activity->chat;
$result->data(['url' => $chat->subjectUrl]);
break;
}
return $result; return $result;
} }
@ -57,8 +66,7 @@ class NewActivityNotification extends Notification
public function toArray(object $notifiable): array public function toArray(object $notifiable): array
{ {
return [ return [
'content' => $this->activity['content'], //
'url' => $this->activity['url'],
]; ];
} }
} }

View File

@ -1,67 +0,0 @@
<?php
namespace App\Policies;
use App\Models\Chat;
use App\Models\User;
use Illuminate\Auth\Access\Response;
class ChatPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return false;
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Chat $chat): bool
{
$chatable = $chat->chatable;
return $user->can('view', $chatable);
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return false;
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Chat $chat): bool
{
return $this->view($user, $chat);
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Chat $chat): bool
{
return false;
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, Chat $chat): bool
{
return false;
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, Chat $chat): bool
{
return false;
}
}

View File

@ -1,31 +0,0 @@
<?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::create('notifications', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('type');
$table->morphs('notifiable');
$table->text('data');
$table->timestamp('read_at')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('notifications');
}
};

View File

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
beStrictAboutTestsThatDoNotTestAnything="false"
colors="true"
processIsolation="false"
stopOnError="false"
stopOnFailure="false"
cacheDirectory=".phpunit.cache"
backupStaticProperties="false">
<testsuites>
<testsuite name="Browser Test Suite">
<directory suffix="Test.php">./tests/Browser</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@ -104,7 +104,6 @@ function submit() {
id="amount" id="amount"
type="number" type="number"
class="c-add-mutation-form__input" class="c-add-mutation-form__input"
data-test="amount-input"
/> />
<div <div
v-if="form.errors.amount" v-if="form.errors.amount"
@ -123,7 +122,6 @@ function submit() {
id="description" id="description"
rows="4" rows="4"
class="c-add-mutation-form__textarea" class="c-add-mutation-form__textarea"
data-test="description-input"
></textarea> ></textarea>
<div <div
v-if="form.errors.description" v-if="form.errors.description"
@ -173,7 +171,6 @@ function submit() {
type="submit" type="submit"
:disabled="form.processing" :disabled="form.processing"
class="c-add-mutation-form__submit-btn" class="c-add-mutation-form__submit-btn"
data-test="add-mutation-button"
> >
Add Mutation Add Mutation
</button> </button>

View File

@ -14,7 +14,7 @@ defineOptions({
}); });
defineProps<{ defineProps<{
unreadDynamics: Array<{ unreadEntities: Array<{
id: number; id: number;
name: string; name: string;
url: string; url: string;
@ -50,17 +50,15 @@ function formatTime(isoString: string): string {
<div class="c-dashboard__container"> <div class="c-dashboard__container">
<h2 class="c-dashboard__title">Recent Activity</h2> <h2 class="c-dashboard__title">Recent Activity</h2>
<div v-if="unreadDynamics.length > 0" class="c-dashboard__grid"> <div v-if="unreadEntities.length > 0" class="c-dashboard__grid">
<div <div
v-for="dynamic in unreadDynamics" v-for="entity in unreadEntities"
:key="dynamic.id" :key="entity.id"
class="c-dashboard__card" class="c-dashboard__card"
> >
<div class="c-dashboard__card-header"> <div class="c-dashboard__card-header">
<div class="c-dashboard__entity-meta"> <div class="c-dashboard__entity-meta">
<span <span class="c-dashboard__badge-type c-dashboard__badge-type--dynamic">
class="c-dashboard__badge-type c-dashboard__badge-type--dynamic"
>
Dynamic Dynamic
</span> </span>
<span class="c-dashboard__unread-count"> <span class="c-dashboard__unread-count">
@ -104,7 +102,9 @@ function formatTime(isoString: string): string {
v-if="dynamic.new_activities.length > 0" v-if="dynamic.new_activities.length > 0"
class="c-dashboard__divider" class="c-dashboard__divider"
> >
<span class="c-dashboard__divider-text">NEW</span> <span class="c-dashboard__divider-text"
>NEW</span
>
</div> </div>
<!-- New / Unread Activities --> <!-- New / Unread Activities -->

View File

@ -8,5 +8,5 @@ Broadcast::channel('App.Models.User.{id}', function ($user, $id) {
}); });
Broadcast::channel('chats.{chat}', function ($user, Chat $chat) { Broadcast::channel('chats.{chat}', function ($user, Chat $chat) {
return $user?->can('view', $chat); return $user->can('view', $chat->chatable);
}); });

View File

@ -1,78 +0,0 @@
<?php
namespace Tests\Browser;
use App\Models\Dynamic;
use App\Models\Ledger;
use App\Models\Mutation;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
class NotificationTest extends DuskTestCase
{
use DatabaseMigrations;
protected function setUp(): void
{
parent::setUp();
$this->artisan('migrate:fresh', ['--seed' => true]);
}
/**
* A basic browser test example.
*/
public function test_it_shows_notifications_on_mutation_activity(): void
{
$owner = User::factory()->create();
$participant = User::factory()->create();
$dynamic = Dynamic::factory()->create();
$dynamic->chat()->create();
$dynamic->participants()->attach($owner->id, ['role' => 'owner']);
$dynamic->participants()->attach($participant->id, ['role' => 'participant']);
$ledger = Ledger::factory()->create(['dynamic_id' => $dynamic->id]);
$this->browse(function (Browser $ownerBrowser, Browser $participantBrowser) use ($owner, $participant, $dynamic, $ledger) {
$ownerBrowser->loginAs($owner)
->visit(route('dynamics.ledgers.show', [$dynamic, $ledger]))
->waitForText($ledger->name)
->assertSee($ledger->name)
->script([
"window.notifications = [];",
"window.Notification = function(title, options) { window.notifications.push({title, options}); };",
]);
$participantBrowser->loginAs($participant)
->visit(route('dynamics.ledgers.show', [$dynamic, $ledger]))
->waitForText('Add Mutation')
->type('[data-test="description-input"]', 'A new task suggestion')
->type('[data-test="amount-input"]', 10)
->press('[data-test="add-mutation-button"]')
->waitForText('A new task suggestion');
$ownerBrowser->pause(1000);
$lastNotification = $ownerBrowser->script("return window.notifications.pop();")[0] ?? null;
$this->assertNotNull($lastNotification, "Owner did not receive the 'new suggestion' notification.");
$this->assertEquals('New Activity', $lastNotification['title']);
$this->assertStringContainsString('suggested a new entry', $lastNotification['options']['body']);
$mutation = Mutation::where('description', 'A new task suggestion')->first();
$ownerBrowser->press("#mutation-{$mutation->id}-approve")
->waitForText('APPROVED');
$participantBrowser->script([
"window.notifications = [];",
"window.Notification = function(title, options) { window.notifications.push({title, options}); };",
]);
$ownerBrowser->pause(1000);
$participantBrowser->pause(1000);
$lastNotification = $participantBrowser->script("return window.notifications.pop();")[0] ?? null;
$this->assertNotNull($lastNotification, "Participant did not receive the 'approved' notification.");
$this->assertEquals('New Activity', $lastNotification['title']);
$this->assertStringContainsString('APPROVED the suggestion', $lastNotification['options']['body']);
});
}
}

View File

@ -31,7 +31,7 @@ abstract class DuskTestCase extends BaseTestCase
$options = (new ChromeOptions)->addArguments((new Collection([ $options = (new ChromeOptions)->addArguments((new Collection([
$this->shouldStartMaximized() ? '--start-maximized' : '--window-size=1920,1080', $this->shouldStartMaximized() ? '--start-maximized' : '--window-size=1920,1080',
'--disable-gpu', '--disable-gpu',
// '--headless=new', '--headless=new',
'--no-sandbox', '--no-sandbox',
'--disable-dev-shm-usage', '--disable-dev-shm-usage',
]))->unless(static::runningInSail(), function (Collection $arguments) { ]))->unless(static::runningInSail(), function (Collection $arguments) {