Compare commits
2 Commits
371b75193a
...
b2a7e2557e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2a7e2557e | ||
|
|
ccb3598da0 |
@ -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();
|
||||||
$unreadEntities = $activityService->getUnreadEntitiesGrouped($user);
|
$unreadDynamics = $activityService->getUnreadEntitiesGrouped($user);
|
||||||
|
|
||||||
return Inertia::render('Dashboard', [
|
return Inertia::render('Dashboard', [
|
||||||
'unreadEntities' => $unreadEntities,
|
'unreadDynamics' => $unreadDynamics,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,7 +30,7 @@ class NewActivityNotification extends Notification
|
|||||||
*/
|
*/
|
||||||
public function via(object $notifiable): array
|
public function via(object $notifiable): array
|
||||||
{
|
{
|
||||||
return [WebPushChannel::class];
|
return ['database', WebPushChannel::class];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -46,15 +46,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,7 +57,8 @@ 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'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
67
app/Policies/ChatPolicy.php
Normal file
67
app/Policies/ChatPolicy.php
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
<?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');
|
||||||
|
}
|
||||||
|
};
|
||||||
15
phpunit.dusk.xml
Normal file
15
phpunit.dusk.xml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?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>
|
||||||
@ -23,7 +23,7 @@
|
|||||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||||
<env name="BROADCAST_CONNECTION" value="null"/>
|
<env name="BROADCAST_CONNECTION" value="null"/>
|
||||||
<env name="CACHE_STORE" value="array"/>
|
<env name="CACHE_STORE" value="array"/>
|
||||||
<env name="DB_CONNECTION" value="sqlite"/>
|
<env name="DB_CONNECTION" value="sqlite" />
|
||||||
<env name="DB_DATABASE" value=":memory:"/>
|
<env name="DB_DATABASE" value=":memory:"/>
|
||||||
<env name="DB_URL" value=""/>
|
<env name="DB_URL" value=""/>
|
||||||
<env name="MAIL_MAILER" value="array"/>
|
<env name="MAIL_MAILER" value="array"/>
|
||||||
|
|||||||
@ -104,6 +104,7 @@ 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"
|
||||||
@ -122,6 +123,7 @@ 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"
|
||||||
@ -171,6 +173,7 @@ 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>
|
||||||
|
|||||||
@ -14,7 +14,7 @@ defineOptions({
|
|||||||
});
|
});
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
unreadEntities: Array<{
|
unreadDynamics: Array<{
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
@ -50,15 +50,17 @@ 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="unreadEntities.length > 0" class="c-dashboard__grid">
|
<div v-if="unreadDynamics.length > 0" class="c-dashboard__grid">
|
||||||
<div
|
<div
|
||||||
v-for="entity in unreadEntities"
|
v-for="dynamic in unreadDynamics"
|
||||||
:key="entity.id"
|
:key="dynamic.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 class="c-dashboard__badge-type c-dashboard__badge-type--dynamic">
|
<span
|
||||||
|
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">
|
||||||
@ -102,9 +104,7 @@ 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"
|
<span class="c-dashboard__divider-text">NEW</span>
|
||||||
>NEW</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- New / Unread Activities -->
|
<!-- New / Unread Activities -->
|
||||||
|
|||||||
@ -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->chatable);
|
return $user?->can('view', $chat);
|
||||||
});
|
});
|
||||||
|
|||||||
78
tests/Browser/NotificationTest.php
Normal file
78
tests/Browser/NotificationTest.php
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<?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']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user