Added telescope, basic project endpoint, basic filament resources, GitHub actions

This commit is contained in:
Constantin Graf
2024-01-28 18:57:28 +01:00
parent e4f0eac834
commit e60e502612
80 changed files with 2563 additions and 135 deletions

View File

@@ -9,6 +9,11 @@ LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug LOG_LEVEL=debug
DB_CONNECTION=sqlite DB_CONNECTION=sqlite
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=root
BROADCAST_DRIVER=log BROADCAST_DRIVER=log
CACHE_DRIVER=file CACHE_DRIVER=file

View File

@@ -4,6 +4,8 @@ APP_KEY=base64:UNQNf1SXeASNkWux01Rj8EnHYx8FO0kAxWNDwktclkk=
APP_DEBUG=true APP_DEBUG=true
APP_URL=http://localhost APP_URL=http://localhost
SUPER_ADMINS=admin@example.com
LOG_CHANNEL=stack LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug LOG_LEVEL=debug

View File

@@ -8,7 +8,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - name: "Checkout code"
uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2 - uses: shivammathur/setup-php@v2
with: with:
php-version: '8.3.1' php-version: '8.3.1'

View File

@@ -8,7 +8,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - name: "Checkout code"
uses: actions/checkout@v4
- name: Use Node.js - name: Use Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:

View File

@@ -8,7 +8,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - name: "Checkout code"
uses: actions/checkout@v4
- name: Use Node.js - name: Use Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:

24
.github/workflows/phpstan.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: Static code analysis (PHPStan)
on: push
jobs:
phpstan:
runs-on: ubuntu-latest
steps:
- name: "Checkout code"
uses: actions/checkout@v4
- name: "Setup PHP"
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv
coverage: none
- name: "Run composer install"
run: composer install -n --prefer-dist
- name: "Run PHPStan"
run: composer analyse

54
.github/workflows/phpunit.yml vendored Normal file
View File

@@ -0,0 +1,54 @@
name: PHPUnit Tests
on: push
jobs:
phpunit:
runs-on: ubuntu-latest
services:
pgsql:
image: postgres:15
env:
PGPASSWORD: 'root'
POSTGRES_DB: 'laravel'
POSTGRES_USER: 'root'
POSTGRES_PASSWORD: 'root'
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: "Checkout code"
uses: actions/checkout@v4
- name: "Setup PHP"
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv
coverage: none
- name: "Run composer install"
run: composer install -n --prefer-dist
- uses: actions/setup-node@v3
with:
node-version: '20.x'
- name: Install dependencies
run: npm ci
- name: Build Frontend
run: npm run build
- name: "Prepare Laravel Application"
run: |
cp .env.ci .env
php artisan key:generate
php artisan passport:keys
- name: "Run PHPUnit"
run: composer test

View File

@@ -4,8 +4,10 @@ jobs:
pint: pint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v1 - name: "Checkout code"
- name: "laravel-pint" uses: actions/checkout@v4
- name: "Check code style"
uses: aglipanci/laravel-pint-action@2.0.0 uses: aglipanci/laravel-pint-action@2.0.0
with: with:
configPath: "pint.json" configPath: "pint.json"

View File

@@ -13,10 +13,12 @@ jobs:
image: 'axllent/mailpit:latest' image: 'axllent/mailpit:latest'
steps: steps:
- uses: actions/checkout@v3 - name: "Checkout code"
uses: actions/checkout@v4
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: '20.x'
- name: Setup PHP - name: Setup PHP
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2

1
.gitignore vendored
View File

@@ -5,6 +5,7 @@
/public/storage /public/storage
/public/css /public/css
/public/js /public/js
/public/vendor
/lang/vendor /lang/vendor
/storage/*.key /storage/*.key
/vendor /vendor

View File

@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources;
use App\Filament\Resources\ClientResource\Pages;
use App\Models\Client;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
class ClientResource extends Resource
{
protected static ?string $model = Client::class;
protected static ?string $navigationIcon = 'heroicon-o-briefcase';
protected static ?string $navigationGroup = 'Timetracking';
protected static ?int $navigationSort = 4;
public static function form(Form $form): Form
{
return $form
->schema([
//
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')
->label('Name')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('organization.name')
->sortable()
->label('Organization'),
Tables\Columns\TextColumn::make('created_at')
->label('Created at')
->sortable(),
Tables\Columns\TextColumn::make('updated_at')
->label('Updated at')
->sortable(),
])
->defaultSort('created_at', 'desc')
->filters([
SelectFilter::make('organization')
->relationship('organization', 'name')
->searchable(),
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListClients::route('/'),
'create' => Pages\CreateClient::route('/create'),
'edit' => Pages\EditClient::route('/{record}/edit'),
];
}
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\ClientResource\Pages;
use App\Filament\Resources\ClientResource;
use Filament\Resources\Pages\CreateRecord;
class CreateClient extends CreateRecord
{
protected static string $resource = ClientResource::class;
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\ClientResource\Pages;
use App\Filament\Resources\ClientResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditClient extends EditRecord
{
protected static string $resource = ClientResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\ClientResource\Pages;
use App\Filament\Resources\ClientResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListClients extends ListRecords
{
protected static string $resource = ClientResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources;
use App\Filament\Resources\OrganizationResource\Pages;
use App\Models\Organization;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
class OrganizationResource extends Resource
{
protected static ?string $model = Organization::class;
protected static ?string $navigationIcon = 'heroicon-o-building-office-2';
protected static ?string $navigationGroup = 'Users';
protected static ?int $navigationSort = 7;
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('name')
->label('Name')
->required()
->maxLength(255),
Forms\Components\Toggle::make('Is personal?')
->label('Is personal?')
->required(),
Forms\Components\Select::make('owner_id')
->relationship(name: 'owner', titleAttribute: 'email')
->searchable(['name', 'email'])
->required(),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')
->searchable()
->sortable(),
Tables\Columns\ToggleColumn::make('is_personal')
->label('Is personal?')
->sortable(),
Tables\Columns\TextColumn::make('owner.email')
->sortable(),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListOrganizations::route('/'),
'create' => Pages\CreateOrganization::route('/create'),
'edit' => Pages\EditOrganization::route('/{record}/edit'),
];
}
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\OrganizationResource\Pages;
use App\Filament\Resources\OrganizationResource;
use Filament\Resources\Pages\CreateRecord;
class CreateOrganization extends CreateRecord
{
protected static string $resource = OrganizationResource::class;
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\OrganizationResource\Pages;
use App\Filament\Resources\OrganizationResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditOrganization extends EditRecord
{
protected static string $resource = OrganizationResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\OrganizationResource\Pages;
use App\Filament\Resources\OrganizationResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListOrganizations extends ListRecords
{
protected static string $resource = OrganizationResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources;
use App\Filament\Resources\ProjectResource\Pages;
use App\Models\Project;
use Filament\Forms;
use Filament\Forms\Components\ColorPicker;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Columns\ColorColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
class ProjectResource extends Resource
{
protected static ?string $model = Project::class;
protected static ?string $navigationIcon = 'heroicon-o-folder';
protected static ?string $navigationGroup = 'Timetracking';
protected static ?int $navigationSort = 2;
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('name')
->label('Name')
->required()
->maxLength(255),
ColorPicker::make('color')
->label('Color')
->required(),
Forms\Components\Select::make('organization_id')
->relationship(name: 'organization', titleAttribute: 'name')
->searchable(['name'])
->required(),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
ColorColumn::make('color'),
TextColumn::make('name')
->searchable()
->sortable(),
TextColumn::make('organization.name')
->sortable(),
TextColumn::make('created_at')
->sortable(),
TextColumn::make('updated_at')
->sortable(),
])
->filters([
SelectFilter::make('organization')
->relationship('organization', 'name')
->searchable(),
])
->defaultSort('created_at', 'desc')
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListProjects::route('/'),
'create' => Pages\CreateProject::route('/create'),
'edit' => Pages\EditProject::route('/{record}/edit'),
];
}
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\ProjectResource\Pages;
use App\Filament\Resources\ProjectResource;
use Filament\Resources\Pages\CreateRecord;
class CreateProject extends CreateRecord
{
protected static string $resource = ProjectResource::class;
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\ProjectResource\Pages;
use App\Filament\Resources\ProjectResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditProject extends EditRecord
{
protected static string $resource = ProjectResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\ProjectResource\Pages;
use App\Filament\Resources\ProjectResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListProjects extends ListRecords
{
protected static string $resource = ProjectResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources;
use App\Filament\Resources\TagResource\Pages;
use App\Models\Tag;
use Filament\Forms;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
class TagResource extends Resource
{
protected static ?string $model = Tag::class;
protected static ?string $navigationIcon = 'heroicon-o-tag';
protected static ?string $navigationGroup = 'Timetracking';
protected static ?int $navigationSort = 5;
public static function form(Form $form): Form
{
return $form
->schema([
TextInput::make('name')
->label('Name')
->required(),
Forms\Components\Select::make('organization_id')
->relationship(name: 'organization', titleAttribute: 'name')
->label('Organization')
->searchable(['name'])
->required(),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')
->label('Name')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('organization.name')
->sortable()
->label('Organization'),
Tables\Columns\TextColumn::make('created_at')
->label('Created at')
->sortable(),
Tables\Columns\TextColumn::make('updated_at')
->label('Updated at')
->sortable(),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListTags::route('/'),
'create' => Pages\CreateTag::route('/create'),
'edit' => Pages\EditTag::route('/{record}/edit'),
];
}
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\TagResource\Pages;
use App\Filament\Resources\TagResource;
use Filament\Resources\Pages\CreateRecord;
class CreateTag extends CreateRecord
{
protected static string $resource = TagResource::class;
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\TagResource\Pages;
use App\Filament\Resources\TagResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditTag extends EditRecord
{
protected static string $resource = TagResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\TagResource\Pages;
use App\Filament\Resources\TagResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListTags extends ListRecords
{
protected static string $resource = TagResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources;
use App\Filament\Resources\TaskResource\Pages;
use App\Models\Task;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
class TaskResource extends Resource
{
protected static ?string $model = Task::class;
protected static ?string $navigationIcon = 'heroicon-o-list-bullet';
protected static ?string $navigationGroup = 'Timetracking';
protected static ?int $navigationSort = 3;
public static function form(Form $form): Form
{
return $form
->schema([
//
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('project.name')
->sortable(),
Tables\Columns\TextColumn::make('organization.name')
->sortable(),
Tables\Columns\TextColumn::make('created_at')
->sortable(),
Tables\Columns\TextColumn::make('updated_at')
->sortable(),
])
->filters([
//
])
->defaultSort('created_at', 'desc')
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListTasks::route('/'),
'create' => Pages\CreateTask::route('/create'),
'edit' => Pages\EditTask::route('/{record}/edit'),
];
}
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\TaskResource\Pages;
use App\Filament\Resources\TaskResource;
use Filament\Resources\Pages\CreateRecord;
class CreateTask extends CreateRecord
{
protected static string $resource = TaskResource::class;
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\TaskResource\Pages;
use App\Filament\Resources\TaskResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditTask extends EditRecord
{
protected static string $resource = TaskResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\TaskResource\Pages;
use App\Filament\Resources\TaskResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListTasks extends ListRecords
{
protected static string $resource = TaskResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources;
use App\Filament\Resources\TimeEntryResource\Pages;
use App\Models\TimeEntry;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class TimeEntryResource extends Resource
{
protected static ?string $model = TimeEntry::class;
protected static ?string $navigationIcon = 'heroicon-o-clock';
protected static ?string $navigationGroup = 'Timetracking';
protected static ?int $navigationSort = 1;
public static function form(Form $form): Form
{
return $form
->schema([
TextInput::make('id')
->label('ID')
->readOnly()
->disabled(),
TextInput::make('description')
->label('Description')
->required()
->maxLength(255),
Toggle::make('billable')
->label('Is Billable?')
->required(),
DateTimePicker::make('start')
->label('Start')
->required(),
DateTimePicker::make('end')
->label('End')
->nullable()
->rules([
'after:start',
]),
Select::make('user_id')
->relationship(name: 'user', titleAttribute: 'email')
->searchable(['name', 'email'])
->required(),
Select::make('project_id')
->relationship(name: 'project', titleAttribute: 'name')
->searchable(['name'])
->nullable(),
// TODO
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('description')
->label('Description'),
TextColumn::make('user.email')
->label('User'),
TextColumn::make('project.name')
->label('Project'),
TextColumn::make('task.name')
->label('Task'),
TextColumn::make('time')
->getStateUsing(function (TimeEntry $record): string {
return ($record->getDuration()?->cascade()?->forHumans() ?? '-').' '.
' ('.$record->start->toDateTimeString('minute').' - '.
($record->end?->toDateTimeString('minute') ?? '...').')';
})
->label('Time'),
Tables\Columns\TextColumn::make('organization.name')
->sortable(),
Tables\Columns\TextColumn::make('created_at')
->sortable(),
Tables\Columns\TextColumn::make('updated_at')
->sortable(),
])
->filters([
//
])
->defaultSort('created_at', 'desc')
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListTimeEntries::route('/'),
'create' => Pages\CreateTimeEntry::route('/create'),
'edit' => Pages\EditTimeEntry::route('/{record}/edit'),
];
}
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\TimeEntryResource\Pages;
use App\Filament\Resources\TimeEntryResource;
use Filament\Resources\Pages\CreateRecord;
class CreateTimeEntry extends CreateRecord
{
protected static string $resource = TimeEntryResource::class;
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\TimeEntryResource\Pages;
use App\Filament\Resources\TimeEntryResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditTimeEntry extends EditRecord
{
protected static string $resource = TimeEntryResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\TimeEntryResource\Pages;
use App\Filament\Resources\TimeEntryResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListTimeEntries extends ListRecords
{
protected static string $resource = TimeEntryResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources;
use App\Filament\Resources\UserResource\Pages;
use App\Models\User;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
class UserResource extends Resource
{
protected static ?string $model = User::class;
protected static ?string $navigationIcon = 'heroicon-o-user';
protected static ?string $navigationGroup = 'Users';
protected static ?int $navigationSort = 6;
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('id')
->label('ID')
->disabled()
->visibleOn(['update', 'show'])
->readOnly()
->maxLength(255),
Forms\Components\TextInput::make('name')
->label('Name')
->required()
->maxLength(255),
Forms\Components\TextInput::make('email')
->label('Email')
->required()
->maxLength(255),
Forms\Components\TextInput::make('password')
->label('Password')
->required()
->password()
->maxLength(255),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('email')
->icon('heroicon-m-envelope')
->searchable()
->sortable(),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListUsers::route('/'),
'create' => Pages\CreateUser::route('/create'),
'edit' => Pages\EditUser::route('/{record}/edit'),
];
}
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource;
use Filament\Resources\Pages\CreateRecord;
class CreateUser extends CreateRecord
{
protected static string $resource = UserResource::class;
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditUser extends EditRecord
{
protected static string $resource = UserResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListUsers extends ListRecords
{
protected static string $resource = UserResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api\V1;
use App\Models\Organization;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Support\Facades\Auth;
class Controller extends \App\Http\Controllers\Controller
{
/**
* @throws AuthorizationException
*/
protected function checkPermission(Organization $organization, string $permission): void
{
if (! Auth::user()->hasTeamPermission($organization, $permission)) {
throw new AuthorizationException();
}
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api\V1;
use App\Http\Requests\V1\Project\ProjectStoreRequest;
use App\Http\Requests\V1\Project\ProjectUpdateRequest;
use App\Http\Resources\V1\Project\ProjectCollection;
use App\Http\Resources\V1\Project\ProjectResource;
use App\Models\Organization;
use App\Models\Project;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\Resources\Json\JsonResource;
class ProjectController extends Controller
{
/**
* @throws AuthorizationException
*/
public function index(Organization $organization): JsonResource
{
$this->checkPermission($organization, 'projects:view');
$projects = Project::query()
->whereBelongsTo($organization, 'organization')
->get();
return new ProjectCollection($projects);
}
/**
* @throws AuthorizationException
*/
public function show(Organization $organization, Project $project): JsonResource
{
$this->checkPermission($organization, 'projects:view');
$project->load('organization');
return new ProjectResource($project);
}
/**
* @throws AuthorizationException
*/
public function store(Organization $organization, ProjectStoreRequest $request): JsonResource
{
$this->checkPermission($organization, 'projects:create');
$project = new Project();
$project->name = $request->input('name');
$project->color = $request->input('color');
$project->organization()->associate($organization);
$project->save();
return new ProjectResource($project);
}
/**
* @throws AuthorizationException
*/
public function update(Organization $organization, Project $project, ProjectUpdateRequest $request): JsonResource
{
$this->checkPermission($organization, 'projects:update');
$project->name = $request->input('name');
$project->color = $request->input('color');
$project->save();
return new ProjectResource($project);
}
/**
* @throws AuthorizationException
*/
public function destroy(Organization $organization, Project $project): JsonResource
{
$this->checkPermission($organization, 'projects:delete');
$project->delete();
return new ProjectResource($project);
}
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Http; namespace App\Http;
use App\Http\Middleware\ForceJsonResponse;
use Illuminate\Foundation\Http\Kernel as HttpKernel; use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel class Kernel extends HttpKernel
@@ -40,12 +41,13 @@ class Kernel extends HttpKernel
\Illuminate\Routing\Middleware\SubstituteBindings::class, \Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\HandleInertiaRequests::class, \App\Http\Middleware\HandleInertiaRequests::class,
\Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class, \Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class,
\Laravel\Passport\Http\Middleware\CreateFreshApiToken::class,
], ],
'api' => [ 'api' => [
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api', \Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class, \Illuminate\Routing\Middleware\SubstituteBindings::class,
ForceJsonResponse::class,
], ],
]; ];

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class ForceJsonResponse
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next, string ...$guards): Response
{
$request->headers->set('Accept', 'application/json');
return $next($request);
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\V1\Project;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class ProjectStoreRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => [
'required',
'string',
'max:255',
],
'color' => [
'required',
'string',
'max:255',
],
];
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\V1\Project;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class ProjectUpdateRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => [
'required',
'string',
'max:255',
],
'color' => [
'required',
'string',
'max:255',
],
];
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\V1\Project;
use Illuminate\Http\Resources\Json\ResourceCollection;
class ProjectCollection extends ResourceCollection
{
/**
* The resource that this resource collects.
*
* @var string
*/
public $collects = ProjectResource::class;
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\V1\Project;
use App\Models\Project;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/**
* @property Project $resource
*/
class ProjectResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, string|boolean|integer>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->resource->id,
'name' => $this->resource->name,
];
}
}

View File

@@ -15,6 +15,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
/** /**
* @property string $id * @property string $id
* @property string $name * @property string $name
* @property string $color
* @property string $organization_id * @property string $organization_id
* @property string $client_id * @property string $client_id
* @property-read Organization $organization * @property-read Organization $organization
@@ -35,6 +36,7 @@ class Project extends Model
*/ */
protected $casts = [ protected $casts = [
'name' => 'string', 'name' => 'string',
'color' => 'string',
]; ];
/** /**

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Models; namespace App\Models;
use Carbon\CarbonInterval;
use Database\Factories\TimeEntryFactory; use Database\Factories\TimeEntryFactory;
use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -43,6 +44,11 @@ class TimeEntry extends Model
'tags' => 'array', 'tags' => 'array',
]; ];
public function getDuration(): ?CarbonInterval
{
return $this->end === null ? null : $this->start->diffAsCarbonInterval($this->end);
}
/** /**
* @return BelongsTo<User, TimeEntry> * @return BelongsTo<User, TimeEntry>
*/ */

View File

@@ -20,6 +20,7 @@ use Laravel\Passport\HasApiTokens;
/** /**
* @property string $id * @property string $id
* @property string $name * @property string $name
* @property string $email
* *
* @method HasMany<Organization> ownedTeams() * @method HasMany<Organization> ownedTeams()
* @method static UserFactory factory() * @method static UserFactory factory()
@@ -64,6 +65,7 @@ class User extends Authenticatable
*/ */
protected $casts = [ protected $casts = [
'email_verified_at' => 'datetime', 'email_verified_at' => 'datetime',
'is_admin' => 'boolean',
]; ];
/** /**
@@ -77,8 +79,7 @@ class User extends Authenticatable
public function canAccessPanel(Panel $panel): bool public function canAccessPanel(Panel $panel): bool
{ {
// TODO: Implement canAccessPanel() method. return in_array($this->email, config('auth.super_admins', []), true);
return false;
} }
/** /**

View File

@@ -6,6 +6,7 @@ namespace App\Policies;
use App\Models\Organization; use App\Models\Organization;
use App\Models\User; use App\Models\User;
use Filament\Facades\Filament;
use Illuminate\Auth\Access\HandlesAuthorization; use Illuminate\Auth\Access\HandlesAuthorization;
class OrganizationPolicy class OrganizationPolicy
@@ -17,7 +18,11 @@ class OrganizationPolicy
*/ */
public function viewAny(User $user): bool public function viewAny(User $user): bool
{ {
return true; if (Filament::isServing()) {
return true;
}
return false;
} }
/** /**
@@ -25,6 +30,10 @@ class OrganizationPolicy
*/ */
public function view(User $user, Organization $organization): bool public function view(User $user, Organization $organization): bool
{ {
if (Filament::isServing()) {
return true;
}
return $user->belongsToTeam($organization); return $user->belongsToTeam($organization);
} }
@@ -33,6 +42,10 @@ class OrganizationPolicy
*/ */
public function create(User $user): bool public function create(User $user): bool
{ {
if (Filament::isServing()) {
return true;
}
return true; return true;
} }
@@ -41,6 +54,10 @@ class OrganizationPolicy
*/ */
public function update(User $user, Organization $organization): bool public function update(User $user, Organization $organization): bool
{ {
if (Filament::isServing()) {
return true;
}
return $user->ownsTeam($organization); return $user->ownsTeam($organization);
} }
@@ -49,6 +66,10 @@ class OrganizationPolicy
*/ */
public function addTeamMember(User $user, Organization $organization): bool public function addTeamMember(User $user, Organization $organization): bool
{ {
if (Filament::isServing()) {
return true;
}
return $user->ownsTeam($organization); return $user->ownsTeam($organization);
} }
@@ -57,6 +78,10 @@ class OrganizationPolicy
*/ */
public function updateTeamMember(User $user, Organization $organization): bool public function updateTeamMember(User $user, Organization $organization): bool
{ {
if (Filament::isServing()) {
return true;
}
return $user->ownsTeam($organization); return $user->ownsTeam($organization);
} }
@@ -65,6 +90,10 @@ class OrganizationPolicy
*/ */
public function removeTeamMember(User $user, Organization $organization): bool public function removeTeamMember(User $user, Organization $organization): bool
{ {
if (Filament::isServing()) {
return true;
}
return $user->ownsTeam($organization); return $user->ownsTeam($organization);
} }
@@ -73,6 +102,10 @@ class OrganizationPolicy
*/ */
public function delete(User $user, Organization $organization): bool public function delete(User $user, Organization $organization): bool
{ {
if (Filament::isServing()) {
return true;
}
return $user->ownsTeam($organization); return $user->ownsTeam($organization);
} }
} }

View File

@@ -4,10 +4,16 @@ declare(strict_types=1);
namespace App\Providers; namespace App\Providers;
use App\Models\Client;
use App\Models\Membership; use App\Models\Membership;
use App\Models\Organization; use App\Models\Organization;
use App\Models\OrganizationInvitation; use App\Models\OrganizationInvitation;
use App\Models\Project;
use App\Models\Tag;
use App\Models\Task;
use App\Models\TimeEntry;
use App\Models\User; use App\Models\User;
use Filament\Forms\Components\Section;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
@@ -27,13 +33,27 @@ class AppServiceProvider extends ServiceProvider
*/ */
public function boot(): void public function boot(): void
{ {
if ($this->app->environment('local')) {
$this->app->register(\Laravel\Telescope\TelescopeServiceProvider::class);
$this->app->register(TelescopeServiceProvider::class);
}
Model::preventLazyLoading(! $this->app->isProduction()); Model::preventLazyLoading(! $this->app->isProduction());
Model::preventSilentlyDiscardingAttributes(! $this->app->isProduction()); Model::preventSilentlyDiscardingAttributes(! $this->app->isProduction());
Relation::enforceMorphMap([ Relation::enforceMorphMap([
'membership' => Membership::class, 'membership' => Membership::class,
'team' => Organization::class, 'organization' => Organization::class,
'team_invitation' => OrganizationInvitation::class, 'organization-invitation' => OrganizationInvitation::class,
'user' => User::class, 'user' => User::class,
'time-entry' => TimeEntry::class,
'project' => Project::class,
'task' => Task::class,
'client' => Client::class,
'tag' => Tag::class,
]); ]);
Model::unguard();
Section::configureUsing(function (Section $section): void {
$section->columns(1);
}, null, true);
} }
} }

View File

@@ -7,6 +7,7 @@ namespace App\Providers\Filament;
use Filament\Http\Middleware\Authenticate; use Filament\Http\Middleware\Authenticate;
use Filament\Http\Middleware\DisableBladeIconComponents; use Filament\Http\Middleware\DisableBladeIconComponents;
use Filament\Http\Middleware\DispatchServingFilamentEvent; use Filament\Http\Middleware\DispatchServingFilamentEvent;
use Filament\Navigation\NavigationGroup;
use Filament\Pages; use Filament\Pages;
use Filament\Panel; use Filament\Panel;
use Filament\PanelProvider; use Filament\PanelProvider;
@@ -18,7 +19,9 @@ use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
use Illuminate\Routing\Middleware\SubstituteBindings; use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Session\Middleware\AuthenticateSession; use Illuminate\Session\Middleware\AuthenticateSession;
use Illuminate\Session\Middleware\StartSession; use Illuminate\Session\Middleware\StartSession;
use Illuminate\Support\Facades\App;
use Illuminate\View\Middleware\ShareErrorsFromSession; use Illuminate\View\Middleware\ShareErrorsFromSession;
use pxlrbt\FilamentEnvironmentIndicator\EnvironmentIndicatorPlugin;
class AdminPanelProvider extends PanelProvider class AdminPanelProvider extends PanelProvider
{ {
@@ -40,6 +43,21 @@ class AdminPanelProvider extends PanelProvider
->widgets([ ->widgets([
Widgets\AccountWidget::class, Widgets\AccountWidget::class,
]) ])
->plugins([
EnvironmentIndicatorPlugin::make()
->color(fn () => match (App::environment()) {
'production' => null,
'staging' => Color::Orange,
default => Color::Blue,
}),
])
->navigationGroups([
NavigationGroup::make()
->label('Timetracking'),
NavigationGroup::make()
->label('Users')
->collapsed(),
])
->middleware([ ->middleware([
EncryptCookies::class, EncryptCookies::class,
AddQueuedCookiesToResponse::class, AddQueuedCookiesToResponse::class,

View File

@@ -49,19 +49,27 @@ class JetstreamServiceProvider extends ServiceProvider
*/ */
protected function configurePermissions(): void protected function configurePermissions(): void
{ {
Jetstream::defaultApiTokenPermissions(['read']); Jetstream::defaultApiTokenPermissions([]);
Jetstream::role('admin', 'Administrator', [ Jetstream::role('admin', 'Administrator', [
'create', 'projects:view',
'read', 'projects:create',
'update', 'projects:update',
'delete', 'projects:delete',
])->description('Administrator users can perform any action.'); ])->description('Administrator users can perform any action.');
Jetstream::role('editor', 'Editor', [ Jetstream::role('manager', 'Manager', [
'read', 'projects:view',
'create', 'projects:create',
'update', 'projects:update',
'projects:delete',
])->description('Editor users have the ability to read, create, and update.');
Jetstream::role('employee', 'Employee', [
'projects:view',
'projects:create',
'projects:update',
'projects:delete',
])->description('Editor users have the ability to read, create, and update.'); ])->description('Editor users have the ability to read, create, and update.');
} }
} }

View File

@@ -33,6 +33,7 @@ class RouteServiceProvider extends ServiceProvider
$this->routes(function () { $this->routes(function () {
Route::middleware('api') Route::middleware('api')
->prefix('api') ->prefix('api')
->name('api.')
->group(base_path('routes/api.php')); ->group(base_path('routes/api.php'));
Route::middleware('web') Route::middleware('web')

View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace App\Providers;
use App\Models\User;
use Illuminate\Support\Facades\Gate;
use Laravel\Telescope\IncomingEntry;
use Laravel\Telescope\Telescope;
use Laravel\Telescope\TelescopeApplicationServiceProvider;
class TelescopeServiceProvider extends TelescopeApplicationServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
Telescope::night();
$this->hideSensitiveRequestDetails();
Telescope::filter(function (IncomingEntry $entry) {
if ($this->app->environment('local')) {
return true;
}
return $entry->isReportableException() ||
$entry->isFailedRequest() ||
$entry->isFailedJob() ||
$entry->isScheduledTask() ||
$entry->hasMonitoredTag();
});
}
/**
* Prevent sensitive request details from being logged by Telescope.
*/
protected function hideSensitiveRequestDetails(): void
{
if ($this->app->environment('local')) {
return;
}
Telescope::hideRequestParameters(['_token']);
Telescope::hideRequestHeaders([
'cookie',
'x-csrf-token',
'x-xsrf-token',
]);
}
/**
* Register the Telescope gate.
*
* This gate determines who can access Telescope in non-local environments.
*/
protected function gate(): void
{
Gate::define('viewTelescope', function (User $user): bool {
// Note: Telescope is only available in local environments, so this should not be relevant.
return false;
});
}
}

View File

@@ -14,6 +14,7 @@
"laravel/jetstream": "^4.2", "laravel/jetstream": "^4.2",
"laravel/passport": "*", "laravel/passport": "*",
"laravel/tinker": "^2.8", "laravel/tinker": "^2.8",
"pxlrbt/filament-environment-indicator": "^2.0",
"tightenco/ziggy": "^1.0", "tightenco/ziggy": "^1.0",
"tpetry/laravel-postgresql-enhanced": "^0.33.0" "tpetry/laravel-postgresql-enhanced": "^0.33.0"
}, },
@@ -24,6 +25,7 @@
"larastan/larastan": "^2.0", "larastan/larastan": "^2.0",
"laravel/pint": "^1.0", "laravel/pint": "^1.0",
"laravel/sail": "^1.18", "laravel/sail": "^1.18",
"laravel/telescope": "^4.17",
"mockery/mockery": "^1.4.4", "mockery/mockery": "^1.4.4",
"nunomaduro/collision": "^7.0", "nunomaduro/collision": "^7.0",
"phpunit/phpunit": "^10.1", "phpunit/phpunit": "^10.1",
@@ -77,7 +79,9 @@
}, },
"extra": { "extra": {
"laravel": { "laravel": {
"dont-discover": [] "dont-discover": [
"laravel/telescope"
]
} }
}, },
"config": { "config": {

328
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "ad3e9deab517156569b424191cdb7871", "content-hash": "53e9ad1ac7367efb5c1d7c971499447b",
"packages": [ "packages": [
{ {
"name": "anourvalar/eloquent-serialize", "name": "anourvalar/eloquent-serialize",
@@ -792,16 +792,16 @@
}, },
{ {
"name": "doctrine/dbal", "name": "doctrine/dbal",
"version": "3.7.3", "version": "3.8.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/doctrine/dbal.git", "url": "https://github.com/doctrine/dbal.git",
"reference": "ce594cbc39a4866c544f1a970d285ff0548221ad" "reference": "d244f2e6e6bf32bff5174e6729b57214923ecec9"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/ce594cbc39a4866c544f1a970d285ff0548221ad", "url": "https://api.github.com/repos/doctrine/dbal/zipball/d244f2e6e6bf32bff5174e6729b57214923ecec9",
"reference": "ce594cbc39a4866c544f1a970d285ff0548221ad", "reference": "d244f2e6e6bf32bff5174e6729b57214923ecec9",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -885,7 +885,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/doctrine/dbal/issues", "issues": "https://github.com/doctrine/dbal/issues",
"source": "https://github.com/doctrine/dbal/tree/3.7.3" "source": "https://github.com/doctrine/dbal/tree/3.8.0"
}, },
"funding": [ "funding": [
{ {
@@ -901,7 +901,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-01-21T07:53:09+00:00" "time": "2024-01-25T21:44:02+00:00"
}, },
{ {
"name": "doctrine/deprecations", "name": "doctrine/deprecations",
@@ -1339,16 +1339,16 @@
}, },
{ {
"name": "filament/actions", "name": "filament/actions",
"version": "v3.2.9", "version": "v3.2.16",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/filamentphp/actions.git", "url": "https://github.com/filamentphp/actions.git",
"reference": "465ef83a1c43b3b7fe122dde50eda2ee0f9138ea" "reference": "2ad35bd1aad0c72f62e9c5f877989056a39cf012"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/filamentphp/actions/zipball/465ef83a1c43b3b7fe122dde50eda2ee0f9138ea", "url": "https://api.github.com/repos/filamentphp/actions/zipball/2ad35bd1aad0c72f62e9c5f877989056a39cf012",
"reference": "465ef83a1c43b3b7fe122dde50eda2ee0f9138ea", "reference": "2ad35bd1aad0c72f62e9c5f877989056a39cf012",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -1388,20 +1388,20 @@
"issues": "https://github.com/filamentphp/filament/issues", "issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament" "source": "https://github.com/filamentphp/filament"
}, },
"time": "2024-01-21T14:44:52+00:00" "time": "2024-01-27T23:30:19+00:00"
}, },
{ {
"name": "filament/filament", "name": "filament/filament",
"version": "v3.2.9", "version": "v3.2.16",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/filamentphp/panels.git", "url": "https://github.com/filamentphp/panels.git",
"reference": "a830f2d38073d3a4cdbe3798c957b69be50d39c3" "reference": "135ee98a43455a8c436367d8c51660d9a8b75ae4"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/filamentphp/panels/zipball/a830f2d38073d3a4cdbe3798c957b69be50d39c3", "url": "https://api.github.com/repos/filamentphp/panels/zipball/135ee98a43455a8c436367d8c51660d9a8b75ae4",
"reference": "a830f2d38073d3a4cdbe3798c957b69be50d39c3", "reference": "135ee98a43455a8c436367d8c51660d9a8b75ae4",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -1453,20 +1453,20 @@
"issues": "https://github.com/filamentphp/filament/issues", "issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament" "source": "https://github.com/filamentphp/filament"
}, },
"time": "2024-01-21T14:44:57+00:00" "time": "2024-01-27T23:30:21+00:00"
}, },
{ {
"name": "filament/forms", "name": "filament/forms",
"version": "v3.2.9", "version": "v3.2.16",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/filamentphp/forms.git", "url": "https://github.com/filamentphp/forms.git",
"reference": "fc37c620f66a2e13e160b516cc4d0e5ad8ae9425" "reference": "693ac4f2413e132501576cc0ca8f8aad636c362e"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/filamentphp/forms/zipball/fc37c620f66a2e13e160b516cc4d0e5ad8ae9425", "url": "https://api.github.com/repos/filamentphp/forms/zipball/693ac4f2413e132501576cc0ca8f8aad636c362e",
"reference": "fc37c620f66a2e13e160b516cc4d0e5ad8ae9425", "reference": "693ac4f2413e132501576cc0ca8f8aad636c362e",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -1509,20 +1509,20 @@
"issues": "https://github.com/filamentphp/filament/issues", "issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament" "source": "https://github.com/filamentphp/filament"
}, },
"time": "2024-01-21T14:44:57+00:00" "time": "2024-01-27T23:30:18+00:00"
}, },
{ {
"name": "filament/infolists", "name": "filament/infolists",
"version": "v3.2.9", "version": "v3.2.16",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/filamentphp/infolists.git", "url": "https://github.com/filamentphp/infolists.git",
"reference": "b071063c45f0cd314c863947d1d841da09d40750" "reference": "4ab39e8985cad7f5907b0c162d38023eb9dd402a"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/filamentphp/infolists/zipball/b071063c45f0cd314c863947d1d841da09d40750", "url": "https://api.github.com/repos/filamentphp/infolists/zipball/4ab39e8985cad7f5907b0c162d38023eb9dd402a",
"reference": "b071063c45f0cd314c863947d1d841da09d40750", "reference": "4ab39e8985cad7f5907b0c162d38023eb9dd402a",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -1560,11 +1560,11 @@
"issues": "https://github.com/filamentphp/filament/issues", "issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament" "source": "https://github.com/filamentphp/filament"
}, },
"time": "2024-01-21T14:44:53+00:00" "time": "2024-01-26T12:42:37+00:00"
}, },
{ {
"name": "filament/notifications", "name": "filament/notifications",
"version": "v3.2.9", "version": "v3.2.16",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/filamentphp/notifications.git", "url": "https://github.com/filamentphp/notifications.git",
@@ -1616,16 +1616,16 @@
}, },
{ {
"name": "filament/support", "name": "filament/support",
"version": "v3.2.9", "version": "v3.2.16",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/filamentphp/support.git", "url": "https://github.com/filamentphp/support.git",
"reference": "3b4d5d197c04e5a0b2a250d97c4761a07da9c85e" "reference": "8df5c195047d2849c49c1d20880951f716f111e0"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/filamentphp/support/zipball/3b4d5d197c04e5a0b2a250d97c4761a07da9c85e", "url": "https://api.github.com/repos/filamentphp/support/zipball/8df5c195047d2849c49c1d20880951f716f111e0",
"reference": "3b4d5d197c04e5a0b2a250d97c4761a07da9c85e", "reference": "8df5c195047d2849c49c1d20880951f716f111e0",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -1669,20 +1669,20 @@
"issues": "https://github.com/filamentphp/filament/issues", "issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament" "source": "https://github.com/filamentphp/filament"
}, },
"time": "2024-01-21T14:44:58+00:00" "time": "2024-01-27T23:30:41+00:00"
}, },
{ {
"name": "filament/tables", "name": "filament/tables",
"version": "v3.2.9", "version": "v3.2.16",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/filamentphp/tables.git", "url": "https://github.com/filamentphp/tables.git",
"reference": "87c55f6a1107d6d70b61a83f2cd7495343fdb19c" "reference": "0b63e4df21b3e6957471ab77ec745cda75e51e85"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/filamentphp/tables/zipball/87c55f6a1107d6d70b61a83f2cd7495343fdb19c", "url": "https://api.github.com/repos/filamentphp/tables/zipball/0b63e4df21b3e6957471ab77ec745cda75e51e85",
"reference": "87c55f6a1107d6d70b61a83f2cd7495343fdb19c", "reference": "0b63e4df21b3e6957471ab77ec745cda75e51e85",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -1722,11 +1722,11 @@
"issues": "https://github.com/filamentphp/filament/issues", "issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament" "source": "https://github.com/filamentphp/filament"
}, },
"time": "2024-01-21T14:45:11+00:00" "time": "2024-01-27T23:30:50+00:00"
}, },
{ {
"name": "filament/widgets", "name": "filament/widgets",
"version": "v3.2.9", "version": "v3.2.16",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/filamentphp/widgets.git", "url": "https://github.com/filamentphp/widgets.git",
@@ -2639,16 +2639,16 @@
}, },
{ {
"name": "laravel/framework", "name": "laravel/framework",
"version": "v10.41.0", "version": "v10.42.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/framework.git", "url": "https://github.com/laravel/framework.git",
"reference": "da31969bd35e6ee0bbcd9e876f88952dc754b012" "reference": "fef1aff874a6749c44f8e142e5764eab8cb96890"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/da31969bd35e6ee0bbcd9e876f88952dc754b012", "url": "https://api.github.com/repos/laravel/framework/zipball/fef1aff874a6749c44f8e142e5764eab8cb96890",
"reference": "da31969bd35e6ee0bbcd9e876f88952dc754b012", "reference": "fef1aff874a6749c44f8e142e5764eab8cb96890",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -2840,7 +2840,7 @@
"issues": "https://github.com/laravel/framework/issues", "issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework" "source": "https://github.com/laravel/framework"
}, },
"time": "2024-01-16T15:23:58+00:00" "time": "2024-01-23T15:07:56+00:00"
}, },
{ {
"name": "laravel/jetstream", "name": "laravel/jetstream",
@@ -2913,16 +2913,16 @@
}, },
{ {
"name": "laravel/passport", "name": "laravel/passport",
"version": "v11.10.1", "version": "v11.10.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/passport.git", "url": "https://github.com/laravel/passport.git",
"reference": "e1a651481cabff0ba174aaefbdc04a59e6a096ec" "reference": "27a4f34aaf8b360eb64f53eb9c555ee50d565560"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/passport/zipball/e1a651481cabff0ba174aaefbdc04a59e6a096ec", "url": "https://api.github.com/repos/laravel/passport/zipball/27a4f34aaf8b360eb64f53eb9c555ee50d565560",
"reference": "e1a651481cabff0ba174aaefbdc04a59e6a096ec", "reference": "27a4f34aaf8b360eb64f53eb9c555ee50d565560",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -2987,7 +2987,7 @@
"issues": "https://github.com/laravel/passport/issues", "issues": "https://github.com/laravel/passport/issues",
"source": "https://github.com/laravel/passport" "source": "https://github.com/laravel/passport"
}, },
"time": "2024-01-10T14:44:24+00:00" "time": "2024-01-17T14:57:00+00:00"
}, },
{ {
"name": "laravel/prompts", "name": "laravel/prompts",
@@ -3641,16 +3641,16 @@
}, },
{ {
"name": "league/flysystem", "name": "league/flysystem",
"version": "3.23.0", "version": "3.23.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/thephpleague/flysystem.git", "url": "https://github.com/thephpleague/flysystem.git",
"reference": "d4ad81e2b67396e33dc9d7e54ec74ccf73151dcc" "reference": "199e1aebbe3e62bd39f4d4fc8c61ce0b3786197e"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/d4ad81e2b67396e33dc9d7e54ec74ccf73151dcc", "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/199e1aebbe3e62bd39f4d4fc8c61ce0b3786197e",
"reference": "d4ad81e2b67396e33dc9d7e54ec74ccf73151dcc", "reference": "199e1aebbe3e62bd39f4d4fc8c61ce0b3786197e",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -3715,7 +3715,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/thephpleague/flysystem/issues", "issues": "https://github.com/thephpleague/flysystem/issues",
"source": "https://github.com/thephpleague/flysystem/tree/3.23.0" "source": "https://github.com/thephpleague/flysystem/tree/3.23.1"
}, },
"funding": [ "funding": [
{ {
@@ -3727,20 +3727,20 @@
"type": "github" "type": "github"
} }
], ],
"time": "2023-12-04T10:16:17+00:00" "time": "2024-01-26T18:42:03+00:00"
}, },
{ {
"name": "league/flysystem-local", "name": "league/flysystem-local",
"version": "3.23.0", "version": "3.23.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/thephpleague/flysystem-local.git", "url": "https://github.com/thephpleague/flysystem-local.git",
"reference": "5cf046ba5f059460e86a997c504dd781a39a109b" "reference": "b884d2bf9b53bb4804a56d2df4902bb51e253f00"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/5cf046ba5f059460e86a997c504dd781a39a109b", "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/b884d2bf9b53bb4804a56d2df4902bb51e253f00",
"reference": "5cf046ba5f059460e86a997c504dd781a39a109b", "reference": "b884d2bf9b53bb4804a56d2df4902bb51e253f00",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -3775,7 +3775,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/thephpleague/flysystem-local/issues", "issues": "https://github.com/thephpleague/flysystem-local/issues",
"source": "https://github.com/thephpleague/flysystem-local/tree/3.23.0" "source": "https://github.com/thephpleague/flysystem-local/tree/3.23.1"
}, },
"funding": [ "funding": [
{ {
@@ -3787,7 +3787,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2023-12-04T10:14:46+00:00" "time": "2024-01-26T18:25:23+00:00"
}, },
{ {
"name": "league/mime-type-detection", "name": "league/mime-type-detection",
@@ -4109,20 +4109,21 @@
}, },
{ {
"name": "livewire/livewire", "name": "livewire/livewire",
"version": "v3.3.5", "version": "v3.4.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/livewire/livewire.git", "url": "https://github.com/livewire/livewire.git",
"reference": "1ef880fbcdc7b6e5e405cc9135a62cd5fdbcd06a" "reference": "ab0baed58b774dde8e0ddbab1bbfd5b3d6334a82"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/livewire/livewire/zipball/1ef880fbcdc7b6e5e405cc9135a62cd5fdbcd06a", "url": "https://api.github.com/repos/livewire/livewire/zipball/ab0baed58b774dde8e0ddbab1bbfd5b3d6334a82",
"reference": "1ef880fbcdc7b6e5e405cc9135a62cd5fdbcd06a", "reference": "ab0baed58b774dde8e0ddbab1bbfd5b3d6334a82",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"illuminate/database": "^10.0|^11.0", "illuminate/database": "^10.0|^11.0",
"illuminate/routing": "^10.0|^11.0",
"illuminate/support": "^10.0|^11.0", "illuminate/support": "^10.0|^11.0",
"illuminate/validation": "^10.0|^11.0", "illuminate/validation": "^10.0|^11.0",
"league/mime-type-detection": "^1.9", "league/mime-type-detection": "^1.9",
@@ -4134,8 +4135,8 @@
"laravel/framework": "^10.0|^11.0", "laravel/framework": "^10.0|^11.0",
"laravel/prompts": "^0.1.6", "laravel/prompts": "^0.1.6",
"mockery/mockery": "^1.3.1", "mockery/mockery": "^1.3.1",
"orchestra/testbench": "^8.0|^9.0", "orchestra/testbench": "8.20.0|^9.0",
"orchestra/testbench-dusk": "^8.0|^9.0", "orchestra/testbench-dusk": "8.20.0|^9.0",
"phpunit/phpunit": "^10.4", "phpunit/phpunit": "^10.4",
"psy/psysh": "^0.11.22|^0.12" "psy/psysh": "^0.11.22|^0.12"
}, },
@@ -4171,7 +4172,7 @@
"description": "A front-end framework for Laravel.", "description": "A front-end framework for Laravel.",
"support": { "support": {
"issues": "https://github.com/livewire/livewire/issues", "issues": "https://github.com/livewire/livewire/issues",
"source": "https://github.com/livewire/livewire/tree/v3.3.5" "source": "https://github.com/livewire/livewire/tree/v3.4.2"
}, },
"funding": [ "funding": [
{ {
@@ -4179,7 +4180,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2024-01-02T14:29:17+00:00" "time": "2024-01-26T14:25:51+00:00"
}, },
{ {
"name": "masterminds/html5", "name": "masterminds/html5",
@@ -4415,16 +4416,16 @@
}, },
{ {
"name": "nesbot/carbon", "name": "nesbot/carbon",
"version": "2.72.1", "version": "2.72.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/briannesbitt/Carbon.git", "url": "https://github.com/briannesbitt/Carbon.git",
"reference": "2b3b3db0a2d0556a177392ff1a3bf5608fa09f78" "reference": "3e7edc41b58d65509baeb0d4a14c8fa41d627130"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/2b3b3db0a2d0556a177392ff1a3bf5608fa09f78", "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/3e7edc41b58d65509baeb0d4a14c8fa41d627130",
"reference": "2b3b3db0a2d0556a177392ff1a3bf5608fa09f78", "reference": "3e7edc41b58d65509baeb0d4a14c8fa41d627130",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -4518,7 +4519,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2023-12-08T23:47:49+00:00" "time": "2024-01-19T00:21:53+00:00"
}, },
{ {
"name": "nette/schema", "name": "nette/schema",
@@ -5877,6 +5878,68 @@
}, },
"time": "2023-12-20T15:28:09+00:00" "time": "2023-12-20T15:28:09+00:00"
}, },
{
"name": "pxlrbt/filament-environment-indicator",
"version": "v2.0.1",
"source": {
"type": "git",
"url": "https://github.com/pxlrbt/filament-environment-indicator.git",
"reference": "8942ad37142298a6eaf7fed747dd9c90402b0ba5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pxlrbt/filament-environment-indicator/zipball/8942ad37142298a6eaf7fed747dd9c90402b0ba5",
"reference": "8942ad37142298a6eaf7fed747dd9c90402b0ba5",
"shasum": ""
},
"require": {
"filament/filament": "^3.0-stable",
"php": "^8.0"
},
"require-dev": {
"laravel/pint": "^1.10"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"\\pxlrbt\\FilamentEnvironmentIndicator\\FilamentEnvironmentIndicatorServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"pxlrbt\\FilamentEnvironmentIndicator\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Dennis Koch",
"email": "info@pixelarbeit.de"
}
],
"description": "Indicator for the current environment inside Filament",
"keywords": [
"environment indicator",
"filament",
"laravel-filament"
],
"support": {
"issues": "https://github.com/pxlrbt/filament-environment-indicator/issues",
"source": "https://github.com/pxlrbt/filament-environment-indicator/tree/v2.0.1"
},
"funding": [
{
"url": "https://github.com/pxlrbt",
"type": "github"
}
],
"time": "2023-09-22T04:12:47+00:00"
},
{ {
"name": "ralouphie/getallheaders", "name": "ralouphie/getallheaders",
"version": "3.0.3", "version": "3.0.3",
@@ -9739,16 +9802,16 @@
}, },
{ {
"name": "laravel/pint", "name": "laravel/pint",
"version": "v1.13.9", "version": "v1.13.10",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/pint.git", "url": "https://github.com/laravel/pint.git",
"reference": "e3e269cc5d874c8efd2dc7962b1c7ff2585fe525" "reference": "e2b5060885694ca30ac008c05dc9d47f10ed1abf"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/pint/zipball/e3e269cc5d874c8efd2dc7962b1c7ff2585fe525", "url": "https://api.github.com/repos/laravel/pint/zipball/e2b5060885694ca30ac008c05dc9d47f10ed1abf",
"reference": "e3e269cc5d874c8efd2dc7962b1c7ff2585fe525", "reference": "e2b5060885694ca30ac008c05dc9d47f10ed1abf",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -9759,8 +9822,8 @@
"php": "^8.1.0" "php": "^8.1.0"
}, },
"require-dev": { "require-dev": {
"friendsofphp/php-cs-fixer": "^3.47.0", "friendsofphp/php-cs-fixer": "^3.47.1",
"illuminate/view": "^10.40.0", "illuminate/view": "^10.41.0",
"larastan/larastan": "^2.8.1", "larastan/larastan": "^2.8.1",
"laravel-zero/framework": "^10.3.0", "laravel-zero/framework": "^10.3.0",
"mockery/mockery": "^1.6.7", "mockery/mockery": "^1.6.7",
@@ -9801,20 +9864,20 @@
"issues": "https://github.com/laravel/pint/issues", "issues": "https://github.com/laravel/pint/issues",
"source": "https://github.com/laravel/pint" "source": "https://github.com/laravel/pint"
}, },
"time": "2024-01-16T17:39:29+00:00" "time": "2024-01-22T09:04:15+00:00"
}, },
{ {
"name": "laravel/sail", "name": "laravel/sail",
"version": "v1.27.1", "version": "v1.27.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/sail.git", "url": "https://github.com/laravel/sail.git",
"reference": "9dc648978e4276f2bfd37a076a52e3bd9394777f" "reference": "2276a8d9d6cfdcaad98bf67a34331d100149d5b6"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/sail/zipball/9dc648978e4276f2bfd37a076a52e3bd9394777f", "url": "https://api.github.com/repos/laravel/sail/zipball/2276a8d9d6cfdcaad98bf67a34331d100149d5b6",
"reference": "9dc648978e4276f2bfd37a076a52e3bd9394777f", "reference": "2276a8d9d6cfdcaad98bf67a34331d100149d5b6",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -9866,7 +9929,78 @@
"issues": "https://github.com/laravel/sail/issues", "issues": "https://github.com/laravel/sail/issues",
"source": "https://github.com/laravel/sail" "source": "https://github.com/laravel/sail"
}, },
"time": "2024-01-13T18:46:48+00:00" "time": "2024-01-21T17:13:42+00:00"
},
{
"name": "laravel/telescope",
"version": "v4.17.4",
"source": {
"type": "git",
"url": "https://github.com/laravel/telescope.git",
"reference": "3cbe70e900a9d070491149f2615d5a4a5b51d4c6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/telescope/zipball/3cbe70e900a9d070491149f2615d5a4a5b51d4c6",
"reference": "3cbe70e900a9d070491149f2615d5a4a5b51d4c6",
"shasum": ""
},
"require": {
"ext-json": "*",
"laravel/framework": "^8.37|^9.0|^10.0",
"php": "^8.0",
"symfony/var-dumper": "^5.0|^6.0"
},
"require-dev": {
"ext-gd": "*",
"guzzlehttp/guzzle": "^6.0|^7.0",
"laravel/octane": "^1.4",
"orchestra/testbench": "^6.0|^7.0|^8.0",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^9.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.x-dev"
},
"laravel": {
"providers": [
"Laravel\\Telescope\\TelescopeServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Laravel\\Telescope\\": "src/",
"Laravel\\Telescope\\Database\\Factories\\": "database/factories/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
},
{
"name": "Mohamed Said",
"email": "mohamed@laravel.com"
}
],
"description": "An elegant debug assistant for the Laravel framework.",
"keywords": [
"debugging",
"laravel",
"monitoring"
],
"support": {
"issues": "https://github.com/laravel/telescope/issues",
"source": "https://github.com/laravel/telescope/tree/v4.17.4"
},
"time": "2024-01-22T16:15:52+00:00"
}, },
{ {
"name": "mockery/mockery", "name": "mockery/mockery",
@@ -10307,16 +10441,16 @@
}, },
{ {
"name": "phpstan/phpstan", "name": "phpstan/phpstan",
"version": "1.10.56", "version": "1.10.57",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpstan/phpstan.git", "url": "https://github.com/phpstan/phpstan.git",
"reference": "27816a01aea996191ee14d010f325434c0ee76fa" "reference": "1627b1d03446904aaa77593f370c5201d2ecc34e"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/27816a01aea996191ee14d010f325434c0ee76fa", "url": "https://api.github.com/repos/phpstan/phpstan/zipball/1627b1d03446904aaa77593f370c5201d2ecc34e",
"reference": "27816a01aea996191ee14d010f325434c0ee76fa", "reference": "1627b1d03446904aaa77593f370c5201d2ecc34e",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -10365,7 +10499,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-01-15T10:43:00+00:00" "time": "2024-01-24T11:51:34+00:00"
}, },
{ {
"name": "phpunit/php-code-coverage", "name": "phpunit/php-code-coverage",
@@ -10690,16 +10824,16 @@
}, },
{ {
"name": "phpunit/phpunit", "name": "phpunit/phpunit",
"version": "10.5.8", "version": "10.5.9",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git", "url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "08f4fa74d5fbfff1ef22abffee47aaedcaea227e" "reference": "0bd663704f0165c9e76fe4f06ffa6a1ca727fdbe"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/08f4fa74d5fbfff1ef22abffee47aaedcaea227e", "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0bd663704f0165c9e76fe4f06ffa6a1ca727fdbe",
"reference": "08f4fa74d5fbfff1ef22abffee47aaedcaea227e", "reference": "0bd663704f0165c9e76fe4f06ffa6a1ca727fdbe",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -10771,7 +10905,7 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues", "issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy", "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.8" "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.9"
}, },
"funding": [ "funding": [
{ {
@@ -10787,7 +10921,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-01-19T07:07:27+00:00" "time": "2024-01-22T14:35:40+00:00"
}, },
{ {
"name": "sebastian/cli-parser", "name": "sebastian/cli-parser",

View File

@@ -171,6 +171,7 @@ return [
App\Providers\EventServiceProvider::class, App\Providers\EventServiceProvider::class,
App\Providers\Filament\AdminPanelProvider::class, App\Providers\Filament\AdminPanelProvider::class,
App\Providers\RouteServiceProvider::class, App\Providers\RouteServiceProvider::class,
App\Providers\TelescopeServiceProvider::class,
App\Providers\FortifyServiceProvider::class, App\Providers\FortifyServiceProvider::class,
App\Providers\JetstreamServiceProvider::class, App\Providers\JetstreamServiceProvider::class,
])->toArray(), ])->toArray(),

View File

@@ -115,4 +115,6 @@ return [
'password_timeout' => 10800, 'password_timeout' => 10800,
'super_admins' => ! is_string(env('SUPER_ADMINS', null)) ? [] : explode(',', env('SUPER_ADMINS')),
]; ];

191
config/telescope.php Normal file
View File

@@ -0,0 +1,191 @@
<?php
declare(strict_types=1);
use Laravel\Telescope\Http\Middleware\Authorize;
use Laravel\Telescope\Watchers;
return [
/*
|--------------------------------------------------------------------------
| Telescope Domain
|--------------------------------------------------------------------------
|
| This is the subdomain where Telescope will be accessible from. If the
| setting is null, Telescope will reside under the same domain as the
| application. Otherwise, this value will be used as the subdomain.
|
*/
'domain' => env('TELESCOPE_DOMAIN'),
/*
|--------------------------------------------------------------------------
| Telescope Path
|--------------------------------------------------------------------------
|
| This is the URI path where Telescope will be accessible from. Feel free
| to change this path to anything you like. Note that the URI will not
| affect the paths of its internal API that aren't exposed to users.
|
*/
'path' => env('TELESCOPE_PATH', 'telescope'),
/*
|--------------------------------------------------------------------------
| Telescope Storage Driver
|--------------------------------------------------------------------------
|
| This configuration options determines the storage driver that will
| be used to store Telescope's data. In addition, you may set any
| custom options as needed by the particular driver you choose.
|
*/
'driver' => env('TELESCOPE_DRIVER', 'database'),
'storage' => [
'database' => [
'connection' => env('DB_CONNECTION', 'mysql'),
'chunk' => 1000,
],
],
/*
|--------------------------------------------------------------------------
| Telescope Master Switch
|--------------------------------------------------------------------------
|
| This option may be used to disable all Telescope watchers regardless
| of their individual configuration, which simply provides a single
| and convenient way to enable or disable Telescope data storage.
|
*/
'enabled' => env('TELESCOPE_ENABLED', true),
/*
|--------------------------------------------------------------------------
| Telescope Route Middleware
|--------------------------------------------------------------------------
|
| These middleware will be assigned to every Telescope route, giving you
| the chance to add your own middleware to this list or change any of
| the existing middleware. Or, you can simply stick with this list.
|
*/
'middleware' => [
'web',
Authorize::class,
],
/*
|--------------------------------------------------------------------------
| Allowed / Ignored Paths & Commands
|--------------------------------------------------------------------------
|
| The following array lists the URI paths and Artisan commands that will
| not be watched by Telescope. In addition to this list, some Laravel
| commands, like migrations and queue commands, are always ignored.
|
*/
'only_paths' => [
// 'api/*'
],
'ignore_paths' => [
'livewire*',
'nova-api*',
'pulse*',
],
'ignore_commands' => [
//
],
/*
|--------------------------------------------------------------------------
| Telescope Watchers
|--------------------------------------------------------------------------
|
| The following array lists the "watchers" that will be registered with
| Telescope. The watchers gather the application's profile data when
| a request or task is executed. Feel free to customize this list.
|
*/
'watchers' => [
Watchers\BatchWatcher::class => env('TELESCOPE_BATCH_WATCHER', true),
Watchers\CacheWatcher::class => [
'enabled' => env('TELESCOPE_CACHE_WATCHER', true),
'hidden' => [],
],
Watchers\ClientRequestWatcher::class => env('TELESCOPE_CLIENT_REQUEST_WATCHER', true),
Watchers\CommandWatcher::class => [
'enabled' => env('TELESCOPE_COMMAND_WATCHER', true),
'ignore' => [],
],
Watchers\DumpWatcher::class => [
'enabled' => env('TELESCOPE_DUMP_WATCHER', true),
'always' => env('TELESCOPE_DUMP_WATCHER_ALWAYS', false),
],
Watchers\EventWatcher::class => [
'enabled' => env('TELESCOPE_EVENT_WATCHER', true),
'ignore' => [],
],
Watchers\ExceptionWatcher::class => env('TELESCOPE_EXCEPTION_WATCHER', true),
Watchers\GateWatcher::class => [
'enabled' => env('TELESCOPE_GATE_WATCHER', true),
'ignore_abilities' => [],
'ignore_packages' => true,
'ignore_paths' => [],
],
Watchers\JobWatcher::class => env('TELESCOPE_JOB_WATCHER', true),
Watchers\LogWatcher::class => [
'enabled' => env('TELESCOPE_LOG_WATCHER', true),
'level' => 'error',
],
Watchers\MailWatcher::class => env('TELESCOPE_MAIL_WATCHER', true),
Watchers\ModelWatcher::class => [
'enabled' => env('TELESCOPE_MODEL_WATCHER', true),
'events' => ['eloquent.*'],
'hydrations' => true,
],
Watchers\NotificationWatcher::class => env('TELESCOPE_NOTIFICATION_WATCHER', true),
Watchers\QueryWatcher::class => [
'enabled' => env('TELESCOPE_QUERY_WATCHER', true),
'ignore_packages' => true,
'ignore_paths' => [],
'slow' => 100,
],
Watchers\RedisWatcher::class => env('TELESCOPE_REDIS_WATCHER', true),
Watchers\RequestWatcher::class => [
'enabled' => env('TELESCOPE_REQUEST_WATCHER', true),
'size_limit' => env('TELESCOPE_RESPONSE_SIZE_LIMIT', 64),
'ignore_http_methods' => [],
'ignore_status_codes' => [],
],
Watchers\ScheduleWatcher::class => env('TELESCOPE_SCHEDULE_WATCHER', true),
Watchers\ViewWatcher::class => env('TELESCOPE_VIEW_WATCHER', true),
],
];

View File

@@ -26,4 +26,11 @@ class OrganizationFactory extends Factory
'personal_team' => true, 'personal_team' => true,
]; ];
} }
public function withOwner(): self
{
return $this->state(fn (array $attributes) => [
'user_id' => User::factory(),
]);
}
} }

View File

@@ -27,6 +27,6 @@ return new class extends Migration
*/ */
public function down(): void public function down(): void
{ {
Schema::dropIfExists('teams'); Schema::dropIfExists('organizations');
} }
}; };

View File

@@ -29,6 +29,6 @@ return new class extends Migration
*/ */
public function down(): void public function down(): void
{ {
Schema::dropIfExists('team_user'); Schema::dropIfExists('organization_user');
} }
}; };

View File

@@ -31,6 +31,6 @@ return new class extends Migration
*/ */
public function down(): void public function down(): void
{ {
Schema::dropIfExists('team_invitations'); Schema::dropIfExists('organization_invitations');
} }
}; };

View File

@@ -22,23 +22,32 @@ class DatabaseSeeder extends Seeder
public function run(): void public function run(): void
{ {
$this->deleteAll(); $this->deleteAll();
$organization = Organization::factory()->create([ $organization1 = Organization::factory()->create([
'name' => 'ACME Corp', 'name' => 'ACME Corp',
]); ]);
$user1 = User::factory()->withPersonalOrganization()->create([ $user1 = User::factory()->withPersonalOrganization()->create([
'name' => 'Test User', 'name' => 'Test User',
'email' => 'test@example.com', 'email' => 'test@example.com',
]); ]);
$employee1 = User::factory()->withPersonalOrganization()->create([
'name' => 'Test User',
'email' => 'employee@example.com',
]);
$userAcmeAdmin = User::factory()->create([ $userAcmeAdmin = User::factory()->create([
'name' => 'ACME Admin', 'name' => 'ACME Admin',
'email' => 'admin@acme.test', 'email' => 'admin@acme.test',
]); ]);
$user1->organizations()->attach($organization, [ $user1->organizations()->attach($organization1, [
'role' => 'editor', 'role' => 'manager',
]); ]);
$userAcmeAdmin->organizations()->attach($organization, [ $userAcmeAdmin->organizations()->attach($organization1, [
'role' => 'admin', 'role' => 'admin',
]); ]);
$timeEntriesEmployees = TimeEntry::factory()
->count(10)
->forUser($employee1)
->forOrganization($organization1)
->create();
$client = Client::factory()->create([ $client = Client::factory()->create([
'name' => 'Big Company', 'name' => 'Big Company',
]); ]);
@@ -50,6 +59,24 @@ class DatabaseSeeder extends Seeder
$internalProject = Project::factory()->create([ $internalProject = Project::factory()->create([
'name' => 'Internal Project', 'name' => 'Internal Project',
]); ]);
$organization2 = Organization::factory()->create([
'name' => 'Rival Corp',
]);
$user1 = User::factory()->withPersonalOrganization()->create([
'name' => 'Other User',
'email' => 'test@rival-company.test',
]);
$user1->organizations()->attach($organization2, [
'role' => 'admin',
]);
$otherCompanyProject = Project::factory()->forClient($client)->create([
'name' => 'Scale Company',
]);
User::factory()->withPersonalOrganization()->create([
'email' => 'admin@example.com',
]);
} }
private function deleteAll(): void private function deleteAll(): void

View File

@@ -21,7 +21,7 @@
<env name="APP_ENV" value="testing"/> <env name="APP_ENV" value="testing"/>
<env name="BCRYPT_ROUNDS" value="4"/> <env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_DRIVER" value="array"/> <env name="CACHE_DRIVER" value="array"/>
<env name="DB_DATABASE" value="testing"/> <env name="DB_CONNECTION" value="pgsql"/>
<env name="MAIL_MAILER" value="array"/> <env name="MAIL_MAILER" value="array"/>
<env name="PULSE_ENABLED" value="false"/> <env name="PULSE_ENABLED" value="false"/>
<env name="QUEUE_CONNECTION" value="sync"/> <env name="QUEUE_CONNECTION" value="sync"/>

View File

@@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
use Illuminate\Http\Request; use App\Http\Controllers\Api\V1\ProjectController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
/* /*
@@ -16,6 +16,13 @@ use Illuminate\Support\Facades\Route;
| |
*/ */
//Route::middleware('auth:api')->get('/user', function (Request $request) { Route::middleware('auth:api')->prefix('v1')->name('v1.')->group(static function () {
// return $request->user(); Route::name('projects.')->group(static function () {
//}); Route::get('/organization/{organization}/projects', [ProjectController::class, 'index'])->name('index');
Route::get('/organization/{organization}/projects/{project}', [ProjectController::class, 'show'])->name('show');
Route::post('/organization/{organization}/projects', [ProjectController::class, 'store'])->name('store');
Route::put('/organization/{organization}/projects/{project}', [ProjectController::class, 'update'])->name('update');
Route::delete('/organization/{organization}/projects/{project}', [ProjectController::class, 'destroy'])->name('destroy');
});
});

View File

@@ -21,11 +21,11 @@ class UpdateTeamMemberRoleTest extends TestCase
); );
$response = $this->put('/teams/'.$user->currentTeam->id.'/members/'.$otherUser->id, [ $response = $this->put('/teams/'.$user->currentTeam->id.'/members/'.$otherUser->id, [
'role' => 'editor', 'role' => 'employee',
]); ]);
$this->assertTrue($otherUser->fresh()->hasTeamRole( $this->assertTrue($otherUser->fresh()->hasTeamRole(
$user->currentTeam->fresh(), 'editor' $user->currentTeam->fresh(), 'employee'
)); ));
} }
@@ -40,7 +40,7 @@ class UpdateTeamMemberRoleTest extends TestCase
$this->actingAs($otherUser); $this->actingAs($otherUser);
$response = $this->put('/teams/'.$user->currentTeam->id.'/members/'.$otherUser->id, [ $response = $this->put('/teams/'.$user->currentTeam->id.'/members/'.$otherUser->id, [
'role' => 'editor', 'role' => 'employee',
]); ]);
$this->assertTrue($otherUser->fresh()->hasTeamRole( $this->assertTrue($otherUser->fresh()->hasTeamRole(

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Database;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Artisan;
class MigrationTest extends \Tests\TestCase
{
use RefreshDatabase;
public function test_fresh_migration_and_rollback_runs_successfully(): void
{
Artisan::call('migrate:fresh');
Artisan::call('migrate:rollback');
$this->assertTrue(true);
}
public function testFreshMigrationWithSeederAndRollbackRunsSuccessfully(): void
{
Artisan::call('migrate:fresh --seed');
Artisan::call('migrate:rollback');
$this->assertTrue(true);
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Database;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Artisan;
class SeederTest extends \Tests\TestCase
{
use RefreshDatabase;
public function test_running_the_seeder_multiple_times_runs_successfully(): void
{
Artisan::call('db:seed');
Artisan::call('db:seed');
$this->assertTrue(true);
}
public function test_fresh_migration_with_seeder_and_rollback_runs_successfully(): void
{
Artisan::call('migrate:fresh --seed');
Artisan::call('migrate:rollback');
$this->assertTrue(true);
}
}

View File

@@ -0,0 +1,186 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Endpoint\Api\V1;
use App\Models\Organization;
use App\Models\Project;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Jetstream\Jetstream;
use Laravel\Passport\Passport;
use Tests\TestCase;
class ProjectEndpointTest extends TestCase
{
use RefreshDatabase;
/**
* @param array<string> $permissions
* @return object{user: User, organization: Organization}
*/
private function createUserWithPermission(array $permissions): object
{
Jetstream::role('custom-test', 'Custom Test', $permissions)->description('Role custom for testing');
$organization = Organization::factory()->create();
$user = User::factory()->create();
$organization->users()->attach($user, [
'role' => 'custom-test',
]);
return (object) [
'user' => $user,
'organization' => $organization,
];
}
public function test_index_endpoint_fails_if_user_has_no_permission_to_view_projects(): void
{
// Arrange
$data = $this->createUserWithPermission([
]);
$projects = Project::factory()->forOrganization($data->organization)->createMany(4);
Passport::actingAs($data->user);
// Act
$response = $this->getJson(route('api.v1.projects.index', [$data->organization->getKey()]));
// Assert
$response->assertStatus(403);
}
public function test_index_endpoint_returns_list_of_all_projects_of_organization(): void
{
// Arrange
$data = $this->createUserWithPermission([
'projects:view',
]);
$projects = Project::factory()->forOrganization($data->organization)->createMany(4);
Passport::actingAs($data->user);
// Act
$response = $this->getJson(route('api.v1.projects.index', [$data->organization->getKey()]));
// Assert
$response->assertStatus(200);
$response->assertJsonCount(4, 'data');
}
public function test_store_endpoint_fails_if_user_has_no_permission_to_create_projects(): void
{
// Arrange
$data = $this->createUserWithPermission([
]);
$project = Project::factory()->forOrganization($data->organization)->make();
Passport::actingAs($data->user);
// Act
$response = $this->postJson(route('api.v1.projects.store', [$data->organization->getKey()]), $project->toArray());
// Assert
$response->assertStatus(403);
}
public function test_store_endpoint_creates_new_project(): void
{
// Arrange
$data = $this->createUserWithPermission([
'projects:create',
]);
$project = Project::factory()->forOrganization($data->organization)->make();
Passport::actingAs($data->user);
// Act
$response = $this->postJson(route('api.v1.projects.store', [$data->organization->getKey()]), [
'name' => $project->name,
'color' => $project->color,
'organization_id' => $project->organization_id,
]);
// Assert
$response->assertStatus(201);
$this->assertDatabaseHas(Project::class, [
'name' => $project->name,
'color' => $project->color,
'organization_id' => $project->organization_id,
]);
}
public function test_update_endpoint_fails_if_user_has_no_permission_to_update_projects(): void
{
// Arrange
$data = $this->createUserWithPermission([
]);
$project = Project::factory()->forOrganization($data->organization)->create();
$projectFake = Project::factory()->make();
Passport::actingAs($data->user);
// Act
$response = $this->putJson(route('api.v1.projects.update', [$data->organization->getKey(), $project->getKey()]), [
'name' => $projectFake->name,
'color' => $projectFake->color,
]);
// Assert
$response->assertStatus(403);
}
public function test_update_endpoint_updates_project(): void
{
// Arrange
$data = $this->createUserWithPermission([
'projects:update',
]);
$project = Project::factory()->forOrganization($data->organization)->create();
$projectFake = Project::factory()->make();
Passport::actingAs($data->user);
// Act
$response = $this->putJson(route('api.v1.projects.update', [$data->organization->getKey(), $project->getKey()]), [
'name' => $projectFake->name,
'color' => $projectFake->color,
]);
// Assert
$response->assertStatus(200);
$this->assertDatabaseHas(Project::class, [
'name' => $projectFake->name,
'color' => $projectFake->color,
]);
}
public function test_destroy_endpoint_fails_if_user_has_no_permission_to_delete_projects(): void
{
// Arrange
$data = $this->createUserWithPermission([
]);
$project = Project::factory()->forOrganization($data->organization)->create();
Passport::actingAs($data->user);
// Act
$response = $this->deleteJson(route('api.v1.projects.destroy', [$data->organization->getKey(), $project->getKey()]));
// Assert
$response->assertStatus(403);
}
public function test_destroy_endpoint_deletes_project(): void
{
// Arrange
$data = $this->createUserWithPermission([
'projects:delete',
]);
$project = Project::factory()->forOrganization($data->organization)->create();
Passport::actingAs($data->user);
// Act
$response = $this->deleteJson(route('api.v1.projects.destroy', [$data->organization->getKey(), $project->getKey()]));
// Assert
$response->assertStatus(200);
$this->assertDatabaseMissing(Project::class, [
'id' => $project->getKey(),
]);
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Filament;
use App\Filament\Resources\ClientResource;
use App\Models\Client;
use App\Models\User;
use Illuminate\Support\Facades\Config;
use Livewire\Livewire;
class ClientResourceTest extends FilamentTestCase
{
protected function setUp(): void
{
parent::setUp();
Config::set('auth.super_admins', ['admin@example.com']);
$user = User::factory()->withPersonalOrganization()->create([
'email' => 'admin@example.com',
]);
$this->actingAs($user);
}
public function test_can_list_clients(): void
{
// Arrange
$clients = Client::factory()->createMany(5);
// Act
$response = Livewire::test(ClientResource\Pages\ListClients::class);
// Assert
$response->assertSuccessful();
$response->assertCanSeeTableRecords($clients);
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Filament;
use Filament\Facades\Filament;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
abstract class FilamentTestCase extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
Filament::setServingStatus();
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Filament;
use App\Filament\Resources\OrganizationResource;
use App\Models\Organization;
use App\Models\User;
use Illuminate\Support\Facades\Config;
use Livewire\Livewire;
class OrganizationResourceTest extends FilamentTestCase
{
protected function setUp(): void
{
parent::setUp();
Config::set('auth.super_admins', ['admin@example.com']);
$user = User::factory()->withPersonalOrganization()->create([
'email' => 'admin@example.com',
]);
$this->actingAs($user);
}
public function test_can_list_organizations(): void
{
// Arrange
$user = User::factory()->create();
$organizations = Organization::factory()->state([
'user_id' => $user->id,
])->createMany(5);
// Act
$response = Livewire::test(OrganizationResource\Pages\ListOrganizations::class);
// Assert
$response->assertSuccessful();
$response->assertCanSeeTableRecords($organizations);
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Filament;
use App\Filament\Resources\ProjectResource;
use App\Models\Project;
use App\Models\User;
use Illuminate\Support\Facades\Config;
use Livewire\Livewire;
class ProjectResourceTest extends FilamentTestCase
{
protected function setUp(): void
{
parent::setUp();
Config::set('auth.super_admins', ['admin@example.com']);
$user = User::factory()->withPersonalOrganization()->create([
'email' => 'admin@example.com',
]);
$this->actingAs($user);
}
public function test_can_list_projects(): void
{
// Arrange
$projects = Project::factory()->createMany(5);
// Act
$response = Livewire::test(ProjectResource\Pages\ListProjects::class);
// Assert
$response->assertSuccessful();
$response->assertCanSeeTableRecords($projects);
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Filament;
use App\Filament\Resources\TagResource;
use App\Models\Tag;
use App\Models\User;
use Illuminate\Support\Facades\Config;
use Livewire\Livewire;
class TagResourceTest extends FilamentTestCase
{
protected function setUp(): void
{
parent::setUp();
Config::set('auth.super_admins', ['admin@example.com']);
$user = User::factory()->withPersonalOrganization()->create([
'email' => 'admin@example.com',
]);
$this->actingAs($user);
}
public function test_can_list_tags(): void
{
// Arrange
$tags = Tag::factory()->createMany(5);
// Act
$response = Livewire::test(TagResource\Pages\ListTags::class);
// Assert
$response->assertSuccessful();
$response->assertCanSeeTableRecords($tags);
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Filament;
use App\Filament\Resources\TaskResource;
use App\Models\Task;
use App\Models\User;
use Illuminate\Support\Facades\Config;
use Livewire\Livewire;
class TaskResourceTest extends FilamentTestCase
{
protected function setUp(): void
{
parent::setUp();
Config::set('auth.super_admins', ['admin@example.com']);
$user = User::factory()->withPersonalOrganization()->create([
'email' => 'admin@example.com',
]);
$this->actingAs($user);
}
public function test_can_list_tasks(): void
{
// Arrange
$tasks = Task::factory()->createMany(5);
// Act
$response = Livewire::test(TaskResource\Pages\ListTasks::class);
// Assert
$response->assertSuccessful();
$response->assertCanSeeTableRecords($tasks);
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Filament;
use App\Filament\Resources\TimeEntryResource;
use App\Models\TimeEntry;
use App\Models\User;
use Illuminate\Support\Facades\Config;
use Livewire\Livewire;
class TimeEntryResourceTest extends FilamentTestCase
{
protected function setUp(): void
{
parent::setUp();
Config::set('auth.super_admins', ['admin@example.com']);
$user = User::factory()->withPersonalOrganization()->create([
'email' => 'admin@example.com',
]);
$this->actingAs($user);
}
public function test_can_list_time_entry(): void
{
// Arrange
$timeEntry = TimeEntry::factory()->createMany(5);
// Act
$response = Livewire::test(TimeEntryResource\Pages\ListTimeEntries::class);
// Assert
$response->assertSuccessful();
$response->assertCanSeeTableRecords($timeEntry);
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Filament;
use App\Filament\Resources\UserResource;
use App\Models\User;
use Illuminate\Support\Facades\Config;
use Livewire\Livewire;
class UserResourceTest extends FilamentTestCase
{
protected function setUp(): void
{
parent::setUp();
Config::set('auth.super_admins', ['admin@example.com']);
$user = User::factory()->withPersonalOrganization()->create([
'email' => 'admin@example.com',
]);
$this->actingAs($user);
}
public function test_can_list_users(): void
{
// Arrange
$users = User::factory()->createMany(5);
// Act
$response = Livewire::test(UserResource\Pages\ListUsers::class);
// Assert
$response->assertSuccessful();
$response->assertCanSeeTableRecords($users);
}
}

View File

@@ -7,12 +7,14 @@ namespace Tests\Unit\Model;
use App\Models\User; use App\Models\User;
use App\Providers\Filament\AdminPanelProvider; use App\Providers\Filament\AdminPanelProvider;
use Filament\Panel; use Filament\Panel;
use Illuminate\Support\Facades\Config;
class UserModelTest extends ModelTestAbstract class UserModelTest extends ModelTestAbstract
{ {
public function test_normal_user_can_not_access_admin_panel(): void public function test_normal_user_can_not_access_admin_panel(): void
{ {
// Arrange // Arrange
Config::set('auth.super_admins', ['some@email.test', 'other@email.test']);
$user = User::factory()->create(); $user = User::factory()->create();
$panelProvider = new AdminPanelProvider(app()); $panelProvider = new AdminPanelProvider(app());
$mainPanel = $panelProvider->panel(Panel::make()); $mainPanel = $panelProvider->panel(Panel::make());
@@ -23,4 +25,21 @@ class UserModelTest extends ModelTestAbstract
// Assert // Assert
$this->assertFalse($canAccess); $this->assertFalse($canAccess);
} }
public function test_user_in_super_admin_config_can_access_admin_panel(): void
{
// Arrange
Config::set('auth.super_admins', ['some@email.test', 'other@email.test']);
$user = User::factory()->create([
'email' => 'some@email.test',
]);
$panelProvider = new AdminPanelProvider(app());
$mainPanel = $panelProvider->panel(Panel::make());
// Act
$canAccess = $user->canAccessPanel($mainPanel);
// Assert
$this->assertTrue($canAccess);
}
} }