diff --git a/.env.ci b/.env.ci index 40ce2768..21ee262b 100644 --- a/.env.ci +++ b/.env.ci @@ -34,7 +34,12 @@ SESSION_DRIVER=database SESSION_LIFETIME=120 # Mail -MAIL_MAILER=log +MAIL_MAILER=smtp +MAIL_HOST=localhost +MAIL_PORT=1025 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_ENCRYPTION=null MAIL_FROM_ADDRESS="no-reply@solidtime.test" MAIL_FROM_NAME="solidtime" MAIL_REPLY_TO_ADDRESS="hello@solidtime.test" @@ -56,3 +61,6 @@ TELESCOPE_ENABLED=false # Services GOTENBERG_URL=http://0.0.0.0:3000 + +# Octane +OCTANE_SERVER=frankenphp diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index d0f9b805..5126cd18 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -6,10 +6,18 @@ jobs: test: runs-on: ubuntu-latest timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + shardIndex: [1, 2, 3, 4, 5, 6, 7, 8] + shardTotal: [8] services: mailpit: image: 'axllent/mailpit:latest' + ports: + - 1025:1025 + - 8025:8025 pgsql_test: image: postgres:15 env: @@ -57,22 +65,63 @@ jobs: - name: "Build Frontend" run: npm run build - - name: "Run Laravel Server" - run: php artisan serve > /dev/null 2>&1 & + - name: "Install FrankenPHP" + run: | + ARCH="$(uname -m)" + curl -fsSL "https://github.com/dunglas/frankenphp/releases/latest/download/frankenphp-linux-${ARCH}" -o /usr/local/bin/frankenphp + chmod +x /usr/local/bin/frankenphp + + - name: "Run Laravel Octane Server" + run: php artisan octane:start --server=frankenphp --host=127.0.0.1 --port=8000 --workers=4 --max-requests=500 > /dev/null 2>&1 & + env: + OCTANE_SERVER: frankenphp - name: "Install Playwright Browsers" run: npx playwright install --with-deps - name: "Run Playwright tests" - run: npx playwright test + run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} env: PLAYWRIGHT_BASE_URL: 'http://127.0.0.1:8000' + MAILPIT_BASE_URL: 'http://localhost:8025' - - name: "Upload test results" + - name: "Upload blob report" uses: actions/upload-artifact@v4 if: always() with: - name: test-results - path: test-results/ - retention-days: 30 + name: blob-report-${{ matrix.shardIndex }} + path: blob-report/ + retention-days: 7 + + merge-reports: + if: always() + needs: [test] + runs-on: ubuntu-latest + steps: + - name: "Checkout code" + uses: actions/checkout@v4 + + - name: "Setup node" + uses: actions/setup-node@v4 + with: + node-version: '20.x' + + - name: "Install dependencies" + run: npm ci + - name: "Download blob reports" + uses: actions/download-artifact@v4 + with: + path: all-blob-reports + pattern: blob-report-* + merge-multiple: true + + - name: "Merge reports" + run: npx playwright merge-reports --reporter html ./all-blob-reports + + - name: "Upload merged HTML report" + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/README.md b/README.md index 7943285b..7002a79a 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,8 @@ If you have a **feature request**, please [**create a discussion**](https://gith Please open an issue or start a discussion and wait for approval before submitting a pull request. This does not apply to tiny fixes or changes however, please keep in mind that we might not merge PRs for various reasons. +**If you submit an AI slop pull request (especially without following the proper procedure), you will be banned from future contributions to solidtime.** + Please read the [CONTRIBUTING.md](./CONTRIBUTING.md) before sumbitting a Pull Request. We do accept contributions in the [documentation repository](https://github.com/solidtime-io/docs) f.e. to add new self-hosting guides. diff --git a/app/Filament/Resources/FailedJobResource.php b/app/Filament/Resources/FailedJobResource.php index 7960f1c3..f47d98d3 100644 --- a/app/Filament/Resources/FailedJobResource.php +++ b/app/Filament/Resources/FailedJobResource.php @@ -50,7 +50,7 @@ public static function form(Form $form): Form TextInput::make('queue')->disabled(), // make text a little bit smaller because often a complete Stack Trace is shown: - TextArea::make('exception')->disabled()->columnSpan(4)->extraInputAttributes(['style' => 'font-size: 80%;']), + Textarea::make('exception')->disabled()->columnSpan(4)->extraInputAttributes(['style' => 'font-size: 80%;']), PrettyJsonField::make('payload')->disabled()->columnSpan(4), ])->columns(4); } diff --git a/app/Filament/Resources/OrganizationInvitationResource.php b/app/Filament/Resources/OrganizationInvitationResource.php index 88ca23a5..c03a3811 100644 --- a/app/Filament/Resources/OrganizationInvitationResource.php +++ b/app/Filament/Resources/OrganizationInvitationResource.php @@ -39,7 +39,7 @@ public static function form(Form $form): Form ->required(), Select::make('role') ->options(Role::class), - Forms\Components\Select::make('organization_id') + Select::make('organization_id') ->label('Organization') ->relationship(name: 'organization', titleAttribute: 'name') ->searchable(['name']) diff --git a/app/Filament/Resources/OrganizationResource.php b/app/Filament/Resources/OrganizationResource.php index 040fa302..460de0dc 100644 --- a/app/Filament/Resources/OrganizationResource.php +++ b/app/Filament/Resources/OrganizationResource.php @@ -55,7 +55,7 @@ public static function form(Form $form): Form ->label('Is personal?') ->hiddenOn(['create']) ->required(), - Forms\Components\Select::make('user_id') + Select::make('user_id') ->label('Owner') ->relationship(name: 'owner', titleAttribute: 'email') ->searchable(['name', 'email']) @@ -76,7 +76,7 @@ public static function form(Form $form): Form Select::make('time_format') ->options(TimeFormat::toSelectArray()) ->required(), - Forms\Components\Select::make('currency') + Select::make('currency') ->label('Currency') ->options(function (): array { $currencies = ISOCurrencyProvider::getInstance()->getAvailableCurrencies(); @@ -114,22 +114,22 @@ public static function table(Table $table): Table { return $table ->columns([ - Tables\Columns\TextColumn::make('name') + TextColumn::make('name') ->searchable() ->sortable(), Tables\Columns\IconColumn::make('personal_team') ->boolean() ->label('Is personal?') ->sortable(), - Tables\Columns\TextColumn::make('owner.email') + TextColumn::make('owner.email') ->sortable(), - Tables\Columns\TextColumn::make('currency'), + TextColumn::make('currency'), TextColumn::make('billable_rate') ->money(fn (Organization $resource) => $resource->currency, divideBy: 100), - Tables\Columns\TextColumn::make('created_at') + TextColumn::make('created_at') ->dateTime() ->sortable(), - Tables\Columns\TextColumn::make('updated_at') + TextColumn::make('updated_at') ->dateTime() ->sortable() ->toggleable(isToggledHiddenByDefault: true), @@ -223,7 +223,7 @@ public static function table(Table $table): Table return $select; }), - Forms\Components\Select::make('timezone') + Select::make('timezone') ->label('Timezone') ->options(fn (): array => app(TimezoneService::class)->getSelectOptions()) ->searchable() diff --git a/app/Filament/Resources/OrganizationResource/RelationManagers/UsersRelationManager.php b/app/Filament/Resources/OrganizationResource/RelationManagers/UsersRelationManager.php index d027d4dc..841ed6ae 100644 --- a/app/Filament/Resources/OrganizationResource/RelationManagers/UsersRelationManager.php +++ b/app/Filament/Resources/OrganizationResource/RelationManagers/UsersRelationManager.php @@ -49,13 +49,13 @@ public function table(Table $table): Table return $table ->recordTitleAttribute('name') ->columns([ - Tables\Columns\TextColumn::make('name'), - Tables\Columns\TextColumn::make('role'), + TextColumn::make('name'), + TextColumn::make('role'), TextColumn::make('billable_rate') ->money($organization->currency, divideBy: 100), ]) ->headerActions([ - Tables\Actions\AttachAction::make() + AttachAction::make() ->recordTitle(fn (User $record): string => "{$record->name} ({$record->email})") ->form(fn (AttachAction $action): array => [ $action->getRecordSelect(), diff --git a/app/Filament/Resources/ReportResource.php b/app/Filament/Resources/ReportResource.php index b757e9fe..6c3081e5 100644 --- a/app/Filament/Resources/ReportResource.php +++ b/app/Filament/Resources/ReportResource.php @@ -63,11 +63,11 @@ public static function form(Form $form): Form return $record->getRawOriginal('properties'); }) ->disabled(), - Forms\Components\DateTimePicker::make('created_at') + DateTimePicker::make('created_at') ->label('Created At') ->hiddenOn(['create']) ->disabled(), - Forms\Components\DateTimePicker::make('updated_at') + DateTimePicker::make('updated_at') ->label('Updated At') ->hiddenOn(['create']) ->disabled(), @@ -78,10 +78,10 @@ public static function table(Table $table): Table { return $table ->columns([ - Tables\Columns\TextColumn::make('name') + TextColumn::make('name') ->searchable() ->sortable(), - Tables\Columns\TextColumn::make('description') + TextColumn::make('description') ->searchable() ->sortable(), ToggleColumn::make('is_public') @@ -90,10 +90,10 @@ public static function table(Table $table): Table TextColumn::make('organization.name') ->searchable() ->sortable(), - Tables\Columns\TextColumn::make('created_at') + TextColumn::make('created_at') ->dateTime() ->sortable(), - Tables\Columns\TextColumn::make('updated_at') + TextColumn::make('updated_at') ->dateTime() ->sortable() ->toggleable(isToggledHiddenByDefault: true), diff --git a/app/Filament/Resources/TimeEntryResource.php b/app/Filament/Resources/TimeEntryResource.php index ffd133b3..53d459c7 100644 --- a/app/Filament/Resources/TimeEntryResource.php +++ b/app/Filament/Resources/TimeEntryResource.php @@ -5,6 +5,7 @@ namespace App\Filament\Resources; use App\Filament\Resources\TimeEntryResource\Pages; +use App\Models\Member; use App\Models\TimeEntry; use Filament\Forms\Components\DateTimePicker; use Filament\Forms\Components\Select; @@ -16,6 +17,7 @@ use Filament\Tables\Columns\TextColumn; use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Table; +use Illuminate\Database\Eloquent\Builder; class TimeEntryResource extends Resource { @@ -51,15 +53,23 @@ public static function form(Form $form): Form ->rules([ 'after_or_equal:start', ]), - Select::make('user_id') - ->relationship(name: 'user', titleAttribute: 'email') - ->searchable(['name', 'email']) + Select::make('member_id') + ->relationship( + name: 'member', + titleAttribute: 'id', + modifyQueryUsing: fn (Builder $query) => $query->with(['user', 'organization']) + ) + ->getOptionLabelFromRecordUsing(fn (Member $record): string => $record->user->email.' ('.$record->organization->name.')') + ->searchable() ->required(), Select::make('project_id') ->relationship(name: 'project', titleAttribute: 'name') ->searchable(['name']) ->nullable(), - // TODO + Select::make('task_id') + ->relationship(name: 'task', titleAttribute: 'name') + ->searchable(['name']) + ->nullable(), ]); } @@ -83,11 +93,11 @@ public static function table(Table $table): Table ($record->end?->toDateTimeString('minute') ?? '...').')'; }) ->label('Time'), - Tables\Columns\TextColumn::make('organization.name') + TextColumn::make('organization.name') ->sortable(), - Tables\Columns\TextColumn::make('created_at') + TextColumn::make('created_at') ->sortable(), - Tables\Columns\TextColumn::make('updated_at') + TextColumn::make('updated_at') ->sortable(), ]) ->filters([ diff --git a/app/Filament/Resources/TimeEntryResource/Pages/CreateTimeEntry.php b/app/Filament/Resources/TimeEntryResource/Pages/CreateTimeEntry.php index 35a85daf..a8bb9498 100644 --- a/app/Filament/Resources/TimeEntryResource/Pages/CreateTimeEntry.php +++ b/app/Filament/Resources/TimeEntryResource/Pages/CreateTimeEntry.php @@ -5,9 +5,28 @@ namespace App\Filament\Resources\TimeEntryResource\Pages; use App\Filament\Resources\TimeEntryResource; +use App\Models\Member; use Filament\Resources\Pages\CreateRecord; class CreateTimeEntry extends CreateRecord { protected static string $resource = TimeEntryResource::class; + + /** + * @param array $data + * @return array + */ + protected function mutateFormDataBeforeCreate(array $data): array + { + if (isset($data['member_id'])) { + /** @var Member|null $member */ + $member = Member::query()->find($data['member_id']); + if ($member !== null) { + $data['user_id'] = $member->user_id; + $data['organization_id'] = $member->organization_id; + } + } + + return $data; + } } diff --git a/app/Filament/Resources/TimeEntryResource/Pages/EditTimeEntry.php b/app/Filament/Resources/TimeEntryResource/Pages/EditTimeEntry.php index 96105ecd..f8ef533f 100644 --- a/app/Filament/Resources/TimeEntryResource/Pages/EditTimeEntry.php +++ b/app/Filament/Resources/TimeEntryResource/Pages/EditTimeEntry.php @@ -5,6 +5,7 @@ namespace App\Filament\Resources\TimeEntryResource\Pages; use App\Filament\Resources\TimeEntryResource; +use App\Models\Member; use Filament\Actions; use Filament\Resources\Pages\EditRecord; @@ -19,4 +20,22 @@ protected function getHeaderActions(): array ->icon('heroicon-m-trash'), ]; } + + /** + * @param array $data + * @return array + */ + protected function mutateFormDataBeforeSave(array $data): array + { + if (isset($data['member_id'])) { + /** @var Member|null $member */ + $member = Member::query()->find($data['member_id']); + if ($member !== null) { + $data['user_id'] = $member->user_id; + $data['organization_id'] = $member->organization_id; + } + } + + return $data; + } } diff --git a/app/Filament/Resources/UserResource.php b/app/Filament/Resources/UserResource.php index c68dcf41..31a04888 100644 --- a/app/Filament/Resources/UserResource.php +++ b/app/Filament/Resources/UserResource.php @@ -47,17 +47,17 @@ public static function form(Form $form): Form return $form ->columns(1) ->schema([ - Forms\Components\TextInput::make('id') + TextInput::make('id') ->label('ID') ->disabled() ->visibleOn(['update', 'show']) ->readOnly() ->maxLength(255), - Forms\Components\TextInput::make('name') + TextInput::make('name') ->label('Name') ->required() ->maxLength(255), - Forms\Components\TextInput::make('email') + TextInput::make('email') ->label('Email') ->required() ->rules($record?->is_placeholder ? [] : [ diff --git a/app/Http/Controllers/Api/V1/ApiTokenController.php b/app/Http/Controllers/Api/V1/ApiTokenController.php index 2dea2694..1fbf12d8 100644 --- a/app/Http/Controllers/Api/V1/ApiTokenController.php +++ b/app/Http/Controllers/Api/V1/ApiTokenController.php @@ -35,6 +35,7 @@ public function index(): ApiTokenCollection /** @var Builder $query */ $query->whereJsonContains('grant_types', 'personal_access'); }) + ->orderBy('created_at', 'desc') ->get(); return new ApiTokenCollection($tokens); diff --git a/app/Http/Controllers/Api/V1/ChartController.php b/app/Http/Controllers/Api/V1/ChartController.php index 3e034df9..167b9ed1 100644 --- a/app/Http/Controllers/Api/V1/ChartController.php +++ b/app/Http/Controllers/Api/V1/ChartController.php @@ -102,7 +102,7 @@ public function dailyTrackedHours(Organization $organization, DashboardService $ $this->checkPermission($organization, 'charts:view:own'); $user = $this->user(); - $dailyTrackedHours = $dashboardService->getDailyTrackedHours($user, $organization, 60); + $dailyTrackedHours = $dashboardService->getDailyTrackedHours($user, $organization, 100); return response()->json($dailyTrackedHours); } diff --git a/app/Http/Controllers/Api/V1/InvitationController.php b/app/Http/Controllers/Api/V1/InvitationController.php index 426a4c59..4e7a0dcd 100644 --- a/app/Http/Controllers/Api/V1/InvitationController.php +++ b/app/Http/Controllers/Api/V1/InvitationController.php @@ -41,6 +41,7 @@ public function index(Organization $organization, InvitationIndexRequest $reques $this->checkPermission($organization, 'invitations:view'); $invitations = $organization->teamInvitations() + ->orderBy('created_at', 'desc') ->paginate(config('app.pagination_per_page_default')); return InvitationCollection::make($invitations); diff --git a/app/Http/Controllers/Api/V1/MemberController.php b/app/Http/Controllers/Api/V1/MemberController.php index c4d55a50..6dde1b6e 100644 --- a/app/Http/Controllers/Api/V1/MemberController.php +++ b/app/Http/Controllers/Api/V1/MemberController.php @@ -60,6 +60,7 @@ public function index(Organization $organization, MemberIndexRequest $request): $members = Member::query() ->whereBelongsTo($organization, 'organization') ->with(['user']) + ->orderBy('created_at', 'desc') ->paginate(config('app.pagination_per_page_default')); return MemberCollection::make($members); diff --git a/app/Http/Controllers/Api/V1/ProjectController.php b/app/Http/Controllers/Api/V1/ProjectController.php index 72ee24f7..060b836f 100644 --- a/app/Http/Controllers/Api/V1/ProjectController.php +++ b/app/Http/Controllers/Api/V1/ProjectController.php @@ -60,7 +60,9 @@ public function index(Organization $organization, ProjectIndexRequest $request): $projectsQuery->whereNull('archived_at'); } - $projects = $projectsQuery->paginate(config('app.pagination_per_page_default')); + $projects = $projectsQuery + ->orderBy('created_at', 'desc') + ->paginate(config('app.pagination_per_page_default')); $showBillableRate = $this->member($organization)->role !== Role::Employee->value || $organization->employees_can_see_billable_rates; diff --git a/app/Http/Controllers/Api/V1/ProjectMemberController.php b/app/Http/Controllers/Api/V1/ProjectMemberController.php index dfad9137..6e981827 100644 --- a/app/Http/Controllers/Api/V1/ProjectMemberController.php +++ b/app/Http/Controllers/Api/V1/ProjectMemberController.php @@ -6,6 +6,7 @@ use App\Exceptions\Api\InactiveUserCanNotBeUsedApiException; use App\Exceptions\Api\UserIsAlreadyMemberOfProjectApiException; +use App\Http\Requests\V1\ProjectMember\ProjectMemberIndexRequest; use App\Http\Requests\V1\ProjectMember\ProjectMemberStoreRequest; use App\Http\Requests\V1\ProjectMember\ProjectMemberUpdateRequest; use App\Http\Resources\V1\ProjectMember\ProjectMemberCollection; @@ -41,12 +42,13 @@ protected function checkPermission(Organization $organization, string $permissio * * @operationId getProjectMembers */ - public function index(Organization $organization, Project $project): ProjectMemberCollection + public function index(Organization $organization, Project $project, ProjectMemberIndexRequest $request): ProjectMemberCollection { $this->checkPermission($organization, 'project-members:view', $project); $projectMembers = ProjectMember::query() ->whereBelongsTo($project, 'project') + ->orderBy('created_at', 'desc') ->paginate(config('app.pagination_per_page_default')); return new ProjectMemberCollection($projectMembers); diff --git a/app/Http/Controllers/Api/V1/ReportController.php b/app/Http/Controllers/Api/V1/ReportController.php index b8f2fd21..1f89fa01 100644 --- a/app/Http/Controllers/Api/V1/ReportController.php +++ b/app/Http/Controllers/Api/V1/ReportController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers\Api\V1; use App\Enums\Weekday; +use App\Http\Requests\V1\Report\ReportIndexRequest; use App\Http\Requests\V1\Report\ReportStoreRequest; use App\Http\Requests\V1\Report\ReportUpdateRequest; use App\Http\Resources\V1\Report\DetailedReportResource; @@ -40,7 +41,7 @@ protected function checkPermission(Organization $organization, string $permissio * * @operationId getReports */ - public function index(Organization $organization): ReportCollection + public function index(Organization $organization, ReportIndexRequest $request): ReportCollection { $this->checkPermission($organization, 'reports:view'); @@ -150,6 +151,9 @@ public function update(Organization $organization, Report $report, ReportUpdateR $report->share_secret = null; $report->public_until = null; } + } elseif ($report->is_public && $request->has('public_until')) { + // Allow updating expiration date on already-public reports + $report->public_until = $request->getPublicUntil(); } $report->save(); diff --git a/app/Http/Controllers/Api/V1/TagController.php b/app/Http/Controllers/Api/V1/TagController.php index 22b6d0a2..2dd17424 100644 --- a/app/Http/Controllers/Api/V1/TagController.php +++ b/app/Http/Controllers/Api/V1/TagController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers\Api\V1; use App\Exceptions\Api\EntityStillInUseApiException; +use App\Http\Requests\V1\Tag\TagIndexRequest; use App\Http\Requests\V1\Tag\TagStoreRequest; use App\Http\Requests\V1\Tag\TagUpdateRequest; use App\Http\Resources\V1\Tag\TagCollection; @@ -34,7 +35,7 @@ protected function checkPermission(Organization $organization, string $permissio * * @throws AuthorizationException */ - public function index(Organization $organization): TagCollection + public function index(Organization $organization, TagIndexRequest $request): TagCollection { $this->checkPermission($organization, 'tags:view'); diff --git a/app/Http/Controllers/Api/V1/TaskController.php b/app/Http/Controllers/Api/V1/TaskController.php index 68a371ed..fe554110 100644 --- a/app/Http/Controllers/Api/V1/TaskController.php +++ b/app/Http/Controllers/Api/V1/TaskController.php @@ -82,7 +82,9 @@ public function index(Organization $organization, TaskIndexRequest $request): Ta $query->whereNull('done_at'); } - $tasks = $query->paginate(config('app.pagination_per_page_default')); + $tasks = $query + ->orderBy('created_at', 'desc') + ->paginate(config('app.pagination_per_page_default')); return new TaskCollection($tasks); } diff --git a/app/Http/Controllers/Api/V1/TimeEntryController.php b/app/Http/Controllers/Api/V1/TimeEntryController.php index ed1ca073..af9fb401 100644 --- a/app/Http/Controllers/Api/V1/TimeEntryController.php +++ b/app/Http/Controllers/Api/V1/TimeEntryController.php @@ -58,7 +58,7 @@ class TimeEntryController extends Controller { - private function assertNoOverlap(Organization $organization, Member $member, \Illuminate\Support\Carbon $start, ?\Illuminate\Support\Carbon $end, ?TimeEntry $exclude = null): void + private function assertNoOverlap(Organization $organization, Member $member, Carbon $start, ?Carbon $end, ?TimeEntry $exclude = null): void { if (! $organization->prevent_overlapping_time_entries) { return; diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 236acc79..4d9af16b 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -4,9 +4,37 @@ namespace App\Http; +use App\Http\Middleware\Authenticate; use App\Http\Middleware\CheckOrganizationBlocked; +use App\Http\Middleware\EncryptCookies; +use App\Http\Middleware\EnsureEmailIsVerified; +use App\Http\Middleware\ForceHttps; use App\Http\Middleware\ForceJsonResponse; +use App\Http\Middleware\HandleInertiaRequests; +use App\Http\Middleware\PreventRequestsDuringMaintenance; +use App\Http\Middleware\RedirectIfAuthenticated; +use App\Http\Middleware\ShareInertiaData; +use App\Http\Middleware\TrimStrings; +use App\Http\Middleware\TrustProxies; +use App\Http\Middleware\ValidateSignature; +use App\Http\Middleware\VerifyCsrfToken; +use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth; +use Illuminate\Auth\Middleware\Authorize; +use Illuminate\Auth\Middleware\RequirePassword; +use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; use Illuminate\Foundation\Http\Kernel as HttpKernel; +use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull; +use Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests; +use Illuminate\Foundation\Http\Middleware\ValidatePostSize; +use Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets; +use Illuminate\Http\Middleware\HandleCors; +use Illuminate\Http\Middleware\SetCacheHeaders; +use Illuminate\Routing\Middleware\SubstituteBindings; +use Illuminate\Routing\Middleware\ThrottleRequests; +use Illuminate\Session\Middleware\AuthenticateSession; +use Illuminate\Session\Middleware\StartSession; +use Illuminate\View\Middleware\ShareErrorsFromSession; +use Laravel\Passport\Http\Middleware\CreateFreshApiToken; class Kernel extends HttpKernel { @@ -18,13 +46,13 @@ class Kernel extends HttpKernel * @var array */ protected $middleware = [ - \App\Http\Middleware\ForceHttps::class, - \App\Http\Middleware\TrustProxies::class, - \Illuminate\Http\Middleware\HandleCors::class, - \App\Http\Middleware\PreventRequestsDuringMaintenance::class, - \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, - \App\Http\Middleware\TrimStrings::class, - \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, + ForceHttps::class, + TrustProxies::class, + HandleCors::class, + PreventRequestsDuringMaintenance::class, + ValidatePostSize::class, + TrimStrings::class, + ConvertEmptyStringsToNull::class, ]; /** @@ -34,21 +62,21 @@ class Kernel extends HttpKernel */ protected $middlewareGroups = [ 'web' => [ - \App\Http\Middleware\EncryptCookies::class, - \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, - \Illuminate\Session\Middleware\StartSession::class, - \Illuminate\View\Middleware\ShareErrorsFromSession::class, - \App\Http\Middleware\VerifyCsrfToken::class, - \Illuminate\Routing\Middleware\SubstituteBindings::class, - \App\Http\Middleware\HandleInertiaRequests::class, - \App\Http\Middleware\ShareInertiaData::class, - \Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class, - \Laravel\Passport\Http\Middleware\CreateFreshApiToken::class, + EncryptCookies::class, + AddQueuedCookiesToResponse::class, + StartSession::class, + ShareErrorsFromSession::class, + VerifyCsrfToken::class, + SubstituteBindings::class, + HandleInertiaRequests::class, + ShareInertiaData::class, + AddLinkHeadersForPreloadedAssets::class, + CreateFreshApiToken::class, ], 'api' => [ - \Illuminate\Routing\Middleware\ThrottleRequests::class.':api', - \Illuminate\Routing\Middleware\SubstituteBindings::class, + ThrottleRequests::class.':api', + SubstituteBindings::class, ForceJsonResponse::class, ], @@ -64,17 +92,17 @@ class Kernel extends HttpKernel * @var array */ protected $middlewareAliases = [ - 'auth' => \App\Http\Middleware\Authenticate::class, - 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, - 'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class, - 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, - 'can' => \Illuminate\Auth\Middleware\Authorize::class, - 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, - 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, - 'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class, - 'signed' => \App\Http\Middleware\ValidateSignature::class, - 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, - 'verified' => \App\Http\Middleware\EnsureEmailIsVerified::class, + 'auth' => Authenticate::class, + 'auth.basic' => AuthenticateWithBasicAuth::class, + 'auth.session' => AuthenticateSession::class, + 'cache.headers' => SetCacheHeaders::class, + 'can' => Authorize::class, + 'guest' => RedirectIfAuthenticated::class, + 'password.confirm' => RequirePassword::class, + 'precognitive' => HandlePrecognitiveRequests::class, + 'signed' => ValidateSignature::class, + 'throttle' => ThrottleRequests::class, + 'verified' => EnsureEmailIsVerified::class, 'check-organization-blocked' => CheckOrganizationBlocked::class, ]; } diff --git a/app/Http/Middleware/ForceHttps.php b/app/Http/Middleware/ForceHttps.php index 2b23e315..07292411 100644 --- a/app/Http/Middleware/ForceHttps.php +++ b/app/Http/Middleware/ForceHttps.php @@ -14,7 +14,7 @@ class ForceHttps /** * Handle an incoming request. * - * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next + * @param Closure(Request): (Response) $next */ public function handle(Request $request, Closure $next, string ...$guards): Response { diff --git a/app/Http/Middleware/ForceJsonResponse.php b/app/Http/Middleware/ForceJsonResponse.php index bae7216c..f3f27d30 100644 --- a/app/Http/Middleware/ForceJsonResponse.php +++ b/app/Http/Middleware/ForceJsonResponse.php @@ -13,7 +13,7 @@ class ForceJsonResponse /** * Handle an incoming request. * - * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next + * @param Closure(Request): (Response) $next */ public function handle(Request $request, Closure $next, string ...$guards): Response { diff --git a/app/Http/Middleware/RedirectIfAuthenticated.php b/app/Http/Middleware/RedirectIfAuthenticated.php index 67666da3..8c37fae3 100644 --- a/app/Http/Middleware/RedirectIfAuthenticated.php +++ b/app/Http/Middleware/RedirectIfAuthenticated.php @@ -15,7 +15,7 @@ class RedirectIfAuthenticated /** * Handle an incoming request. * - * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next + * @param Closure(Request): (Response) $next */ public function handle(Request $request, Closure $next, string ...$guards): Response { diff --git a/app/Http/Requests/V1/Invitation/InvitationIndexRequest.php b/app/Http/Requests/V1/Invitation/InvitationIndexRequest.php index 5c013c4e..94d896eb 100644 --- a/app/Http/Requests/V1/Invitation/InvitationIndexRequest.php +++ b/app/Http/Requests/V1/Invitation/InvitationIndexRequest.php @@ -21,6 +21,11 @@ class InvitationIndexRequest extends BaseFormRequest public function rules(): array { return [ + 'page' => [ + 'integer', + 'min:1', + 'max:2147483647', + ], ]; } } diff --git a/app/Http/Requests/V1/Member/MemberIndexRequest.php b/app/Http/Requests/V1/Member/MemberIndexRequest.php index 706dc3c7..b3d350aa 100644 --- a/app/Http/Requests/V1/Member/MemberIndexRequest.php +++ b/app/Http/Requests/V1/Member/MemberIndexRequest.php @@ -21,6 +21,11 @@ class MemberIndexRequest extends BaseFormRequest public function rules(): array { return [ + 'page' => [ + 'integer', + 'min:1', + 'max:2147483647', + ], ]; } } diff --git a/app/Http/Requests/V1/Member/MemberMergeIntoRequest.php b/app/Http/Requests/V1/Member/MemberMergeIntoRequest.php index cf63ae28..409c3bb2 100644 --- a/app/Http/Requests/V1/Member/MemberMergeIntoRequest.php +++ b/app/Http/Requests/V1/Member/MemberMergeIntoRequest.php @@ -7,6 +7,7 @@ use App\Http\Requests\V1\BaseFormRequest; use App\Models\Member; use App\Models\Organization; +use Illuminate\Contracts\Validation\Rule; use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Database\Eloquent\Builder; use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent; @@ -19,7 +20,7 @@ class MemberMergeIntoRequest extends BaseFormRequest /** * Get the validation rules that apply to the request. * - * @return array> + * @return array> */ public function rules(): array { diff --git a/app/Http/Requests/V1/ProjectMember/ProjectMemberIndexRequest.php b/app/Http/Requests/V1/ProjectMember/ProjectMemberIndexRequest.php new file mode 100644 index 00000000..169933c0 --- /dev/null +++ b/app/Http/Requests/V1/ProjectMember/ProjectMemberIndexRequest.php @@ -0,0 +1,27 @@ +> + */ + public function rules(): array + { + return [ + 'page' => [ + 'integer', + 'min:1', + 'max:2147483647', + ], + ]; + } +} diff --git a/app/Http/Requests/V1/Report/ReportIndexRequest.php b/app/Http/Requests/V1/Report/ReportIndexRequest.php new file mode 100644 index 00000000..cde3289e --- /dev/null +++ b/app/Http/Requests/V1/Report/ReportIndexRequest.php @@ -0,0 +1,27 @@ +> + */ + public function rules(): array + { + return [ + 'page' => [ + 'integer', + 'min:1', + 'max:2147483647', + ], + ]; + } +} diff --git a/app/Http/Requests/V1/Report/ReportStoreRequest.php b/app/Http/Requests/V1/Report/ReportStoreRequest.php index da609ba1..443bf01c 100644 --- a/app/Http/Requests/V1/Report/ReportStoreRequest.php +++ b/app/Http/Requests/V1/Report/ReportStoreRequest.php @@ -10,9 +10,11 @@ use App\Enums\Weekday; use App\Http\Requests\V1\BaseFormRequest; use App\Models\Organization; +use App\Service\TimeEntryFilter; use Illuminate\Contracts\Validation\Rule as LegacyValidationRule; use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Support\Carbon; +use Illuminate\Support\Str; use Illuminate\Validation\Rule; /** @@ -23,7 +25,7 @@ class ReportStoreRequest extends BaseFormRequest /** * Get the validation rules that apply to the request. * - * @return array> + * @return array> */ public function rules(): array { @@ -81,7 +83,14 @@ public function rules(): array ], 'properties.client_ids.*' => [ 'string', - 'uuid', + function (string $attribute, mixed $value, \Closure $fail): void { + if ($value === TimeEntryFilter::NONE_VALUE) { + return; + } + if (! Str::isUuid($value)) { + $fail('The '.$attribute.' must be a valid UUID.'); + } + }, ], // Filter by project IDs, project IDs are OR combined 'properties.project_ids' => [ @@ -90,7 +99,14 @@ public function rules(): array ], 'properties.project_ids.*' => [ 'string', - 'uuid', + function (string $attribute, mixed $value, \Closure $fail): void { + if ($value === TimeEntryFilter::NONE_VALUE) { + return; + } + if (! Str::isUuid($value)) { + $fail('The '.$attribute.' must be a valid UUID.'); + } + }, ], // Filter by tag IDs, tag IDs are OR combined 'properties.tag_ids' => [ @@ -99,7 +115,14 @@ public function rules(): array ], 'properties.tag_ids.*' => [ 'string', - 'uuid', + function (string $attribute, mixed $value, \Closure $fail): void { + if ($value === TimeEntryFilter::NONE_VALUE) { + return; + } + if (! Str::isUuid($value)) { + $fail('The '.$attribute.' must be a valid UUID.'); + } + }, ], 'properties.task_ids' => [ 'nullable', @@ -107,7 +130,14 @@ public function rules(): array ], 'properties.task_ids.*' => [ 'string', - 'uuid', + function (string $attribute, mixed $value, \Closure $fail): void { + if ($value === TimeEntryFilter::NONE_VALUE) { + return; + } + if (! Str::isUuid($value)) { + $fail('The '.$attribute.' must be a valid UUID.'); + } + }, ], 'properties.group' => [ 'required', diff --git a/app/Http/Requests/V1/Tag/TagIndexRequest.php b/app/Http/Requests/V1/Tag/TagIndexRequest.php new file mode 100644 index 00000000..e1f706b9 --- /dev/null +++ b/app/Http/Requests/V1/Tag/TagIndexRequest.php @@ -0,0 +1,27 @@ +> + */ + public function rules(): array + { + return [ + 'page' => [ + 'integer', + 'min:1', + 'max:2147483647', + ], + ]; + } +} diff --git a/app/Http/Requests/V1/Task/TaskIndexRequest.php b/app/Http/Requests/V1/Task/TaskIndexRequest.php index 6fd80307..e9305b09 100644 --- a/app/Http/Requests/V1/Task/TaskIndexRequest.php +++ b/app/Http/Requests/V1/Task/TaskIndexRequest.php @@ -26,6 +26,11 @@ class TaskIndexRequest extends BaseFormRequest public function rules(): array { return [ + 'page' => [ + 'integer', + 'min:1', + 'max:2147483647', + ], 'project_id' => [ ExistsEloquent::make(Project::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ diff --git a/app/Http/Requests/V1/TimeEntry/TimeEntryAggregateExportRequest.php b/app/Http/Requests/V1/TimeEntry/TimeEntryAggregateExportRequest.php index 35f84519..a356198c 100644 --- a/app/Http/Requests/V1/TimeEntry/TimeEntryAggregateExportRequest.php +++ b/app/Http/Requests/V1/TimeEntry/TimeEntryAggregateExportRequest.php @@ -16,6 +16,7 @@ use App\Models\Tag; use App\Models\Task; use App\Models\User; +use App\Service\TimeEntryFilter; use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Carbon; @@ -30,7 +31,7 @@ class TimeEntryAggregateExportRequest extends BaseFormRequest /** * Get the validation rules that apply to the request. * - * @return array> + * @return array> */ public function rules(): array { @@ -94,10 +95,15 @@ public function rules(): array ], 'project_ids.*' => [ 'string', - ExistsEloquent::make(Project::class, null, function (Builder $builder): Builder { - /** @var Builder $builder */ - return $builder->whereBelongsTo($this->organization, 'organization'); - })->uuid(), + function (string $attribute, mixed $value, \Closure $fail): void { + if ($value === TimeEntryFilter::NONE_VALUE) { + return; + } + ExistsEloquent::make(Project::class, null, function (Builder $builder): Builder { + /** @var Builder $builder */ + return $builder->whereBelongsTo($this->organization, 'organization'); + })->uuid()->validate($attribute, $value, $fail); + }, ], // Filter by client IDs, client IDs are OR combined 'client_ids' => [ @@ -106,10 +112,15 @@ public function rules(): array ], 'client_ids.*' => [ 'string', - ExistsEloquent::make(Client::class, null, function (Builder $builder): Builder { - /** @var Builder $builder */ - return $builder->whereBelongsTo($this->organization, 'organization'); - })->uuid(), + function (string $attribute, mixed $value, \Closure $fail): void { + if ($value === TimeEntryFilter::NONE_VALUE) { + return; + } + ExistsEloquent::make(Client::class, null, function (Builder $builder): Builder { + /** @var Builder $builder */ + return $builder->whereBelongsTo($this->organization, 'organization'); + })->uuid()->validate($attribute, $value, $fail); + }, ], // Filter by tag IDs, tag IDs are OR combined 'tag_ids' => [ @@ -118,10 +129,15 @@ public function rules(): array ], 'tag_ids.*' => [ 'string', - ExistsEloquent::make(Tag::class, null, function (Builder $builder): Builder { - /** @var Builder $builder */ - return $builder->whereBelongsTo($this->organization, 'organization'); - })->uuid(), + function (string $attribute, mixed $value, \Closure $fail): void { + if ($value === TimeEntryFilter::NONE_VALUE) { + return; + } + ExistsEloquent::make(Tag::class, null, function (Builder $builder): Builder { + /** @var Builder $builder */ + return $builder->whereBelongsTo($this->organization, 'organization'); + })->uuid()->validate($attribute, $value, $fail); + }, ], // Filter by task IDs, task IDs are OR combined 'task_ids' => [ @@ -130,9 +146,14 @@ public function rules(): array ], 'task_ids.*' => [ 'string', - ExistsEloquent::make(Task::class, null, function (Builder $builder): Builder { - return $builder->whereBelongsTo($this->organization, 'organization'); - })->uuid(), + function (string $attribute, mixed $value, \Closure $fail): void { + if ($value === TimeEntryFilter::NONE_VALUE) { + return; + } + ExistsEloquent::make(Task::class, null, function (Builder $builder): Builder { + return $builder->whereBelongsTo($this->organization, 'organization'); + })->uuid()->validate($attribute, $value, $fail); + }, ], // Filter only time entries that have a start date after the given timestamp in UTC (example: 2021-01-01T00:00:00Z) 'start' => [ diff --git a/app/Http/Requests/V1/TimeEntry/TimeEntryAggregateRequest.php b/app/Http/Requests/V1/TimeEntry/TimeEntryAggregateRequest.php index 39c9270e..92378f82 100644 --- a/app/Http/Requests/V1/TimeEntry/TimeEntryAggregateRequest.php +++ b/app/Http/Requests/V1/TimeEntry/TimeEntryAggregateRequest.php @@ -14,6 +14,7 @@ use App\Models\Tag; use App\Models\Task; use App\Models\User; +use App\Service\TimeEntryFilter; use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Carbon; @@ -28,7 +29,7 @@ class TimeEntryAggregateRequest extends BaseFormRequest /** * Get the validation rules that apply to the request. * - * @return array> + * @return array> */ public function rules(): array { @@ -80,10 +81,15 @@ public function rules(): array ], 'project_ids.*' => [ 'string', - ExistsEloquent::make(Project::class, null, function (Builder $builder): Builder { - /** @var Builder $builder */ - return $builder->whereBelongsTo($this->organization, 'organization'); - })->uuid(), + function (string $attribute, mixed $value, \Closure $fail): void { + if ($value === TimeEntryFilter::NONE_VALUE) { + return; + } + ExistsEloquent::make(Project::class, null, function (Builder $builder): Builder { + /** @var Builder $builder */ + return $builder->whereBelongsTo($this->organization, 'organization'); + })->uuid()->validate($attribute, $value, $fail); + }, ], // Filter by client IDs, client IDs are OR combined 'client_ids' => [ @@ -92,10 +98,15 @@ public function rules(): array ], 'client_ids.*' => [ 'string', - ExistsEloquent::make(Client::class, null, function (Builder $builder): Builder { - /** @var Builder $builder */ - return $builder->whereBelongsTo($this->organization, 'organization'); - })->uuid(), + function (string $attribute, mixed $value, \Closure $fail): void { + if ($value === TimeEntryFilter::NONE_VALUE) { + return; + } + ExistsEloquent::make(Client::class, null, function (Builder $builder): Builder { + /** @var Builder $builder */ + return $builder->whereBelongsTo($this->organization, 'organization'); + })->uuid()->validate($attribute, $value, $fail); + }, ], // Filter by tag IDs, tag IDs are OR combined 'tag_ids' => [ @@ -104,10 +115,15 @@ public function rules(): array ], 'tag_ids.*' => [ 'string', - ExistsEloquent::make(Tag::class, null, function (Builder $builder): Builder { - /** @var Builder $builder */ - return $builder->whereBelongsTo($this->organization, 'organization'); - })->uuid(), + function (string $attribute, mixed $value, \Closure $fail): void { + if ($value === TimeEntryFilter::NONE_VALUE) { + return; + } + ExistsEloquent::make(Tag::class, null, function (Builder $builder): Builder { + /** @var Builder $builder */ + return $builder->whereBelongsTo($this->organization, 'organization'); + })->uuid()->validate($attribute, $value, $fail); + }, ], // Filter by task IDs, task IDs are OR combined 'task_ids' => [ @@ -116,9 +132,14 @@ public function rules(): array ], 'task_ids.*' => [ 'string', - ExistsEloquent::make(Task::class, null, function (Builder $builder): Builder { - return $builder->whereBelongsTo($this->organization, 'organization'); - })->uuid(), + function (string $attribute, mixed $value, \Closure $fail): void { + if ($value === TimeEntryFilter::NONE_VALUE) { + return; + } + ExistsEloquent::make(Task::class, null, function (Builder $builder): Builder { + return $builder->whereBelongsTo($this->organization, 'organization'); + })->uuid()->validate($attribute, $value, $fail); + }, ], // Filter only time entries that have a start date after the given timestamp in UTC (example: 2021-01-01T00:00:00Z) 'start' => [ diff --git a/app/Http/Requests/V1/TimeEntry/TimeEntryIndexExportRequest.php b/app/Http/Requests/V1/TimeEntry/TimeEntryIndexExportRequest.php index 6c3180b2..70447609 100644 --- a/app/Http/Requests/V1/TimeEntry/TimeEntryIndexExportRequest.php +++ b/app/Http/Requests/V1/TimeEntry/TimeEntryIndexExportRequest.php @@ -6,11 +6,13 @@ use App\Enums\ExportFormat; use App\Enums\TimeEntryRoundingType; +use App\Models\Client; use App\Models\Member; use App\Models\Organization; use App\Models\Project; use App\Models\Tag; use App\Models\Task; +use App\Service\TimeEntryFilter; use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Carbon; @@ -25,7 +27,7 @@ class TimeEntryIndexExportRequest extends TimeEntryIndexRequest /** * Get the validation rules that apply to the request. * - * @return array> + * @return array> */ public function rules(): array { @@ -57,6 +59,23 @@ public function rules(): array return $builder->whereBelongsTo($this->organization, 'organization'); }), ], + // Filter by client IDs, client IDs are OR combined + 'client_ids' => [ + 'array', + 'min:1', + ], + 'client_ids.*' => [ + 'string', + function (string $attribute, mixed $value, \Closure $fail): void { + if ($value === TimeEntryFilter::NONE_VALUE) { + return; + } + ExistsEloquent::make(Client::class, null, function (Builder $builder): Builder { + /** @var Builder $builder */ + return $builder->whereBelongsTo($this->organization, 'organization'); + })->uuid()->validate($attribute, $value, $fail); + }, + ], // Filter by project IDs, project IDs are OR combined 'project_ids' => [ 'array', @@ -64,11 +83,15 @@ public function rules(): array ], 'project_ids.*' => [ 'string', - 'uuid', - new ExistsEloquent(Project::class, null, function (Builder $builder): Builder { - /** @var Builder $builder */ - return $builder->whereBelongsTo($this->organization, 'organization'); - }), + function (string $attribute, mixed $value, \Closure $fail): void { + if ($value === TimeEntryFilter::NONE_VALUE) { + return; + } + ExistsEloquent::make(Project::class, null, function (Builder $builder): Builder { + /** @var Builder $builder */ + return $builder->whereBelongsTo($this->organization, 'organization'); + })->uuid()->validate($attribute, $value, $fail); + }, ], // Filter by tag IDs, tag IDs are OR combined 'tag_ids' => [ @@ -77,11 +100,15 @@ public function rules(): array ], 'tag_ids.*' => [ 'string', - 'uuid', - new ExistsEloquent(Tag::class, null, function (Builder $builder): Builder { - /** @var Builder $builder */ - return $builder->whereBelongsTo($this->organization, 'organization'); - }), + function (string $attribute, mixed $value, \Closure $fail): void { + if ($value === TimeEntryFilter::NONE_VALUE) { + return; + } + ExistsEloquent::make(Tag::class, null, function (Builder $builder): Builder { + /** @var Builder $builder */ + return $builder->whereBelongsTo($this->organization, 'organization'); + })->uuid()->validate($attribute, $value, $fail); + }, ], // Filter by task IDs, task IDs are OR combined 'task_ids' => [ @@ -90,11 +117,15 @@ public function rules(): array ], 'task_ids.*' => [ 'string', - 'uuid', - new ExistsEloquent(Task::class, null, function (Builder $builder): Builder { - /** @var Builder $builder */ - return $builder->whereBelongsTo($this->organization, 'organization'); - }), + function (string $attribute, mixed $value, \Closure $fail): void { + if ($value === TimeEntryFilter::NONE_VALUE) { + return; + } + ExistsEloquent::make(Task::class, null, function (Builder $builder): Builder { + /** @var Builder $builder */ + return $builder->whereBelongsTo($this->organization, 'organization'); + })->uuid()->validate($attribute, $value, $fail); + }, ], // Filter only time entries that have a start date after the given timestamp in UTC (example: 2021-01-01T00:00:00Z) 'start' => [ diff --git a/app/Http/Requests/V1/TimeEntry/TimeEntryIndexRequest.php b/app/Http/Requests/V1/TimeEntry/TimeEntryIndexRequest.php index 2c6dd61e..230e5134 100644 --- a/app/Http/Requests/V1/TimeEntry/TimeEntryIndexRequest.php +++ b/app/Http/Requests/V1/TimeEntry/TimeEntryIndexRequest.php @@ -12,6 +12,7 @@ use App\Models\Project; use App\Models\Tag; use App\Models\Task; +use App\Service\TimeEntryFilter; use Illuminate\Contracts\Validation\Rule as RuleContract; use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Database\Eloquent\Builder; @@ -26,7 +27,7 @@ class TimeEntryIndexRequest extends BaseFormRequest /** * Get the validation rules that apply to the request. * - * @return array> + * @return array> */ public function rules(): array { @@ -58,10 +59,15 @@ public function rules(): array ], 'client_ids.*' => [ 'string', - ExistsEloquent::make(Client::class, null, function (Builder $builder): Builder { - /** @var Builder $builder */ - return $builder->whereBelongsTo($this->organization, 'organization'); - })->uuid(), + function (string $attribute, mixed $value, \Closure $fail): void { + if ($value === TimeEntryFilter::NONE_VALUE) { + return; + } + ExistsEloquent::make(Client::class, null, function (Builder $builder): Builder { + /** @var Builder $builder */ + return $builder->whereBelongsTo($this->organization, 'organization'); + })->uuid()->validate($attribute, $value, $fail); + }, ], // Filter by project IDs, project IDs are OR combined 'project_ids' => [ @@ -70,10 +76,15 @@ public function rules(): array ], 'project_ids.*' => [ 'string', - ExistsEloquent::make(Project::class, null, function (Builder $builder): Builder { - /** @var Builder $builder */ - return $builder->whereBelongsTo($this->organization, 'organization'); - })->uuid(), + function (string $attribute, mixed $value, \Closure $fail): void { + if ($value === TimeEntryFilter::NONE_VALUE) { + return; + } + ExistsEloquent::make(Project::class, null, function (Builder $builder): Builder { + /** @var Builder $builder */ + return $builder->whereBelongsTo($this->organization, 'organization'); + })->uuid()->validate($attribute, $value, $fail); + }, ], // Filter by tag IDs, tag IDs are OR combined 'tag_ids' => [ @@ -82,10 +93,15 @@ public function rules(): array ], 'tag_ids.*' => [ 'string', - ExistsEloquent::make(Tag::class, null, function (Builder $builder): Builder { - /** @var Builder $builder */ - return $builder->whereBelongsTo($this->organization, 'organization'); - })->uuid(), + function (string $attribute, mixed $value, \Closure $fail): void { + if ($value === TimeEntryFilter::NONE_VALUE) { + return; + } + ExistsEloquent::make(Tag::class, null, function (Builder $builder): Builder { + /** @var Builder $builder */ + return $builder->whereBelongsTo($this->organization, 'organization'); + })->uuid()->validate($attribute, $value, $fail); + }, ], // Filter by task IDs, task IDs are OR combined 'task_ids' => [ @@ -94,10 +110,15 @@ public function rules(): array ], 'task_ids.*' => [ 'string', - ExistsEloquent::make(Task::class, null, function (Builder $builder): Builder { - /** @var Builder $builder */ - return $builder->whereBelongsTo($this->organization, 'organization'); - })->uuid(), + function (string $attribute, mixed $value, \Closure $fail): void { + if ($value === TimeEntryFilter::NONE_VALUE) { + return; + } + ExistsEloquent::make(Task::class, null, function (Builder $builder): Builder { + /** @var Builder $builder */ + return $builder->whereBelongsTo($this->organization, 'organization'); + })->uuid()->validate($attribute, $value, $fail); + }, ], // Filter only time entries that have a start date after the given timestamp in UTC (example: 2021-01-01T00:00:00Z) 'start' => [ diff --git a/app/Http/Requests/V1/TimeEntry/TimeEntryStoreRequest.php b/app/Http/Requests/V1/TimeEntry/TimeEntryStoreRequest.php index b4d3840c..ec51f84e 100644 --- a/app/Http/Requests/V1/TimeEntry/TimeEntryStoreRequest.php +++ b/app/Http/Requests/V1/TimeEntry/TimeEntryStoreRequest.php @@ -10,8 +10,10 @@ use App\Models\Project; use App\Models\Tag; use App\Models\Task; +use App\Service\PermissionStore; use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Facades\Auth; use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent; /** @@ -42,7 +44,16 @@ public function rules(): array 'required_with:task_id', ExistsEloquent::make(Project::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ - return $builder->whereBelongsTo($this->organization, 'organization'); + $builder = $builder->whereBelongsTo($this->organization, 'organization'); + + // If user doesn't have 'all' permission for time entries or projects, only allow access to public projects or projects they're a member of + $permissionStore = app(PermissionStore::class); + if (! $permissionStore->has($this->organization, 'time-entries:create:all') + && ! $permissionStore->has($this->organization, 'projects:view:all')) { + $builder = $builder->visibleByEmployee(Auth::user()); + } + + return $builder; })->uuid(), ], // ID of the task that the time entry should belong to diff --git a/app/Http/Requests/V1/TimeEntry/TimeEntryUpdateMultipleRequest.php b/app/Http/Requests/V1/TimeEntry/TimeEntryUpdateMultipleRequest.php index 9f198654..e7c4b600 100644 --- a/app/Http/Requests/V1/TimeEntry/TimeEntryUpdateMultipleRequest.php +++ b/app/Http/Requests/V1/TimeEntry/TimeEntryUpdateMultipleRequest.php @@ -10,8 +10,10 @@ use App\Models\Project; use App\Models\Tag; use App\Models\Task; +use App\Service\PermissionStore; use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Facades\Auth; use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent; /** @@ -54,7 +56,16 @@ public function rules(): array 'required_with:task_id', ExistsEloquent::make(Project::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ - return $builder->whereBelongsTo($this->organization, 'organization'); + $builder = $builder->whereBelongsTo($this->organization, 'organization'); + + // If user doesn't have 'all' permission for time entries or projects, only allow access to public projects or projects they're a member of + $permissionStore = app(PermissionStore::class); + if (! $permissionStore->has($this->organization, 'time-entries:update:all') + && ! $permissionStore->has($this->organization, 'projects:view:all')) { + $builder = $builder->visibleByEmployee(Auth::user()); + } + + return $builder; })->uuid(), ], // ID of the task that the time entry should belong to diff --git a/app/Http/Requests/V1/TimeEntry/TimeEntryUpdateRequest.php b/app/Http/Requests/V1/TimeEntry/TimeEntryUpdateRequest.php index e9e6795a..d895d98f 100644 --- a/app/Http/Requests/V1/TimeEntry/TimeEntryUpdateRequest.php +++ b/app/Http/Requests/V1/TimeEntry/TimeEntryUpdateRequest.php @@ -10,8 +10,10 @@ use App\Models\Project; use App\Models\Tag; use App\Models\Task; +use App\Service\PermissionStore; use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Facades\Auth; use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent; /** @@ -42,7 +44,16 @@ public function rules(): array 'required_with:task_id', ExistsEloquent::make(Project::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ - return $builder->whereBelongsTo($this->organization, 'organization'); + $builder = $builder->whereBelongsTo($this->organization, 'organization'); + + // If user doesn't have 'all' permission for time entries or projects, only allow access to public projects or projects they're a member of + $permissionStore = app(PermissionStore::class); + if (! $permissionStore->has($this->organization, 'time-entries:update:all') + && ! $permissionStore->has($this->organization, 'projects:view:all')) { + $builder = $builder->visibleByEmployee(Auth::user()); + } + + return $builder; })->uuid(), ], // ID of the task that the time entry should belong to diff --git a/app/Http/Resources/V1/Client/ClientCollection.php b/app/Http/Resources/V1/Client/ClientCollection.php index 9c5de3cb..24e57bbd 100644 --- a/app/Http/Resources/V1/Client/ClientCollection.php +++ b/app/Http/Resources/V1/Client/ClientCollection.php @@ -4,9 +4,10 @@ namespace App\Http\Resources\V1\Client; +use App\Http\Resources\PaginatedResourceCollection; use Illuminate\Http\Resources\Json\ResourceCollection; -class ClientCollection extends ResourceCollection +class ClientCollection extends ResourceCollection implements PaginatedResourceCollection { /** * The resource that this resource collects. diff --git a/app/Http/Resources/V1/Tag/TagCollection.php b/app/Http/Resources/V1/Tag/TagCollection.php index 67c33977..707b9fa8 100644 --- a/app/Http/Resources/V1/Tag/TagCollection.php +++ b/app/Http/Resources/V1/Tag/TagCollection.php @@ -4,9 +4,10 @@ namespace App\Http\Resources\V1\Tag; +use App\Http\Resources\PaginatedResourceCollection; use Illuminate\Http\Resources\Json\ResourceCollection; -class TagCollection extends ResourceCollection +class TagCollection extends ResourceCollection implements PaginatedResourceCollection { /** * The resource that this resource collects. diff --git a/app/Models/Organization.php b/app/Models/Organization.php index 3cfab262..c910df3a 100644 --- a/app/Models/Organization.php +++ b/app/Models/Organization.php @@ -24,6 +24,7 @@ use Laravel\Jetstream\Events\TeamCreated; use Laravel\Jetstream\Events\TeamDeleted; use Laravel\Jetstream\Events\TeamUpdated; +use Laravel\Jetstream\Team; use Laravel\Jetstream\Team as JetstreamTeam; use OwenIt\Auditing\Contracts\Auditable as AuditableContract; @@ -176,7 +177,7 @@ public function realUsers(): BelongsToMany * * @param array $columns */ - public function findOrFail(string $id, array $columns = ['*']): \Laravel\Jetstream\Team + public function findOrFail(string $id, array $columns = ['*']): Team { if (! Str::isUuid($id)) { throw (new ModelNotFoundException)->setModel( diff --git a/app/Policies/OrganizationPolicy.php b/app/Policies/OrganizationPolicy.php index 520a2135..c0c1bc62 100644 --- a/app/Policies/OrganizationPolicy.php +++ b/app/Policies/OrganizationPolicy.php @@ -6,6 +6,7 @@ use App\Models\Organization; use App\Models\User; +use App\Service\PermissionStore; use Filament\Facades\Filament; use Illuminate\Auth\Access\HandlesAuthorization; @@ -58,7 +59,7 @@ public function update(User $user, Organization $organization): bool return true; } - return $user->ownsTeam($organization); + return app(PermissionStore::class)->userHas($organization, $user, 'organizations:update'); } /** diff --git a/app/Service/DashboardService.php b/app/Service/DashboardService.php index 456a782b..250bc7f9 100644 --- a/app/Service/DashboardService.php +++ b/app/Service/DashboardService.php @@ -266,7 +266,8 @@ public function totalWeeklyBillableAmount(User $user, Organization $organization ) as aggregate')) ->where('billable', '=', true) ->whereNotNull('billable_rate') - ->where('user_id', '=', $user->id); + ->where('user_id', '=', $user->getKey()) + ->where('organization_id', '=', $organization->getKey()); $query = $this->constrainDateByPossibleDates($query, $possibleDays, $timezone); /** @var Collection $resultDb */ diff --git a/app/Service/Dto/ReportPropertiesDto.php b/app/Service/Dto/ReportPropertiesDto.php index ac056d0f..a3ff85db 100644 --- a/app/Service/Dto/ReportPropertiesDto.php +++ b/app/Service/Dto/ReportPropertiesDto.php @@ -8,6 +8,7 @@ use App\Enums\TimeEntryAggregationTypeInterval; use App\Enums\TimeEntryRoundingType; use App\Enums\Weekday; +use App\Service\TimeEntryFilter; use Illuminate\Contracts\Database\Eloquent\Castable; use Illuminate\Contracts\Database\Eloquent\CastsAttributes; use Illuminate\Database\Eloquent\Model; @@ -174,7 +175,7 @@ public static function idArrayToCollection(array $ids): Collection if (! is_string($id)) { throw new \InvalidArgumentException('The given ID is not a string'); } - if (! Str::isUuid($id)) { + if ($id !== TimeEntryFilter::NONE_VALUE && ! Str::isUuid($id)) { throw new \InvalidArgumentException('The given ID is not a valid UUID'); } $collection->push($id); diff --git a/app/Service/MemberService.php b/app/Service/MemberService.php index e5c1b1ee..5c0c2267 100644 --- a/app/Service/MemberService.php +++ b/app/Service/MemberService.php @@ -196,6 +196,7 @@ public function makeMemberToPlaceholder(Member $member, bool $makeSureUserHasAtL $placeholderUser = $user->replicate(); $placeholderUser->is_placeholder = true; + $placeholderUser->current_team_id = $member->organization_id; $placeholderUser->save(); $member->user()->associate($placeholderUser); diff --git a/app/Service/ReportExport/CsvExport.php b/app/Service/ReportExport/CsvExport.php index 8708cb3c..53e500f5 100644 --- a/app/Service/ReportExport/CsvExport.php +++ b/app/Service/ReportExport/CsvExport.php @@ -10,6 +10,9 @@ use Illuminate\Http\File; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Storage; +use League\Csv\CannotInsertRecord; +use League\Csv\Exception; +use League\Csv\UnavailableStream; use League\Csv\Writer; use Spatie\TemporaryDirectory\TemporaryDirectory; @@ -58,9 +61,9 @@ public function __construct(string $disk, string $folderPath, string $filename, abstract public function mapRow(Model $model): array; /** - * @throws \League\Csv\CannotInsertRecord - * @throws \League\Csv\Exception - * @throws \League\Csv\UnavailableStream + * @throws CannotInsertRecord + * @throws Exception + * @throws UnavailableStream */ public function export(): void { diff --git a/app/Service/TimeEntryFilter.php b/app/Service/TimeEntryFilter.php index 160adddc..02fbe689 100644 --- a/app/Service/TimeEntryFilter.php +++ b/app/Service/TimeEntryFilter.php @@ -12,6 +12,8 @@ class TimeEntryFilter { + public const string NONE_VALUE = 'none'; + /** * @var Builder */ @@ -149,7 +151,17 @@ public function addClientIdsFilter(?array $clientIds): self if ($clientIds === null) { return $this; } - $this->builder->whereIn('client_id', $clientIds); + $includeNone = in_array(self::NONE_VALUE, $clientIds, true); + $clientIds = array_values(array_filter($clientIds, fn (string $id): bool => $id !== self::NONE_VALUE)); + + $this->builder->where(function (Builder $builder) use ($clientIds, $includeNone): void { + if (count($clientIds) > 0) { + $builder->whereIn('client_id', $clientIds); + } + if ($includeNone) { + $builder->orWhereNull('client_id'); + } + }); return $this; } @@ -162,7 +174,17 @@ public function addProjectIdsFilter(?array $projectIds): self if ($projectIds === null) { return $this; } - $this->builder->whereIn('project_id', $projectIds); + $includeNone = in_array(self::NONE_VALUE, $projectIds, true); + $projectIds = array_values(array_filter($projectIds, fn (string $id): bool => $id !== self::NONE_VALUE)); + + $this->builder->where(function (Builder $builder) use ($projectIds, $includeNone): void { + if (count($projectIds) > 0) { + $builder->whereIn('project_id', $projectIds); + } + if ($includeNone) { + $builder->orWhereNull('project_id'); + } + }); return $this; } @@ -175,10 +197,18 @@ public function addTagIdsFilter(?array $tagIds): self if ($tagIds === null) { return $this; } - $this->builder->where(function (Builder $builder) use ($tagIds): void { + $includeNone = in_array(self::NONE_VALUE, $tagIds, true); + $tagIds = array_values(array_filter($tagIds, fn (string $id): bool => $id !== self::NONE_VALUE)); + + $this->builder->where(function (Builder $builder) use ($tagIds, $includeNone): void { foreach ($tagIds as $tagId) { $builder->orWhereJsonContains('tags', $tagId); } + if ($includeNone) { + $builder->orWhere(function (Builder $query): void { + $query->whereJsonLength('tags', 0)->orWhereNull('tags'); + }); + } }); return $this; @@ -192,7 +222,17 @@ public function addTaskIdsFilter(?array $taskIds): self if ($taskIds === null) { return $this; } - $this->builder->whereIn('task_id', $taskIds); + $includeNone = in_array(self::NONE_VALUE, $taskIds, true); + $taskIds = array_values(array_filter($taskIds, fn (string $id): bool => $id !== self::NONE_VALUE)); + + $this->builder->where(function (Builder $builder) use ($taskIds, $includeNone): void { + if (count($taskIds) > 0) { + $builder->whereIn('task_id', $taskIds); + } + if ($includeNone) { + $builder->orWhereNull('task_id'); + } + }); return $this; } diff --git a/app/Service/TimeEntryService.php b/app/Service/TimeEntryService.php index 6cd97aa5..633f97d7 100644 --- a/app/Service/TimeEntryService.php +++ b/app/Service/TimeEntryService.php @@ -31,12 +31,17 @@ public function getEndSelectRawForRounding(?TimeEntryRoundingType $roundingType, throw new LogicException('Rounding minutes must be greater than 0'); } $end = 'coalesce("end", \''.Carbon::now()->toDateTimeString().'\')'; + $start = $this->getStartSelectRawForRounding($roundingType, $roundingMinutes); if ($roundingType === TimeEntryRoundingType::Down) { - return 'date_bin(\''.$roundingMinutes.' minutes\', '.$end.', '.$this->getStartSelectRawForRounding($roundingType, $roundingMinutes).')'; + return 'date_bin(\''.$roundingMinutes.' minutes\', '.$end.', '.$start.')'; } elseif ($roundingType === TimeEntryRoundingType::Up) { - return 'date_bin(\''.$roundingMinutes.' minutes\', '.$end.' + interval \''.$roundingMinutes.' minutes\', '.$this->getStartSelectRawForRounding($roundingType, $roundingMinutes).')'; + // If end is already on a boundary, keep it; otherwise round up to next boundary + return 'CASE WHEN '.$end.' = date_bin(\''.$roundingMinutes.' minutes\', '.$end.', '.$start.') '. + 'THEN '.$end.' '. + 'ELSE date_bin(\''.$roundingMinutes.' minutes\', '.$end.' + interval \''.$roundingMinutes.' minutes\', '.$start.') '. + 'END'; } elseif ($roundingType === TimeEntryRoundingType::Nearest) { - return 'date_bin(\''.$roundingMinutes.' minutes\', '.$end.' + interval \''.($roundingMinutes / 2).' minutes\', '.$this->getStartSelectRawForRounding($roundingType, $roundingMinutes).')'; + return 'date_bin(\''.$roundingMinutes.' minutes\', '.$end.' + interval \''.($roundingMinutes / 2).' minutes\', '.$start.')'; } } } diff --git a/bootstrap/app.php b/bootstrap/app.php index 08d677b8..4b2989c9 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -1,6 +1,10 @@ singleton( Illuminate\Contracts\Http\Kernel::class, - App\Http\Kernel::class + Kernel::class ); $app->singleton( @@ -39,8 +43,8 @@ ); $app->singleton( - Illuminate\Contracts\Debug\ExceptionHandler::class, - App\Exceptions\Handler::class + ExceptionHandler::class, + Handler::class ); /* diff --git a/composer.json b/composer.json index 53c40842..0ce21587 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "keywords": [], "license": "AGPL-3.0-or-later", "require": { - "php": "8.3.*", + "php": "8.4.*", "ext-zip": "*", "brick/money": "^0.10.0", "datomatic/laravel-enum-helper": "^2.0.0", diff --git a/composer.lock b/composer.lock index 10fe2ae2..8fb08abe 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5c66bc515959a78418203fcc8439ed42", + "content-hash": "04b2d938d0164cf6ff7348cf4c640327", "packages": [ { "name": "anourvalar/eloquent-serialize", - "version": "1.3.3", + "version": "1.3.5", "source": { "type": "git", "url": "https://github.com/AnourValar/eloquent-serialize.git", - "reference": "2f05023f1e465a91dc4f08483e6710325641a444" + "reference": "1a7dead8d532657e5358f8f27c0349373517681e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/AnourValar/eloquent-serialize/zipball/2f05023f1e465a91dc4f08483e6710325641a444", - "reference": "2f05023f1e465a91dc4f08483e6710325641a444", + "url": "https://api.github.com/repos/AnourValar/eloquent-serialize/zipball/1a7dead8d532657e5358f8f27c0349373517681e", + "reference": "1a7dead8d532657e5358f8f27c0349373517681e", "shasum": "" }, "require": { @@ -68,9 +68,9 @@ ], "support": { "issues": "https://github.com/AnourValar/eloquent-serialize/issues", - "source": "https://github.com/AnourValar/eloquent-serialize/tree/1.3.3" + "source": "https://github.com/AnourValar/eloquent-serialize/tree/1.3.5" }, - "time": "2025-05-28T17:07:28+00:00" + "time": "2025-12-04T13:38:21+00:00" }, { "name": "aws/aws-crt-php", @@ -128,16 +128,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.349.3", + "version": "3.373.2", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "b2d4718786398f47626add9c29840fc416175ef2" + "reference": "483fba51c28b3a0c0647bf5100e0edca82090b18" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/b2d4718786398f47626add9c29840fc416175ef2", - "reference": "b2d4718786398f47626add9c29840fc416175ef2", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/483fba51c28b3a0c0647bf5100e0edca82090b18", + "reference": "483fba51c28b3a0c0647bf5100e0edca82090b18", "shasum": "" }, "require": { @@ -150,24 +150,23 @@ "guzzlehttp/psr7": "^2.4.5", "mtdowling/jmespath.php": "^2.8.0", "php": ">=8.1", - "psr/http-message": "^2.0" + "psr/http-message": "^1.0 || ^2.0", + "symfony/filesystem": "^v5.4.45 || ^v6.4.3 || ^v7.1.0 || ^v8.0.0" }, "require-dev": { "andrewsville/php-token-reflection": "^1.4", "aws/aws-php-sns-message-validator": "~1.0", "behat/behat": "~3.0", "composer/composer": "^2.7.8", - "dms/phpunit-arraysubset-asserts": "^0.4.0", + "dms/phpunit-arraysubset-asserts": "^v0.5.0", "doctrine/cache": "~1.4", "ext-dom": "*", "ext-openssl": "*", - "ext-pcntl": "*", "ext-sockets": "*", - "phpunit/phpunit": "^5.6.3 || ^8.5 || ^9.5", + "phpunit/phpunit": "^10.0", "psr/cache": "^2.0 || ^3.0", "psr/simple-cache": "^2.0 || ^3.0", "sebastian/comparator": "^1.2.3 || ^4.0 || ^5.0", - "symfony/filesystem": "^v6.4.0 || ^v7.1.0", "yoast/phpunit-polyfills": "^2.0" }, "suggest": { @@ -175,6 +174,7 @@ "doctrine/cache": "To use the DoctrineCacheAdapter", "ext-curl": "To send requests using cURL", "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages", + "ext-pcntl": "To use client-side monitoring", "ext-sockets": "To use client-side monitoring" }, "type": "library", @@ -201,11 +201,11 @@ "authors": [ { "name": "Amazon Web Services", - "homepage": "http://aws.amazon.com" + "homepage": "https://aws.amazon.com" } ], "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project", - "homepage": "http://aws.amazon.com/sdkforphp", + "homepage": "https://aws.amazon.com/sdk-for-php", "keywords": [ "amazon", "aws", @@ -219,22 +219,22 @@ "support": { "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.349.3" + "source": "https://github.com/aws/aws-sdk-php/tree/3.373.2" }, - "time": "2025-07-09T18:10:17+00:00" + "time": "2026-03-13T18:08:30+00:00" }, { "name": "bacon/bacon-qr-code", - "version": "v3.0.1", + "version": "v3.0.4", "source": { "type": "git", "url": "https://github.com/Bacon/BaconQrCode.git", - "reference": "f9cc1f52b5a463062251d666761178dbdb6b544f" + "reference": "3feed0e212b8412cc5d2612706744789b0615824" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/f9cc1f52b5a463062251d666761178dbdb6b544f", - "reference": "f9cc1f52b5a463062251d666761178dbdb6b544f", + "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/3feed0e212b8412cc5d2612706744789b0615824", + "reference": "3feed0e212b8412cc5d2612706744789b0615824", "shasum": "" }, "require": { @@ -244,8 +244,9 @@ }, "require-dev": { "phly/keep-a-changelog": "^2.12", - "phpunit/phpunit": "^10.5.11 || 11.0.4", + "phpunit/phpunit": "^10.5.11 || ^11.0.4", "spatie/phpunit-snapshot-assertions": "^5.1.5", + "spatie/pixelmatch-php": "^1.2.0", "squizlabs/php_codesniffer": "^3.9" }, "suggest": { @@ -273,9 +274,9 @@ "homepage": "https://github.com/Bacon/BaconQrCode", "support": { "issues": "https://github.com/Bacon/BaconQrCode/issues", - "source": "https://github.com/Bacon/BaconQrCode/tree/v3.0.1" + "source": "https://github.com/Bacon/BaconQrCode/tree/v3.0.4" }, - "time": "2024-10-01T13:55:55+00:00" + "time": "2026-03-16T01:01:30+00:00" }, { "name": "blade-ui-kit/blade-heroicons", @@ -348,30 +349,30 @@ }, { "name": "blade-ui-kit/blade-icons", - "version": "1.8.0", + "version": "1.9.0", "source": { "type": "git", "url": "https://github.com/driesvints/blade-icons.git", - "reference": "7b743f27476acb2ed04cb518213d78abe096e814" + "reference": "caa92fde675d7a651c38bf73ca582ddada56f318" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/driesvints/blade-icons/zipball/7b743f27476acb2ed04cb518213d78abe096e814", - "reference": "7b743f27476acb2ed04cb518213d78abe096e814", + "url": "https://api.github.com/repos/driesvints/blade-icons/zipball/caa92fde675d7a651c38bf73ca582ddada56f318", + "reference": "caa92fde675d7a651c38bf73ca582ddada56f318", "shasum": "" }, "require": { - "illuminate/contracts": "^8.0|^9.0|^10.0|^11.0|^12.0", - "illuminate/filesystem": "^8.0|^9.0|^10.0|^11.0|^12.0", - "illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0", - "illuminate/view": "^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/contracts": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/filesystem": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/view": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", "php": "^7.4|^8.0", - "symfony/console": "^5.3|^6.0|^7.0", - "symfony/finder": "^5.3|^6.0|^7.0" + "symfony/console": "^5.3|^6.0|^7.0|^8.0", + "symfony/finder": "^5.3|^6.0|^7.0|^8.0" }, "require-dev": { "mockery/mockery": "^1.5.1", - "orchestra/testbench": "^6.0|^7.0|^8.0|^9.0|^10.0", + "orchestra/testbench": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", "phpunit/phpunit": "^9.0|^10.5|^11.0" }, "bin": [ @@ -404,7 +405,7 @@ } ], "description": "A package to easily make use of icons in your Laravel Blade views.", - "homepage": "https://github.com/blade-ui-kit/blade-icons", + "homepage": "https://github.com/driesvints/blade-icons", "keywords": [ "blade", "icons", @@ -412,8 +413,8 @@ "svg" ], "support": { - "issues": "https://github.com/blade-ui-kit/blade-icons/issues", - "source": "https://github.com/blade-ui-kit/blade-icons" + "issues": "https://github.com/driesvints/blade-icons/issues", + "source": "https://github.com/driesvints/blade-icons" }, "funding": [ { @@ -425,29 +426,29 @@ "type": "paypal" } ], - "time": "2025-02-13T20:35:06+00:00" + "time": "2026-02-23T10:42:23+00:00" }, { "name": "brick/math", - "version": "0.13.1", + "version": "0.14.8", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04" + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/fc7ed316430118cc7836bf45faff18d5dfc8de04", - "reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04", + "url": "https://api.github.com/repos/brick/math/zipball/63422359a44b7f06cae63c3b429b59e8efcc0629", + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629", "shasum": "" }, "require": { - "php": "^8.1" + "php": "^8.2" }, "require-dev": { "php-coveralls/php-coveralls": "^2.2", - "phpunit/phpunit": "^10.1", - "vimeo/psalm": "6.8.8" + "phpstan/phpstan": "2.1.22", + "phpunit/phpunit": "^11.5" }, "type": "library", "autoload": { @@ -477,7 +478,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.13.1" + "source": "https://github.com/brick/math/tree/0.14.8" }, "funding": [ { @@ -485,24 +486,24 @@ "type": "github" } ], - "time": "2025-03-29T13:50:30+00:00" + "time": "2026-02-10T14:33:43+00:00" }, { "name": "brick/money", - "version": "0.10.1", + "version": "0.10.3", "source": { "type": "git", "url": "https://github.com/brick/money.git", - "reference": "779c1d3b708e4dd37fe8a32a189b9f241b52f194" + "reference": "b1b0bb6035d26a58f29b1c06b1265c01a0b5c9c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/money/zipball/779c1d3b708e4dd37fe8a32a189b9f241b52f194", - "reference": "779c1d3b708e4dd37fe8a32a189b9f241b52f194", + "url": "https://api.github.com/repos/brick/money/zipball/b1b0bb6035d26a58f29b1c06b1265c01a0b5c9c3", + "reference": "b1b0bb6035d26a58f29b1c06b1265c01a0b5c9c3", "shasum": "" }, "require": { - "brick/math": "~0.12.0|~0.13.0", + "brick/math": "~0.12.0|~0.13.0|~0.14.0", "php": "^8.1" }, "require-dev": { @@ -534,7 +535,7 @@ ], "support": { "issues": "https://github.com/brick/money/issues", - "source": "https://github.com/brick/money/tree/0.10.1" + "source": "https://github.com/brick/money/tree/0.10.3" }, "funding": [ { @@ -542,7 +543,7 @@ "type": "github" } ], - "time": "2025-03-05T13:00:01+00:00" + "time": "2025-09-03T09:55:48+00:00" }, { "name": "carbonphp/carbon-doctrine-types", @@ -615,16 +616,16 @@ }, { "name": "composer/ca-bundle", - "version": "1.5.7", + "version": "1.5.10", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "d665d22c417056996c59019579f1967dfe5c1e82" + "reference": "961a5e4056dd2e4a2eedcac7576075947c28bf63" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/d665d22c417056996c59019579f1967dfe5c1e82", - "reference": "d665d22c417056996c59019579f1967dfe5c1e82", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/961a5e4056dd2e4a2eedcac7576075947c28bf63", + "reference": "961a5e4056dd2e4a2eedcac7576075947c28bf63", "shasum": "" }, "require": { @@ -671,7 +672,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.5.7" + "source": "https://github.com/composer/ca-bundle/tree/1.5.10" }, "funding": [ { @@ -681,32 +682,28 @@ { "url": "https://github.com/composer", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2025-05-26T15:08:54+00:00" + "time": "2025-12-08T15:06:51+00:00" }, { "name": "composer/class-map-generator", - "version": "1.6.1", + "version": "1.7.1", "source": { "type": "git", "url": "https://github.com/composer/class-map-generator.git", - "reference": "134b705ddb0025d397d8318a75825fe3c9d1da34" + "reference": "8f5fa3cc214230e71f54924bd0197a3bcc705eb1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/class-map-generator/zipball/134b705ddb0025d397d8318a75825fe3c9d1da34", - "reference": "134b705ddb0025d397d8318a75825fe3c9d1da34", + "url": "https://api.github.com/repos/composer/class-map-generator/zipball/8f5fa3cc214230e71f54924bd0197a3bcc705eb1", + "reference": "8f5fa3cc214230e71f54924bd0197a3bcc705eb1", "shasum": "" }, "require": { "composer/pcre": "^2.1 || ^3.1", "php": "^7.2 || ^8.0", - "symfony/finder": "^4.4 || ^5.3 || ^6 || ^7" + "symfony/finder": "^4.4 || ^5.3 || ^6 || ^7 || ^8" }, "require-dev": { "phpstan/phpstan": "^1.12 || ^2", @@ -714,7 +711,7 @@ "phpstan/phpstan-phpunit": "^1 || ^2", "phpstan/phpstan-strict-rules": "^1.1 || ^2", "phpunit/phpunit": "^8", - "symfony/filesystem": "^5.4 || ^6" + "symfony/filesystem": "^5.4 || ^6 || ^7 || ^8" }, "type": "library", "extra": { @@ -744,7 +741,7 @@ ], "support": { "issues": "https://github.com/composer/class-map-generator/issues", - "source": "https://github.com/composer/class-map-generator/tree/1.6.1" + "source": "https://github.com/composer/class-map-generator/tree/1.7.1" }, "funding": [ { @@ -754,50 +751,48 @@ { "url": "https://github.com/composer", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2025-03-24T13:50:44+00:00" + "time": "2025-12-29T13:15:25+00:00" }, { "name": "composer/composer", - "version": "2.8.10", + "version": "2.9.5", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "53834f587d7ab2527eb237459d7b94d1fb9d4c5a" + "reference": "72a8f8e653710e18d83e5dd531eb5a71fc3223e6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/53834f587d7ab2527eb237459d7b94d1fb9d4c5a", - "reference": "53834f587d7ab2527eb237459d7b94d1fb9d4c5a", + "url": "https://api.github.com/repos/composer/composer/zipball/72a8f8e653710e18d83e5dd531eb5a71fc3223e6", + "reference": "72a8f8e653710e18d83e5dd531eb5a71fc3223e6", "shasum": "" }, "require": { "composer/ca-bundle": "^1.5", "composer/class-map-generator": "^1.4.0", "composer/metadata-minifier": "^1.0", - "composer/pcre": "^2.2 || ^3.2", + "composer/pcre": "^2.3 || ^3.3", "composer/semver": "^3.3", "composer/spdx-licenses": "^1.5.7", "composer/xdebug-handler": "^2.0.2 || ^3.0.3", - "justinrainbow/json-schema": "^6.3.1", + "ext-json": "*", + "justinrainbow/json-schema": "^6.5.1", "php": "^7.2.5 || ^8.0", "psr/log": "^1.0 || ^2.0 || ^3.0", - "react/promise": "^2.11 || ^3.2", + "react/promise": "^3.3", "seld/jsonlint": "^1.4", "seld/phar-utils": "^1.2", "seld/signal-handler": "^2.0", - "symfony/console": "^5.4.35 || ^6.3.12 || ^7.0.3", - "symfony/filesystem": "^5.4.35 || ^6.3.12 || ^7.0.3", - "symfony/finder": "^5.4.35 || ^6.3.12 || ^7.0.3", + "symfony/console": "^5.4.47 || ^6.4.25 || ^7.1.10 || ^8.0", + "symfony/filesystem": "^5.4.45 || ^6.4.24 || ^7.1.10 || ^8.0", + "symfony/finder": "^5.4.45 || ^6.4.24 || ^7.1.10 || ^8.0", "symfony/polyfill-php73": "^1.24", "symfony/polyfill-php80": "^1.24", "symfony/polyfill-php81": "^1.24", - "symfony/process": "^5.4.35 || ^6.3.12 || ^7.0.3" + "symfony/polyfill-php84": "^1.30", + "symfony/process": "^5.4.47 || ^6.4.25 || ^7.1.10 || ^8.0" }, "require-dev": { "phpstan/phpstan": "^1.11.8", @@ -805,12 +800,13 @@ "phpstan/phpstan-phpunit": "^1.4.0", "phpstan/phpstan-strict-rules": "^1.6.0", "phpstan/phpstan-symfony": "^1.4.0", - "symfony/phpunit-bridge": "^6.4.3 || ^7.0.1" + "symfony/phpunit-bridge": "^6.4.25 || ^7.3.3 || ^8.0" }, "suggest": { - "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages", - "ext-zip": "Enabling the zip extension allows you to unzip archives", - "ext-zlib": "Allow gzip compression of HTTP requests" + "ext-curl": "Provides HTTP support (will fallback to PHP streams if missing)", + "ext-openssl": "Enables access to repositories and packages over HTTPS", + "ext-zip": "Allows direct extraction of ZIP archives (unzip/7z binaries will be used instead if available)", + "ext-zlib": "Enables gzip for HTTP requests" }, "bin": [ "bin/composer" @@ -823,7 +819,7 @@ ] }, "branch-alias": { - "dev-main": "2.8-dev" + "dev-main": "2.9-dev" } }, "autoload": { @@ -858,7 +854,7 @@ "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/composer/issues", "security": "https://github.com/composer/composer/security/policy", - "source": "https://github.com/composer/composer/tree/2.8.10" + "source": "https://github.com/composer/composer/tree/2.9.5" }, "funding": [ { @@ -868,13 +864,9 @@ { "url": "https://github.com/composer", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2025-07-10T17:08:33+00:00" + "time": "2026-01-29T10:40:53+00:00" }, { "name": "composer/metadata-minifier", @@ -1026,16 +1018,16 @@ }, { "name": "composer/semver", - "version": "3.4.3", + "version": "3.4.4", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", - "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", "shasum": "" }, "require": { @@ -1087,7 +1079,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.4.3" + "source": "https://github.com/composer/semver/tree/3.4.4" }, "funding": [ { @@ -1097,13 +1089,9 @@ { "url": "https://github.com/composer", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2024-09-19T14:15:21+00:00" + "time": "2025-08-20T19:15:30+00:00" }, { "name": "composer/spdx-licenses", @@ -1358,16 +1346,16 @@ }, { "name": "dasprid/enum", - "version": "1.0.6", + "version": "1.0.7", "source": { "type": "git", "url": "https://github.com/DASPRiD/Enum.git", - "reference": "8dfd07c6d2cf31c8da90c53b83c026c7696dda90" + "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/8dfd07c6d2cf31c8da90c53b83c026c7696dda90", - "reference": "8dfd07c6d2cf31c8da90c53b83c026c7696dda90", + "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/b5874fa9ed0043116c72162ec7f4fb50e02e7cce", + "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce", "shasum": "" }, "require": { @@ -1402,22 +1390,22 @@ ], "support": { "issues": "https://github.com/DASPRiD/Enum/issues", - "source": "https://github.com/DASPRiD/Enum/tree/1.0.6" + "source": "https://github.com/DASPRiD/Enum/tree/1.0.7" }, - "time": "2024-08-09T14:30:48+00:00" + "time": "2025-09-16T12:23:56+00:00" }, { "name": "datomatic/enum-helper", - "version": "v2.0.1", + "version": "v2.1.1", "source": { "type": "git", "url": "https://github.com/datomatic/enum-helper.git", - "reference": "90f1986d755c7d7cc5c716ca9b2cfb1b58adf3d0" + "reference": "b110466a42aa6cb17bc90dd68f13b8598154f972" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/datomatic/enum-helper/zipball/90f1986d755c7d7cc5c716ca9b2cfb1b58adf3d0", - "reference": "90f1986d755c7d7cc5c716ca9b2cfb1b58adf3d0", + "url": "https://api.github.com/repos/datomatic/enum-helper/zipball/b110466a42aa6cb17bc90dd68f13b8598154f972", + "reference": "b110466a42aa6cb17bc90dd68f13b8598154f972", "shasum": "" }, "require": { @@ -1450,22 +1438,22 @@ "description": "Simple opinionated framework agnostic PHP 8.1 enum helper", "support": { "issues": "https://github.com/datomatic/enum-helper/issues", - "source": "https://github.com/datomatic/enum-helper/tree/v2.0.1" + "source": "https://github.com/datomatic/enum-helper/tree/v2.1.1" }, - "time": "2025-02-22T09:51:21+00:00" + "time": "2025-12-29T08:59:22+00:00" }, { "name": "datomatic/laravel-enum-helper", - "version": "v2.1.1", + "version": "v2.1.2", "source": { "type": "git", "url": "https://github.com/datomatic/laravel-enum-helper.git", - "reference": "5ee2d7b30c547b55fb518e266ca491ecbf5d7393" + "reference": "c63fd71122ad780b25d6d91ebd5287237e5a3b56" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/datomatic/laravel-enum-helper/zipball/5ee2d7b30c547b55fb518e266ca491ecbf5d7393", - "reference": "5ee2d7b30c547b55fb518e266ca491ecbf5d7393", + "url": "https://api.github.com/repos/datomatic/laravel-enum-helper/zipball/c63fd71122ad780b25d6d91ebd5287237e5a3b56", + "reference": "c63fd71122ad780b25d6d91ebd5287237e5a3b56", "shasum": "" }, "require": { @@ -1480,7 +1468,7 @@ "require-dev": { "friendsofphp/php-cs-fixer": "^3.8", "laravel/pint": "^1.18", - "nunomaduro/larastan": "^2.0", + "nunomaduro/larastan": "^2.0|^3.0", "orchestra/testbench": "^6.23|^7.0|^8.0|^9.0|^10.0", "pestphp/pest": "^1.0|^2.0|^3.0", "pestphp/pest-plugin-laravel": "^1.0|^2.0|^3.0", @@ -1512,22 +1500,22 @@ "description": "Simple opinionated framework agnostic PHP 8.1 enum helper for Laravel", "support": { "issues": "https://github.com/datomatic/laravel-enum-helper/issues", - "source": "https://github.com/datomatic/laravel-enum-helper/tree/v2.1.1" + "source": "https://github.com/datomatic/laravel-enum-helper/tree/v2.1.2" }, - "time": "2025-03-08T08:26:26+00:00" + "time": "2025-09-24T08:36:45+00:00" }, { "name": "dedoc/scramble", - "version": "v0.12.23", + "version": "v0.12.36", "source": { "type": "git", "url": "https://github.com/dedoc/scramble.git", - "reference": "5b650167c81c59138e844c2ae550c14dc1a249d0" + "reference": "e2741add99b5f9360a7a58a58ce97781569e4cc6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dedoc/scramble/zipball/5b650167c81c59138e844c2ae550c14dc1a249d0", - "reference": "5b650167c81c59138e844c2ae550c14dc1a249d0", + "url": "https://api.github.com/repos/dedoc/scramble/zipball/e2741add99b5f9360a7a58a58ce97781569e4cc6", + "reference": "e2741add99b5f9360a7a58a58ce97781569e4cc6", "shasum": "" }, "require": { @@ -1586,7 +1574,7 @@ ], "support": { "issues": "https://github.com/dedoc/scramble/issues", - "source": "https://github.com/dedoc/scramble/tree/v0.12.23" + "source": "https://github.com/dedoc/scramble/tree/v0.12.36" }, "funding": [ { @@ -1594,7 +1582,7 @@ "type": "github" } ], - "time": "2025-06-15T09:04:49+00:00" + "time": "2025-10-20T08:06:09+00:00" }, { "name": "defuse/php-encryption", @@ -1740,16 +1728,16 @@ }, { "name": "doctrine/dbal", - "version": "4.3.0", + "version": "4.4.2", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "5fe09532be619202d59c70956c6fb20e97933ee3" + "reference": "476f7f0fa6ea4aa5364926db7fabdf6049075722" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/5fe09532be619202d59c70956c6fb20e97933ee3", - "reference": "5fe09532be619202d59c70956c6fb20e97933ee3", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/476f7f0fa6ea4aa5364926db7fabdf6049075722", + "reference": "476f7f0fa6ea4aa5364926db7fabdf6049075722", "shasum": "" }, "require": { @@ -1759,17 +1747,17 @@ "psr/log": "^1|^2|^3" }, "require-dev": { - "doctrine/coding-standard": "13.0.0", + "doctrine/coding-standard": "14.0.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2023.2", - "phpstan/phpstan": "2.1.17", - "phpstan/phpstan-phpunit": "2.0.6", + "phpstan/phpstan": "2.1.30", + "phpstan/phpstan-phpunit": "2.0.7", "phpstan/phpstan-strict-rules": "^2", - "phpunit/phpunit": "11.5.23", - "slevomat/coding-standard": "8.16.2", - "squizlabs/php_codesniffer": "3.13.1", - "symfony/cache": "^6.3.8|^7.0", - "symfony/console": "^5.4|^6.3|^7.0" + "phpunit/phpunit": "11.5.50", + "slevomat/coding-standard": "8.27.1", + "squizlabs/php_codesniffer": "4.0.1", + "symfony/cache": "^6.3.8|^7.0|^8.0", + "symfony/console": "^5.4|^6.3|^7.0|^8.0" }, "suggest": { "symfony/console": "For helpful console commands such as SQL execution and import of files." @@ -1826,7 +1814,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/4.3.0" + "source": "https://github.com/doctrine/dbal/tree/4.4.2" }, "funding": [ { @@ -1842,33 +1830,33 @@ "type": "tidelift" } ], - "time": "2025-06-16T19:31:04+00:00" + "time": "2026-02-26T12:12:19+00:00" }, { "name": "doctrine/deprecations", - "version": "1.1.5", + "version": "1.1.6", "source": { "type": "git", "url": "https://github.com/doctrine/deprecations.git", - "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38" + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", - "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, "conflict": { - "phpunit/phpunit": "<=7.5 || >=13" + "phpunit/phpunit": "<=7.5 || >=14" }, "require-dev": { - "doctrine/coding-standard": "^9 || ^12 || ^13", - "phpstan/phpstan": "1.4.10 || 2.1.11", + "doctrine/coding-standard": "^9 || ^12 || ^14", + "phpstan/phpstan": "1.4.10 || 2.1.30", "phpstan/phpstan-phpunit": "^1.0 || ^2", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", "psr/log": "^1 || ^2 || ^3" }, "suggest": { @@ -1888,39 +1876,38 @@ "homepage": "https://www.doctrine-project.org/", "support": { "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/1.1.5" + "source": "https://github.com/doctrine/deprecations/tree/1.1.6" }, - "time": "2025-04-07T20:06:18+00:00" + "time": "2026-02-07T07:09:04+00:00" }, { "name": "doctrine/inflector", - "version": "2.0.10", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/doctrine/inflector.git", - "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc" + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc", - "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", "shasum": "" }, "require": { "php": "^7.2 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^11.0", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-phpunit": "^1.1", - "phpstan/phpstan-strict-rules": "^1.3", - "phpunit/phpunit": "^8.5 || ^9.5", - "vimeo/psalm": "^4.25 || ^5.4" + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" }, "type": "library", "autoload": { "psr-4": { - "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" + "Doctrine\\Inflector\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1965,7 +1952,7 @@ ], "support": { "issues": "https://github.com/doctrine/inflector/issues", - "source": "https://github.com/doctrine/inflector/tree/2.0.10" + "source": "https://github.com/doctrine/inflector/tree/2.1.0" }, "funding": [ { @@ -1981,7 +1968,7 @@ "type": "tidelift" } ], - "time": "2024-02-18T20:23:39+00:00" + "time": "2025-08-10T19:31:58+00:00" }, { "name": "doctrine/lexer", @@ -2062,29 +2049,28 @@ }, { "name": "dragonmantank/cron-expression", - "version": "v3.4.0", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/dragonmantank/cron-expression.git", - "reference": "8c784d071debd117328803d86b2097615b457500" + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/8c784d071debd117328803d86b2097615b457500", - "reference": "8c784d071debd117328803d86b2097615b457500", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013", "shasum": "" }, "require": { - "php": "^7.2|^8.0", - "webmozart/assert": "^1.0" + "php": "^8.2|^8.3|^8.4|^8.5" }, "replace": { "mtdowling/cron-expression": "^1.0" }, "require-dev": { - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^1.0", - "phpunit/phpunit": "^7.0|^8.0|^9.0" + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.32|^2.1.31", + "phpunit/phpunit": "^8.5.48|^9.0" }, "type": "library", "extra": { @@ -2115,7 +2101,7 @@ ], "support": { "issues": "https://github.com/dragonmantank/cron-expression/issues", - "source": "https://github.com/dragonmantank/cron-expression/tree/v3.4.0" + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.6.0" }, "funding": [ { @@ -2123,7 +2109,7 @@ "type": "github" } ], - "time": "2024-10-09T13:47:03+00:00" + "time": "2025-10-31T18:51:33+00:00" }, { "name": "egulias/email-validator", @@ -2194,20 +2180,20 @@ }, { "name": "ezyang/htmlpurifier", - "version": "v4.18.0", + "version": "v4.19.0", "source": { "type": "git", "url": "https://github.com/ezyang/htmlpurifier.git", - "reference": "cb56001e54359df7ae76dc522d08845dc741621b" + "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/cb56001e54359df7ae76dc522d08845dc741621b", - "reference": "cb56001e54359df7ae76dc522d08845dc741621b", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/b287d2a16aceffbf6e0295559b39662612b77fcf", + "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf", "shasum": "" }, "require": { - "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" + "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" }, "require-dev": { "cerdic/css-tidy": "^1.7 || ^2.0", @@ -2249,22 +2235,22 @@ ], "support": { "issues": "https://github.com/ezyang/htmlpurifier/issues", - "source": "https://github.com/ezyang/htmlpurifier/tree/v4.18.0" + "source": "https://github.com/ezyang/htmlpurifier/tree/v4.19.0" }, - "time": "2024-11-01T03:51:45+00:00" + "time": "2025-10-17T16:34:55+00:00" }, { "name": "filament/actions", - "version": "v3.3.31", + "version": "v3.3.49", "source": { "type": "git", "url": "https://github.com/filamentphp/actions.git", - "reference": "3fee2c23368e5ee8d4613786b1bed8d98a8073fc" + "reference": "3cb3e1f9094ed3b4bc102616966c365138c908bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/actions/zipball/3fee2c23368e5ee8d4613786b1bed8d98a8073fc", - "reference": "3fee2c23368e5ee8d4613786b1bed8d98a8073fc", + "url": "https://api.github.com/repos/filamentphp/actions/zipball/3cb3e1f9094ed3b4bc102616966c365138c908bc", + "reference": "3cb3e1f9094ed3b4bc102616966c365138c908bc", "shasum": "" }, "require": { @@ -2304,20 +2290,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2025-07-08T20:42:21+00:00" + "time": "2026-02-07T21:52:11+00:00" }, { "name": "filament/filament", - "version": "v3.3.31", + "version": "v3.3.49", "source": { "type": "git", "url": "https://github.com/filamentphp/panels.git", - "reference": "b432b938c35467b9626978fb8b72578ec4d162ae" + "reference": "6098e568b4257dc438ff68aced0a260f06ba6d52" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/panels/zipball/b432b938c35467b9626978fb8b72578ec4d162ae", - "reference": "b432b938c35467b9626978fb8b72578ec4d162ae", + "url": "https://api.github.com/repos/filamentphp/panels/zipball/6098e568b4257dc438ff68aced0a260f06ba6d52", + "reference": "6098e568b4257dc438ff68aced0a260f06ba6d52", "shasum": "" }, "require": { @@ -2369,20 +2355,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2025-07-01T09:34:40+00:00" + "time": "2026-02-07T21:52:21+00:00" }, { "name": "filament/forms", - "version": "v3.3.31", + "version": "v3.3.49", "source": { "type": "git", "url": "https://github.com/filamentphp/forms.git", - "reference": "e348d8a92c96fc87b002830848e48559eb0ef715" + "reference": "c64bf142f808d292b0c6c21fdd3c75cbef9e9d30" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/forms/zipball/e348d8a92c96fc87b002830848e48559eb0ef715", - "reference": "e348d8a92c96fc87b002830848e48559eb0ef715", + "url": "https://api.github.com/repos/filamentphp/forms/zipball/c64bf142f808d292b0c6c21fdd3c75cbef9e9d30", + "reference": "c64bf142f808d292b0c6c21fdd3c75cbef9e9d30", "shasum": "" }, "require": { @@ -2425,20 +2411,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2025-07-08T20:42:22+00:00" + "time": "2026-02-19T23:07:33+00:00" }, { "name": "filament/infolists", - "version": "v3.3.31", + "version": "v3.3.49", "source": { "type": "git", "url": "https://github.com/filamentphp/infolists.git", - "reference": "89a3f1f236863e2035be3d7b0c68987508dd06fa" + "reference": "9cef7bf9f46756a8adf762ced62952e8c239b840" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/infolists/zipball/89a3f1f236863e2035be3d7b0c68987508dd06fa", - "reference": "89a3f1f236863e2035be3d7b0c68987508dd06fa", + "url": "https://api.github.com/repos/filamentphp/infolists/zipball/9cef7bf9f46756a8adf762ced62952e8c239b840", + "reference": "9cef7bf9f46756a8adf762ced62952e8c239b840", "shasum": "" }, "require": { @@ -2476,20 +2462,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2025-06-23T10:46:53+00:00" + "time": "2026-02-07T21:51:54+00:00" }, { "name": "filament/notifications", - "version": "v3.3.31", + "version": "v3.3.49", "source": { "type": "git", "url": "https://github.com/filamentphp/notifications.git", - "reference": "adc118c7fc34a423f3c01d6936ad0316f489949c" + "reference": "3a6ef54b6a8cefc79858e7033e4d6b65fb2d859b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/notifications/zipball/adc118c7fc34a423f3c01d6936ad0316f489949c", - "reference": "adc118c7fc34a423f3c01d6936ad0316f489949c", + "url": "https://api.github.com/repos/filamentphp/notifications/zipball/3a6ef54b6a8cefc79858e7033e4d6b65fb2d859b", + "reference": "3a6ef54b6a8cefc79858e7033e4d6b65fb2d859b", "shasum": "" }, "require": { @@ -2528,20 +2514,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2025-07-08T20:42:18+00:00" + "time": "2026-01-01T16:29:16+00:00" }, { "name": "filament/support", - "version": "v3.3.31", + "version": "v3.3.49", "source": { "type": "git", "url": "https://github.com/filamentphp/support.git", - "reference": "ec8c7e9e6b49d4f1150a19bfd6fc585c717a857d" + "reference": "493bd79d4f4ae7b9256c317af742354318a6f4e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/support/zipball/ec8c7e9e6b49d4f1150a19bfd6fc585c717a857d", - "reference": "ec8c7e9e6b49d4f1150a19bfd6fc585c717a857d", + "url": "https://api.github.com/repos/filamentphp/support/zipball/493bd79d4f4ae7b9256c317af742354318a6f4e0", + "reference": "493bd79d4f4ae7b9256c317af742354318a6f4e0", "shasum": "" }, "require": { @@ -2587,20 +2573,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2025-07-08T20:42:46+00:00" + "time": "2026-02-07T21:51:58+00:00" }, { "name": "filament/tables", - "version": "v3.3.31", + "version": "v3.3.49", "source": { "type": "git", "url": "https://github.com/filamentphp/tables.git", - "reference": "3f0d827c960f1ee4a67ab71c416ad67e24747dc4" + "reference": "fb0ab986950dc8129725f676bdb310851b18403f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/tables/zipball/3f0d827c960f1ee4a67ab71c416ad67e24747dc4", - "reference": "3f0d827c960f1ee4a67ab71c416ad67e24747dc4", + "url": "https://api.github.com/repos/filamentphp/tables/zipball/fb0ab986950dc8129725f676bdb310851b18403f", + "reference": "fb0ab986950dc8129725f676bdb310851b18403f", "shasum": "" }, "require": { @@ -2639,20 +2625,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2025-07-08T20:42:18+00:00" + "time": "2026-02-07T21:52:06+00:00" }, { "name": "filament/widgets", - "version": "v3.3.31", + "version": "v3.3.49", "source": { "type": "git", "url": "https://github.com/filamentphp/widgets.git", - "reference": "5b956f884aaef479f6091463cb829e7c9f2afc2c" + "reference": "f58ff26e81ca2557205e3111e1d9d05c258cc206" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/widgets/zipball/5b956f884aaef479f6091463cb829e7c9f2afc2c", - "reference": "5b956f884aaef479f6091463cb829e7c9f2afc2c", + "url": "https://api.github.com/repos/filamentphp/widgets/zipball/f58ff26e81ca2557205e3111e1d9d05c258cc206", + "reference": "f58ff26e81ca2557205e3111e1d9d05c258cc206", "shasum": "" }, "require": { @@ -2683,20 +2669,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2025-06-12T15:11:14+00:00" + "time": "2026-02-07T21:51:58+00:00" }, { "name": "firebase/php-jwt", - "version": "v6.11.1", + "version": "v7.0.3", "source": { "type": "git", "url": "https://github.com/firebase/php-jwt.git", - "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66" + "reference": "28aa0694bcfdfa5e2959c394d5a1ee7a5083629e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", - "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/28aa0694bcfdfa5e2959c394d5a1ee7a5083629e", + "reference": "28aa0694bcfdfa5e2959c394d5a1ee7a5083629e", "shasum": "" }, "require": { @@ -2744,9 +2730,9 @@ ], "support": { "issues": "https://github.com/firebase/php-jwt/issues", - "source": "https://github.com/firebase/php-jwt/tree/v6.11.1" + "source": "https://github.com/firebase/php-jwt/tree/v7.0.3" }, - "time": "2025-04-09T20:32:01+00:00" + "time": "2026-02-25T22:16:40+00:00" }, { "name": "flowframe/laravel-trend", @@ -2824,31 +2810,31 @@ }, { "name": "fruitcake/php-cors", - "version": "v1.3.0", + "version": "v1.4.0", "source": { "type": "git", "url": "https://github.com/fruitcake/php-cors.git", - "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b" + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/3d158f36e7875e2f040f37bc0573956240a5a38b", - "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", "shasum": "" }, "require": { - "php": "^7.4|^8.0", - "symfony/http-foundation": "^4.4|^5.4|^6|^7" + "php": "^8.1", + "symfony/http-foundation": "^5.4|^6.4|^7.3|^8" }, "require-dev": { - "phpstan/phpstan": "^1.4", + "phpstan/phpstan": "^2", "phpunit/phpunit": "^9", - "squizlabs/php_codesniffer": "^3.5" + "squizlabs/php_codesniffer": "^4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2-dev" + "dev-master": "1.3-dev" } }, "autoload": { @@ -2879,7 +2865,7 @@ ], "support": { "issues": "https://github.com/fruitcake/php-cors/issues", - "source": "https://github.com/fruitcake/php-cors/tree/v1.3.0" + "source": "https://github.com/fruitcake/php-cors/tree/v1.4.0" }, "funding": [ { @@ -2891,36 +2877,36 @@ "type": "github" } ], - "time": "2023-10-12T05:21:21+00:00" + "time": "2025-12-03T09:33:47+00:00" }, { "name": "gotenberg/gotenberg-php", - "version": "v2.14.0", + "version": "v2.17.0", "source": { "type": "git", "url": "https://github.com/gotenberg/gotenberg-php.git", - "reference": "748efe0a981b3e8eb676593246a75925448324ad" + "reference": "7e6b574fb500d35584c29159c0559e834e71d99d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/gotenberg/gotenberg-php/zipball/748efe0a981b3e8eb676593246a75925448324ad", - "reference": "748efe0a981b3e8eb676593246a75925448324ad", + "url": "https://api.github.com/repos/gotenberg/gotenberg-php/zipball/7e6b574fb500d35584c29159c0559e834e71d99d", + "reference": "7e6b574fb500d35584c29159c0559e834e71d99d", "shasum": "" }, "require": { "ext-json": "*", "ext-mbstring": "*", "guzzlehttp/psr7": "^1 || ^2.1", - "php": "^8.1|^8.2|^8.3|^8.4", + "php": "^8.1|^8.2|^8.3|^8.4|^8.5", "php-http/discovery": "^1.14", "psr/http-client": "^1.0", "psr/http-message": "^1.0|^2.0" }, "require-dev": { - "doctrine/coding-standard": "^12.0", - "pestphp/pest": "^2.28", + "doctrine/coding-standard": "^14.0", "phpstan/phpstan": "^1.12", - "squizlabs/php_codesniffer": "^3.10" + "phpunit/phpunit": "^10.5", + "squizlabs/php_codesniffer": "^4.0" }, "type": "library", "autoload": { @@ -2964,7 +2950,7 @@ ], "support": { "issues": "https://github.com/gotenberg/gotenberg-php/issues", - "source": "https://github.com/gotenberg/gotenberg-php/tree/v2.14.0" + "source": "https://github.com/gotenberg/gotenberg-php/tree/v2.17.0" }, "funding": [ { @@ -2972,28 +2958,28 @@ "type": "github" } ], - "time": "2025-05-20T10:00:34+00:00" + "time": "2026-02-22T17:01:55+00:00" }, { "name": "graham-campbell/result-type", - "version": "v1.1.3", + "version": "v1.1.4", "source": { "type": "git", "url": "https://github.com/GrahamCampbell/Result-Type.git", - "reference": "3ba905c11371512af9d9bdd27d99b782216b6945" + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945", - "reference": "3ba905c11371512af9d9bdd27d99b782216b6945", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9.3" + "phpoption/phpoption": "^1.9.5" }, "require-dev": { - "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7" }, "type": "library", "autoload": { @@ -3022,7 +3008,7 @@ ], "support": { "issues": "https://github.com/GrahamCampbell/Result-Type/issues", - "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3" + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4" }, "funding": [ { @@ -3034,26 +3020,26 @@ "type": "tidelift" } ], - "time": "2024-07-20T21:45:45+00:00" + "time": "2025-12-27T19:43:20+00:00" }, { "name": "guzzlehttp/guzzle", - "version": "7.9.3", + "version": "7.10.0", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77" + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", - "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.5.3 || ^2.0.3", - "guzzlehttp/psr7": "^2.7.0", + "guzzlehttp/promises": "^2.3", + "guzzlehttp/psr7": "^2.8", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.2 || ^3.0" @@ -3144,7 +3130,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.9.3" + "source": "https://github.com/guzzle/guzzle/tree/7.10.0" }, "funding": [ { @@ -3160,20 +3146,20 @@ "type": "tidelift" } ], - "time": "2025-03-27T13:37:11+00:00" + "time": "2025-08-23T22:36:01+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.2.0", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c" + "reference": "481557b130ef3790cf82b713667b43030dc9c957" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/7c69f28996b0a6920945dd20b3857e499d9ca96c", - "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c", + "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", + "reference": "481557b130ef3790cf82b713667b43030dc9c957", "shasum": "" }, "require": { @@ -3181,7 +3167,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.39 || ^9.6.20" + "phpunit/phpunit": "^8.5.44 || ^9.6.25" }, "type": "library", "extra": { @@ -3227,7 +3213,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.2.0" + "source": "https://github.com/guzzle/promises/tree/2.3.0" }, "funding": [ { @@ -3243,20 +3229,20 @@ "type": "tidelift" } ], - "time": "2025-03-27T13:27:01+00:00" + "time": "2025-08-22T14:34:08+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.7.1", + "version": "2.9.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16" + "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16", - "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/7d0ed42f28e42d61352a7a79de682e5e67fec884", + "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884", "shasum": "" }, "require": { @@ -3272,7 +3258,8 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", "http-interop/http-factory-tests": "0.9.0", - "phpunit/phpunit": "^8.5.39 || ^9.6.20" + "jshttp/mime-db": "1.54.0.1", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -3343,7 +3330,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.7.1" + "source": "https://github.com/guzzle/psr7/tree/2.9.0" }, "funding": [ { @@ -3359,20 +3346,20 @@ "type": "tidelift" } ], - "time": "2025-03-27T12:30:47+00:00" + "time": "2026-03-10T16:41:02+00:00" }, { "name": "guzzlehttp/uri-template", - "version": "v1.0.4", + "version": "v1.0.5", "source": { "type": "git", "url": "https://github.com/guzzle/uri-template.git", - "reference": "30e286560c137526eccd4ce21b2de477ab0676d2" + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/uri-template/zipball/30e286560c137526eccd4ce21b2de477ab0676d2", - "reference": "30e286560c137526eccd4ce21b2de477ab0676d2", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1", "shasum": "" }, "require": { @@ -3381,7 +3368,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.36 || ^9.6.15", + "phpunit/phpunit": "^8.5.44 || ^9.6.25", "uri-template/tests": "1.0.0" }, "type": "library", @@ -3429,7 +3416,7 @@ ], "support": { "issues": "https://github.com/guzzle/uri-template/issues", - "source": "https://github.com/guzzle/uri-template/tree/v1.0.4" + "source": "https://github.com/guzzle/uri-template/tree/v1.0.5" }, "funding": [ { @@ -3445,33 +3432,38 @@ "type": "tidelift" } ], - "time": "2025-02-03T10:55:03+00:00" + "time": "2025-08-22T14:27:06+00:00" }, { "name": "inertiajs/inertia-laravel", - "version": "v2.0.3", + "version": "v2.0.22", "source": { "type": "git", "url": "https://github.com/inertiajs/inertia-laravel.git", - "reference": "b732a5cc33423b2c2366fea38b17dc637d2a0b4f" + "reference": "4d5849328d4c64231f886d1422fdc945882f9094" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/inertiajs/inertia-laravel/zipball/b732a5cc33423b2c2366fea38b17dc637d2a0b4f", - "reference": "b732a5cc33423b2c2366fea38b17dc637d2a0b4f", + "url": "https://api.github.com/repos/inertiajs/inertia-laravel/zipball/4d5849328d4c64231f886d1422fdc945882f9094", + "reference": "4d5849328d4c64231f886d1422fdc945882f9094", "shasum": "" }, "require": { "ext-json": "*", - "laravel/framework": "^10.0|^11.0|^12.0", + "laravel/framework": "^10.0|^11.0|^12.0|^13.0", "php": "^8.1.0", - "symfony/console": "^6.2|^7.0" + "symfony/console": "^6.2|^7.0|^8.0" + }, + "conflict": { + "laravel/boost": "<2.2.0" }, "require-dev": { + "guzzlehttp/guzzle": "^7.2", + "larastan/larastan": "^3.0", "laravel/pint": "^1.16", "mockery/mockery": "^1.3.3", - "orchestra/testbench": "^8.0|^9.2|^10.0", - "phpunit/phpunit": "^10.4|^11.5", + "orchestra/testbench": "^8.0|^9.2|^10.0|^11.0", + "phpunit/phpunit": "^10.4|^11.5|^12.0", "roave/security-advisories": "dev-master" }, "suggest": { @@ -3511,9 +3503,9 @@ ], "support": { "issues": "https://github.com/inertiajs/inertia-laravel/issues", - "source": "https://github.com/inertiajs/inertia-laravel/tree/v2.0.3" + "source": "https://github.com/inertiajs/inertia-laravel/tree/v2.0.22" }, - "time": "2025-06-20T07:38:21+00:00" + "time": "2026-03-11T15:51:16+00:00" }, { "name": "jawira/case-converter", @@ -3583,26 +3575,26 @@ }, { "name": "justinrainbow/json-schema", - "version": "6.4.2", + "version": "v6.7.2", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "ce1fd2d47799bb60668643bc6220f6278a4c1d02" + "reference": "6fea66c7204683af437864e7c4e7abf383d14bc0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/ce1fd2d47799bb60668643bc6220f6278a4c1d02", - "reference": "ce1fd2d47799bb60668643bc6220f6278a4c1d02", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/6fea66c7204683af437864e7c4e7abf383d14bc0", + "reference": "6fea66c7204683af437864e7c4e7abf383d14bc0", "shasum": "" }, "require": { "ext-json": "*", - "marc-mabe/php-enum": "^4.0", + "marc-mabe/php-enum": "^4.4", "php": "^7.2 || ^8.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "3.3.0", - "json-schema/json-schema-test-suite": "1.2.0", + "json-schema/json-schema-test-suite": "^23.2", "marc-mabe/php-enum-phpstan": "^2.0", "phpspec/prophecy": "^1.19", "phpstan/phpstan": "^1.12", @@ -3652,22 +3644,22 @@ ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/6.4.2" + "source": "https://github.com/jsonrainbow/json-schema/tree/v6.7.2" }, - "time": "2025-06-03T18:27:04+00:00" + "time": "2026-02-15T15:06:22+00:00" }, { "name": "kirschbaum-development/eloquent-power-joins", - "version": "4.2.6", + "version": "4.2.11", "source": { "type": "git", "url": "https://github.com/kirschbaum-development/eloquent-power-joins.git", - "reference": "72cff1e838bb3f826dc09a5566219ad7fa56237f" + "reference": "0e3e3372992e4bf82391b3c7b84b435c3db73588" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/kirschbaum-development/eloquent-power-joins/zipball/72cff1e838bb3f826dc09a5566219ad7fa56237f", - "reference": "72cff1e838bb3f826dc09a5566219ad7fa56237f", + "url": "https://api.github.com/repos/kirschbaum-development/eloquent-power-joins/zipball/0e3e3372992e4bf82391b3c7b84b435c3db73588", + "reference": "0e3e3372992e4bf82391b3c7b84b435c3db73588", "shasum": "" }, "require": { @@ -3715,9 +3707,9 @@ ], "support": { "issues": "https://github.com/kirschbaum-development/eloquent-power-joins/issues", - "source": "https://github.com/kirschbaum-development/eloquent-power-joins/tree/4.2.6" + "source": "https://github.com/kirschbaum-development/eloquent-power-joins/tree/4.2.11" }, - "time": "2025-07-10T16:55:34+00:00" + "time": "2025-12-17T00:37:48+00:00" }, { "name": "korridor/laravel-computed-attributes", @@ -3988,27 +3980,27 @@ }, { "name": "laminas/laminas-code", - "version": "4.16.0", + "version": "4.17.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-code.git", - "reference": "1793e78dad4108b594084d05d1fb818b85b110af" + "reference": "40d61e2899ec17c5d08bbc0a2d586b3ca17ab9bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-code/zipball/1793e78dad4108b594084d05d1fb818b85b110af", - "reference": "1793e78dad4108b594084d05d1fb818b85b110af", + "url": "https://api.github.com/repos/laminas/laminas-code/zipball/40d61e2899ec17c5d08bbc0a2d586b3ca17ab9bd", + "reference": "40d61e2899ec17c5d08bbc0a2d586b3ca17ab9bd", "shasum": "" }, "require": { - "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" }, "require-dev": { "doctrine/annotations": "^2.0.1", "ext-phar": "*", "laminas/laminas-coding-standard": "^3.0.0", "laminas/laminas-stdlib": "^3.18.0", - "phpunit/phpunit": "^10.5.37", + "phpunit/phpunit": "^10.5.58", "psalm/plugin-phpunit": "^0.19.0", "vimeo/psalm": "^5.15.0" }, @@ -4047,24 +4039,24 @@ "type": "community_bridge" } ], - "time": "2024-11-20T13:15:13+00:00" + "time": "2025-11-01T09:38:14+00:00" }, { "name": "laminas/laminas-diactoros", - "version": "3.6.0", + "version": "3.8.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-diactoros.git", - "reference": "b068eac123f21c0e592de41deeb7403b88e0a89f" + "reference": "60c182916b2749480895601649563970f3f12ec4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-diactoros/zipball/b068eac123f21c0e592de41deeb7403b88e0a89f", - "reference": "b068eac123f21c0e592de41deeb7403b88e0a89f", + "url": "https://api.github.com/repos/laminas/laminas-diactoros/zipball/60c182916b2749480895601649563970f3f12ec4", + "reference": "60c182916b2749480895601649563970f3f12ec4", "shasum": "" }, "require": { - "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", "psr/http-factory": "^1.1", "psr/http-message": "^1.1 || ^2.0" }, @@ -4081,11 +4073,11 @@ "ext-gd": "*", "ext-libxml": "*", "http-interop/http-factory-tests": "^2.2.0", - "laminas/laminas-coding-standard": "~3.0.0", + "laminas/laminas-coding-standard": "~3.1.0", "php-http/psr7-integration-tests": "^1.4.0", "phpunit/phpunit": "^10.5.36", - "psalm/plugin-phpunit": "^0.19.0", - "vimeo/psalm": "^5.26.1" + "psalm/plugin-phpunit": "^0.19.5", + "vimeo/psalm": "^6.13" }, "type": "library", "extra": { @@ -4135,35 +4127,33 @@ "type": "community_bridge" } ], - "time": "2025-05-05T16:03:34+00:00" + "time": "2025-10-12T15:31:36+00:00" }, { "name": "laravel/fortify", - "version": "v1.27.0", + "version": "v1.36.1", "source": { "type": "git", "url": "https://github.com/laravel/fortify.git", - "reference": "0fb2ec99dfee77ed66884668fc06683acca91ebd" + "reference": "cad8bfeb63f6818f173d40090725c565c92651d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/fortify/zipball/0fb2ec99dfee77ed66884668fc06683acca91ebd", - "reference": "0fb2ec99dfee77ed66884668fc06683acca91ebd", + "url": "https://api.github.com/repos/laravel/fortify/zipball/cad8bfeb63f6818f173d40090725c565c92651d4", + "reference": "cad8bfeb63f6818f173d40090725c565c92651d4", "shasum": "" }, "require": { "bacon/bacon-qr-code": "^3.0", "ext-json": "*", - "illuminate/support": "^10.0|^11.0|^12.0", + "illuminate/console": "^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", "php": "^8.1", - "pragmarx/google2fa": "^8.0", - "symfony/console": "^6.0|^7.0" + "pragmarx/google2fa": "^9.0" }, "require-dev": { - "mockery/mockery": "^1.0", - "orchestra/testbench": "^8.16|^9.0|^10.0", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^10.4|^11.3" + "orchestra/testbench": "^8.36|^9.15|^10.8|^11.0", + "phpstan/phpstan": "^1.10" }, "type": "library", "extra": { @@ -4200,24 +4190,24 @@ "issues": "https://github.com/laravel/fortify/issues", "source": "https://github.com/laravel/fortify" }, - "time": "2025-06-11T14:30:52+00:00" + "time": "2026-03-10T19:59:49+00:00" }, { "name": "laravel/framework", - "version": "v12.20.0", + "version": "v12.54.1", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "1b9a00f8caf5503c92aa436279172beae1a484ff" + "reference": "325497463e7599cd14224c422c6e5dd2fe832868" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/1b9a00f8caf5503c92aa436279172beae1a484ff", - "reference": "1b9a00f8caf5503c92aa436279172beae1a484ff", + "url": "https://api.github.com/repos/laravel/framework/zipball/325497463e7599cd14224c422c6e5dd2fe832868", + "reference": "325497463e7599cd14224c422c6e5dd2fe832868", "shasum": "" }, "require": { - "brick/math": "^0.11|^0.12|^0.13", + "brick/math": "^0.11|^0.12|^0.13|^0.14", "composer-runtime-api": "^2.2", "doctrine/inflector": "^2.0.5", "dragonmantank/cron-expression": "^3.4", @@ -4234,7 +4224,7 @@ "guzzlehttp/uri-template": "^1.0", "laravel/prompts": "^0.3.0", "laravel/serializable-closure": "^1.3|^2.0", - "league/commonmark": "^2.7", + "league/commonmark": "^2.8.1", "league/flysystem": "^3.25.1", "league/flysystem-local": "^3.25.1", "league/uri": "^7.5.1", @@ -4253,7 +4243,9 @@ "symfony/http-kernel": "^7.2.0", "symfony/mailer": "^7.2.0", "symfony/mime": "^7.2.0", - "symfony/polyfill-php83": "^1.31", + "symfony/polyfill-php83": "^1.33", + "symfony/polyfill-php84": "^1.33", + "symfony/polyfill-php85": "^1.33", "symfony/process": "^7.2.0", "symfony/routing": "^7.2.0", "symfony/uid": "^7.2.0", @@ -4289,6 +4281,7 @@ "illuminate/filesystem": "self.version", "illuminate/hashing": "self.version", "illuminate/http": "self.version", + "illuminate/json-schema": "self.version", "illuminate/log": "self.version", "illuminate/macroable": "self.version", "illuminate/mail": "self.version", @@ -4298,6 +4291,7 @@ "illuminate/process": "self.version", "illuminate/queue": "self.version", "illuminate/redis": "self.version", + "illuminate/reflection": "self.version", "illuminate/routing": "self.version", "illuminate/session": "self.version", "illuminate/support": "self.version", @@ -4321,13 +4315,14 @@ "league/flysystem-read-only": "^3.25.1", "league/flysystem-sftp-v3": "^3.25.1", "mockery/mockery": "^1.6.10", - "orchestra/testbench-core": "^10.0.0", + "opis/json-schema": "^2.4.1", + "orchestra/testbench-core": "^10.9.0", "pda/pheanstalk": "^5.0.6|^7.0.0", "php-http/discovery": "^1.15", "phpstan/phpstan": "^2.0", "phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1", "predis/predis": "^2.3|^3.0", - "resend/resend-php": "^0.10.0", + "resend/resend-php": "^0.10.0|^1.0", "symfony/cache": "^7.2.0", "symfony/http-client": "^7.2.0", "symfony/psr-http-message-bridge": "^7.2.0", @@ -4346,7 +4341,7 @@ "ext-pdo": "Required to use all database features.", "ext-posix": "Required to use all features of the queue worker.", "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0|^6.0).", - "fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).", + "fakerphp/faker": "Required to generate fake data using the fake() helper (^1.23).", "filp/whoops": "Required for friendly error pages in development (^2.14.3).", "laravel/tinker": "Required to use the tinker console command (^2.0).", "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", @@ -4361,7 +4356,7 @@ "predis/predis": "Required to use the predis connector (^2.3|^3.0).", "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", - "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0).", + "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0|^1.0).", "symfony/cache": "Required to PSR-6 cache bridge (^7.2).", "symfony/filesystem": "Required to enable support for relative symbolic links (^7.2).", "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.2).", @@ -4383,6 +4378,7 @@ "src/Illuminate/Filesystem/functions.php", "src/Illuminate/Foundation/helpers.php", "src/Illuminate/Log/functions.php", + "src/Illuminate/Reflection/helpers.php", "src/Illuminate/Support/functions.php", "src/Illuminate/Support/helpers.php" ], @@ -4391,7 +4387,8 @@ "Illuminate\\Support\\": [ "src/Illuminate/Macroable/", "src/Illuminate/Collections/", - "src/Illuminate/Conditionable/" + "src/Illuminate/Conditionable/", + "src/Illuminate/Reflection/" ] } }, @@ -4415,26 +4412,26 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-07-08T15:02:21+00:00" + "time": "2026-03-10T20:25:56+00:00" }, { "name": "laravel/jetstream", - "version": "v5.3.7", + "version": "v5.5.1", "source": { "type": "git", "url": "https://github.com/laravel/jetstream.git", - "reference": "b606c21daeaa38547f853789212e3802b0f6ff08" + "reference": "dbe57e175873fc62ea854eb689bb6afea1a6da71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/jetstream/zipball/b606c21daeaa38547f853789212e3802b0f6ff08", - "reference": "b606c21daeaa38547f853789212e3802b0f6ff08", + "url": "https://api.github.com/repos/laravel/jetstream/zipball/dbe57e175873fc62ea854eb689bb6afea1a6da71", + "reference": "dbe57e175873fc62ea854eb689bb6afea1a6da71", "shasum": "" }, "require": { "ext-json": "*", - "illuminate/console": "^11.0|^12.0", - "illuminate/support": "^11.0|^12.0", + "illuminate/console": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", "laravel/fortify": "^1.20", "mobiledetect/mobiledetectlib": "^4.8.08", "php": "^8.2.0", @@ -4445,9 +4442,8 @@ "laravel/sanctum": "^4.0", "livewire/livewire": "^3.3", "mockery/mockery": "^1.0", - "orchestra/testbench": "^9.0|^10.0", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^11.0" + "orchestra/testbench": "^9.15|^10.8|^11.0", + "phpstan/phpstan": "^1.10" }, "type": "library", "extra": { @@ -4482,20 +4478,20 @@ "issues": "https://github.com/laravel/jetstream/issues", "source": "https://github.com/laravel/jetstream" }, - "time": "2025-06-16T13:27:00+00:00" + "time": "2026-03-10T20:00:07+00:00" }, { "name": "laravel/octane", - "version": "v2.11.0", + "version": "v2.15.0", "source": { "type": "git", "url": "https://github.com/laravel/octane.git", - "reference": "00e4d40047a24c267c9d3d0abfb47a6e27a7dc7f" + "reference": "37b6175920bde50584ee4e2fa30ce4ae0f11ae36" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/octane/zipball/00e4d40047a24c267c9d3d0abfb47a6e27a7dc7f", - "reference": "00e4d40047a24c267c9d3d0abfb47a6e27a7dc7f", + "url": "https://api.github.com/repos/laravel/octane/zipball/37b6175920bde50584ee4e2fa30ce4ae0f11ae36", + "reference": "37b6175920bde50584ee4e2fa30ce4ae0f11ae36", "shasum": "" }, "require": { @@ -4572,48 +4568,46 @@ "issues": "https://github.com/laravel/octane/issues", "source": "https://github.com/laravel/octane" }, - "time": "2025-06-28T17:28:13+00:00" + "time": "2026-03-10T14:27:22+00:00" }, { "name": "laravel/passport", - "version": "v13.0.5", + "version": "v13.6.0", "source": { "type": "git", "url": "https://github.com/laravel/passport.git", - "reference": "9a3b47bc784dc9334aad67f690b13230b0434960" + "reference": "d97be1147f3dc2857e5a5cc4be0842c3ed46c5d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/passport/zipball/9a3b47bc784dc9334aad67f690b13230b0434960", - "reference": "9a3b47bc784dc9334aad67f690b13230b0434960", + "url": "https://api.github.com/repos/laravel/passport/zipball/d97be1147f3dc2857e5a5cc4be0842c3ed46c5d8", + "reference": "d97be1147f3dc2857e5a5cc4be0842c3ed46c5d8", "shasum": "" }, "require": { "ext-json": "*", "ext-openssl": "*", - "firebase/php-jwt": "^6.4", - "illuminate/auth": "^11.35|^12.0", - "illuminate/console": "^11.35|^12.0", - "illuminate/container": "^11.35|^12.0", - "illuminate/contracts": "^11.35|^12.0", - "illuminate/cookie": "^11.35|^12.0", - "illuminate/database": "^11.35|^12.0", - "illuminate/encryption": "^11.35|^12.0", - "illuminate/http": "^11.35|^12.0", - "illuminate/support": "^11.35|^12.0", + "firebase/php-jwt": "^6.4|^7.0", + "illuminate/auth": "^11.35|^12.0|^13.0", + "illuminate/console": "^11.35|^12.0|^13.0", + "illuminate/container": "^11.35|^12.0|^13.0", + "illuminate/contracts": "^11.35|^12.0|^13.0", + "illuminate/cookie": "^11.35|^12.0|^13.0", + "illuminate/database": "^11.35|^12.0|^13.0", + "illuminate/encryption": "^11.35|^12.0|^13.0", + "illuminate/http": "^11.35|^12.0|^13.0", + "illuminate/support": "^11.35|^12.0|^13.0", "league/oauth2-server": "^9.2", "php": "^8.2", "php-http/discovery": "^1.20", "phpseclib/phpseclib": "^3.0", "psr/http-factory-implementation": "*", - "symfony/console": "^7.1", - "symfony/psr-http-message-bridge": "^7.1" + "symfony/console": "^7.1|^8.0", + "symfony/psr-http-message-bridge": "^7.1|^8.0" }, "require-dev": { - "mockery/mockery": "^1.6", - "orchestra/testbench": "^9.9|^10.0", - "phpstan/phpstan": "^2.0", - "phpunit/phpunit": "^11.5|^12.0" + "orchestra/testbench": "^9.15|^10.8|^11.0", + "phpstan/phpstan": "^2.0" }, "type": "library", "extra": { @@ -4649,38 +4643,38 @@ "issues": "https://github.com/laravel/passport/issues", "source": "https://github.com/laravel/passport" }, - "time": "2025-06-12T15:12:08+00:00" + "time": "2026-03-05T16:26:09+00:00" }, { "name": "laravel/prompts", - "version": "v0.3.6", + "version": "v0.3.14", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "86a8b692e8661d0fb308cec64f3d176821323077" + "reference": "9f0e371244eedfe2ebeaa72c79c54bb5df6e0176" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/86a8b692e8661d0fb308cec64f3d176821323077", - "reference": "86a8b692e8661d0fb308cec64f3d176821323077", + "url": "https://api.github.com/repos/laravel/prompts/zipball/9f0e371244eedfe2ebeaa72c79c54bb5df6e0176", + "reference": "9f0e371244eedfe2ebeaa72c79c54bb5df6e0176", "shasum": "" }, "require": { "composer-runtime-api": "^2.2", "ext-mbstring": "*", "php": "^8.1", - "symfony/console": "^6.2|^7.0" + "symfony/console": "^6.2|^7.0|^8.0" }, "conflict": { "illuminate/console": ">=10.17.0 <10.25.0", "laravel/framework": ">=10.17.0 <10.25.0" }, "require-dev": { - "illuminate/collections": "^10.0|^11.0|^12.0", + "illuminate/collections": "^10.0|^11.0|^12.0|^13.0", "mockery/mockery": "^1.5", - "pestphp/pest": "^2.3|^3.4", - "phpstan/phpstan": "^1.11", - "phpstan/phpstan-mockery": "^1.1" + "pestphp/pest": "^2.3|^3.4|^4.0", + "phpstan/phpstan": "^1.12.28", + "phpstan/phpstan-mockery": "^1.1.3" }, "suggest": { "ext-pcntl": "Required for the spinner to be animated." @@ -4706,33 +4700,33 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.3.6" + "source": "https://github.com/laravel/prompts/tree/v0.3.14" }, - "time": "2025-07-07T14:17:42+00:00" + "time": "2026-03-01T09:02:38+00:00" }, { "name": "laravel/serializable-closure", - "version": "v2.0.4", + "version": "v2.0.10", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841" + "reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b352cf0534aa1ae6b4d825d1e762e35d43f8a841", - "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/870fc81d2f879903dfc5b60bf8a0f94a1609e669", + "reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669", "shasum": "" }, "require": { "php": "^8.1" }, "require-dev": { - "illuminate/support": "^10.0|^11.0|^12.0", + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", "nesbot/carbon": "^2.67|^3.0", - "pestphp/pest": "^2.36|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", "phpstan/phpstan": "^2.0", - "symfony/var-dumper": "^6.2.0|^7.0.0" + "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0" }, "type": "library", "extra": { @@ -4769,20 +4763,20 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2025-03-19T13:51:03+00:00" + "time": "2026-02-20T19:59:49+00:00" }, { "name": "laravel/tinker", - "version": "v2.10.1", + "version": "v2.11.1", "source": { "type": "git", "url": "https://github.com/laravel/tinker.git", - "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3" + "reference": "c9f80cc835649b5c1842898fb043f8cc098dd741" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/tinker/zipball/22177cc71807d38f2810c6204d8f7183d88a57d3", - "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3", + "url": "https://api.github.com/repos/laravel/tinker/zipball/c9f80cc835649b5c1842898fb043f8cc098dd741", + "reference": "c9f80cc835649b5c1842898fb043f8cc098dd741", "shasum": "" }, "require": { @@ -4791,7 +4785,7 @@ "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", "php": "^7.2.5|^8.0", "psy/psysh": "^0.11.1|^0.12.0", - "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0" + "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0|^8.0" }, "require-dev": { "mockery/mockery": "~1.3.3|^1.4.2", @@ -4833,40 +4827,40 @@ ], "support": { "issues": "https://github.com/laravel/tinker/issues", - "source": "https://github.com/laravel/tinker/tree/v2.10.1" + "source": "https://github.com/laravel/tinker/tree/v2.11.1" }, - "time": "2025-01-27T14:24:01+00:00" + "time": "2026-02-06T14:12:35+00:00" }, { "name": "lcobucci/clock", - "version": "3.3.1", + "version": "3.5.0", "source": { "type": "git", "url": "https://github.com/lcobucci/clock.git", - "reference": "db3713a61addfffd615b79bf0bc22f0ccc61b86b" + "reference": "a3139d9e97d47826f27e6a17bb63f13621f86058" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lcobucci/clock/zipball/db3713a61addfffd615b79bf0bc22f0ccc61b86b", - "reference": "db3713a61addfffd615b79bf0bc22f0ccc61b86b", + "url": "https://api.github.com/repos/lcobucci/clock/zipball/a3139d9e97d47826f27e6a17bb63f13621f86058", + "reference": "a3139d9e97d47826f27e6a17bb63f13621f86058", "shasum": "" }, "require": { - "php": "~8.2.0 || ~8.3.0 || ~8.4.0", + "php": "~8.3.0 || ~8.4.0 || ~8.5.0", "psr/clock": "^1.0" }, "provide": { "psr/clock-implementation": "1.0" }, "require-dev": { - "infection/infection": "^0.29", - "lcobucci/coding-standard": "^11.1.0", + "infection/infection": "^0.31", + "lcobucci/coding-standard": "^11.2.0", "phpstan/extension-installer": "^1.3.1", - "phpstan/phpstan": "^1.10.25", - "phpstan/phpstan-deprecation-rules": "^1.1.3", - "phpstan/phpstan-phpunit": "^1.3.13", - "phpstan/phpstan-strict-rules": "^1.5.1", - "phpunit/phpunit": "^11.3.6" + "phpstan/phpstan": "^2.0.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0.0", + "phpstan/phpstan-strict-rules": "^2.0.0", + "phpunit/phpunit": "^12.0.0" }, "type": "library", "autoload": { @@ -4887,7 +4881,7 @@ "description": "Yet another clock abstraction", "support": { "issues": "https://github.com/lcobucci/clock/issues", - "source": "https://github.com/lcobucci/clock/tree/3.3.1" + "source": "https://github.com/lcobucci/clock/tree/3.5.0" }, "funding": [ { @@ -4899,26 +4893,26 @@ "type": "patreon" } ], - "time": "2024-09-24T20:45:14+00:00" + "time": "2025-10-27T09:03:17+00:00" }, { "name": "lcobucci/jwt", - "version": "5.5.0", + "version": "5.6.0", "source": { "type": "git", "url": "https://github.com/lcobucci/jwt.git", - "reference": "a835af59b030d3f2967725697cf88300f579088e" + "reference": "bb3e9f21e4196e8afc41def81ef649c164bca25e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lcobucci/jwt/zipball/a835af59b030d3f2967725697cf88300f579088e", - "reference": "a835af59b030d3f2967725697cf88300f579088e", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/bb3e9f21e4196e8afc41def81ef649c164bca25e", + "reference": "bb3e9f21e4196e8afc41def81ef649c164bca25e", "shasum": "" }, "require": { "ext-openssl": "*", "ext-sodium": "*", - "php": "~8.2.0 || ~8.3.0 || ~8.4.0", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", "psr/clock": "^1.0" }, "require-dev": { @@ -4960,7 +4954,7 @@ ], "support": { "issues": "https://github.com/lcobucci/jwt/issues", - "source": "https://github.com/lcobucci/jwt/tree/5.5.0" + "source": "https://github.com/lcobucci/jwt/tree/5.6.0" }, "funding": [ { @@ -4972,20 +4966,20 @@ "type": "patreon" } ], - "time": "2025-01-26T21:29:45+00:00" + "time": "2025-10-17T11:30:53+00:00" }, { "name": "league/commonmark", - "version": "2.7.0", + "version": "2.8.1", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "6fbb36d44824ed4091adbcf4c7d4a3923cdb3405" + "reference": "84b1ca48347efdbe775426f108622a42735a6579" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/6fbb36d44824ed4091adbcf4c7d4a3923cdb3405", - "reference": "6fbb36d44824ed4091adbcf4c7d4a3923cdb3405", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/84b1ca48347efdbe775426f108622a42735a6579", + "reference": "84b1ca48347efdbe775426f108622a42735a6579", "shasum": "" }, "require": { @@ -5010,11 +5004,11 @@ "phpstan/phpstan": "^1.8.2", "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", "scrutinizer/ocular": "^1.8.1", - "symfony/finder": "^5.3 | ^6.0 | ^7.0", - "symfony/process": "^5.4 | ^6.0 | ^7.0", - "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0", + "symfony/finder": "^5.3 | ^6.0 | ^7.0 || ^8.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0 || ^8.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0 || ^8.0", "unleashedtech/php-coding-standard": "^3.1.1", - "vimeo/psalm": "^4.24.0 || ^5.0.0" + "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" }, "suggest": { "symfony/yaml": "v2.3+ required if using the Front Matter extension" @@ -5022,7 +5016,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.8-dev" + "dev-main": "2.9-dev" } }, "autoload": { @@ -5079,7 +5073,7 @@ "type": "tidelift" } ], - "time": "2025-05-05T12:20:28+00:00" + "time": "2026-03-05T21:37:03+00:00" }, { "name": "league/config", @@ -5165,16 +5159,16 @@ }, { "name": "league/csv", - "version": "9.24.1", + "version": "9.28.0", "source": { "type": "git", "url": "https://github.com/thephpleague/csv.git", - "reference": "e0221a3f16aa2a823047d59fab5809d552e29bc8" + "reference": "6582ace29ae09ba5b07049d40ea13eb19c8b5073" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/csv/zipball/e0221a3f16aa2a823047d59fab5809d552e29bc8", - "reference": "e0221a3f16aa2a823047d59fab5809d552e29bc8", + "url": "https://api.github.com/repos/thephpleague/csv/zipball/6582ace29ae09ba5b07049d40ea13eb19c8b5073", + "reference": "6582ace29ae09ba5b07049d40ea13eb19c8b5073", "shasum": "" }, "require": { @@ -5184,14 +5178,14 @@ "require-dev": { "ext-dom": "*", "ext-xdebug": "*", - "friendsofphp/php-cs-fixer": "^3.75.0", - "phpbench/phpbench": "^1.4.1", - "phpstan/phpstan": "^1.12.27", + "friendsofphp/php-cs-fixer": "^3.92.3", + "phpbench/phpbench": "^1.4.3", + "phpstan/phpstan": "^1.12.32", "phpstan/phpstan-deprecation-rules": "^1.2.1", "phpstan/phpstan-phpunit": "^1.4.2", "phpstan/phpstan-strict-rules": "^1.6.2", - "phpunit/phpunit": "^10.5.16 || ^11.5.22", - "symfony/var-dumper": "^6.4.8 || ^7.3.0" + "phpunit/phpunit": "^10.5.16 || ^11.5.22 || ^12.5.4", + "symfony/var-dumper": "^6.4.8 || ^7.4.0 || ^8.0" }, "suggest": { "ext-dom": "Required to use the XMLConverter and the HTMLConverter classes", @@ -5252,7 +5246,7 @@ "type": "github" } ], - "time": "2025-06-25T14:53:51+00:00" + "time": "2025-12-27T15:18:42+00:00" }, { "name": "league/event", @@ -5315,16 +5309,16 @@ }, { "name": "league/flysystem", - "version": "3.30.0", + "version": "3.32.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "2203e3151755d874bb2943649dae1eb8533ac93e" + "reference": "254b1595b16b22dbddaaef9ed6ca9fdac4956725" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/2203e3151755d874bb2943649dae1eb8533ac93e", - "reference": "2203e3151755d874bb2943649dae1eb8533ac93e", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/254b1595b16b22dbddaaef9ed6ca9fdac4956725", + "reference": "254b1595b16b22dbddaaef9ed6ca9fdac4956725", "shasum": "" }, "require": { @@ -5392,22 +5386,22 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.30.0" + "source": "https://github.com/thephpleague/flysystem/tree/3.32.0" }, - "time": "2025-06-25T13:29:59+00:00" + "time": "2026-02-25T17:01:41+00:00" }, { "name": "league/flysystem-aws-s3-v3", - "version": "3.29.0", + "version": "3.32.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git", - "reference": "c6ff6d4606e48249b63f269eba7fabdb584e76a9" + "reference": "a1979df7c9784d334ea6df356aed3d18ac6673d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/c6ff6d4606e48249b63f269eba7fabdb584e76a9", - "reference": "c6ff6d4606e48249b63f269eba7fabdb584e76a9", + "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/a1979df7c9784d334ea6df356aed3d18ac6673d0", + "reference": "a1979df7c9784d334ea6df356aed3d18ac6673d0", "shasum": "" }, "require": { @@ -5447,22 +5441,22 @@ "storage" ], "support": { - "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.29.0" + "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.32.0" }, - "time": "2024-08-17T13:10:48+00:00" + "time": "2026-02-25T16:46:44+00:00" }, { "name": "league/flysystem-local", - "version": "3.30.0", + "version": "3.31.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-local.git", - "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10" + "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/6691915f77c7fb69adfb87dcd550052dc184ee10", - "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/2f669db18a4c20c755c2bb7d3a7b0b2340488079", + "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079", "shasum": "" }, "require": { @@ -5496,22 +5490,22 @@ "local" ], "support": { - "source": "https://github.com/thephpleague/flysystem-local/tree/3.30.0" + "source": "https://github.com/thephpleague/flysystem-local/tree/3.31.0" }, - "time": "2025-05-21T10:34:19+00:00" + "time": "2026-01-23T15:30:45+00:00" }, { "name": "league/iso3166", - "version": "4.3.3", + "version": "4.4.0", "source": { "type": "git", "url": "https://github.com/alcohol/iso3166.git", - "reference": "3f692113a1c07859ec69303a0127b43da8a66768" + "reference": "928ac7ecc569db9123a83ef5b1c6efc279e7cb49" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/alcohol/iso3166/zipball/3f692113a1c07859ec69303a0127b43da8a66768", - "reference": "3f692113a1c07859ec69303a0127b43da8a66768", + "url": "https://api.github.com/repos/alcohol/iso3166/zipball/928ac7ecc569db9123a83ef5b1c6efc279e7cb49", + "reference": "928ac7ecc569db9123a83ef5b1c6efc279e7cb49", "shasum": "" }, "require": { @@ -5565,7 +5559,7 @@ "type": "github" } ], - "time": "2025-06-05T08:06:30+00:00" + "time": "2026-01-02T09:49:36+00:00" }, { "name": "league/mime-type-detection", @@ -5625,16 +5619,16 @@ }, { "name": "league/oauth2-server", - "version": "9.2.0", + "version": "9.3.0", "source": { "type": "git", "url": "https://github.com/thephpleague/oauth2-server.git", - "reference": "00323013403e1a1e0f424affafca56c28b60c22c" + "reference": "d8e2f39f645a82b207bbac441694d6e6079357cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/oauth2-server/zipball/00323013403e1a1e0f424affafca56c28b60c22c", - "reference": "00323013403e1a1e0f424affafca56c28b60c22c", + "url": "https://api.github.com/repos/thephpleague/oauth2-server/zipball/d8e2f39f645a82b207bbac441694d6e6079357cb", + "reference": "d8e2f39f645a82b207bbac441694d6e6079357cb", "shasum": "" }, "require": { @@ -5645,7 +5639,7 @@ "lcobucci/jwt": "^5.0", "league/event": "^3.0", "league/uri": "^7.0", - "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", "psr/http-message": "^2.0", "psr/http-server-middleware": "^1.0" }, @@ -5657,11 +5651,11 @@ "laminas/laminas-diactoros": "^3.5", "php-parallel-lint/php-parallel-lint": "^1.3.2", "phpstan/extension-installer": "^1.3.1", - "phpstan/phpstan": "^1.12", - "phpstan/phpstan-deprecation-rules": "^1.1.4", - "phpstan/phpstan-phpunit": "^1.3.15", - "phpstan/phpstan-strict-rules": "^1.5.2", - "phpunit/phpunit": "^9.6.21", + "phpstan/phpstan": "^1.12|^2.0", + "phpstan/phpstan-deprecation-rules": "^1.1.4|^2.0", + "phpstan/phpstan-phpunit": "^1.3.15|^2.0", + "phpstan/phpstan-strict-rules": "^1.5.2|^2.0", + "phpunit/phpunit": "^10.5|^11.5|^12.0", "roave/security-advisories": "dev-master", "slevomat/coding-standard": "^8.14.1", "squizlabs/php_codesniffer": "^3.8" @@ -5709,7 +5703,7 @@ ], "support": { "issues": "https://github.com/thephpleague/oauth2-server/issues", - "source": "https://github.com/thephpleague/oauth2-server/tree/9.2.0" + "source": "https://github.com/thephpleague/oauth2-server/tree/9.3.0" }, "funding": [ { @@ -5717,37 +5711,42 @@ "type": "github" } ], - "time": "2025-02-15T00:49:10+00:00" + "time": "2025-11-25T22:51:15+00:00" }, { "name": "league/uri", - "version": "7.5.1", + "version": "7.8.1", "source": { "type": "git", "url": "https://github.com/thephpleague/uri.git", - "reference": "81fb5145d2644324614cc532b28efd0215bda430" + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/81fb5145d2644324614cc532b28efd0215bda430", - "reference": "81fb5145d2644324614cc532b28efd0215bda430", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/08cf38e3924d4f56238125547b5720496fac8fd4", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4", "shasum": "" }, "require": { - "league/uri-interfaces": "^7.5", - "php": "^8.1" + "league/uri-interfaces": "^7.8.1", + "php": "^8.1", + "psr/http-factory": "^1" }, "conflict": { "league/uri-schemes": "^1.0" }, "suggest": { "ext-bcmath": "to improve IPV4 host parsing", + "ext-dom": "to convert the URI into an HTML anchor tag", "ext-fileinfo": "to create Data URI from file contennts", "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", - "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", - "league/uri-components": "Needed to easily manipulate URI objects components", + "ext-uri": "to use the PHP native URI class", + "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", + "league/uri-components": "to provide additional tools to manipulate URI objects components", + "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -5775,6 +5774,7 @@ "description": "URI manipulation library", "homepage": "https://uri.thephpleague.com", "keywords": [ + "URN", "data-uri", "file-uri", "ftp", @@ -5787,9 +5787,11 @@ "psr-7", "query-string", "querystring", + "rfc2141", "rfc3986", "rfc3987", "rfc6570", + "rfc8141", "uri", "uri-template", "url", @@ -5799,7 +5801,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.5.1" + "source": "https://github.com/thephpleague/uri/tree/7.8.1" }, "funding": [ { @@ -5807,26 +5809,25 @@ "type": "github" } ], - "time": "2024-12-08T08:40:02+00:00" + "time": "2026-03-15T20:22:25+00:00" }, { "name": "league/uri-interfaces", - "version": "7.5.0", + "version": "7.8.1", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742" + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", - "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928", "shasum": "" }, "require": { "ext-filter": "*", "php": "^8.1", - "psr/http-factory": "^1", "psr/http-message": "^1.1 || ^2.0" }, "suggest": { @@ -5834,6 +5835,7 @@ "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -5858,7 +5860,7 @@ "homepage": "https://nyamsprod.com" } ], - "description": "Common interfaces and classes for URI representation and interaction", + "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI", "homepage": "https://uri.thephpleague.com", "keywords": [ "data-uri", @@ -5883,7 +5885,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.5.0" + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.1" }, "funding": [ { @@ -5891,40 +5893,40 @@ "type": "github" } ], - "time": "2024-12-08T08:18:47+00:00" + "time": "2026-03-08T20:05:35+00:00" }, { "name": "livewire/livewire", - "version": "v3.6.3", + "version": "v3.7.11", "source": { "type": "git", "url": "https://github.com/livewire/livewire.git", - "reference": "56aa1bb63a46e06181c56fa64717a7287e19115e" + "reference": "addd6e8e9234df75f29e6a327ee2a745a7d67bb6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/livewire/zipball/56aa1bb63a46e06181c56fa64717a7287e19115e", - "reference": "56aa1bb63a46e06181c56fa64717a7287e19115e", + "url": "https://api.github.com/repos/livewire/livewire/zipball/addd6e8e9234df75f29e6a327ee2a745a7d67bb6", + "reference": "addd6e8e9234df75f29e6a327ee2a745a7d67bb6", "shasum": "" }, "require": { - "illuminate/database": "^10.0|^11.0|^12.0", - "illuminate/routing": "^10.0|^11.0|^12.0", - "illuminate/support": "^10.0|^11.0|^12.0", - "illuminate/validation": "^10.0|^11.0|^12.0", + "illuminate/database": "^10.0|^11.0|^12.0|^13.0", + "illuminate/routing": "^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "illuminate/validation": "^10.0|^11.0|^12.0|^13.0", "laravel/prompts": "^0.1.24|^0.2|^0.3", "league/mime-type-detection": "^1.9", "php": "^8.1", - "symfony/console": "^6.0|^7.0", - "symfony/http-kernel": "^6.2|^7.0" + "symfony/console": "^6.0|^7.0|^8.0", + "symfony/http-kernel": "^6.2|^7.0|^8.0" }, "require-dev": { "calebporzio/sushi": "^2.1", - "laravel/framework": "^10.15.0|^11.0|^12.0", + "laravel/framework": "^10.15.0|^11.0|^12.0|^13.0", "mockery/mockery": "^1.3.1", - "orchestra/testbench": "^8.21.0|^9.0|^10.0", - "orchestra/testbench-dusk": "^8.24|^9.1|^10.0", - "phpunit/phpunit": "^10.4|^11.5", + "orchestra/testbench": "^8.21.0|^9.0|^10.0|^11.0", + "orchestra/testbench-dusk": "^8.24|^9.1|^10.0|^11.0", + "phpunit/phpunit": "^10.4|^11.5|^12.5", "psy/psysh": "^0.11.22|^0.12" }, "type": "library", @@ -5959,7 +5961,7 @@ "description": "A front-end framework for Laravel.", "support": { "issues": "https://github.com/livewire/livewire/issues", - "source": "https://github.com/livewire/livewire/tree/v3.6.3" + "source": "https://github.com/livewire/livewire/tree/v3.7.11" }, "funding": [ { @@ -5967,20 +5969,20 @@ "type": "github" } ], - "time": "2025-04-12T22:26:52+00:00" + "time": "2026-02-26T00:58:19+00:00" }, { "name": "maatwebsite/excel", - "version": "3.1.64", + "version": "3.1.67", "source": { "type": "git", "url": "https://github.com/SpartnerNL/Laravel-Excel.git", - "reference": "e25d44a2d91da9179cd2d7fec952313548597a79" + "reference": "e508e34a502a3acc3329b464dad257378a7edb4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/SpartnerNL/Laravel-Excel/zipball/e25d44a2d91da9179cd2d7fec952313548597a79", - "reference": "e25d44a2d91da9179cd2d7fec952313548597a79", + "url": "https://api.github.com/repos/SpartnerNL/Laravel-Excel/zipball/e508e34a502a3acc3329b464dad257378a7edb4d", + "reference": "e508e34a502a3acc3329b464dad257378a7edb4d", "shasum": "" }, "require": { @@ -5988,7 +5990,7 @@ "ext-json": "*", "illuminate/support": "5.8.*||^6.0||^7.0||^8.0||^9.0||^10.0||^11.0||^12.0", "php": "^7.0||^8.0", - "phpoffice/phpspreadsheet": "^1.29.9", + "phpoffice/phpspreadsheet": "^1.30.0", "psr/simple-cache": "^1.0||^2.0||^3.0" }, "require-dev": { @@ -6036,7 +6038,7 @@ ], "support": { "issues": "https://github.com/SpartnerNL/Laravel-Excel/issues", - "source": "https://github.com/SpartnerNL/Laravel-Excel/tree/3.1.64" + "source": "https://github.com/SpartnerNL/Laravel-Excel/tree/3.1.67" }, "funding": [ { @@ -6048,35 +6050,35 @@ "type": "github" } ], - "time": "2025-02-24T11:12:50+00:00" + "time": "2025-08-26T09:13:16+00:00" }, { "name": "maennchen/zipstream-php", - "version": "3.1.2", + "version": "3.2.1", "source": { "type": "git", "url": "https://github.com/maennchen/ZipStream-PHP.git", - "reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f" + "reference": "682f1098a8fddbaf43edac2306a691c7ad508ec5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/aeadcf5c412332eb426c0f9b4485f6accba2a99f", - "reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/682f1098a8fddbaf43edac2306a691c7ad508ec5", + "reference": "682f1098a8fddbaf43edac2306a691c7ad508ec5", "shasum": "" }, "require": { "ext-mbstring": "*", "ext-zlib": "*", - "php-64bit": "^8.2" + "php-64bit": "^8.3" }, "require-dev": { "brianium/paratest": "^7.7", "ext-zip": "*", - "friendsofphp/php-cs-fixer": "^3.16", + "friendsofphp/php-cs-fixer": "^3.86", "guzzlehttp/guzzle": "^7.5", "mikey179/vfsstream": "^1.6", "php-coveralls/php-coveralls": "^2.5", - "phpunit/phpunit": "^11.0", + "phpunit/phpunit": "^12.0", "vimeo/psalm": "^6.0" }, "suggest": { @@ -6118,7 +6120,7 @@ ], "support": { "issues": "https://github.com/maennchen/ZipStream-PHP/issues", - "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.2" + "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.1" }, "funding": [ { @@ -6126,20 +6128,20 @@ "type": "github" } ], - "time": "2025-01-27T12:07:53+00:00" + "time": "2025-12-10T09:58:31+00:00" }, { "name": "marc-mabe/php-enum", - "version": "v4.7.1", + "version": "v4.7.2", "source": { "type": "git", "url": "https://github.com/marc-mabe/php-enum.git", - "reference": "7159809e5cfa041dca28e61f7f7ae58063aae8ed" + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/marc-mabe/php-enum/zipball/7159809e5cfa041dca28e61f7f7ae58063aae8ed", - "reference": "7159809e5cfa041dca28e61f7f7ae58063aae8ed", + "url": "https://api.github.com/repos/marc-mabe/php-enum/zipball/bb426fcdd65c60fb3638ef741e8782508fda7eef", + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef", "shasum": "" }, "require": { @@ -6197,9 +6199,9 @@ ], "support": { "issues": "https://github.com/marc-mabe/php-enum/issues", - "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.1" + "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.2" }, - "time": "2024-11-28T04:54:44+00:00" + "time": "2025-09-14T11:18:39+00:00" }, { "name": "markbaker/complex", @@ -6310,16 +6312,16 @@ }, { "name": "masterminds/html5", - "version": "2.9.0", + "version": "2.10.0", "source": { "type": "git", "url": "https://github.com/Masterminds/html5-php.git", - "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6" + "reference": "fcf91eb64359852f00d921887b219479b4f21251" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", - "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fcf91eb64359852f00d921887b219479b4f21251", + "reference": "fcf91eb64359852f00d921887b219479b4f21251", "shasum": "" }, "require": { @@ -6371,35 +6373,34 @@ ], "support": { "issues": "https://github.com/Masterminds/html5-php/issues", - "source": "https://github.com/Masterminds/html5-php/tree/2.9.0" + "source": "https://github.com/Masterminds/html5-php/tree/2.10.0" }, - "time": "2024-03-31T07:05:07+00:00" + "time": "2025-07-25T09:04:22+00:00" }, { "name": "mobiledetect/mobiledetectlib", - "version": "4.8.09", + "version": "4.8.10", "source": { "type": "git", "url": "https://github.com/serbanghita/Mobile-Detect.git", - "reference": "a06fe2e546a06bb8c2639d6823d5250b2efb3209" + "reference": "96b1e1fa9a968de7660a031106ab529f659d0192" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/serbanghita/Mobile-Detect/zipball/a06fe2e546a06bb8c2639d6823d5250b2efb3209", - "reference": "a06fe2e546a06bb8c2639d6823d5250b2efb3209", + "url": "https://api.github.com/repos/serbanghita/Mobile-Detect/zipball/96b1e1fa9a968de7660a031106ab529f659d0192", + "reference": "96b1e1fa9a968de7660a031106ab529f659d0192", "shasum": "" }, "require": { "php": ">=8.0", - "psr/cache": "^3.0", "psr/simple-cache": "^3" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^v3.65.0", + "friendsofphp/php-cs-fixer": "^v3.75.0", "phpbench/phpbench": "^1.2", - "phpstan/phpstan": "^1.12.x-dev", - "phpunit/phpunit": "^9.6.18", - "squizlabs/php_codesniffer": "^3.11.1" + "phpstan/phpstan": "^2.1.11", + "phpunit/phpunit": "^9.6.22", + "squizlabs/php_codesniffer": "^3.12.1" }, "type": "library", "autoload": { @@ -6430,7 +6431,7 @@ ], "support": { "issues": "https://github.com/serbanghita/Mobile-Detect/issues", - "source": "https://github.com/serbanghita/Mobile-Detect/tree/4.8.09" + "source": "https://github.com/serbanghita/Mobile-Detect/tree/4.8.10" }, "funding": [ { @@ -6438,20 +6439,20 @@ "type": "github" } ], - "time": "2024-12-10T15:32:06+00:00" + "time": "2026-01-09T16:21:59+00:00" }, { "name": "monolog/monolog", - "version": "3.9.0", + "version": "3.10.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6" + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6", - "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0", "shasum": "" }, "require": { @@ -6469,7 +6470,7 @@ "graylog2/gelf-php": "^1.4.2 || ^2.0", "guzzlehttp/guzzle": "^7.4.5", "guzzlehttp/psr7": "^2.2", - "mongodb/mongodb": "^1.8", + "mongodb/mongodb": "^1.8 || ^2.0", "php-amqplib/php-amqplib": "~2.4 || ^3", "php-console/php-console": "^3.1.8", "phpstan/phpstan": "^2", @@ -6529,7 +6530,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/3.9.0" + "source": "https://github.com/Seldaek/monolog/tree/3.10.0" }, "funding": [ { @@ -6541,7 +6542,7 @@ "type": "tidelift" } ], - "time": "2025-03-24T10:02:05+00:00" + "time": "2026-01-02T08:56:05+00:00" }, { "name": "mtdowling/jmespath.php", @@ -6611,16 +6612,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.13.3", + "version": "1.13.4", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "faed855a7b5f4d4637717c2b3863e277116beb36" + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/faed855a7b5f4d4637717c2b3863e277116beb36", - "reference": "faed855a7b5f4d4637717c2b3863e277116beb36", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { @@ -6659,7 +6660,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.3" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, "funding": [ { @@ -6667,20 +6668,20 @@ "type": "tidelift" } ], - "time": "2025-07-05T12:25:42+00:00" + "time": "2025-08-01T08:46:24+00:00" }, { "name": "nesbot/carbon", - "version": "3.10.1", + "version": "3.11.3", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "1fd1935b2d90aef2f093c5e35f7ae1257c448d00" + "reference": "6a7e652845bb018c668220c2a545aded8594fbbf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/1fd1935b2d90aef2f093c5e35f7ae1257c448d00", - "reference": "1fd1935b2d90aef2f093c5e35f7ae1257c448d00", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/6a7e652845bb018c668220c2a545aded8594fbbf", + "reference": "6a7e652845bb018c668220c2a545aded8594fbbf", "shasum": "" }, "require": { @@ -6688,9 +6689,9 @@ "ext-json": "*", "php": "^8.1", "psr/clock": "^1.0", - "symfony/clock": "^6.3.12 || ^7.0", + "symfony/clock": "^6.3.12 || ^7.0 || ^8.0", "symfony/polyfill-mbstring": "^1.0", - "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0" + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0" }, "provide": { "psr/clock-implementation": "1.0" @@ -6698,13 +6699,13 @@ "require-dev": { "doctrine/dbal": "^3.6.3 || ^4.0", "doctrine/orm": "^2.15.2 || ^3.0", - "friendsofphp/php-cs-fixer": "^3.75.0", + "friendsofphp/php-cs-fixer": "^v3.87.1", "kylekatarnls/multi-tester": "^2.5.3", "phpmd/phpmd": "^2.15.0", "phpstan/extension-installer": "^1.4.3", - "phpstan/phpstan": "^2.1.17", - "phpunit/phpunit": "^10.5.46", - "squizlabs/php_codesniffer": "^3.13.0" + "phpstan/phpstan": "^2.1.22", + "phpunit/phpunit": "^10.5.53", + "squizlabs/php_codesniffer": "^3.13.4 || ^4.0.0" }, "bin": [ "bin/carbon" @@ -6747,14 +6748,14 @@ } ], "description": "An API extension for DateTime that supports 281 different languages.", - "homepage": "https://carbon.nesbot.com", + "homepage": "https://carbonphp.github.io/carbon/", "keywords": [ "date", "datetime", "time" ], "support": { - "docs": "https://carbon.nesbot.com/docs", + "docs": "https://carbonphp.github.io/carbon/guide/getting-started/introduction.html", "issues": "https://github.com/CarbonPHP/carbon/issues", "source": "https://github.com/CarbonPHP/carbon" }, @@ -6772,29 +6773,31 @@ "type": "tidelift" } ], - "time": "2025-06-21T15:19:35+00:00" + "time": "2026-03-11T17:23:39+00:00" }, { "name": "nette/schema", - "version": "v1.3.2", + "version": "v1.3.5", "source": { "type": "git", "url": "https://github.com/nette/schema.git", - "reference": "da801d52f0354f70a638673c4a0f04e16529431d" + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/schema/zipball/da801d52f0354f70a638673c4a0f04e16529431d", - "reference": "da801d52f0354f70a638673c4a0f04e16529431d", + "url": "https://api.github.com/repos/nette/schema/zipball/f0ab1a3cda782dbc5da270d28545236aa80c4002", + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002", "shasum": "" }, "require": { "nette/utils": "^4.0", - "php": "8.1 - 8.4" + "php": "8.1 - 8.5" }, "require-dev": { - "nette/tester": "^2.5.2", - "phpstan/phpstan-nette": "^1.0", + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.6", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1.39@stable", "tracy/tracy": "^2.8" }, "type": "library", @@ -6804,6 +6807,9 @@ } }, "autoload": { + "psr-4": { + "Nette\\": "src" + }, "classmap": [ "src/" ] @@ -6832,35 +6838,37 @@ ], "support": { "issues": "https://github.com/nette/schema/issues", - "source": "https://github.com/nette/schema/tree/v1.3.2" + "source": "https://github.com/nette/schema/tree/v1.3.5" }, - "time": "2024-10-06T23:10:23+00:00" + "time": "2026-02-23T03:47:12+00:00" }, { "name": "nette/utils", - "version": "v4.0.7", + "version": "v4.1.3", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2" + "reference": "bb3ea637e3d131d72acc033cfc2746ee893349fe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/e67c4061eb40b9c113b218214e42cb5a0dda28f2", - "reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2", + "url": "https://api.github.com/repos/nette/utils/zipball/bb3ea637e3d131d72acc033cfc2746ee893349fe", + "reference": "bb3ea637e3d131d72acc033cfc2746ee893349fe", "shasum": "" }, "require": { - "php": "8.0 - 8.4" + "php": "8.2 - 8.5" }, "conflict": { "nette/finder": "<3", "nette/schema": "<1.2.2" }, "require-dev": { - "jetbrains/phpstorm-attributes": "dev-master", + "jetbrains/phpstorm-attributes": "^1.2", + "nette/phpstan-rules": "^1.0", "nette/tester": "^2.5", - "phpstan/phpstan": "^1.0", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1@stable", "tracy/tracy": "^2.9" }, "suggest": { @@ -6874,10 +6882,13 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-master": "4.1-dev" } }, "autoload": { + "psr-4": { + "Nette\\": "src" + }, "classmap": [ "src/" ] @@ -6918,22 +6929,22 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.0.7" + "source": "https://github.com/nette/utils/tree/v4.1.3" }, - "time": "2025-06-03T04:55:08+00:00" + "time": "2026-02-13T03:05:33+00:00" }, { "name": "nikic/php-parser", - "version": "v5.5.0", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "ae59794362fe85e051a58ad36b289443f57be7a9" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ae59794362fe85e051a58ad36b289443f57be7a9", - "reference": "ae59794362fe85e051a58ad36b289443f57be7a9", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -6952,7 +6963,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.x-dev" } }, "autoload": { @@ -6976,22 +6987,22 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.5.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2025-05-31T08:24:38+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "novadaemon/filament-pretty-json", - "version": "v2.5.0", + "version": "v2.7.0", "source": { "type": "git", "url": "https://github.com/novadaemon/filament-pretty-json.git", - "reference": "e1577286c6b26d85ba0deaf95fcd13ee6d09c831" + "reference": "19f0333efd1d142882a0c82d54d88ace465a95dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/novadaemon/filament-pretty-json/zipball/e1577286c6b26d85ba0deaf95fcd13ee6d09c831", - "reference": "e1577286c6b26d85ba0deaf95fcd13ee6d09c831", + "url": "https://api.github.com/repos/novadaemon/filament-pretty-json/zipball/19f0333efd1d142882a0c82d54d88ace465a95dc", + "reference": "19f0333efd1d142882a0c82d54d88ace465a95dc", "shasum": "" }, "require": { @@ -7050,35 +7061,35 @@ "type": "github" } ], - "time": "2025-05-12T12:42:29+00:00" + "time": "2025-08-20T15:45:27+00:00" }, { "name": "nunomaduro/termwind", - "version": "v2.3.1", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/nunomaduro/termwind.git", - "reference": "dfa08f390e509967a15c22493dc0bac5733d9123" + "reference": "712a31b768f5daea284c2169a7d227031001b9a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/dfa08f390e509967a15c22493dc0bac5733d9123", - "reference": "dfa08f390e509967a15c22493dc0bac5733d9123", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/712a31b768f5daea284c2169a7d227031001b9a8", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8", "shasum": "" }, "require": { "ext-mbstring": "*", "php": "^8.2", - "symfony/console": "^7.2.6" + "symfony/console": "^7.4.4 || ^8.0.4" }, "require-dev": { - "illuminate/console": "^11.44.7", - "laravel/pint": "^1.22.0", + "illuminate/console": "^11.47.0", + "laravel/pint": "^1.27.1", "mockery/mockery": "^1.6.12", - "pestphp/pest": "^2.36.0 || ^3.8.2", - "phpstan/phpstan": "^1.12.25", + "pestphp/pest": "^2.36.0 || ^3.8.4 || ^4.3.2", + "phpstan/phpstan": "^1.12.32", "phpstan/phpstan-strict-rules": "^1.6.2", - "symfony/var-dumper": "^7.2.6", + "symfony/var-dumper": "^7.3.5 || ^8.0.4", "thecodingmachine/phpstan-strict-rules": "^1.0.0" }, "type": "library", @@ -7110,7 +7121,7 @@ "email": "enunomaduro@gmail.com" } ], - "description": "Its like Tailwind CSS, but for the console.", + "description": "It's like Tailwind CSS, but for the console.", "keywords": [ "cli", "console", @@ -7121,7 +7132,7 @@ ], "support": { "issues": "https://github.com/nunomaduro/termwind/issues", - "source": "https://github.com/nunomaduro/termwind/tree/v2.3.1" + "source": "https://github.com/nunomaduro/termwind/tree/v2.4.0" }, "funding": [ { @@ -7137,7 +7148,7 @@ "type": "github" } ], - "time": "2025-05-08T08:14:37+00:00" + "time": "2026-02-16T23:10:27+00:00" }, { "name": "nwidart/laravel-modules", @@ -7230,16 +7241,16 @@ }, { "name": "openspout/openspout", - "version": "v4.30.1", + "version": "v4.32.0", "source": { "type": "git", "url": "https://github.com/openspout/openspout.git", - "reference": "4550fc0dbf01aff86d12691f8a7f6ce22d2b2edc" + "reference": "41f045c1f632e1474e15d4c7bc3abcb4a153563d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/openspout/openspout/zipball/4550fc0dbf01aff86d12691f8a7f6ce22d2b2edc", - "reference": "4550fc0dbf01aff86d12691f8a7f6ce22d2b2edc", + "url": "https://api.github.com/repos/openspout/openspout/zipball/41f045c1f632e1474e15d4c7bc3abcb4a153563d", + "reference": "41f045c1f632e1474e15d4c7bc3abcb4a153563d", "shasum": "" }, "require": { @@ -7249,17 +7260,17 @@ "ext-libxml": "*", "ext-xmlreader": "*", "ext-zip": "*", - "php": "~8.3.0 || ~8.4.0" + "php": "~8.3.0 || ~8.4.0 || ~8.5.0" }, "require-dev": { "ext-zlib": "*", - "friendsofphp/php-cs-fixer": "^3.80.0", - "infection/infection": "^0.30.1", + "friendsofphp/php-cs-fixer": "^3.86.0", + "infection/infection": "^0.31.2", "phpbench/phpbench": "^1.4.1", - "phpstan/phpstan": "^2.1.17", - "phpstan/phpstan-phpunit": "^2.0.6", - "phpstan/phpstan-strict-rules": "^2.0.4", - "phpunit/phpunit": "^12.2.6" + "phpstan/phpstan": "^2.1.22", + "phpstan/phpstan-phpunit": "^2.0.7", + "phpstan/phpstan-strict-rules": "^2.0.6", + "phpunit/phpunit": "^12.3.7" }, "suggest": { "ext-iconv": "To handle non UTF-8 CSV files (if \"php-mbstring\" is not already installed or is too limited)", @@ -7307,7 +7318,7 @@ ], "support": { "issues": "https://github.com/openspout/openspout/issues", - "source": "https://github.com/openspout/openspout/tree/v4.30.1" + "source": "https://github.com/openspout/openspout/tree/v4.32.0" }, "funding": [ { @@ -7319,7 +7330,7 @@ "type": "github" } ], - "time": "2025-07-07T06:15:55+00:00" + "time": "2025-09-03T16:03:54+00:00" }, { "name": "owen-it/laravel-auditing", @@ -7407,24 +7418,26 @@ }, { "name": "paragonie/constant_time_encoding", - "version": "v3.0.0", + "version": "v3.1.3", "source": { "type": "git", "url": "https://github.com/paragonie/constant_time_encoding.git", - "reference": "df1e7fde177501eee2037dd159cf04f5f301a512" + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/df1e7fde177501eee2037dd159cf04f5f301a512", - "reference": "df1e7fde177501eee2037dd159cf04f5f301a512", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", "shasum": "" }, "require": { "php": "^8" }, "require-dev": { - "phpunit/phpunit": "^9", - "vimeo/psalm": "^4|^5" + "infection/infection": "^0", + "nikic/php-fuzzer": "^0", + "phpunit/phpunit": "^9|^10|^11", + "vimeo/psalm": "^4|^5|^6" }, "type": "library", "autoload": { @@ -7470,7 +7483,7 @@ "issues": "https://github.com/paragonie/constant_time_encoding/issues", "source": "https://github.com/paragonie/constant_time_encoding" }, - "time": "2024-05-08T12:36:18+00:00" + "time": "2025-09-24T15:06:41+00:00" }, { "name": "paragonie/random_compat", @@ -7603,16 +7616,16 @@ }, { "name": "phpoffice/phpspreadsheet", - "version": "1.29.11", + "version": "1.30.2", "source": { "type": "git", "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", - "reference": "05b6c4378ddf3e81b460ea645c42b46432c0db25" + "reference": "09cdde5e2f078b9a3358dd217e2c8cb4dac84be2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/05b6c4378ddf3e81b460ea645c42b46432c0db25", - "reference": "05b6c4378ddf3e81b460ea645c42b46432c0db25", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/09cdde5e2f078b9a3358dd217e2c8cb4dac84be2", + "reference": "09cdde5e2f078b9a3358dd217e2c8cb4dac84be2", "shasum": "" }, "require": { @@ -7634,13 +7647,12 @@ "maennchen/zipstream-php": "^2.1 || ^3.0", "markbaker/complex": "^3.0", "markbaker/matrix": "^3.0", - "php": "^7.4 || ^8.0", - "psr/http-client": "^1.0", - "psr/http-factory": "^1.0", + "php": ">=7.4.0 <8.5.0", "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "dev-main", + "doctrine/instantiator": "^1.5", "dompdf/dompdf": "^1.0 || ^2.0 || ^3.0", "friendsofphp/php-cs-fixer": "^3.2", "mitoteam/jpgraph": "^10.3", @@ -7687,6 +7699,9 @@ }, { "name": "Adrien Crivelli" + }, + { + "name": "Owen Leibman" } ], "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", @@ -7703,22 +7718,22 @@ ], "support": { "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", - "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.29.11" + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.30.2" }, - "time": "2025-06-23T01:22:06+00:00" + "time": "2026-01-11T05:58:24+00:00" }, { "name": "phpoption/phpoption", - "version": "1.9.3", + "version": "1.9.5", "source": { "type": "git", "url": "https://github.com/schmittjoh/php-option.git", - "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54" + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/e3fac8b24f56113f7cb96af14958c0dd16330f54", - "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be", "shasum": "" }, "require": { @@ -7726,7 +7741,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" }, "type": "library", "extra": { @@ -7768,7 +7783,7 @@ ], "support": { "issues": "https://github.com/schmittjoh/php-option/issues", - "source": "https://github.com/schmittjoh/php-option/tree/1.9.3" + "source": "https://github.com/schmittjoh/php-option/tree/1.9.5" }, "funding": [ { @@ -7780,20 +7795,20 @@ "type": "tidelift" } ], - "time": "2024-07-20T21:41:07+00:00" + "time": "2025-12-27T19:41:33+00:00" }, { "name": "phpseclib/phpseclib", - "version": "3.0.46", + "version": "3.0.49", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6" + "reference": "6233a1e12584754e6b5daa69fe1289b47775c1b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6", - "reference": "56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/6233a1e12584754e6b5daa69fe1289b47775c1b9", + "reference": "6233a1e12584754e6b5daa69fe1289b47775c1b9", "shasum": "" }, "require": { @@ -7874,7 +7889,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.46" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.49" }, "funding": [ { @@ -7890,20 +7905,20 @@ "type": "tidelift" } ], - "time": "2025-06-26T16:29:55+00:00" + "time": "2026-01-27T09:17:28+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "2.2.0", + "version": "2.3.2", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "b9e61a61e39e02dd90944e9115241c7f7e76bfd8" + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/b9e61a61e39e02dd90944e9115241c7f7e76bfd8", - "reference": "b9e61a61e39e02dd90944e9115241c7f7e76bfd8", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a", "shasum": "" }, "require": { @@ -7935,22 +7950,22 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.2.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.2" }, - "time": "2025-07-13T07:04:09+00:00" + "time": "2026-01-25T14:56:51+00:00" }, { "name": "pragmarx/google2fa", - "version": "v8.0.3", + "version": "v9.0.0", "source": { "type": "git", "url": "https://github.com/antonioribeiro/google2fa.git", - "reference": "6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad" + "reference": "e6bc62dd6ae83acc475f57912e27466019a1f2cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad", - "reference": "6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad", + "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/e6bc62dd6ae83acc475f57912e27466019a1f2cf", + "reference": "e6bc62dd6ae83acc475f57912e27466019a1f2cf", "shasum": "" }, "require": { @@ -7987,9 +8002,9 @@ ], "support": { "issues": "https://github.com/antonioribeiro/google2fa/issues", - "source": "https://github.com/antonioribeiro/google2fa/tree/v8.0.3" + "source": "https://github.com/antonioribeiro/google2fa/tree/v9.0.0" }, - "time": "2024-09-05T11:56:40+00:00" + "time": "2025-09-19T22:51:08+00:00" }, { "name": "psr/cache", @@ -8567,16 +8582,16 @@ }, { "name": "psy/psysh", - "version": "v0.12.9", + "version": "v0.12.21", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "1b801844becfe648985372cb4b12ad6840245ace" + "reference": "4821fab5b7cd8c49a673a9fd5754dc9162bb9e97" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/1b801844becfe648985372cb4b12ad6840245ace", - "reference": "1b801844becfe648985372cb4b12ad6840245ace", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/4821fab5b7cd8c49a673a9fd5754dc9162bb9e97", + "reference": "4821fab5b7cd8c49a673a9fd5754dc9162bb9e97", "shasum": "" }, "require": { @@ -8584,18 +8599,19 @@ "ext-tokenizer": "*", "nikic/php-parser": "^5.0 || ^4.0", "php": "^8.0 || ^7.4", - "symfony/console": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", - "symfony/var-dumper": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" + "symfony/console": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", + "symfony/var-dumper": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" }, "conflict": { "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.2" + "bamarni/composer-bin-plugin": "^1.2", + "composer/class-map-generator": "^1.6" }, "suggest": { + "composer/class-map-generator": "Improved tab completion performance with better class discovery.", "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", - "ext-pdo-sqlite": "The doc command requires SQLite to work.", "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." }, "bin": [ @@ -8626,12 +8642,11 @@ "authors": [ { "name": "Justin Hileman", - "email": "justin@justinhileman.info", - "homepage": "http://justinhileman.com" + "email": "justin@justinhileman.info" } ], "description": "An interactive shell for modern PHP.", - "homepage": "http://psysh.org", + "homepage": "https://psysh.org", "keywords": [ "REPL", "console", @@ -8640,26 +8655,26 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.9" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.21" }, - "time": "2025-06-23T02:35:06+00:00" + "time": "2026-03-06T21:21:28+00:00" }, { "name": "pxlrbt/filament-environment-indicator", - "version": "v2.1.0", + "version": "v2.2.0", "source": { "type": "git", "url": "https://github.com/pxlrbt/filament-environment-indicator.git", - "reference": "e55bca20af0c9cff8a90d0a8779b09df5c4bb9d4" + "reference": "4f2cb470df70b6c261020823309e65047713cbdd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pxlrbt/filament-environment-indicator/zipball/e55bca20af0c9cff8a90d0a8779b09df5c4bb9d4", - "reference": "e55bca20af0c9cff8a90d0a8779b09df5c4bb9d4", + "url": "https://api.github.com/repos/pxlrbt/filament-environment-indicator/zipball/4f2cb470df70b6c261020823309e65047713cbdd", + "reference": "4f2cb470df70b6c261020823309e65047713cbdd", "shasum": "" }, "require": { - "filament/filament": "^3.0-stable", + "filament/filament": "^3.0", "php": "^8.0" }, "require-dev": { @@ -8696,7 +8711,7 @@ ], "support": { "issues": "https://github.com/pxlrbt/filament-environment-indicator/issues", - "source": "https://github.com/pxlrbt/filament-environment-indicator/tree/v2.1.0" + "source": "https://github.com/pxlrbt/filament-environment-indicator/tree/v2.2.0" }, "funding": [ { @@ -8704,7 +8719,7 @@ "type": "github" } ], - "time": "2024-11-18T13:24:51+00:00" + "time": "2025-07-25T14:36:56+00:00" }, { "name": "ralouphie/getallheaders", @@ -8828,20 +8843,20 @@ }, { "name": "ramsey/uuid", - "version": "4.9.0", + "version": "4.9.2", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0" + "reference": "8429c78ca35a09f27565311b98101e2826affde0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/4e0e23cc785f0724a0e838279a9eb03f28b092a0", - "reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0", + "reference": "8429c78ca35a09f27565311b98101e2826affde0", "shasum": "" }, "require": { - "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13", + "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", "php": "^8.0", "ramsey/collection": "^1.2 || ^2.0" }, @@ -8900,29 +8915,29 @@ ], "support": { "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.9.0" + "source": "https://github.com/ramsey/uuid/tree/4.9.2" }, - "time": "2025-06-25T14:20:11+00:00" + "time": "2025-12-14T04:43:48+00:00" }, { "name": "react/promise", - "version": "v3.2.0", + "version": "v3.3.0", "source": { "type": "git", "url": "https://github.com/reactphp/promise.git", - "reference": "8a164643313c71354582dc850b42b33fa12a4b63" + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", - "reference": "8a164643313c71354582dc850b42b33fa12a4b63", + "url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a", "shasum": "" }, "require": { "php": ">=7.1.0" }, "require-dev": { - "phpstan/phpstan": "1.10.39 || 1.4.10", + "phpstan/phpstan": "1.12.28 || 1.4.10", "phpunit/phpunit": "^9.6 || ^7.5" }, "type": "library", @@ -8967,7 +8982,7 @@ ], "support": { "issues": "https://github.com/reactphp/promise/issues", - "source": "https://github.com/reactphp/promise/tree/v3.2.0" + "source": "https://github.com/reactphp/promise/tree/v3.3.0" }, "funding": [ { @@ -8975,7 +8990,7 @@ "type": "open_collective" } ], - "time": "2024-05-24T10:39:05+00:00" + "time": "2025-08-19T18:57:03+00:00" }, { "name": "ryangjchandler/blade-capture-directive", @@ -9348,29 +9363,29 @@ }, { "name": "spatie/laravel-package-tools", - "version": "1.92.6", + "version": "1.93.0", "source": { "type": "git", "url": "https://github.com/spatie/laravel-package-tools.git", - "reference": "afa90e37741a953d33728e7106a1f24a13fdd808" + "reference": "0d097bce95b2bf6802fb1d83e1e753b0f5a948e7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/afa90e37741a953d33728e7106a1f24a13fdd808", - "reference": "afa90e37741a953d33728e7106a1f24a13fdd808", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/0d097bce95b2bf6802fb1d83e1e753b0f5a948e7", + "reference": "0d097bce95b2bf6802fb1d83e1e753b0f5a948e7", "shasum": "" }, "require": { - "illuminate/contracts": "^9.28|^10.0|^11.0|^12.0", - "php": "^8.0" + "illuminate/contracts": "^10.0|^11.0|^12.0|^13.0", + "php": "^8.1" }, "require-dev": { "mockery/mockery": "^1.5", - "orchestra/testbench": "^7.7|^8.0|^9.0|^10.0", - "pestphp/pest": "^1.23|^2.1|^3.1", - "phpunit/php-code-coverage": "^9.0|^10.0|^11.0", - "phpunit/phpunit": "^9.5.24|^10.5|^11.5", - "spatie/pest-plugin-test-time": "^1.1|^2.2" + "orchestra/testbench": "^8.0|^9.2|^10.0|^11.0", + "pestphp/pest": "^2.1|^3.1|^4.0", + "phpunit/php-code-coverage": "^10.0|^11.0|^12.0", + "phpunit/phpunit": "^10.5|^11.5|^12.5", + "spatie/pest-plugin-test-time": "^2.2|^3.0" }, "type": "library", "autoload": { @@ -9397,7 +9412,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-package-tools/issues", - "source": "https://github.com/spatie/laravel-package-tools/tree/1.92.6" + "source": "https://github.com/spatie/laravel-package-tools/tree/1.93.0" }, "funding": [ { @@ -9405,7 +9420,7 @@ "type": "github" } ], - "time": "2025-07-14T08:02:47+00:00" + "time": "2026-02-21T12:49:54+00:00" }, { "name": "spatie/regex", @@ -9472,16 +9487,16 @@ }, { "name": "spatie/temporary-directory", - "version": "2.3.0", + "version": "2.3.1", "source": { "type": "git", "url": "https://github.com/spatie/temporary-directory.git", - "reference": "580eddfe9a0a41a902cac6eeb8f066b42e65a32b" + "reference": "662e481d6ec07ef29fd05010433428851a42cd07" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/temporary-directory/zipball/580eddfe9a0a41a902cac6eeb8f066b42e65a32b", - "reference": "580eddfe9a0a41a902cac6eeb8f066b42e65a32b", + "url": "https://api.github.com/repos/spatie/temporary-directory/zipball/662e481d6ec07ef29fd05010433428851a42cd07", + "reference": "662e481d6ec07ef29fd05010433428851a42cd07", "shasum": "" }, "require": { @@ -9517,7 +9532,7 @@ ], "support": { "issues": "https://github.com/spatie/temporary-directory/issues", - "source": "https://github.com/spatie/temporary-directory/tree/2.3.0" + "source": "https://github.com/spatie/temporary-directory/tree/2.3.1" }, "funding": [ { @@ -9529,7 +9544,7 @@ "type": "github" } ], - "time": "2025-01-13T13:04:43+00:00" + "time": "2026-01-12T07:42:22+00:00" }, { "name": "staudenmeir/eloquent-has-many-deep-contracts", @@ -9574,16 +9589,16 @@ }, { "name": "staudenmeir/eloquent-json-relations", - "version": "v1.14.1", + "version": "v1.14.2", "source": { "type": "git", "url": "https://github.com/staudenmeir/eloquent-json-relations.git", - "reference": "48b76d3094e528993abc77dc2414c58f7531f31f" + "reference": "9e39de2270edaec445362a3d20562d863b0b50de" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/staudenmeir/eloquent-json-relations/zipball/48b76d3094e528993abc77dc2414c58f7531f31f", - "reference": "48b76d3094e528993abc77dc2414c58f7531f31f", + "url": "https://api.github.com/repos/staudenmeir/eloquent-json-relations/zipball/9e39de2270edaec445362a3d20562d863b0b50de", + "reference": "9e39de2270edaec445362a3d20562d863b0b50de", "shasum": "" }, "require": { @@ -9626,7 +9641,7 @@ "description": "Laravel Eloquent relationships with JSON keys", "support": { "issues": "https://github.com/staudenmeir/eloquent-json-relations/issues", - "source": "https://github.com/staudenmeir/eloquent-json-relations/tree/v1.14.1" + "source": "https://github.com/staudenmeir/eloquent-json-relations/tree/v1.14.2" }, "funding": [ { @@ -9634,7 +9649,7 @@ "type": "custom" } ], - "time": "2025-02-25T21:36:38+00:00" + "time": "2026-03-01T11:12:21+00:00" }, { "name": "stechstudio/filament-impersonate", @@ -9680,22 +9695,21 @@ }, { "name": "symfony/clock", - "version": "v7.3.0", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/clock.git", - "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24" + "reference": "832119f9b8dbc6c8e6f65f30c5969eca1e88764f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/clock/zipball/b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", - "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "url": "https://api.github.com/repos/symfony/clock/zipball/832119f9b8dbc6c8e6f65f30c5969eca1e88764f", + "reference": "832119f9b8dbc6c8e6f65f30c5969eca1e88764f", "shasum": "" }, "require": { - "php": ">=8.2", - "psr/clock": "^1.0", - "symfony/polyfill-php83": "^1.28" + "php": ">=8.4", + "psr/clock": "^1.0" }, "provide": { "psr/clock-implementation": "1.0" @@ -9734,7 +9748,7 @@ "time" ], "support": { - "source": "https://github.com/symfony/clock/tree/v7.3.0" + "source": "https://github.com/symfony/clock/tree/v8.0.0" }, "funding": [ { @@ -9745,25 +9759,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2025-11-12T15:46:48+00:00" }, { "name": "symfony/console", - "version": "v7.3.1", + "version": "v7.4.7", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "9e27aecde8f506ba0fd1d9989620c04a87697101" + "reference": "e1e6770440fb9c9b0cf725f81d1361ad1835329d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/9e27aecde8f506ba0fd1d9989620c04a87697101", - "reference": "9e27aecde8f506ba0fd1d9989620c04a87697101", + "url": "https://api.github.com/repos/symfony/console/zipball/e1e6770440fb9c9b0cf725f81d1361ad1835329d", + "reference": "e1e6770440fb9c9b0cf725f81d1361ad1835329d", "shasum": "" }, "require": { @@ -9771,7 +9789,7 @@ "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^7.2" + "symfony/string": "^7.2|^8.0" }, "conflict": { "symfony/dependency-injection": "<6.4", @@ -9785,16 +9803,16 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -9828,7 +9846,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.1" + "source": "https://github.com/symfony/console/tree/v7.4.7" }, "funding": [ { @@ -9839,29 +9857,33 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-27T19:55:54+00:00" + "time": "2026-03-06T14:06:20+00:00" }, { "name": "symfony/css-selector", - "version": "v7.3.0", + "version": "v8.0.6", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2" + "reference": "2a178bf80f05dbbe469a337730eba79d61315262" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/601a5ce9aaad7bf10797e3663faefce9e26c24e2", - "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/2a178bf80f05dbbe469a337730eba79d61315262", + "reference": "2a178bf80f05dbbe469a337730eba79d61315262", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.4" }, "type": "library", "autoload": { @@ -9893,7 +9915,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v7.3.0" + "source": "https://github.com/symfony/css-selector/tree/v8.0.6" }, "funding": [ { @@ -9904,12 +9926,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2026-02-17T13:07:04+00:00" }, { "name": "symfony/deprecation-contracts", @@ -9980,32 +10006,33 @@ }, { "name": "symfony/error-handler", - "version": "v7.3.1", + "version": "v7.4.4", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "35b55b166f6752d6aaf21aa042fc5ed280fce235" + "reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/35b55b166f6752d6aaf21aa042fc5ed280fce235", - "reference": "35b55b166f6752d6aaf21aa042fc5ed280fce235", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/8da531f364ddfee53e36092a7eebbbd0b775f6b8", + "reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8", "shasum": "" }, "require": { "php": ">=8.2", "psr/log": "^1|^2|^3", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "conflict": { "symfony/deprecation-contracts": "<2.5", "symfony/http-kernel": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0|^8.0", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/serializer": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", "symfony/webpack-encore-bundle": "^1.0|^2.0" }, "bin": [ @@ -10037,7 +10064,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v7.3.1" + "source": "https://github.com/symfony/error-handler/tree/v7.4.4" }, "funding": [ { @@ -10048,33 +10075,37 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-13T07:48:40+00:00" + "time": "2026-01-20T16:42:42+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v7.3.0", + "version": "v8.0.4", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "497f73ac996a598c92409b44ac43b6690c4f666d" + "reference": "99301401da182b6cfaa4700dbe9987bb75474b47" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/497f73ac996a598c92409b44ac43b6690c4f666d", - "reference": "497f73ac996a598c92409b44ac43b6690c4f666d", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/99301401da182b6cfaa4700dbe9987bb75474b47", + "reference": "99301401da182b6cfaa4700dbe9987bb75474b47", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/event-dispatcher-contracts": "^2.5|^3" }, "conflict": { - "symfony/dependency-injection": "<6.4", + "symfony/security-http": "<7.4", "symfony/service-contracts": "<2.5" }, "provide": { @@ -10083,13 +10114,14 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/error-handler": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^6.4|^7.0" + "symfony/stopwatch": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -10117,7 +10149,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.0" + "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.4" }, "funding": [ { @@ -10128,12 +10160,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-22T09:11:45+00:00" + "time": "2026-01-05T11:45:55+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -10213,25 +10249,25 @@ }, { "name": "symfony/filesystem", - "version": "v7.3.0", + "version": "v8.0.6", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb" + "reference": "7bf9162d7a0dff98d079b72948508fa48018a770" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/b8dce482de9d7c9fe2891155035a7248ab5c7fdb", - "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/7bf9162d7a0dff98d079b72948508fa48018a770", + "reference": "7bf9162d7a0dff98d079b72948508fa48018a770", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.8" }, "require-dev": { - "symfony/process": "^6.4|^7.0" + "symfony/process": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -10259,7 +10295,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.3.0" + "source": "https://github.com/symfony/filesystem/tree/v8.0.6" }, "funding": [ { @@ -10270,32 +10306,36 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-10-25T15:15:23+00:00" + "time": "2026-02-25T16:59:43+00:00" }, { "name": "symfony/finder", - "version": "v7.3.0", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d" + "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/ec2344cf77a48253bbca6939aa3d2477773ea63d", - "reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d", + "url": "https://api.github.com/repos/symfony/finder/zipball/8655bf1076b7a3a346cb11413ffdabff50c7ffcf", + "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "symfony/filesystem": "^6.4|^7.0" + "symfony/filesystem": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -10323,7 +10363,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.3.0" + "source": "https://github.com/symfony/finder/tree/v7.4.6" }, "funding": [ { @@ -10334,32 +10374,37 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-12-30T19:00:26+00:00" + "time": "2026-01-29T09:40:50+00:00" }, { "name": "symfony/html-sanitizer", - "version": "v7.3.0", + "version": "v7.4.7", "source": { "type": "git", "url": "https://github.com/symfony/html-sanitizer.git", - "reference": "cf21254e982b12276329940ca4af5e623ee06c58" + "reference": "e4df2abd9391ce3263baa1aea9e993f6da74a3c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/html-sanitizer/zipball/cf21254e982b12276329940ca4af5e623ee06c58", - "reference": "cf21254e982b12276329940ca4af5e623ee06c58", + "url": "https://api.github.com/repos/symfony/html-sanitizer/zipball/e4df2abd9391ce3263baa1aea9e993f6da74a3c7", + "reference": "e4df2abd9391ce3263baa1aea9e993f6da74a3c7", "shasum": "" }, "require": { "ext-dom": "*", "league/uri": "^6.5|^7.0", "masterminds/html5": "^2.7.2", - "php": ">=8.2" + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" }, "type": "library", "autoload": { @@ -10392,7 +10437,7 @@ "sanitizer" ], "support": { - "source": "https://github.com/symfony/html-sanitizer/tree/v7.3.0" + "source": "https://github.com/symfony/html-sanitizer/tree/v7.4.7" }, "funding": [ { @@ -10403,32 +10448,35 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-03-31T08:49:55+00:00" + "time": "2026-03-06T13:15:18+00:00" }, { "name": "symfony/http-foundation", - "version": "v7.3.1", + "version": "v7.4.7", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "23dd60256610c86a3414575b70c596e5deff6ed9" + "reference": "f94b3e7b7dafd40e666f0c9ff2084133bae41e81" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/23dd60256610c86a3414575b70c596e5deff6ed9", - "reference": "23dd60256610c86a3414575b70c596e5deff6ed9", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f94b3e7b7dafd40e666f0c9ff2084133bae41e81", + "reference": "f94b3e7b7dafd40e666f0c9ff2084133bae41e81", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", - "symfony/polyfill-mbstring": "~1.1", - "symfony/polyfill-php83": "^1.27" + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" }, "conflict": { "doctrine/dbal": "<3.6", @@ -10437,13 +10485,13 @@ "require-dev": { "doctrine/dbal": "^3.6|^4", "predis/predis": "^1.1|^2.0", - "symfony/cache": "^6.4.12|^7.1.5", - "symfony/clock": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/mime": "^6.4|^7.0", - "symfony/rate-limiter": "^6.4|^7.0" + "symfony/cache": "^6.4.12|^7.1.5|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -10471,7 +10519,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.3.1" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.7" }, "funding": [ { @@ -10482,34 +10530,38 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-23T15:07:14+00:00" + "time": "2026-03-06T13:15:18+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.3.1", + "version": "v7.4.7", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "1644879a66e4aa29c36fe33dfa6c54b450ce1831" + "reference": "3b3fcf386c809be990c922e10e4c620d6367cab1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/1644879a66e4aa29c36fe33dfa6c54b450ce1831", - "reference": "1644879a66e4aa29c36fe33dfa6c54b450ce1831", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/3b3fcf386c809be990c922e10e4c620d6367cab1", + "reference": "3b3fcf386c809be990c922e10e4c620d6367cab1", "shasum": "" }, "require": { "php": ">=8.2", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/error-handler": "^6.4|^7.0", - "symfony/event-dispatcher": "^7.3", - "symfony/http-foundation": "^7.3", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^7.3|^8.0", + "symfony/http-foundation": "^7.4|^8.0", "symfony/polyfill-ctype": "^1.8" }, "conflict": { @@ -10519,6 +10571,7 @@ "symfony/console": "<6.4", "symfony/dependency-injection": "<6.4", "symfony/doctrine-bridge": "<6.4", + "symfony/flex": "<2.10", "symfony/form": "<6.4", "symfony/http-client": "<6.4", "symfony/http-client-contracts": "<2.5", @@ -10536,27 +10589,27 @@ }, "require-dev": { "psr/cache": "^1.0|^2.0|^3.0", - "symfony/browser-kit": "^6.4|^7.0", - "symfony/clock": "^6.4|^7.0", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/css-selector": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/dom-crawler": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4.1|^7.0.1|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", "symfony/http-client-contracts": "^2.5|^3", - "symfony/process": "^6.4|^7.0", - "symfony/property-access": "^7.1", - "symfony/routing": "^6.4|^7.0", - "symfony/serializer": "^7.1", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/translation": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^7.1|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.1|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", "symfony/translation-contracts": "^2.5|^3", - "symfony/uid": "^6.4|^7.0", - "symfony/validator": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0", - "symfony/var-exporter": "^6.4|^7.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0", "twig/twig": "^3.12" }, "type": "library", @@ -10585,7 +10638,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.3.1" + "source": "https://github.com/symfony/http-kernel/tree/v7.4.7" }, "funding": [ { @@ -10596,25 +10649,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-28T08:24:55+00:00" + "time": "2026-03-06T16:33:18+00:00" }, { "name": "symfony/mailer", - "version": "v7.3.1", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "b5db5105b290bdbea5ab27b89c69effcf1cb3368" + "reference": "b02726f39a20bc65e30364f5c750c4ddbf1f58e9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/b5db5105b290bdbea5ab27b89c69effcf1cb3368", - "reference": "b5db5105b290bdbea5ab27b89c69effcf1cb3368", + "url": "https://api.github.com/repos/symfony/mailer/zipball/b02726f39a20bc65e30364f5c750c4ddbf1f58e9", + "reference": "b02726f39a20bc65e30364f5c750c4ddbf1f58e9", "shasum": "" }, "require": { @@ -10622,8 +10679,8 @@ "php": ">=8.2", "psr/event-dispatcher": "^1", "psr/log": "^1|^2|^3", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/mime": "^7.2", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/mime": "^7.2|^8.0", "symfony/service-contracts": "^2.5|^3" }, "conflict": { @@ -10634,10 +10691,10 @@ "symfony/twig-bridge": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0", - "symfony/http-client": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/twig-bridge": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/twig-bridge": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -10665,7 +10722,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.3.1" + "source": "https://github.com/symfony/mailer/tree/v7.4.6" }, "funding": [ { @@ -10676,48 +10733,53 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-27T19:55:54+00:00" + "time": "2026-02-25T16:50:00+00:00" }, { "name": "symfony/mime", - "version": "v7.3.0", + "version": "v7.4.7", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "0e7b19b2f399c31df0cdbe5d8cbf53f02f6cfcd9" + "reference": "da5ab4fde3f6c88ab06e96185b9922f48b677cd1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/0e7b19b2f399c31df0cdbe5d8cbf53f02f6cfcd9", - "reference": "0e7b19b2f399c31df0cdbe5d8cbf53f02f6cfcd9", + "url": "https://api.github.com/repos/symfony/mime/zipball/da5ab4fde3f6c88ab06e96185b9922f48b677cd1", + "reference": "da5ab4fde3f6c88ab06e96185b9922f48b677cd1", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-intl-idn": "^1.10", "symfony/polyfill-mbstring": "^1.0" }, "conflict": { "egulias/email-validator": "~3.0.0", - "phpdocumentor/reflection-docblock": "<3.2.2", - "phpdocumentor/type-resolver": "<1.4.0", + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", "symfony/mailer": "<6.4", "symfony/serializer": "<6.4.3|>7.0,<7.0.3" }, "require-dev": { "egulias/email-validator": "^2.1.10|^3.1|^4", "league/html-to-markdown": "^5.0", - "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/property-access": "^6.4|^7.0", - "symfony/property-info": "^6.4|^7.0", - "symfony/serializer": "^6.4.3|^7.0.3" + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.3|^7.0.3|^8.0" }, "type": "library", "autoload": { @@ -10749,7 +10811,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.3.0" + "source": "https://github.com/symfony/mime/tree/v7.4.7" }, "funding": [ { @@ -10760,16 +10822,20 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-02-19T08:51:26+00:00" + "time": "2026-03-05T15:24:09+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -10828,7 +10894,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" }, "funding": [ { @@ -10839,6 +10905,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -10848,16 +10918,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", "shasum": "" }, "require": { @@ -10906,7 +10976,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" }, "funding": [ { @@ -10917,16 +10987,20 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-06-27T09:58:17+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", @@ -10989,7 +11063,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0" }, "funding": [ { @@ -11000,6 +11074,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -11009,7 +11087,7 @@ }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -11070,7 +11148,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" }, "funding": [ { @@ -11081,6 +11159,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -11090,7 +11172,7 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", @@ -11151,7 +11233,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" }, "funding": [ { @@ -11162,6 +11244,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -11171,7 +11257,7 @@ }, { "name": "symfony/polyfill-php73", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", @@ -11227,7 +11313,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.33.0" }, "funding": [ { @@ -11238,6 +11324,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -11247,7 +11337,7 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", @@ -11307,7 +11397,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" }, "funding": [ { @@ -11318,6 +11408,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -11327,7 +11421,7 @@ }, { "name": "symfony/polyfill-php81", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", @@ -11383,7 +11477,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.33.0" }, "funding": [ { @@ -11394,6 +11488,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -11403,16 +11501,16 @@ }, { "name": "symfony/polyfill-php83", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", - "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", "shasum": "" }, "require": { @@ -11459,7 +11557,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" }, "funding": [ { @@ -11470,36 +11568,34 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-07-08T02:45:35+00:00" }, { - "name": "symfony/polyfill-uuid", - "version": "v1.32.0", + "name": "symfony/polyfill-php84", + "version": "v1.33.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-uuid.git", - "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2" + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2", - "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", "shasum": "" }, "require": { "php": ">=7.2" }, - "provide": { - "ext-uuid": "*" - }, - "suggest": { - "ext-uuid": "For best performance" - }, "type": "library", "extra": { "thanks": { @@ -11512,8 +11608,11 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Uuid\\": "" - } + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -11521,24 +11620,24 @@ ], "authors": [ { - "name": "Grégoire Pineau", - "email": "lyrixx@lyrixx.info" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for uuid functions", + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", "polyfill", "portable", - "uuid" + "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-uuid/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" }, "funding": [ { @@ -11549,37 +11648,50 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-06-24T13:30:11+00:00" }, { - "name": "symfony/process", - "version": "v7.3.0", + "name": "symfony/polyfill-php85", + "version": "v1.33.0", "source": { "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af" + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", - "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=7.2" }, "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Symfony\\Component\\Process\\": "" + "Symfony\\Polyfill\\Php85\\": "" }, - "exclude-from-classmap": [ - "/Tests/" + "classmap": [ + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", @@ -11588,18 +11700,24 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Executes commands in sub-processes", + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], "support": { - "source": "https://github.com/symfony/process/tree/v7.3.0" + "source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0" }, "funding": [ { @@ -11610,47 +11728,200 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-17T09:11:12+00:00" + "time": "2025-06-23T16:12:55+00:00" }, { - "name": "symfony/psr-http-message-bridge", - "version": "v7.3.0", + "name": "symfony/polyfill-uuid", + "version": "v1.33.0", "source": { "type": "git", - "url": "https://github.com/symfony/psr-http-message-bridge.git", - "reference": "03f2f72319e7acaf2a9f6fcbe30ef17eec51594f" + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/03f2f72319e7acaf2a9f6fcbe30ef17eec51594f", - "reference": "03f2f72319e7acaf2a9f6fcbe30ef17eec51594f", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2", "shasum": "" }, "require": { - "php": ">=8.2", - "psr/http-message": "^1.0|^2.0", - "symfony/http-foundation": "^6.4|^7.0" + "php": ">=7.2" }, - "conflict": { - "php-http/discovery": "<1.15", - "symfony/http-kernel": "<6.4" + "provide": { + "ext-uuid": "*" }, - "require-dev": { - "nyholm/psr7": "^1.1", - "php-http/discovery": "^1.15", - "psr/log": "^1.1.4|^2|^3", - "symfony/browser-kit": "^6.4|^7.0", - "symfony/config": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/framework-bundle": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0" + "suggest": { + "ext-uuid": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } }, - "type": "symfony-bridge", + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Uuid\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for uuid functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/process", + "version": "v7.4.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "608476f4604102976d687c483ac63a79ba18cc97" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/608476f4604102976d687c483ac63a79ba18cc97", + "reference": "608476f4604102976d687c483ac63a79ba18cc97", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.4.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-26T15:07:59+00:00" + }, + { + "name": "symfony/psr-http-message-bridge", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/psr-http-message-bridge.git", + "reference": "929ffe10bbfbb92e711ac3818d416f9daffee067" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/929ffe10bbfbb92e711ac3818d416f9daffee067", + "reference": "929ffe10bbfbb92e711ac3818d416f9daffee067", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/http-message": "^1.0|^2.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0" + }, + "conflict": { + "php-http/discovery": "<1.15", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "nyholm/psr7": "^1.1", + "php-http/discovery": "^1.15", + "psr/log": "^1.1.4|^2|^3", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4.13|^7.1.6|^8.0", + "symfony/http-kernel": "^6.4.13|^7.1.6|^8.0", + "symfony/runtime": "^6.4.13|^7.1.6|^8.0" + }, + "type": "symfony-bridge", "autoload": { "psr-4": { "Symfony\\Bridge\\PsrHttpMessage\\": "" @@ -11682,7 +11953,7 @@ "psr-7" ], "support": { - "source": "https://github.com/symfony/psr-http-message-bridge/tree/v7.3.0" + "source": "https://github.com/symfony/psr-http-message-bridge/tree/v7.4.4" }, "funding": [ { @@ -11693,25 +11964,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-26T08:57:56+00:00" + "time": "2026-01-03T23:30:35+00:00" }, { "name": "symfony/routing", - "version": "v7.3.0", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "8e213820c5fea844ecea29203d2a308019007c15" + "reference": "238d749c56b804b31a9bf3e26519d93b65a60938" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/8e213820c5fea844ecea29203d2a308019007c15", - "reference": "8e213820c5fea844ecea29203d2a308019007c15", + "url": "https://api.github.com/repos/symfony/routing/zipball/238d749c56b804b31a9bf3e26519d93b65a60938", + "reference": "238d749c56b804b31a9bf3e26519d93b65a60938", "shasum": "" }, "require": { @@ -11725,11 +12000,11 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -11763,7 +12038,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.3.0" + "source": "https://github.com/symfony/routing/tree/v7.4.6" }, "funding": [ { @@ -11774,25 +12049,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-24T20:43:28+00:00" + "time": "2026-02-25T16:50:00+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.6.0", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", "shasum": "" }, "require": { @@ -11846,7 +12125,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" }, "funding": [ { @@ -11857,44 +12136,47 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-25T09:37:31+00:00" + "time": "2025-07-15T11:30:57+00:00" }, { "name": "symfony/string", - "version": "v7.3.0", + "version": "v8.0.6", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125" + "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f3570b8c61ca887a9e2938e85cb6458515d2b125", - "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125", + "url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4", + "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", - "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0" + "php": ">=8.4", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" }, "conflict": { "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/emoji": "^7.1", - "symfony/error-handler": "^6.4|^7.0", - "symfony/http-client": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.4|^7.0" + "symfony/var-exporter": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -11933,7 +12215,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.0" + "source": "https://github.com/symfony/string/tree/v8.0.6" }, "funding": [ { @@ -11944,43 +12226,40 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-20T20:19:01+00:00" + "time": "2026-02-09T10:14:57+00:00" }, { "name": "symfony/translation", - "version": "v7.3.1", + "version": "v8.0.6", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "241d5ac4910d256660238a7ecf250deba4c73063" + "reference": "13ff19bcf2bea492d3c2fbeaa194dd6f4599ce1b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/241d5ac4910d256660238a7ecf250deba4c73063", - "reference": "241d5ac4910d256660238a7ecf250deba4c73063", + "url": "https://api.github.com/repos/symfony/translation/zipball/13ff19bcf2bea492d3c2fbeaa194dd6f4599ce1b", + "reference": "13ff19bcf2bea492d3c2fbeaa194dd6f4599ce1b", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "~1.0", - "symfony/translation-contracts": "^2.5|^3.0" + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation-contracts": "^3.6.1" }, "conflict": { "nikic/php-parser": "<5.0", - "symfony/config": "<6.4", - "symfony/console": "<6.4", - "symfony/dependency-injection": "<6.4", "symfony/http-client-contracts": "<2.5", - "symfony/http-kernel": "<6.4", - "symfony/service-contracts": "<2.5", - "symfony/twig-bundle": "<6.4", - "symfony/yaml": "<6.4" + "symfony/service-contracts": "<2.5" }, "provide": { "symfony/translation-implementation": "2.3|3.0" @@ -11988,17 +12267,17 @@ "require-dev": { "nikic/php-parser": "^5.0", "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", "symfony/http-client-contracts": "^2.5|^3.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", "symfony/polyfill-intl-icu": "^1.21", - "symfony/routing": "^6.4|^7.0", + "symfony/routing": "^7.4|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/yaml": "^6.4|^7.0" + "symfony/yaml": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -12029,7 +12308,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.3.1" + "source": "https://github.com/symfony/translation/tree/v8.0.6" }, "funding": [ { @@ -12040,25 +12319,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-27T19:55:54+00:00" + "time": "2026-02-17T13:07:04+00:00" }, { "name": "symfony/translation-contracts", - "version": "v3.6.0", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d" + "reference": "65a8bc82080447fae78373aa10f8d13b38338977" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/df210c7a2573f1913b2d17cc95f90f53a73d8f7d", - "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977", "shasum": "" }, "require": { @@ -12107,7 +12390,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.1" }, "funding": [ { @@ -12118,25 +12401,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-27T08:32:26+00:00" + "time": "2025-07-15T13:41:35+00:00" }, { "name": "symfony/uid", - "version": "v7.3.1", + "version": "v7.4.4", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb" + "reference": "7719ce8aba76be93dfe249192f1fbfa52c588e36" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/a69f69f3159b852651a6bf45a9fdd149520525bb", - "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb", + "url": "https://api.github.com/repos/symfony/uid/zipball/7719ce8aba76be93dfe249192f1fbfa52c588e36", + "reference": "7719ce8aba76be93dfe249192f1fbfa52c588e36", "shasum": "" }, "require": { @@ -12144,7 +12431,7 @@ "symfony/polyfill-uuid": "^1.15" }, "require-dev": { - "symfony/console": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -12181,7 +12468,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.3.1" + "source": "https://github.com/symfony/uid/tree/v7.4.4" }, "funding": [ { @@ -12192,25 +12479,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-27T19:55:54+00:00" + "time": "2026-01-03T23:30:35+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.3.1", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "6e209fbe5f5a7b6043baba46fe5735a4b85d0d42" + "reference": "045321c440ac18347b136c63d2e9bf28a2dc0291" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/6e209fbe5f5a7b6043baba46fe5735a4b85d0d42", - "reference": "6e209fbe5f5a7b6043baba46fe5735a4b85d0d42", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/045321c440ac18347b136c63d2e9bf28a2dc0291", + "reference": "045321c440ac18347b136c63d2e9bf28a2dc0291", "shasum": "" }, "require": { @@ -12222,11 +12513,10 @@ "symfony/console": "<6.4" }, "require-dev": { - "ext-iconv": "*", - "symfony/console": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/uid": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", "twig/twig": "^3.12" }, "bin": [ @@ -12265,7 +12555,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.3.1" + "source": "https://github.com/symfony/var-dumper/tree/v7.4.6" }, "funding": [ { @@ -12276,25 +12566,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-27T19:55:54+00:00" + "time": "2026-02-15T10:53:20+00:00" }, { "name": "tightenco/ziggy", - "version": "v2.5.3", + "version": "v2.6.2", "source": { "type": "git", "url": "https://github.com/tighten/ziggy.git", - "reference": "0b3b521d2c55fbdb04b6721532f7f5f49d32f52b" + "reference": "8a0b645921623f77dceaf543d61ecd51a391d96e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tighten/ziggy/zipball/0b3b521d2c55fbdb04b6721532f7f5f49d32f52b", - "reference": "0b3b521d2c55fbdb04b6721532f7f5f49d32f52b", + "url": "https://api.github.com/repos/tighten/ziggy/zipball/8a0b645921623f77dceaf543d61ecd51a391d96e", + "reference": "8a0b645921623f77dceaf543d61ecd51a391d96e", "shasum": "" }, "require": { @@ -12304,9 +12598,9 @@ }, "require-dev": { "laravel/folio": "^1.1", - "orchestra/testbench": "^7.0 || ^8.0 || ^9.0 || ^10.0", - "pestphp/pest": "^2.26|^3.0", - "pestphp/pest-plugin-laravel": "^2.4|^3.0" + "orchestra/testbench": "^8.0 || ^9.0 || ^10.0", + "pestphp/pest": "^2.0 || ^3.0 || ^4.0", + "pestphp/pest-plugin-laravel": "^2.0 || ^3.0 || ^4.0" }, "type": "library", "extra": { @@ -12349,29 +12643,29 @@ ], "support": { "issues": "https://github.com/tighten/ziggy/issues", - "source": "https://github.com/tighten/ziggy/tree/v2.5.3" + "source": "https://github.com/tighten/ziggy/tree/v2.6.2" }, - "time": "2025-05-17T18:15:19+00:00" + "time": "2026-03-05T14:41:19+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", - "version": "v2.3.0", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", - "reference": "0d72ac1c00084279c1816675284073c5a337c20d" + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/0d72ac1c00084279c1816675284073c5a337c20d", - "reference": "0d72ac1c00084279c1816675284073c5a337c20d", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/f0292ccf0ec75843d65027214426b6b163b48b41", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "php": "^7.4 || ^8.0", - "symfony/css-selector": "^5.4 || ^6.0 || ^7.0" + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.0" }, "require-dev": { "phpstan/phpstan": "^2.0", @@ -12404,22 +12698,22 @@ "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", "support": { "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", - "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.3.0" + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.4.0" }, - "time": "2024-12-21T16:25:41+00:00" + "time": "2025-12-02T11:56:42+00:00" }, { "name": "tpetry/laravel-postgresql-enhanced", - "version": "3.0.0", + "version": "3.5.1", "source": { "type": "git", "url": "https://github.com/tpetry/laravel-postgresql-enhanced.git", - "reference": "816f36fc3b3e0a3daf90f61d8d9dd76eb15ec6f4" + "reference": "dcfa5869671260aaf4057155deb66de6b600bda5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tpetry/laravel-postgresql-enhanced/zipball/816f36fc3b3e0a3daf90f61d8d9dd76eb15ec6f4", - "reference": "816f36fc3b3e0a3daf90f61d8d9dd76eb15ec6f4", + "url": "https://api.github.com/repos/tpetry/laravel-postgresql-enhanced/zipball/dcfa5869671260aaf4057155deb66de6b600bda5", + "reference": "dcfa5869671260aaf4057155deb66de6b600bda5", "shasum": "" }, "require": { @@ -12434,7 +12728,7 @@ "friendsofphp/php-cs-fixer": "^2.19.3|^3.5.0", "larastan/larastan": "^1.0|^2.1|^3.0", "nesbot/carbon": "^2.7|^3.3", - "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0|^8.0|^9.0|^10.0", + "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0|^8.0|^9.15.0|^10.0", "phpstan/extension-installer": "^1.1", "phpstan/phpstan": "^1.5|^2.0", "phpunit/phpunit": "^8.5.23|^9.5.13|^10.5|^11.4", @@ -12476,32 +12770,32 @@ ], "support": { "issues": "https://github.com/tpetry/laravel-postgresql-enhanced/issues", - "source": "https://github.com/tpetry/laravel-postgresql-enhanced/tree/3.0.0" + "source": "https://github.com/tpetry/laravel-postgresql-enhanced/tree/3.5.1" }, - "time": "2025-04-23T10:19:01+00:00" + "time": "2026-03-10T19:24:48+00:00" }, { "name": "vlucas/phpdotenv", - "version": "v5.6.2", + "version": "v5.6.3", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af" + "reference": "955e7815d677a3eaa7075231212f2110983adecc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af", - "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc", + "reference": "955e7815d677a3eaa7075231212f2110983adecc", "shasum": "" }, "require": { "ext-pcre": "*", - "graham-campbell/result-type": "^1.1.3", + "graham-campbell/result-type": "^1.1.4", "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9.3", - "symfony/polyfill-ctype": "^1.24", - "symfony/polyfill-mbstring": "^1.24", - "symfony/polyfill-php80": "^1.24" + "phpoption/phpoption": "^1.9.5", + "symfony/polyfill-ctype": "^1.26", + "symfony/polyfill-mbstring": "^1.26", + "symfony/polyfill-php80": "^1.26" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", @@ -12550,7 +12844,7 @@ ], "support": { "issues": "https://github.com/vlucas/phpdotenv/issues", - "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2" + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3" }, "funding": [ { @@ -12562,7 +12856,7 @@ "type": "tidelift" } ], - "time": "2025-04-30T23:37:27+00:00" + "time": "2025-12-27T19:49:13+00:00" }, { "name": "voku/portable-ascii", @@ -12638,64 +12932,6 @@ ], "time": "2024-11-21T01:49:47+00:00" }, - { - "name": "webmozart/assert", - "version": "1.11.0", - "source": { - "type": "git", - "url": "https://github.com/webmozarts/assert.git", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", - "shasum": "" - }, - "require": { - "ext-ctype": "*", - "php": "^7.2 || ^8.0" - }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<4.6.1 || 4.6.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.13" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.10-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "support": { - "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.11.0" - }, - "time": "2022-06-03T18:03:27+00:00" - }, { "name": "wikimedia/composer-merge-plugin", "version": "v2.1.0", @@ -12756,20 +12992,20 @@ "packages-dev": [ { "name": "barryvdh/laravel-ide-helper", - "version": "v3.5.5", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-ide-helper.git", - "reference": "8d441ec99f8612b942b55f5183151d91591b618a" + "reference": "b106f7ee85f263c4f103eca49e7bf3862c2e5e75" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/8d441ec99f8612b942b55f5183151d91591b618a", - "reference": "8d441ec99f8612b942b55f5183151d91591b618a", + "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/b106f7ee85f263c4f103eca49e7bf3862c2e5e75", + "reference": "b106f7ee85f263c4f103eca49e7bf3862c2e5e75", "shasum": "" }, "require": { - "barryvdh/reflection-docblock": "^2.3", + "barryvdh/reflection-docblock": "^2.4", "composer/class-map-generator": "^1.0", "ext-json": "*", "illuminate/console": "^11.15 || ^12", @@ -12834,7 +13070,7 @@ ], "support": { "issues": "https://github.com/barryvdh/laravel-ide-helper/issues", - "source": "https://github.com/barryvdh/laravel-ide-helper/tree/v3.5.5" + "source": "https://github.com/barryvdh/laravel-ide-helper/tree/v3.6.1" }, "funding": [ { @@ -12846,20 +13082,20 @@ "type": "github" } ], - "time": "2025-02-11T13:59:46+00:00" + "time": "2025-12-10T09:11:07+00:00" }, { "name": "barryvdh/reflection-docblock", - "version": "v2.3.1", + "version": "v2.4.1", "source": { "type": "git", "url": "https://github.com/barryvdh/ReflectionDocBlock.git", - "reference": "b6ff9f93603561f50e53b64310495d20b8dff5d8" + "reference": "4f5ba70c30c81f2ce03a16a9965832cfcc31ed3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/ReflectionDocBlock/zipball/b6ff9f93603561f50e53b64310495d20b8dff5d8", - "reference": "b6ff9f93603561f50e53b64310495d20b8dff5d8", + "url": "https://api.github.com/repos/barryvdh/ReflectionDocBlock/zipball/4f5ba70c30c81f2ce03a16a9965832cfcc31ed3b", + "reference": "4f5ba70c30c81f2ce03a16a9965832cfcc31ed3b", "shasum": "" }, "require": { @@ -12896,22 +13132,22 @@ } ], "support": { - "source": "https://github.com/barryvdh/ReflectionDocBlock/tree/v2.3.1" + "source": "https://github.com/barryvdh/ReflectionDocBlock/tree/v2.4.1" }, - "time": "2025-01-18T19:26:32+00:00" + "time": "2026-03-05T20:09:01+00:00" }, { "name": "brianium/paratest", - "version": "v7.10.3", + "version": "v7.19.2", "source": { "type": "git", "url": "https://github.com/paratestphp/paratest.git", - "reference": "cfee22cc949d170e61e7111c89ea9fc86aa02ffb" + "reference": "66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/cfee22cc949d170e61e7111c89ea9fc86aa02ffb", - "reference": "cfee22cc949d170e61e7111c89ea9fc86aa02ffb", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9", + "reference": "66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9", "shasum": "" }, "require": { @@ -12919,28 +13155,27 @@ "ext-pcre": "*", "ext-reflection": "*", "ext-simplexml": "*", - "fidry/cpu-core-counter": "^1.2.0", + "fidry/cpu-core-counter": "^1.3.0", "jean85/pretty-package-versions": "^2.1.1", - "php": "~8.3.0 || ~8.4.0", - "phpunit/php-code-coverage": "^12.3.1", - "phpunit/php-file-iterator": "^6", - "phpunit/php-timer": "^8", - "phpunit/phpunit": "^12.2.3", - "sebastian/environment": "^8.0.2", - "symfony/console": "^6.4.20 || ^7.3.0", - "symfony/process": "^6.4.20 || ^7.3.0" + "php": "~8.3.0 || ~8.4.0 || ~8.5.0", + "phpunit/php-code-coverage": "^12.5.3 || ^13.0.1", + "phpunit/php-file-iterator": "^6.0.1 || ^7", + "phpunit/php-timer": "^8 || ^9", + "phpunit/phpunit": "^12.5.14 || ^13.0.5", + "sebastian/environment": "^8.0.3 || ^9", + "symfony/console": "^7.4.7 || ^8.0.7", + "symfony/process": "^7.4.5 || ^8.0.5" }, "require-dev": { - "doctrine/coding-standard": "^13.0.1", + "doctrine/coding-standard": "^14.0.0", "ext-pcntl": "*", "ext-pcov": "*", "ext-posix": "*", - "phpstan/phpstan": "^2.1.17", - "phpstan/phpstan-deprecation-rules": "^2.0.3", - "phpstan/phpstan-phpunit": "^2.0.6", - "phpstan/phpstan-strict-rules": "^2.0.4", - "squizlabs/php_codesniffer": "^3.13.2", - "symfony/filesystem": "^6.4.13 || ^7.3.0" + "phpstan/phpstan": "^2.1.40", + "phpstan/phpstan-deprecation-rules": "^2.0.4", + "phpstan/phpstan-phpunit": "^2.0.16", + "phpstan/phpstan-strict-rules": "^2.0.10", + "symfony/filesystem": "^7.4.6 || ^8.0.6" }, "bin": [ "bin/paratest", @@ -12980,7 +13215,7 @@ ], "support": { "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v7.10.3" + "source": "https://github.com/paratestphp/paratest/tree/v7.19.2" }, "funding": [ { @@ -12992,7 +13227,7 @@ "type": "paypal" } ], - "time": "2025-06-22T16:27:15+00:00" + "time": "2026-03-09T14:33:17+00:00" }, { "name": "fakerphp/faker", @@ -13059,16 +13294,16 @@ }, { "name": "fidry/cpu-core-counter", - "version": "1.2.0", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/theofidry/cpu-core-counter.git", - "reference": "8520451a140d3f46ac33042715115e290cf5785f" + "reference": "db9508f7b1474469d9d3c53b86f817e344732678" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", - "reference": "8520451a140d3f46ac33042715115e290cf5785f", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678", "shasum": "" }, "require": { @@ -13078,10 +13313,10 @@ "fidry/makefile": "^0.2.0", "fidry/php-cs-fixer-config": "^1.1.2", "phpstan/extension-installer": "^1.2.0", - "phpstan/phpstan": "^1.9.2", - "phpstan/phpstan-deprecation-rules": "^1.0.0", - "phpstan/phpstan-phpunit": "^1.2.2", - "phpstan/phpstan-strict-rules": "^1.4.4", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpunit": "^8.5.31 || ^9.5.26", "webmozarts/strict-phpunit": "^7.5" }, @@ -13108,7 +13343,7 @@ ], "support": { "issues": "https://github.com/theofidry/cpu-core-counter/issues", - "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0" }, "funding": [ { @@ -13116,20 +13351,20 @@ "type": "github" } ], - "time": "2024-08-06T10:04:20+00:00" + "time": "2025-08-14T07:29:31+00:00" }, { "name": "filp/whoops", - "version": "2.18.3", + "version": "2.18.4", "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "59a123a3d459c5a23055802237cb317f609867e5" + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/59a123a3d459c5a23055802237cb317f609867e5", - "reference": "59a123a3d459c5a23055802237cb317f609867e5", + "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d", "shasum": "" }, "require": { @@ -13179,7 +13414,7 @@ ], "support": { "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.18.3" + "source": "https://github.com/filp/whoops/tree/2.18.4" }, "funding": [ { @@ -13187,23 +13422,24 @@ "type": "github" } ], - "time": "2025-06-16T00:02:10+00:00" + "time": "2025-08-08T12:00:00+00:00" }, { "name": "fumeapp/modeltyper", - "version": "v3.3.0", + "version": "v3.9.0", "source": { "type": "git", "url": "https://github.com/fumeapp/modeltyper.git", - "reference": "03556593e6332b25aaed188b2891ffe9f900fb84" + "reference": "58afa178b751501bac9a75d23389bd298e441dfe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fumeapp/modeltyper/zipball/03556593e6332b25aaed188b2891ffe9f900fb84", - "reference": "03556593e6332b25aaed188b2891ffe9f900fb84", + "url": "https://api.github.com/repos/fumeapp/modeltyper/zipball/58afa178b751501bac9a75d23389bd298e441dfe", + "reference": "58afa178b751501bac9a75d23389bd298e441dfe", "shasum": "" }, "require": { + "composer/class-map-generator": "^1.6", "illuminate/console": "^11.33.0|^12.0", "illuminate/database": "^11.33.0|^12.0", "illuminate/support": "^11.33.0|^12.0", @@ -13248,9 +13484,9 @@ "description": "Generate TypeScript interfaces from Laravel Models", "support": { "issues": "https://github.com/fumeapp/modeltyper/issues", - "source": "https://github.com/fumeapp/modeltyper/tree/v3.3.0" + "source": "https://github.com/fumeapp/modeltyper/tree/v3.9.0" }, - "time": "2025-06-18T14:16:32+00:00" + "time": "2026-03-11T13:41:31+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -13305,16 +13541,16 @@ }, { "name": "iamcal/sql-parser", - "version": "v0.6", + "version": "v0.7", "source": { "type": "git", "url": "https://github.com/iamcal/SQLParser.git", - "reference": "947083e2dca211a6f12fb1beb67a01e387de9b62" + "reference": "610392f38de49a44dab08dc1659960a29874c4b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/iamcal/SQLParser/zipball/947083e2dca211a6f12fb1beb67a01e387de9b62", - "reference": "947083e2dca211a6f12fb1beb67a01e387de9b62", + "url": "https://api.github.com/repos/iamcal/SQLParser/zipball/610392f38de49a44dab08dc1659960a29874c4b8", + "reference": "610392f38de49a44dab08dc1659960a29874c4b8", "shasum": "" }, "require-dev": { @@ -13340,9 +13576,9 @@ "description": "MySQL schema parser", "support": { "issues": "https://github.com/iamcal/SQLParser/issues", - "source": "https://github.com/iamcal/SQLParser/tree/v0.6" + "source": "https://github.com/iamcal/SQLParser/tree/v0.7" }, - "time": "2025-03-17T16:59:46+00:00" + "time": "2026-01-28T22:20:33+00:00" }, { "name": "jean85/pretty-package-versions", @@ -13406,43 +13642,44 @@ }, { "name": "larastan/larastan", - "version": "v3.5.0", + "version": "v3.9.3", "source": { "type": "git", "url": "https://github.com/larastan/larastan.git", - "reference": "e8ccd73008487ba91da9877b373f8c447743f1ce" + "reference": "64a52bcc5347c89fdf131cb59f96ebfbc8d1ad65" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/larastan/larastan/zipball/e8ccd73008487ba91da9877b373f8c447743f1ce", - "reference": "e8ccd73008487ba91da9877b373f8c447743f1ce", + "url": "https://api.github.com/repos/larastan/larastan/zipball/64a52bcc5347c89fdf131cb59f96ebfbc8d1ad65", + "reference": "64a52bcc5347c89fdf131cb59f96ebfbc8d1ad65", "shasum": "" }, "require": { "ext-json": "*", - "iamcal/sql-parser": "^0.6.0", - "illuminate/console": "^11.44.2 || ^12.4.1", - "illuminate/container": "^11.44.2 || ^12.4.1", - "illuminate/contracts": "^11.44.2 || ^12.4.1", - "illuminate/database": "^11.44.2 || ^12.4.1", - "illuminate/http": "^11.44.2 || ^12.4.1", - "illuminate/pipeline": "^11.44.2 || ^12.4.1", - "illuminate/support": "^11.44.2 || ^12.4.1", + "iamcal/sql-parser": "^0.7.0", + "illuminate/console": "^11.44.2 || ^12.4.1 || ^13", + "illuminate/container": "^11.44.2 || ^12.4.1 || ^13", + "illuminate/contracts": "^11.44.2 || ^12.4.1 || ^13", + "illuminate/database": "^11.44.2 || ^12.4.1 || ^13", + "illuminate/http": "^11.44.2 || ^12.4.1 || ^13", + "illuminate/pipeline": "^11.44.2 || ^12.4.1 || ^13", + "illuminate/support": "^11.44.2 || ^12.4.1 || ^13", "php": "^8.2", - "phpstan/phpstan": "^2.1.11" + "phpstan/phpstan": "^2.1.32" }, "require-dev": { "doctrine/coding-standard": "^13", - "laravel/framework": "^11.44.2 || ^12.7.2", + "laravel/framework": "^11.44.2 || ^12.7.2 || ^13", "mockery/mockery": "^1.6.12", "nikic/php-parser": "^5.4", - "orchestra/canvas": "^v9.2.2 || ^10.0.1", - "orchestra/testbench-core": "^9.12.0 || ^10.1", + "orchestra/canvas": "^v9.2.2 || ^10.0.1 || ^11", + "orchestra/testbench-core": "^9.12.0 || ^10.1 || ^11", "phpstan/phpstan-deprecation-rules": "^2.0.1", - "phpunit/phpunit": "^10.5.35 || ^11.5.15" + "phpunit/phpunit": "^10.5.35 || ^11.5.15 || ^12.5.8" }, "suggest": { - "orchestra/testbench": "Using Larastan for analysing a package needs Testbench" + "orchestra/testbench": "Using Larastan for analysing a package needs Testbench", + "phpmyadmin/sql-parser": "Install to enable Larastan's optional phpMyAdmin-based SQL parser automatically" }, "type": "phpstan-extension", "extra": { @@ -13483,7 +13720,7 @@ ], "support": { "issues": "https://github.com/larastan/larastan/issues", - "source": "https://github.com/larastan/larastan/tree/v3.5.0" + "source": "https://github.com/larastan/larastan/tree/v3.9.3" }, "funding": [ { @@ -13491,20 +13728,20 @@ "type": "github" } ], - "time": "2025-06-19T22:41:50+00:00" + "time": "2026-02-20T12:07:12+00:00" }, { "name": "laravel/pint", - "version": "v1.24.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "0345f3b05f136801af8c339f9d16ef29e6b4df8a" + "reference": "bdec963f53172c5e36330f3a400604c69bf02d39" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/0345f3b05f136801af8c339f9d16ef29e6b4df8a", - "reference": "0345f3b05f136801af8c339f9d16ef29e6b4df8a", + "url": "https://api.github.com/repos/laravel/pint/zipball/bdec963f53172c5e36330f3a400604c69bf02d39", + "reference": "bdec963f53172c5e36330f3a400604c69bf02d39", "shasum": "" }, "require": { @@ -13515,22 +13752,20 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.82.2", - "illuminate/view": "^11.45.1", - "larastan/larastan": "^3.5.0", - "laravel-zero/framework": "^11.45.0", + "friendsofphp/php-cs-fixer": "^3.94.2", + "illuminate/view": "^12.54.1", + "larastan/larastan": "^3.9.3", + "laravel-zero/framework": "^12.0.5", "mockery/mockery": "^1.6.12", - "nunomaduro/termwind": "^2.3.1", - "pestphp/pest": "^2.36.0" + "nunomaduro/termwind": "^2.4.0", + "pestphp/pest": "^3.8.6", + "shipfastlabs/agent-detector": "^1.1.0" }, "bin": [ "builds/pint" ], "type": "project", "autoload": { - "files": [ - "overrides/Runner/Parallel/ProcessFactory.php" - ], "psr-4": { "App\\": "app/", "Database\\Seeders\\": "database/seeders/", @@ -13550,6 +13785,7 @@ "description": "An opinionated code formatter for PHP.", "homepage": "https://laravel.com", "keywords": [ + "dev", "format", "formatter", "lint", @@ -13560,33 +13796,33 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2025-07-10T18:09:32+00:00" + "time": "2026-03-12T15:51:39+00:00" }, { "name": "laravel/sail", - "version": "v1.43.1", + "version": "v1.53.0", "source": { "type": "git", "url": "https://github.com/laravel/sail.git", - "reference": "3e7d899232a8c5e3ea4fc6dee7525ad583887e72" + "reference": "e340eaa2bea9b99192570c48ed837155dbf24fbb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sail/zipball/3e7d899232a8c5e3ea4fc6dee7525ad583887e72", - "reference": "3e7d899232a8c5e3ea4fc6dee7525ad583887e72", + "url": "https://api.github.com/repos/laravel/sail/zipball/e340eaa2bea9b99192570c48ed837155dbf24fbb", + "reference": "e340eaa2bea9b99192570c48ed837155dbf24fbb", "shasum": "" }, "require": { - "illuminate/console": "^9.52.16|^10.0|^11.0|^12.0", - "illuminate/contracts": "^9.52.16|^10.0|^11.0|^12.0", - "illuminate/support": "^9.52.16|^10.0|^11.0|^12.0", + "illuminate/console": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "illuminate/contracts": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^9.52.16|^10.0|^11.0|^12.0|^13.0", "php": "^8.0", - "symfony/console": "^6.0|^7.0", - "symfony/yaml": "^6.0|^7.0" + "symfony/console": "^6.0|^7.0|^8.0", + "symfony/yaml": "^6.0|^7.0|^8.0" }, "require-dev": { - "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0", - "phpstan/phpstan": "^1.10" + "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0|^11.0", + "phpstan/phpstan": "^2.0" }, "bin": [ "bin/sail" @@ -13623,25 +13859,85 @@ "issues": "https://github.com/laravel/sail/issues", "source": "https://github.com/laravel/sail" }, - "time": "2025-05-19T13:19:21+00:00" + "time": "2026-02-06T12:16:02+00:00" + }, + { + "name": "laravel/sentinel", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/sentinel.git", + "reference": "7a98db53e0d9d6f61387f3141c07477f97425603" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sentinel/zipball/7a98db53e0d9d6f61387f3141c07477f97425603", + "reference": "7a98db53e0d9d6f61387f3141c07477f97425603", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/container": "^8.37|^9.0|^10.0|^11.0|^12.0|^13.0", + "php": "^8.0" + }, + "require-dev": { + "laravel/pint": "^1.27", + "orchestra/testbench": "^6.47.1|^7.56|^8.37|^9.16|^10.9|^11.0", + "phpstan/phpstan": "^2.1.33" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sentinel\\SentinelServiceProvider" + ] + }, + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sentinel\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Mior Muhammad Zaki", + "email": "mior@laravel.com" + } + ], + "support": { + "source": "https://github.com/laravel/sentinel/tree/v1.0.1" + }, + "time": "2026-02-12T13:32:54+00:00" }, { "name": "laravel/telescope", - "version": "v5.10.0", + "version": "v5.18.0", "source": { "type": "git", "url": "https://github.com/laravel/telescope.git", - "reference": "fc0a8662682c0375b534033873debb780c003486" + "reference": "6e2aead19de0efb767f703559cc6539036b7fc59" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/telescope/zipball/fc0a8662682c0375b534033873debb780c003486", - "reference": "fc0a8662682c0375b534033873debb780c003486", + "url": "https://api.github.com/repos/laravel/telescope/zipball/6e2aead19de0efb767f703559cc6539036b7fc59", + "reference": "6e2aead19de0efb767f703559cc6539036b7fc59", "shasum": "" }, "require": { "ext-json": "*", - "laravel/framework": "^8.37|^9.0|^10.0|^11.0|^12.0", + "laravel/framework": "^8.37|^9.0|^10.0|^11.0|^12.0|^13.0", + "laravel/sentinel": "^1.0", "php": "^8.0", "symfony/console": "^5.3|^6.0|^7.0", "symfony/var-dumper": "^5.0|^6.0|^7.0" @@ -13649,10 +13945,9 @@ "require-dev": { "ext-gd": "*", "guzzlehttp/guzzle": "^6.0|^7.0", - "laravel/octane": "^1.4|^2.0|dev-develop", - "orchestra/testbench": "^6.40|^7.37|^8.17|^9.0|^10.0", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^9.0|^10.5|^11.5" + "laravel/octane": "^1.4|^2.0", + "orchestra/testbench": "^6.47.1|^7.55|^8.36|^9.15|^10.8|^11.0", + "phpstan/phpstan": "^1.10" }, "type": "library", "extra": { @@ -13690,9 +13985,9 @@ ], "support": { "issues": "https://github.com/laravel/telescope/issues", - "source": "https://github.com/laravel/telescope/tree/v5.10.0" + "source": "https://github.com/laravel/telescope/tree/v5.18.0" }, - "time": "2025-07-07T14:47:19+00:00" + "time": "2026-03-05T15:53:11+00:00" }, { "name": "mockery/mockery", @@ -13779,39 +14074,36 @@ }, { "name": "nunomaduro/collision", - "version": "v8.8.2", + "version": "v8.9.1", "source": { "type": "git", "url": "https://github.com/nunomaduro/collision.git", - "reference": "60207965f9b7b7a4ce15a0f75d57f9dadb105bdb" + "reference": "a1ed3fa530fd60bc515f9303e8520fcb7d4bd935" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/60207965f9b7b7a4ce15a0f75d57f9dadb105bdb", - "reference": "60207965f9b7b7a4ce15a0f75d57f9dadb105bdb", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/a1ed3fa530fd60bc515f9303e8520fcb7d4bd935", + "reference": "a1ed3fa530fd60bc515f9303e8520fcb7d4bd935", "shasum": "" }, "require": { - "filp/whoops": "^2.18.1", - "nunomaduro/termwind": "^2.3.1", + "filp/whoops": "^2.18.4", + "nunomaduro/termwind": "^2.4.0", "php": "^8.2.0", - "symfony/console": "^7.3.0" + "symfony/console": "^7.4.4 || ^8.0.4" }, "conflict": { - "laravel/framework": "<11.44.2 || >=13.0.0", - "phpunit/phpunit": "<11.5.15 || >=13.0.0" + "laravel/framework": "<11.48.0 || >=14.0.0", + "phpunit/phpunit": "<11.5.50 || >=14.0.0" }, "require-dev": { - "brianium/paratest": "^7.8.3", - "larastan/larastan": "^3.4.2", - "laravel/framework": "^11.44.2 || ^12.18", - "laravel/pint": "^1.22.1", - "laravel/sail": "^1.43.1", - "laravel/sanctum": "^4.1.1", - "laravel/tinker": "^2.10.1", - "orchestra/testbench-core": "^9.12.0 || ^10.4", - "pestphp/pest": "^3.8.2", - "sebastian/environment": "^7.2.1 || ^8.0" + "brianium/paratest": "^7.8.5", + "larastan/larastan": "^3.9.2", + "laravel/framework": "^11.48.0 || ^12.52.0", + "laravel/pint": "^1.27.1", + "orchestra/testbench-core": "^9.12.0 || ^10.9.0", + "pestphp/pest": "^3.8.5 || ^4.4.1 || ^5.0.0", + "sebastian/environment": "^7.2.1 || ^8.0.3 || ^9.0.0" }, "type": "library", "extra": { @@ -13874,7 +14166,7 @@ "type": "patreon" } ], - "time": "2025-06-25T02:12:12+00:00" + "time": "2026-02-17T17:33:08+00:00" }, { "name": "phar-io/manifest", @@ -13996,16 +14288,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.17", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpstan.git", - "reference": "89b5ef665716fa2a52ecd2633f21007a6a349053" - }, + "version": "2.1.40", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/89b5ef665716fa2a52ecd2633f21007a6a349053", - "reference": "89b5ef665716fa2a52ecd2633f21007a6a349053", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b", + "reference": "9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b", "shasum": "" }, "require": { @@ -14050,38 +14337,38 @@ "type": "github" } ], - "time": "2025-05-21T20:55:28+00:00" + "time": "2026-02-23T15:04:35+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "12.3.1", + "version": "12.5.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "ddec29dfc128eba9c204389960f2063f3b7fa170" + "reference": "b015312f28dd75b75d3422ca37dff2cd1a565e8d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ddec29dfc128eba9c204389960f2063f3b7fa170", - "reference": "ddec29dfc128eba9c204389960f2063f3b7fa170", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/b015312f28dd75b75d3422ca37dff2cd1a565e8d", + "reference": "b015312f28dd75b75d3422ca37dff2cd1a565e8d", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^5.4.0", + "nikic/php-parser": "^5.7.0", "php": ">=8.3", "phpunit/php-file-iterator": "^6.0", "phpunit/php-text-template": "^5.0", "sebastian/complexity": "^5.0", - "sebastian/environment": "^8.0", + "sebastian/environment": "^8.0.3", "sebastian/lines-of-code": "^4.0", "sebastian/version": "^6.0", - "theseer/tokenizer": "^1.2.3" + "theseer/tokenizer": "^2.0.1" }, "require-dev": { - "phpunit/phpunit": "^12.1" + "phpunit/phpunit": "^12.5.1" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -14090,7 +14377,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "12.3.x-dev" + "dev-main": "12.5.x-dev" } }, "autoload": { @@ -14119,7 +14406,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.3.1" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.3" }, "funding": [ { @@ -14139,20 +14426,20 @@ "type": "tidelift" } ], - "time": "2025-06-18T08:58:13+00:00" + "time": "2026-02-06T06:01:44+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "6.0.0", + "version": "6.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "961bc913d42fe24a257bfff826a5068079ac7782" + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/961bc913d42fe24a257bfff826a5068079ac7782", - "reference": "961bc913d42fe24a257bfff826a5068079ac7782", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", "shasum": "" }, "require": { @@ -14192,15 +14479,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.0" + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" } ], - "time": "2025-02-07T04:58:37+00:00" + "time": "2026-02-02T14:04:18+00:00" }, { "name": "phpunit/php-invoker", @@ -14388,16 +14687,16 @@ }, { "name": "phpunit/phpunit", - "version": "12.2.7", + "version": "12.5.14", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "8b1348b254e5959acaf1539c6bd790515fb49414" + "reference": "47283cfd98d553edcb1353591f4e255dc1bb61f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/8b1348b254e5959acaf1539c6bd790515fb49414", - "reference": "8b1348b254e5959acaf1539c6bd790515fb49414", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/47283cfd98d553edcb1353591f4e255dc1bb61f0", + "reference": "47283cfd98d553edcb1353591f4e255dc1bb61f0", "shasum": "" }, "require": { @@ -14407,23 +14706,24 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.13.3", + "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.3", - "phpunit/php-code-coverage": "^12.3.1", - "phpunit/php-file-iterator": "^6.0.0", + "phpunit/php-code-coverage": "^12.5.3", + "phpunit/php-file-iterator": "^6.0.1", "phpunit/php-invoker": "^6.0.0", "phpunit/php-text-template": "^5.0.0", "phpunit/php-timer": "^8.0.0", - "sebastian/cli-parser": "^4.0.0", - "sebastian/comparator": "^7.1.0", + "sebastian/cli-parser": "^4.2.0", + "sebastian/comparator": "^7.1.4", "sebastian/diff": "^7.0.0", - "sebastian/environment": "^8.0.2", - "sebastian/exporter": "^7.0.0", - "sebastian/global-state": "^8.0.0", + "sebastian/environment": "^8.0.3", + "sebastian/exporter": "^7.0.2", + "sebastian/global-state": "^8.0.2", "sebastian/object-enumerator": "^7.0.0", - "sebastian/type": "^6.0.2", + "sebastian/recursion-context": "^7.0.1", + "sebastian/type": "^6.0.3", "sebastian/version": "^6.0.0", "staabm/side-effects-detector": "^1.0.5" }, @@ -14433,7 +14733,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "12.2-dev" + "dev-main": "12.5-dev" } }, "autoload": { @@ -14465,7 +14765,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/12.2.7" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.14" }, "funding": [ { @@ -14489,20 +14789,20 @@ "type": "tidelift" } ], - "time": "2025-07-11T04:11:13+00:00" + "time": "2026-02-18T12:38:40+00:00" }, { "name": "sebastian/cli-parser", - "version": "4.0.0", + "version": "4.2.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "6d584c727d9114bcdc14c86711cd1cad51778e7c" + "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/6d584c727d9114bcdc14c86711cd1cad51778e7c", - "reference": "6d584c727d9114bcdc14c86711cd1cad51778e7c", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/90f41072d220e5c40df6e8635f5dafba2d9d4d04", + "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04", "shasum": "" }, "require": { @@ -14514,7 +14814,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "4.0-dev" + "dev-main": "4.2-dev" } }, "autoload": { @@ -14538,28 +14838,40 @@ "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.0.0" + "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser", + "type": "tidelift" } ], - "time": "2025-02-07T04:53:50+00:00" + "time": "2025-09-14T09:36:45+00:00" }, { "name": "sebastian/comparator", - "version": "7.1.0", + "version": "7.1.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "03d905327dccc0851c9a08d6a979dfc683826b6f" + "reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/03d905327dccc0851c9a08d6a979dfc683826b6f", - "reference": "03d905327dccc0851c9a08d6a979dfc683826b6f", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/6a7de5df2e094f9a80b40a522391a7e6022df5f6", + "reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6", "shasum": "" }, "require": { @@ -14618,7 +14930,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.0" + "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.4" }, "funding": [ { @@ -14638,7 +14950,7 @@ "type": "tidelift" } ], - "time": "2025-06-17T07:41:58+00:00" + "time": "2026-01-24T09:28:48+00:00" }, { "name": "sebastian/complexity", @@ -14767,16 +15079,16 @@ }, { "name": "sebastian/environment", - "version": "8.0.2", + "version": "8.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "d364b9e5d0d3b18a2573351a1786fbf96b7e0792" + "reference": "7b8842c2d8e85d0c3a5831236bf5869af6ab2a11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/d364b9e5d0d3b18a2573351a1786fbf96b7e0792", - "reference": "d364b9e5d0d3b18a2573351a1786fbf96b7e0792", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/7b8842c2d8e85d0c3a5831236bf5869af6ab2a11", + "reference": "7b8842c2d8e85d0c3a5831236bf5869af6ab2a11", "shasum": "" }, "require": { @@ -14819,7 +15131,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/8.0.2" + "source": "https://github.com/sebastianbergmann/environment/tree/8.0.4" }, "funding": [ { @@ -14839,20 +15151,20 @@ "type": "tidelift" } ], - "time": "2025-05-21T15:05:44+00:00" + "time": "2026-03-15T07:05:40+00:00" }, { "name": "sebastian/exporter", - "version": "7.0.0", + "version": "7.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "76432aafc58d50691a00d86d0632f1217a47b688" + "reference": "016951ae10980765e4e7aee491eb288c64e505b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/76432aafc58d50691a00d86d0632f1217a47b688", - "reference": "76432aafc58d50691a00d86d0632f1217a47b688", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/016951ae10980765e4e7aee491eb288c64e505b7", + "reference": "016951ae10980765e4e7aee491eb288c64e505b7", "shasum": "" }, "require": { @@ -14909,28 +15221,40 @@ "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.0" + "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.2" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2025-02-07T04:56:42+00:00" + "time": "2025-09-24T06:16:11+00:00" }, { "name": "sebastian/global-state", - "version": "8.0.0", + "version": "8.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "570a2aeb26d40f057af686d63c4e99b075fb6cbc" + "reference": "ef1377171613d09edd25b7816f05be8313f9115d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/570a2aeb26d40f057af686d63c4e99b075fb6cbc", - "reference": "570a2aeb26d40f057af686d63c4e99b075fb6cbc", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ef1377171613d09edd25b7816f05be8313f9115d", + "reference": "ef1377171613d09edd25b7816f05be8313f9115d", "shasum": "" }, "require": { @@ -14971,15 +15295,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", "security": "https://github.com/sebastianbergmann/global-state/security/policy", - "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.0" + "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.2" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" } ], - "time": "2025-02-07T04:56:59+00:00" + "time": "2025-08-29T11:29:25+00:00" }, { "name": "sebastian/lines-of-code", @@ -15155,16 +15491,16 @@ }, { "name": "sebastian/recursion-context", - "version": "7.0.0", + "version": "7.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "c405ae3a63e01b32eb71577f8ec1604e39858a7c" + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/c405ae3a63e01b32eb71577f8ec1604e39858a7c", - "reference": "c405ae3a63e01b32eb71577f8ec1604e39858a7c", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", "shasum": "" }, "require": { @@ -15207,28 +15543,40 @@ "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.0" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" } ], - "time": "2025-02-07T05:00:01+00:00" + "time": "2025-08-13T04:44:59+00:00" }, { "name": "sebastian/type", - "version": "6.0.2", + "version": "6.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "1d7cd6e514384c36d7a390347f57c385d4be6069" + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/1d7cd6e514384c36d7a390347f57c385d4be6069", - "reference": "1d7cd6e514384c36d7a390347f57c385d4be6069", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/e549163b9760b8f71f191651d22acf32d56d6d4d", + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d", "shasum": "" }, "require": { @@ -15264,15 +15612,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/type/issues", "security": "https://github.com/sebastianbergmann/type/security/policy", - "source": "https://github.com/sebastianbergmann/type/tree/6.0.2" + "source": "https://github.com/sebastianbergmann/type/tree/6.0.3" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" } ], - "time": "2025-03-18T13:37:31+00:00" + "time": "2025-08-09T06:57:12+00:00" }, { "name": "sebastian/version", @@ -15330,16 +15690,16 @@ }, { "name": "spatie/backtrace", - "version": "1.7.4", + "version": "1.8.2", "source": { "type": "git", "url": "https://github.com/spatie/backtrace.git", - "reference": "cd37a49fce7137359ac30ecc44ef3e16404cccbe" + "reference": "8ffe78be5ed355b5009e3dd989d183433e9a5adc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/backtrace/zipball/cd37a49fce7137359ac30ecc44ef3e16404cccbe", - "reference": "cd37a49fce7137359ac30ecc44ef3e16404cccbe", + "url": "https://api.github.com/repos/spatie/backtrace/zipball/8ffe78be5ed355b5009e3dd989d183433e9a5adc", + "reference": "8ffe78be5ed355b5009e3dd989d183433e9a5adc", "shasum": "" }, "require": { @@ -15350,7 +15710,7 @@ "laravel/serializable-closure": "^1.3 || ^2.0", "phpunit/phpunit": "^9.3 || ^11.4.3", "spatie/phpunit-snapshot-assertions": "^4.2 || ^5.1.6", - "symfony/var-dumper": "^5.1 || ^6.0 || ^7.0" + "symfony/var-dumper": "^5.1|^6.0|^7.0|^8.0" }, "type": "library", "autoload": { @@ -15377,7 +15737,8 @@ "spatie" ], "support": { - "source": "https://github.com/spatie/backtrace/tree/1.7.4" + "issues": "https://github.com/spatie/backtrace/issues", + "source": "https://github.com/spatie/backtrace/tree/1.8.2" }, "funding": [ { @@ -15389,7 +15750,7 @@ "type": "other" } ], - "time": "2025-05-08T15:41:09+00:00" + "time": "2026-03-11T13:48:28+00:00" }, { "name": "spatie/error-solutions", @@ -15619,38 +15980,39 @@ }, { "name": "spatie/laravel-ignition", - "version": "2.9.1", + "version": "2.11.0", "source": { "type": "git", "url": "https://github.com/spatie/laravel-ignition.git", - "reference": "1baee07216d6748ebd3a65ba97381b051838707a" + "reference": "11f38d1ff7abc583a61c96bf3c1b03610a69cccd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/1baee07216d6748ebd3a65ba97381b051838707a", - "reference": "1baee07216d6748ebd3a65ba97381b051838707a", + "url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/11f38d1ff7abc583a61c96bf3c1b03610a69cccd", + "reference": "11f38d1ff7abc583a61c96bf3c1b03610a69cccd", "shasum": "" }, "require": { "ext-curl": "*", "ext-json": "*", "ext-mbstring": "*", - "illuminate/support": "^10.0|^11.0|^12.0", - "php": "^8.1", - "spatie/ignition": "^1.15", - "symfony/console": "^6.2.3|^7.0", - "symfony/var-dumper": "^6.2.3|^7.0" + "illuminate/support": "^11.0|^12.0|^13.0", + "nesbot/carbon": "^2.72|^3.0", + "php": "^8.2", + "spatie/ignition": "^1.15.1", + "symfony/console": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" }, "require-dev": { - "livewire/livewire": "^2.11|^3.3.5", - "mockery/mockery": "^1.5.1", - "openai-php/client": "^0.8.1|^0.10", - "orchestra/testbench": "8.22.3|^9.0|^10.0", - "pestphp/pest": "^2.34|^3.7", - "phpstan/extension-installer": "^1.3.1", - "phpstan/phpstan-deprecation-rules": "^1.1.1|^2.0", - "phpstan/phpstan-phpunit": "^1.3.16|^2.0", - "vlucas/phpdotenv": "^5.5" + "livewire/livewire": "^3.7.0|^4.0|dev-josh/v3-laravel-13-support", + "mockery/mockery": "^1.6.12", + "openai-php/client": "^0.10.3|^0.19", + "orchestra/testbench": "^v9.16.0|^10.6|^11.0", + "pestphp/pest": "^3.7|^4.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan-deprecation-rules": "^2.0.3", + "phpstan/phpstan-phpunit": "^2.0.8", + "vlucas/phpdotenv": "^5.6.2" }, "suggest": { "openai-php/client": "Require get solutions from OpenAI", @@ -15706,7 +16068,7 @@ "type": "github" } ], - "time": "2025-02-20T13:13:55+00:00" + "time": "2026-02-22T19:14:05+00:00" }, { "name": "staabm/side-effects-detector", @@ -15762,28 +16124,27 @@ }, { "name": "symfony/yaml", - "version": "v7.3.1", + "version": "v8.0.6", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "0c3555045a46ab3cd4cc5a69d161225195230edb" + "reference": "5f006c50a981e1630bbb70ad409c5d85f9a716e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/0c3555045a46ab3cd4cc5a69d161225195230edb", - "reference": "0c3555045a46ab3cd4cc5a69d161225195230edb", + "url": "https://api.github.com/repos/symfony/yaml/zipball/5f006c50a981e1630bbb70ad409c5d85f9a716e0", + "reference": "5f006c50a981e1630bbb70ad409c5d85f9a716e0", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", + "php": ">=8.4", "symfony/polyfill-ctype": "^1.8" }, "conflict": { - "symfony/console": "<6.4" + "symfony/console": "<7.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0" + "symfony/console": "^7.4|^8.0" }, "bin": [ "Resources/bin/yaml-lint" @@ -15814,7 +16175,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.3.1" + "source": "https://github.com/symfony/yaml/tree/v8.0.6" }, "funding": [ { @@ -15825,32 +16186,36 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-03T06:57:57+00:00" + "time": "2026-02-09T10:14:57+00:00" }, { "name": "theseer/tokenizer", - "version": "1.2.3", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4", "shasum": "" }, "require": { "ext-dom": "*", "ext-tokenizer": "*", "ext-xmlwriter": "*", - "php": "^7.2 || ^8.0" + "php": "^8.1" }, "type": "library", "autoload": { @@ -15872,7 +16237,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + "source": "https://github.com/theseer/tokenizer/tree/2.0.1" }, "funding": [ { @@ -15880,20 +16245,20 @@ "type": "github" } ], - "time": "2024-03-03T12:36:25+00:00" + "time": "2025-12-08T11:19:18+00:00" }, { "name": "timacdonald/log-fake", - "version": "v2.4.0", + "version": "v2.4.1", "source": { "type": "git", "url": "https://github.com/timacdonald/log-fake.git", - "reference": "6e052e5b32fe3d1854dafcacd4435c5b83b9721e" + "reference": "386f9829198a65c8229ad9f358374b79908b7b9d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/timacdonald/log-fake/zipball/6e052e5b32fe3d1854dafcacd4435c5b83b9721e", - "reference": "6e052e5b32fe3d1854dafcacd4435c5b83b9721e", + "url": "https://api.github.com/repos/timacdonald/log-fake/zipball/386f9829198a65c8229ad9f358374b79908b7b9d", + "reference": "386f9829198a65c8229ad9f358374b79908b7b9d", "shasum": "" }, "require": { @@ -15902,7 +16267,7 @@ "illuminate/log": "^11.0 || ^12.0", "illuminate/support": "^11.0 || ^12.0", "php": "^8.2", - "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0 || ^13.0", "psr/log": "^1.0 || ^2.0 || ^3.0", "symfony/var-dumper": "^7.0" }, @@ -15938,9 +16303,9 @@ ], "support": { "issues": "https://github.com/timacdonald/log-fake/issues", - "source": "https://github.com/timacdonald/log-fake/tree/v2.4.0" + "source": "https://github.com/timacdonald/log-fake/tree/v2.4.1" }, - "time": "2025-03-10T23:38:55+00:00" + "time": "2026-02-17T21:58:04+00:00" } ], "aliases": [], @@ -15949,9 +16314,9 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "8.3.*", + "php": "8.4.*", "ext-zip": "*" }, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/config/app.php b/config/app.php index a15c3759..2374875e 100644 --- a/config/app.php +++ b/config/app.php @@ -7,6 +7,13 @@ use App\Enums\IntervalFormat; use App\Enums\NumberFormat; use App\Enums\TimeFormat; +use App\Providers\AppServiceProvider; +use App\Providers\AuthServiceProvider; +use App\Providers\EventServiceProvider; +use App\Providers\Filament\AdminPanelProvider; +use App\Providers\FortifyServiceProvider; +use App\Providers\JetstreamServiceProvider; +use App\Providers\RouteServiceProvider; use Illuminate\Support\Facades\Facade; use Illuminate\Support\ServiceProvider; use Nwidart\Modules\LaravelModulesServiceProvider; @@ -190,13 +197,13 @@ /* * Application Service Providers... */ - App\Providers\AppServiceProvider::class, - App\Providers\AuthServiceProvider::class, - App\Providers\EventServiceProvider::class, - App\Providers\Filament\AdminPanelProvider::class, - App\Providers\RouteServiceProvider::class, - App\Providers\FortifyServiceProvider::class, - App\Providers\JetstreamServiceProvider::class, + AppServiceProvider::class, + AuthServiceProvider::class, + EventServiceProvider::class, + AdminPanelProvider::class, + RouteServiceProvider::class, + FortifyServiceProvider::class, + JetstreamServiceProvider::class, // Warning: Do not add TelescopeServiceProvider here since it is already conditionally registered in AppServiceProvider LaravelModulesServiceProvider::class, ])->toArray(), diff --git a/config/audit.php b/config/audit.php index 518aa43a..81fc8076 100644 --- a/config/audit.php +++ b/config/audit.php @@ -1,6 +1,11 @@ OwenIt\Auditing\Models\Audit::class, + 'implementation' => Audit::class, /* |-------------------------------------------------------------------------- @@ -32,7 +37,7 @@ 'web', 'api', ], - 'resolver' => OwenIt\Auditing\Resolvers\UserResolver::class, + 'resolver' => UserResolver::class, ], /* @@ -44,9 +49,9 @@ | */ 'resolvers' => [ - 'ip_address' => App\Extensions\Auditing\Resolvers\CustomIpAddressResolver::class, - 'user_agent' => OwenIt\Auditing\Resolvers\UserAgentResolver::class, - 'url' => OwenIt\Auditing\Resolvers\UrlResolver::class, + 'ip_address' => CustomIpAddressResolver::class, + 'user_agent' => UserAgentResolver::class, + 'url' => UrlResolver::class, ], /* diff --git a/config/auth.php b/config/auth.php index 8143c2ee..5127b741 100644 --- a/config/auth.php +++ b/config/auth.php @@ -1,6 +1,7 @@ [ 'users' => [ 'driver' => 'eloquent', - 'model' => App\Models\User::class, + 'model' => User::class, ], ], diff --git a/config/excel.php b/config/excel.php index 02dc56f5..dfde7d06 100644 --- a/config/excel.php +++ b/config/excel.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use Maatwebsite\Excel\DefaultValueBinder; use Maatwebsite\Excel\Excel; use PhpOffice\PhpSpreadsheet\Reader\Csv; @@ -226,7 +227,7 @@ | */ 'value_binder' => [ - 'default' => Maatwebsite\Excel\DefaultValueBinder::class, + 'default' => DefaultValueBinder::class, ], 'cache' => [ diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index a3e0a598..3efe037d 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -9,6 +9,7 @@ use App\Models\Organization; use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; +use Illuminate\Http\FileHelpers; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; @@ -90,7 +91,7 @@ public function attachToOrganization(Organization $organization, array $pivot = public function withProfilePicture(): static { $profilePhoto = $this->faker->image(null, 500, 500); - /** @see \Illuminate\Http\FileHelpers::hashName */ + /** @see FileHelpers::hashName */ $path = 'profile-photos/'.Str::random(40).'.png'; Storage::disk(config('jetstream.profile_photo_disk', 'public'))->put($path, $profilePhoto); diff --git a/docker-compose.yml b/docker-compose.yml index 53d73ed4..99148a2e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -107,7 +107,7 @@ services: - sail - reverse-proxy playwright: - image: mcr.microsoft.com/playwright:v1.51.1-jammy + image: mcr.microsoft.com/playwright:v1.58.1-jammy command: ['npx', 'playwright', 'test', '--ui-port=8080', '--ui-host=0.0.0.0'] working_dir: /src extra_hosts: diff --git a/e2e/auth.spec.ts b/e2e/auth.spec.ts index 634e71ea..b0d7720d 100644 --- a/e2e/auth.spec.ts +++ b/e2e/auth.spec.ts @@ -1,5 +1,6 @@ import { expect, test } from '@playwright/test'; import { PLAYWRIGHT_BASE_URL } from '../playwright/config'; +import { getPasswordResetUrl } from './utils/mailpit'; async function registerNewUser(page, email, password) { await page.goto(PLAYWRIGHT_BASE_URL + '/register'); @@ -35,14 +36,198 @@ test('can register and delete account', async ({ page }) => { await registerNewUser(page, email, password); await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile'); await page.getByRole('button', { name: 'Delete Account' }).click(); + await expect(page.getByRole('dialog')).toBeVisible(); await page.getByPlaceholder('Password').fill(password); - await page.getByRole('button', { name: 'Delete Account' }).click(); + await page.getByRole('dialog').getByRole('button', { name: 'Delete Account' }).click(); await page.waitForURL(PLAYWRIGHT_BASE_URL + '/login'); await page.goto(PLAYWRIGHT_BASE_URL + '/login'); await page.getByLabel('Email').fill(email); await page.getByLabel('Password').fill(password); await page.getByRole('button', { name: 'Log in' }).click(); - await expect(page.getByRole('paragraph')).toContainText( + await expect(page.getByRole('alert')).toContainText( 'These credentials do not match our records.' ); }); + +test('shows error for invalid email on forgot password', async ({ page }) => { + await page.goto(PLAYWRIGHT_BASE_URL + '/forgot-password'); + + // Request password reset with non-existent email + await page.getByLabel('Email').fill('nonexistent@example.com'); + await page.getByRole('button', { name: 'Email Password Reset Link' }).click(); + + // Should show error message + await expect(page.getByText("We can't find a user with that email address.")).toBeVisible(); +}); + +test('shows browser validation for invalid email format on forgot password', async ({ page }) => { + await page.goto(PLAYWRIGHT_BASE_URL + '/forgot-password'); + + // Request password reset with invalid email format + const emailInput = page.getByLabel('Email'); + await emailInput.fill('notanemail'); + + // Check for browser validation - the input should be invalid + const isInvalid = await emailInput.evaluate((el: HTMLInputElement) => !el.validity.valid); + expect(isInvalid).toBe(true); +}); + +test('shows browser validation for empty email on forgot password', async ({ page }) => { + await page.goto(PLAYWRIGHT_BASE_URL + '/forgot-password'); + + // The email input is required, so it should be invalid when empty + const emailInput = page.getByLabel('Email'); + + // Check for browser validation - the input should be invalid because it's required and empty + const isInvalid = await emailInput.evaluate((el: HTMLInputElement) => el.validity.valueMissing); + expect(isInvalid).toBe(true); +}); + +test('can reset password via email link', async ({ page, request }) => { + // First register a new user + const email = `john+${Math.round(Math.random() * 10000)}@doe.com`; + const originalPassword = 'suchagreatpassword123'; + const newPassword = 'mynewsecurepassword456'; + await registerNewUser(page, email, originalPassword); + + // Log out + await page.getByTestId('current_user_button').click(); + await page.getByText('Log Out').click(); + await page.waitForURL(PLAYWRIGHT_BASE_URL + '/login'); + + // Request password reset + await page.goto(PLAYWRIGHT_BASE_URL + '/forgot-password'); + await page.getByLabel('Email').fill(email); + await page.getByRole('button', { name: 'Email Password Reset Link' }).click(); + await expect(page.getByText('We have emailed your password reset link.')).toBeVisible(); + + // Get password reset URL from email + const resetUrl = await getPasswordResetUrl(request, email); + + // Navigate to reset page + await page.goto(resetUrl); + + // Fill in new password + await page.getByLabel('Password', { exact: true }).fill(newPassword); + await page.getByLabel('Confirm Password').fill(newPassword); + await page.getByRole('button', { name: 'Reset Password' }).click(); + + // Should redirect to login page after successful reset + await page.waitForURL(PLAYWRIGHT_BASE_URL + '/login'); + + // Try logging in with new password + await page.getByLabel('Email').fill(email); + await page.getByLabel('Password').fill(newPassword); + await page.getByRole('button', { name: 'Log in' }).click(); + await expect(page.getByTestId('dashboard_view')).toBeVisible(); +}); + +test('shows validation error for password mismatch on reset', async ({ page, request }) => { + // First register a new user + const email = `john+${Math.round(Math.random() * 10000)}@doe.com`; + const originalPassword = 'suchagreatpassword123'; + await registerNewUser(page, email, originalPassword); + + // Log out + await page.getByTestId('current_user_button').click(); + await page.getByText('Log Out').click(); + await page.waitForURL(PLAYWRIGHT_BASE_URL + '/login'); + + // Request password reset + await page.goto(PLAYWRIGHT_BASE_URL + '/forgot-password'); + await page.getByLabel('Email').fill(email); + await page.getByRole('button', { name: 'Email Password Reset Link' }).click(); + await expect(page.getByText('We have emailed your password reset link.')).toBeVisible(); + + // Get password reset URL from email + const resetUrl = await getPasswordResetUrl(request, email); + + // Navigate to reset page + await page.goto(resetUrl); + + // Fill in mismatched passwords + await page.getByLabel('Password', { exact: true }).fill('newpassword123'); + await page.getByLabel('Confirm Password').fill('differentpassword456'); + await page.getByRole('button', { name: 'Reset Password' }).click(); + + // Should show validation error + await expect(page.getByText('The password field confirmation does not match.')).toBeVisible(); +}); + +test('shows validation error for short password on reset', async ({ page, request }) => { + // First register a new user + const email = `john+${Math.round(Math.random() * 10000)}@doe.com`; + const originalPassword = 'suchagreatpassword123'; + await registerNewUser(page, email, originalPassword); + + // Log out + await page.getByTestId('current_user_button').click(); + await page.getByText('Log Out').click(); + await page.waitForURL(PLAYWRIGHT_BASE_URL + '/login'); + + // Request password reset + await page.goto(PLAYWRIGHT_BASE_URL + '/forgot-password'); + await page.getByLabel('Email').fill(email); + await page.getByRole('button', { name: 'Email Password Reset Link' }).click(); + await expect(page.getByText('We have emailed your password reset link.')).toBeVisible(); + + // Get password reset URL from email + const resetUrl = await getPasswordResetUrl(request, email); + + // Navigate to reset page + await page.goto(resetUrl); + + // Fill in short password + await page.getByLabel('Password', { exact: true }).fill('short'); + await page.getByLabel('Confirm Password').fill('short'); + await page.getByRole('button', { name: 'Reset Password' }).click(); + + // Should show validation error about minimum length + await expect(page.getByText('must be at least')).toBeVisible(); +}); + +test('shows error for invalid login credentials', async ({ page }) => { + await page.goto(PLAYWRIGHT_BASE_URL + '/login'); + await page.getByLabel('Email').fill('nonexistent@example.com'); + await page.getByLabel('Password').fill('wrongpassword123'); + await page.getByRole('button', { name: 'Log in' }).click(); + + await expect(page.getByText('These credentials do not match our records.')).toBeVisible(); +}); + +test('shows error when registering with existing email', async ({ page }) => { + const email = `john+${Math.round(Math.random() * 10000)}@doe.com`; + const password = 'suchagreatpassword123'; + + // Register first user + await registerNewUser(page, email, password); + + // Log out + await page.getByTestId('current_user_button').click(); + await page.getByText('Log Out').click(); + await page.waitForURL(PLAYWRIGHT_BASE_URL + '/login'); + + // Try to register with the same email + await page.goto(PLAYWRIGHT_BASE_URL + '/register'); + await page.getByLabel('Name').fill('Another User'); + await page.getByLabel('Email').fill(email); + await page.getByLabel('Password', { exact: true }).fill(password); + await page.getByLabel('Confirm Password').fill(password); + await page.getByLabel('I agree to the Terms of').click(); + await page.getByRole('button', { name: 'Register' }).click(); + + // Should show error about email already taken + await expect(page.getByText('The resource already exists.')).toBeVisible(); +}); + +test('shows validation error for weak password on registration', async ({ page }) => { + await page.goto(PLAYWRIGHT_BASE_URL + '/register'); + await page.getByLabel('Name').fill('Weak Password User'); + await page.getByLabel('Email').fill(`weak+${Math.round(Math.random() * 10000)}@test.com`); + await page.getByLabel('Password', { exact: true }).fill('short'); + await page.getByLabel('Confirm Password').fill('short'); + await page.getByLabel('I agree to the Terms of').click(); + await page.getByRole('button', { name: 'Register' }).click(); + + await expect(page.getByText('must be at least')).toBeVisible(); +}); diff --git a/e2e/calendar-settings.spec.ts b/e2e/calendar-settings.spec.ts new file mode 100644 index 00000000..e70db659 --- /dev/null +++ b/e2e/calendar-settings.spec.ts @@ -0,0 +1,172 @@ +import { PLAYWRIGHT_BASE_URL } from '../playwright/config'; +import { test } from '../playwright/fixtures'; +import { expect } from '@playwright/test'; +import type { Page } from '@playwright/test'; + +async function goToCalendar(page: Page) { + await page.goto(PLAYWRIGHT_BASE_URL + '/calendar'); + await expect(page.locator('.fc')).toBeVisible(); +} + +async function openSettingsPopover(page: Page) { + await page.getByRole('button', { name: 'Calendar settings' }).click(); + await expect(page.getByText('Calendar Settings')).toBeVisible(); +} + +async function clearCalendarSettings(page: Page) { + await page.evaluate(() => localStorage.removeItem('solidtime:calendar-settings')); +} + +test.describe('Calendar Settings', () => { + test.beforeEach(async ({ page }) => { + await clearCalendarSettings(page); + }); + + test('settings popover shows all fields with correct defaults', async ({ page }) => { + await goToCalendar(page); + await openSettingsPopover(page); + + await expect(page.getByLabel('Snap Interval')).toContainText('15 min'); + await expect(page.getByLabel('Start Time')).toContainText('12:00 AM'); + await expect(page.getByLabel('End Time')).toContainText('12:00 AM (next)'); + await expect(page.getByLabel('Grid Scale')).toContainText('15 min'); + }); + + test('snap interval can be changed and persists across reload', async ({ page }) => { + await goToCalendar(page); + await openSettingsPopover(page); + + // Change snap interval to 30 min + await page.getByLabel('Snap Interval').click(); + await page.getByRole('option', { name: '30 min' }).click(); + await page.locator('.fc-toolbar-title').click(); + + // Verify localStorage was updated + const stored = await page.evaluate(() => + JSON.parse(localStorage.getItem('solidtime:calendar-settings') || '{}') + ); + expect(stored.snapMinutes).toBe(30); + + // Reload and verify persistence + await page.reload(); + await expect(page.locator('.fc')).toBeVisible(); + await openSettingsPopover(page); + await expect(page.getByLabel('Snap Interval')).toContainText('30 min'); + }); + + test('start time change is applied to calendar and rejects values >= end time', async ({ + page, + }) => { + await goToCalendar(page); + + // Verify 7 AM slot exists with default start (00:00) + await expect(page.locator('.fc-timegrid-slot[data-time="07:00:00"]')).not.toHaveCount(0); + + await openSettingsPopover(page); + + // Set end time to 6 PM first + await page.getByLabel('End Time').click(); + await page.getByRole('option', { name: '6:00 PM' }).click(); + + // Change start time to 8 AM (valid) + await page.getByLabel('Start Time').click(); + await page.getByRole('option', { name: '8:00 AM' }).click(); + await page.locator('.fc-toolbar-title').click(); + + // Calendar should no longer show hours before 8 AM + await expect(page.locator('.fc-timegrid-slot[data-time="07:00:00"]')).toHaveCount(0); + await expect(page.locator('.fc-timegrid-slot[data-time="08:00:00"]')).not.toHaveCount(0); + + // Try to set start time to 6 PM (invalid: equals end time) + await openSettingsPopover(page); + await page.getByLabel('Start Time').click(); + await page.getByRole('option', { name: '6:00 PM' }).click(); + + // Should be rejected — start time stays at 8 AM + await expect(page.getByLabel('Start Time')).toContainText('8:00 AM'); + }); + + test('end time change is applied to calendar and rejects values <= start time', async ({ + page, + }) => { + await goToCalendar(page); + + // Verify 19:00 slot exists with default end (24:00) + await expect(page.locator('.fc-timegrid-slot[data-time="19:00:00"]')).not.toHaveCount(0); + + await openSettingsPopover(page); + + // Set start time to 8 AM first + await page.getByLabel('Start Time').click(); + await page.getByRole('option', { name: '8:00 AM' }).click(); + + // Change end time to 6 PM (valid) + await page.getByLabel('End Time').click(); + await page.getByRole('option', { name: '6:00 PM' }).click(); + await page.locator('.fc-toolbar-title').click(); + + // Calendar should no longer show hours at or after 6 PM + await expect(page.locator('.fc-timegrid-slot[data-time="18:00:00"]')).toHaveCount(0); + await expect(page.locator('.fc-timegrid-slot[data-time="17:00:00"]')).not.toHaveCount(0); + + // Try to set end time to 8 AM (invalid: equals start time) + await openSettingsPopover(page); + await page.getByLabel('End Time').click(); + await page.getByRole('option', { name: '8:00 AM' }).click(); + + // Should be rejected — end time stays at 6 PM + await expect(page.getByLabel('End Time')).toContainText('6:00 PM'); + }); + + test('grid scale affects number of calendar slots', async ({ page }) => { + await goToCalendar(page); + + // Count slots with default 15-min scale + const defaultSlotCount = await page.locator('.fc-timegrid-slot').count(); + + // Change to 30 min scale (should halve the slots) + await openSettingsPopover(page); + await page.getByLabel('Grid Scale').click(); + await page.getByRole('option', { name: '30 min' }).click(); + await page.locator('.fc-toolbar-title').click(); + + const largerSlotCount = await page.locator('.fc-timegrid-slot').count(); + expect(largerSlotCount).toBeLessThan(defaultSlotCount); + + // Change to 5 min scale (should have many more slots) + await openSettingsPopover(page); + await page.getByLabel('Grid Scale').click(); + await page.getByRole('option', { name: '5 min', exact: true }).click(); + await page.locator('.fc-toolbar-title').click(); + + const smallerSlotCount = await page.locator('.fc-timegrid-slot').count(); + expect(smallerSlotCount).toBeGreaterThan(defaultSlotCount); + }); + + test('all settings persist across navigation', async ({ page }) => { + await goToCalendar(page); + await openSettingsPopover(page); + + // Change every setting + await page.getByLabel('Snap Interval').click(); + await page.getByRole('option', { name: '5 min', exact: true }).click(); + await page.getByLabel('Start Time').click(); + await page.getByRole('option', { name: '6:00 AM' }).click(); + await page.getByLabel('End Time').click(); + await page.getByRole('option', { name: '10:00 PM' }).click(); + await page.getByLabel('Grid Scale').click(); + await page.getByRole('option', { name: '30 min' }).click(); + await page.locator('.fc-toolbar-title').click(); + + // Navigate away and back + await page.goto(PLAYWRIGHT_BASE_URL + '/time'); + await goToCalendar(page); + + // Verify all settings persisted + await openSettingsPopover(page); + await expect(page.getByLabel('Snap Interval')).toContainText('5 min'); + await expect(page.getByLabel('Start Time')).toContainText('6:00 AM'); + await expect(page.getByLabel('End Time')).toContainText('10:00 PM'); + await expect(page.getByLabel('Grid Scale')).toContainText('30 min'); + }); +}); diff --git a/e2e/calendar.spec.ts b/e2e/calendar.spec.ts new file mode 100644 index 00000000..ec2b5b2b --- /dev/null +++ b/e2e/calendar.spec.ts @@ -0,0 +1,326 @@ +import { PLAYWRIGHT_BASE_URL } from '../playwright/config'; +import { test } from '../playwright/fixtures'; +import { expect } from '@playwright/test'; +import type { Page } from '@playwright/test'; +import { + createBillableProjectViaApi, + createProjectViaApi, + createBareTimeEntryViaApi, + createTimeEntryViaApi, +} from './utils/api'; + +async function goToCalendar(page: Page) { + await page.goto(PLAYWRIGHT_BASE_URL + '/calendar'); +} + +/** + * These tests verify that changing the project on a time entry via the calendar + * updates the billable status to match the new project's is_billable setting. + * + * Issue: https://github.com/solidtime-io/solidtime/issues/981 + */ + +test('test that changing project in calendar edit modal from non-billable to billable updates billable status', async ({ + page, + ctx, +}) => { + const billableProjectName = 'Billable Cal Project ' + Math.floor(1 + Math.random() * 10000); + + await createBillableProjectViaApi(ctx, { name: billableProjectName }); + await createBareTimeEntryViaApi(ctx, 'Test billable calendar', '1h'); + + await goToCalendar(page); + + // Click on the time entry event in the calendar + await page.locator('.fc-event').filter({ hasText: 'Test billable calendar' }).first().click(); + await expect(page.getByRole('dialog')).toBeVisible(); + + // Verify initially non-billable + await expect( + page.getByRole('dialog').getByRole('combobox').filter({ hasText: 'Non-Billable' }) + ).toBeVisible(); + + // Select the billable project + await page.getByRole('dialog').getByRole('button', { name: 'No Project' }).click(); + await page.getByRole('option', { name: billableProjectName }).click(); + + // Verify the billable dropdown updated to Billable + await expect( + page.getByRole('dialog').getByRole('combobox').filter({ hasText: 'Billable' }) + ).toBeVisible(); + + // Save and verify + const [updateResponse] = await Promise.all([ + page.waitForResponse( + (response) => + response.url().includes('/time-entries/') && + response.request().method() === 'PUT' && + response.status() === 200 + ), + page.getByRole('button', { name: 'Update Time Entry' }).click(), + ]); + const responseBody = await updateResponse.json(); + expect(responseBody.data.billable).toBe(true); +}); + +test('test that changing project in calendar edit modal from billable to non-billable updates billable status', async ({ + page, + ctx, +}) => { + const billableProjectName = 'Billable Cal Rev Project ' + Math.floor(1 + Math.random() * 10000); + const nonBillableProjectName = + 'NonBillable Cal Rev Project ' + Math.floor(1 + Math.random() * 10000); + + await createBillableProjectViaApi(ctx, { name: billableProjectName }); + await createProjectViaApi(ctx, { name: nonBillableProjectName }); + await createBareTimeEntryViaApi(ctx, 'Test billable cal reverse', '1h'); + + await goToCalendar(page); + + // Click on the time entry event in the calendar + await page + .locator('.fc-event') + .filter({ hasText: 'Test billable cal reverse' }) + .first() + .click(); + await expect(page.getByRole('dialog')).toBeVisible(); + + // First assign the billable project + await page.getByRole('dialog').getByRole('button', { name: 'No Project' }).click(); + await page.getByRole('option', { name: billableProjectName }).click(); + + // Verify billable status flipped to Billable + await expect( + page.getByRole('dialog').getByRole('combobox').filter({ hasText: 'Billable' }) + ).toBeVisible(); + + // Now switch to the non-billable project + await page.getByRole('dialog').getByRole('button', { name: billableProjectName }).click(); + await page.getByRole('option', { name: nonBillableProjectName }).click(); + + // Verify billable status reverted to Non-Billable + await expect( + page.getByRole('dialog').getByRole('combobox').filter({ hasText: 'Non-Billable' }) + ).toBeVisible(); + + // Save and verify + const [updateResponse] = await Promise.all([ + page.waitForResponse( + (response) => + response.url().includes('/time-entries/') && + response.request().method() === 'PUT' && + response.status() === 200 + ), + page.getByRole('button', { name: 'Update Time Entry' }).click(), + ]); + const responseBody = await updateResponse.json(); + expect(responseBody.data.billable).toBe(false); +}); + +test('test that opening calendar edit modal for a time entry with manually overridden billable status preserves that status', async ({ + page, + ctx, +}) => { + const billableProjectName = + 'Billable Cal Persist Project ' + Math.floor(1 + Math.random() * 10000); + + await createBillableProjectViaApi(ctx, { name: billableProjectName }); + await createBareTimeEntryViaApi(ctx, 'Test cal persist override', '1h'); + + await goToCalendar(page); + + // Click on the time entry event in the calendar + await page + .locator('.fc-event') + .filter({ hasText: 'Test cal persist override' }) + .first() + .click(); + await expect(page.getByRole('dialog')).toBeVisible(); + + // Assign the billable project + await page.getByRole('dialog').getByRole('button', { name: 'No Project' }).click(); + await page.getByRole('option', { name: billableProjectName }).click(); + + // Verify it auto-set to Billable + await expect( + page.getByRole('dialog').getByRole('combobox').filter({ hasText: 'Billable' }) + ).toBeVisible(); + + // Now manually override billable to Non-Billable via the dropdown + await page.getByRole('dialog').getByRole('combobox').filter({ hasText: 'Billable' }).click(); + await page.getByRole('option', { name: 'Non Billable' }).click(); + + // Verify it shows Non-Billable now + await expect( + page.getByRole('dialog').getByRole('combobox').filter({ hasText: 'Non-Billable' }) + ).toBeVisible(); + + // Save + const [firstSaveResponse] = await Promise.all([ + page.waitForResponse( + (response) => + response.url().includes('/time-entries/') && + response.request().method() === 'PUT' && + response.status() === 200 + ), + page.getByRole('button', { name: 'Update Time Entry' }).click(), + ]); + const firstBody = await firstSaveResponse.json(); + expect(firstBody.data.billable).toBe(false); + + // Re-open the edit modal from the calendar — the project_id watcher should NOT override billable + await page + .locator('.fc-event') + .filter({ hasText: 'Test cal persist override' }) + .first() + .click(); + await expect(page.getByRole('dialog')).toBeVisible(); + + // The billable dropdown should still show Non-Billable + await expect( + page.getByRole('dialog').getByRole('combobox').filter({ hasText: 'Non-Billable' }) + ).toBeVisible(); + + // Save without changes and verify the response still has billable=false + const [updateResponse] = await Promise.all([ + page.waitForResponse( + (response) => + response.url().includes('/time-entries/') && + response.request().method() === 'PUT' && + response.status() === 200 + ), + page.getByRole('button', { name: 'Update Time Entry' }).click(), + ]); + const responseBody = await updateResponse.json(); + expect(responseBody.data.billable).toBe(false); +}); + +test('test that calendar page loads and displays time entries', async ({ page, ctx }) => { + await createBareTimeEntryViaApi(ctx, 'Calendar display test', '1h'); + + await goToCalendar(page); + + // Calendar container should be visible + await expect(page.locator('.fc')).toBeVisible(); + + // The time entry should appear as a calendar event + await expect( + page.locator('.fc-event').filter({ hasText: 'Calendar display test' }).first() + ).toBeVisible(); +}); + +test('test that calendar navigation buttons work', async ({ page }) => { + await goToCalendar(page); + await expect(page.locator('.fc')).toBeVisible(); + + // Click the "next" button to navigate forward + await page.locator('button.fc-next-button').click(); + await expect(page.locator('.fc')).toBeVisible(); + + // Click the "prev" button to navigate back + await page.locator('button.fc-prev-button').click(); + await expect(page.locator('.fc')).toBeVisible(); + + // Navigate forward first so "today" button becomes enabled, then click it + await page.locator('button.fc-next-button').click(); + await page.locator('button.fc-today-button').click(); + await expect(page.locator('.fc')).toBeVisible(); +}); + +test('test that editing time entry description via calendar modal works', async ({ page, ctx }) => { + const originalDescription = 'Edit me in calendar ' + Math.floor(1 + Math.random() * 10000); + const updatedDescription = 'Updated in calendar ' + Math.floor(1 + Math.random() * 10000); + await createBareTimeEntryViaApi(ctx, originalDescription, '1h'); + + await goToCalendar(page); + + // Click on the time entry event + await page.locator('.fc-event').filter({ hasText: originalDescription }).first().click(); + await expect(page.getByRole('dialog')).toBeVisible(); + + // Update the description (edit modal uses placeholder, not data-testid) + const descriptionInput = page.getByRole('dialog').getByPlaceholder('What did you work on?'); + await descriptionInput.fill(updatedDescription); + + // Save and verify + const [editResponse] = await Promise.all([ + page.waitForResponse( + (response) => + response.url().includes('/time-entries/') && + response.request().method() === 'PUT' && + response.status() === 200 + ), + page.getByRole('button', { name: 'Update Time Entry' }).click(), + ]); + const editBody = await editResponse.json(); + expect(editBody.data.description).toBe(updatedDescription); + + // Verify the updated description is shown in the calendar UI + await expect( + page.locator('.fc-event').filter({ hasText: updatedDescription }).first() + ).toBeVisible(); + // Verify the old description is no longer shown + await expect( + page.locator('.fc-event').filter({ hasText: originalDescription }) + ).not.toBeVisible(); +}); + +test('test that deleting time entry from calendar modal works', async ({ page, ctx }) => { + const description = 'Delete me from calendar ' + Math.floor(1 + Math.random() * 10000); + await createBareTimeEntryViaApi(ctx, description, '1h'); + + await goToCalendar(page); + + // Click on the time entry event + await page.locator('.fc-event').filter({ hasText: description }).first().click(); + await expect(page.getByRole('dialog')).toBeVisible(); + + // Click the delete button + await Promise.all([ + page.waitForResponse( + (response) => + response.url().includes('/time-entries/') && + response.request().method() === 'DELETE' && + response.status() === 204 + ), + page.getByRole('dialog').getByRole('button', { name: 'Delete' }).click(), + ]); + + // Verify the event is removed from the calendar + await expect(page.locator('.fc-event').filter({ hasText: description })).not.toBeVisible(); +}); + +// ============================================= +// Employee Permission Tests +// ============================================= + +test.describe('Employee Calendar Isolation', () => { + test('employee can only see their own time entries on the calendar', async ({ + ctx, + employee, + }) => { + // Owner creates a time entry for today + const ownerDescription = 'OwnerCalEntry ' + Math.floor(Math.random() * 10000); + await createBareTimeEntryViaApi(ctx, ownerDescription, '1h'); + + // Create a time entry for the employee for today + const employeeDescription = 'EmpCalEntry ' + Math.floor(Math.random() * 10000); + await createTimeEntryViaApi( + { ...ctx, memberId: employee.memberId }, + { description: employeeDescription, duration: '30min' } + ); + + await employee.page.goto(PLAYWRIGHT_BASE_URL + '/calendar'); + await expect(employee.page.locator('.fc')).toBeVisible({ timeout: 10000 }); + + // Employee's event IS visible + await expect( + employee.page.locator('.fc-event').filter({ hasText: employeeDescription }).first() + ).toBeVisible({ timeout: 10000 }); + + // Owner's event is NOT visible + await expect( + employee.page.locator('.fc-event').filter({ hasText: ownerDescription }) + ).not.toBeVisible(); + }); +}); diff --git a/e2e/clients.spec.ts b/e2e/clients.spec.ts index dab9c81f..843095cc 100644 --- a/e2e/clients.spec.ts +++ b/e2e/clients.spec.ts @@ -1,15 +1,23 @@ -import { expect, Page } from '@playwright/test'; +import { expect } from '@playwright/test'; +import type { Page } from '@playwright/test'; import { PLAYWRIGHT_BASE_URL } from '../playwright/config'; import { test } from '../playwright/fixtures'; +import { + createClientViaApi, + createProjectMemberViaApi, + createProjectViaApi, + createPublicProjectViaApi, +} from './utils/api'; +import { getTableRowNames } from './utils/table'; -async function goToProjectsOverview(page: Page) { +async function goToClientsOverview(page: Page) { await page.goto(PLAYWRIGHT_BASE_URL + '/clients'); } -// Create new project via modal +// Create new client via modal test('test that creating and deleting a new client via the modal works', async ({ page }) => { const newClientName = 'New Project ' + Math.floor(1 + Math.random() * 10000); - await goToProjectsOverview(page); + await goToClientsOverview(page); await page.getByRole('button', { name: 'Create Client' }).click(); await page.getByPlaceholder('Client Name').fill(newClientName); await Promise.all([ @@ -26,7 +34,7 @@ test('test that creating and deleting a new client via the modal works', async ( await expect(page.getByTestId('client_table')).toContainText(newClientName); const moreButton = page.locator("[aria-label='Actions for Client " + newClientName + "']"); - moreButton.click(); + await moreButton.click(); const deleteButton = page.locator("[aria-label='Delete Client " + newClientName + "']"); await Promise.all([ @@ -41,13 +49,11 @@ test('test that creating and deleting a new client via the modal works', async ( await expect(page.getByTestId('client_table')).not.toContainText(newClientName); }); -test('test that archiving and unarchiving clients works', async ({ page }) => { +test('test that archiving and unarchiving clients works', async ({ page, ctx }) => { const newClientName = 'New Client ' + Math.floor(1 + Math.random() * 10000); - await goToProjectsOverview(page); - await page.getByRole('button', { name: 'Create Client' }).click(); - await page.getByLabel('Client Name').fill(newClientName); + await createClientViaApi(ctx, { name: newClientName }); - await page.getByRole('button', { name: 'Create Client' }).click(); + await goToClientsOverview(page); await expect(page.getByText(newClientName)).toBeVisible(); await page.getByRole('row').first().getByRole('button').click(); @@ -71,4 +77,226 @@ test('test that archiving and unarchiving clients works', async ({ page }) => { ]); }); -// TODO: Add Name Update Test +test('test that editing a client name works', async ({ page, ctx }) => { + const originalName = 'Original Client ' + Math.floor(1 + Math.random() * 10000); + const updatedName = 'Updated Client ' + Math.floor(1 + Math.random() * 10000); + await createClientViaApi(ctx, { name: originalName }); + + await goToClientsOverview(page); + await expect(page.getByText(originalName)).toBeVisible(); + + // Open edit modal via actions menu + const moreButton = page.locator("[aria-label='Actions for Client " + originalName + "']"); + await moreButton.click(); + await page.getByTestId('client_edit').click(); + + // Update the client name + await page.getByPlaceholder('Client Name').fill(updatedName); + await Promise.all([ + page.getByRole('button', { name: 'Update Client' }).click(), + page.waitForResponse( + async (response) => + response.url().includes('/clients') && + response.request().method() === 'PUT' && + response.status() === 200 + ), + ]); + + // Verify updated name is shown and old name is gone + await expect(page.getByTestId('client_table')).toContainText(updatedName); + await expect(page.getByTestId('client_table')).not.toContainText(originalName); +}); + +test('test that deleting a client via actions menu works', async ({ page, ctx }) => { + const clientName = 'DeleteMe Client ' + Math.floor(1 + Math.random() * 10000); + + await createClientViaApi(ctx, { name: clientName }); + + await goToClientsOverview(page); + await expect(page.getByTestId('client_table')).toContainText(clientName); + + const moreButton = page.locator("[aria-label='Actions for Client " + clientName + "']"); + await moreButton.click(); + const deleteButton = page.locator("[aria-label='Delete Client " + clientName + "']"); + + await Promise.all([ + deleteButton.click(), + page.waitForResponse( + (response) => + response.url().includes('/clients') && + response.request().method() === 'DELETE' && + response.status() === 204 + ), + ]); + + await expect(page.getByTestId('client_table')).not.toContainText(clientName); +}); + +// ============================================= +// Sorting Tests +// ============================================= + +async function clearClientTableState(page: Page) { + await page.evaluate(() => { + localStorage.removeItem('client-table-state'); + }); +} + +test('test that sorting clients by name and status works', async ({ page, ctx }) => { + await createClientViaApi(ctx, { name: 'AAA SortClient' }); + await createClientViaApi(ctx, { name: 'ZZZ SortClient' }); + + await goToClientsOverview(page); + await clearClientTableState(page); + await page.reload(); + + const table = page.getByTestId('client_table'); + await expect(table).toBeVisible(); + + // -- Name sorting (default is name asc) -- + let names = await getTableRowNames(table); + expect(names.indexOf('AAA SortClient')).toBeLessThan(names.indexOf('ZZZ SortClient')); + + const nameHeader = table.getByText('Name').first(); + await nameHeader.click(); // toggle to desc + names = await getTableRowNames(table); + expect(names.indexOf('ZZZ SortClient')).toBeLessThan(names.indexOf('AAA SortClient')); + + // -- Status sorting -- + const statusHeader = table.getByText('Status').first(); + await statusHeader.click(); // asc + await expect(statusHeader.locator('svg')).toBeVisible(); + await statusHeader.click(); // desc + await expect(statusHeader.locator('svg')).toBeVisible(); +}); + +test('test that sorting clients by project count works', async ({ page, ctx }) => { + const clientWithMany = await createClientViaApi(ctx, { name: 'ManyProjects Client' }); + const clientWithNone = await createClientViaApi(ctx, { name: 'NoProjects Client' }); + + // Create projects for the first client + await createProjectViaApi(ctx, { name: 'Proj1', client_id: clientWithMany.id }); + await createProjectViaApi(ctx, { name: 'Proj2', client_id: clientWithMany.id }); + + await goToClientsOverview(page); + await clearClientTableState(page); + await page.reload(); + + const table = page.getByTestId('client_table'); + await expect(table).toBeVisible(); + + // Click Projects header - first click should sort desc (most projects first) + const projectsHeader = table.getByText('Projects').first(); + await projectsHeader.click(); + await expect(projectsHeader.locator('svg')).toBeVisible(); + let names = await getTableRowNames(table); + expect(names.indexOf('ManyProjects Client')).toBeLessThan(names.indexOf('NoProjects Client')); + + // Second click toggles to asc (least projects first) + await projectsHeader.click(); + names = await getTableRowNames(table); + expect(names.indexOf('NoProjects Client')).toBeLessThan(names.indexOf('ManyProjects Client')); +}); + +test('test that client sort state persists after page reload', async ({ page }) => { + await goToClientsOverview(page); + await clearClientTableState(page); + await page.reload(); + + const table = page.getByTestId('client_table'); + await expect(table).toBeVisible(); + + const nameHeader = table.getByText('Name').first(); + await nameHeader.click(); // toggle to desc + await expect(nameHeader.locator('svg')).toBeVisible(); + + await page.reload(); + + await expect(page.getByTestId('client_table')).toBeVisible(); + await expect( + page.getByTestId('client_table').getByText('Name').first().locator('svg') + ).toBeVisible(); +}); + +// ============================================= +// Employee Permission Tests +// ============================================= + +test.describe('Employee Clients Restrictions', () => { + test('employee can view clients but cannot create', async ({ ctx, employee }) => { + // Create a client with a public project so the employee can see the client + const clientName = 'EmpViewClient ' + Math.floor(Math.random() * 10000); + const client = await createClientViaApi(ctx, { name: clientName }); + await createPublicProjectViaApi(ctx, { name: 'EmpClientProj', client_id: client.id }); + + await employee.page.goto(PLAYWRIGHT_BASE_URL + '/clients'); + await expect(employee.page.getByTestId('clients_view')).toBeVisible({ + timeout: 10000, + }); + + // Employee can see the client + await expect(employee.page.getByText(clientName)).toBeVisible({ timeout: 10000 }); + + // Employee cannot see Create Client button + await expect( + employee.page.getByRole('button', { name: 'Create Client' }) + ).not.toBeVisible(); + }); + + test('employee cannot see edit/delete/archive actions on clients', async ({ + ctx, + employee, + }) => { + const clientName = 'EmpActionsClient ' + Math.floor(Math.random() * 10000); + const client = await createClientViaApi(ctx, { name: clientName }); + await createPublicProjectViaApi(ctx, { name: 'EmpClientActProj', client_id: client.id }); + + await employee.page.goto(PLAYWRIGHT_BASE_URL + '/clients'); + await expect(employee.page.getByText(clientName)).toBeVisible({ timeout: 10000 }); + + // Click the actions dropdown trigger to open the menu + const actionsButton = employee.page.locator( + `[aria-label='Actions for Client ${clientName}']` + ); + await actionsButton.click(); + + // The dropdown menu items (Edit, Archive, Delete) should NOT be visible + await expect( + employee.page.locator(`[aria-label='Edit Client ${clientName}']`) + ).not.toBeVisible(); + await expect( + employee.page.locator(`[aria-label='Archive Client ${clientName}']`) + ).not.toBeVisible(); + await expect( + employee.page.locator(`[aria-label='Delete Client ${clientName}']`) + ).not.toBeVisible(); + }); + + test('employee can see client when they are a member of its private project', async ({ + ctx, + employee, + }) => { + const clientName = 'EmpPrivateClient ' + Math.floor(Math.random() * 10000); + const client = await createClientViaApi(ctx, { name: clientName }); + + // Create a private project under this client + const project = await createProjectViaApi(ctx, { + name: 'PrivateProj', + client_id: client.id, + is_public: false, + }); + + // Add the employee as a project member + await createProjectMemberViaApi(ctx, project.id, { + member_id: employee.memberId, + }); + + await employee.page.goto(PLAYWRIGHT_BASE_URL + '/clients'); + await expect(employee.page.getByTestId('clients_view')).toBeVisible({ + timeout: 10000, + }); + + // Employee can see the client because they are a member of its private project + await expect(employee.page.getByText(clientName)).toBeVisible({ timeout: 10000 }); + }); +}); diff --git a/e2e/command-palette.spec.ts b/e2e/command-palette.spec.ts new file mode 100644 index 00000000..535a4900 --- /dev/null +++ b/e2e/command-palette.spec.ts @@ -0,0 +1,474 @@ +import { expect, test } from '../playwright/fixtures'; +import { PLAYWRIGHT_BASE_URL } from '../playwright/config'; +import type { Page } from '@playwright/test'; + +const TIMER_BUTTON_SELECTOR = '[data-testid="dashboard_timer"] [data-testid="timer_button"]'; + +async function goToDashboard(page: Page) { + await page.goto(PLAYWRIGHT_BASE_URL + '/dashboard'); +} + +async function openCommandPalette(page: Page) { + await page.getByTestId('command_palette_button').click(); + await expect(page.locator('[role="dialog"]')).toBeVisible({ timeout: 5000 }); +} + +async function closeCommandPalette(page: Page) { + await page.keyboard.press('Escape'); + await expect(page.locator('[role="dialog"]')).not.toBeVisible(); +} + +async function searchInCommandPalette(page: Page, query: string) { + await page.locator('[role="dialog"] input').fill(query); + // Wait for search debounce to settle (command palette uses a debounced search) + await page.waitForTimeout(300); +} + +async function selectCommand(page: Page, name: string) { + const option = page.getByRole('option', { name, exact: true }); + await option.scrollIntoViewIfNeeded(); + await option.click(); +} + +async function assertTimerIsRunning(page: Page) { + await expect(page.locator(TIMER_BUTTON_SELECTOR).and(page.locator(':visible'))).toHaveClass( + /bg-red-400\/80/, + { + timeout: 10000, + } + ); +} + +async function assertTimerIsStopped(page: Page) { + await expect(page.locator(TIMER_BUTTON_SELECTOR).and(page.locator(':visible'))).toHaveClass( + /bg-accent-300\/70/, + { + timeout: 10000, + } + ); +} + +test.describe('Command Palette', () => { + test.describe('Opening and Closing', () => { + test('opens via search button and closes with Escape', async ({ page }) => { + await goToDashboard(page); + await openCommandPalette(page); + await expect( + page.locator('[role="dialog"] input[placeholder*="command"]') + ).toBeVisible(); + + await closeCommandPalette(page); + await expect(page.locator('[role="dialog"]')).not.toBeVisible(); + }); + + test('opens with keyboard shortcut', async ({ page }) => { + await goToDashboard(page); + // Click on body to ensure page has focus + await page.locator('body').click(); + // Use ControlOrMeta which resolves to Ctrl on Linux/Windows and Meta on macOS + await page.keyboard.press('ControlOrMeta+k'); + await expect(page.locator('[role="dialog"]')).toBeVisible({ timeout: 5000 }); + }); + + test('clears search on close', async ({ page }) => { + await goToDashboard(page); + await openCommandPalette(page); + await searchInCommandPalette(page, 'dashboard'); + await closeCommandPalette(page); + + await openCommandPalette(page); + await expect(page.locator('[role="dialog"] input')).toHaveValue(''); + }); + }); + + test.describe('Command Display', () => { + test('displays navigation and timer commands', async ({ page }) => { + await goToDashboard(page); + await openCommandPalette(page); + + // Navigation commands + await expect(page.getByRole('option', { name: 'Go to Dashboard' })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Go to Time' })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Go to Calendar' })).toBeVisible(); + + // Timer commands + await expect(page.getByRole('option', { name: 'Start Timer' })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Create Time Entry' })).toBeVisible(); + }); + + test('displays create commands', async ({ page }) => { + await goToDashboard(page); + await openCommandPalette(page); + + await expect(page.getByRole('option', { name: 'Create Project' })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Create Client' })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Create Tag' })).toBeVisible(); + }); + }); + + test.describe('Navigation Commands', () => { + // Tests use element visibility assertions for consistency with codebase patterns + const navigationTests = [ + ['Go to Dashboard', 'dashboard_view', '/time'], + ['Go to Time', 'time_view', '/dashboard'], + ['Go to Calendar', 'calendar_view', '/dashboard'], + ['Go to Projects', 'projects_view', '/dashboard'], + ['Go to Clients', 'clients_view', '/dashboard'], + ['Go to Members', 'members_view', '/dashboard'], + ['Go to Tags', 'tags_view', '/dashboard'], + ] as const; + + for (const [commandName, expectedTestId, startUrl] of navigationTests) { + test(`${commandName}`, async ({ page }) => { + await page.goto(PLAYWRIGHT_BASE_URL + startUrl); + await openCommandPalette(page); + await searchInCommandPalette(page, commandName.replace('Go to ', '')); + await selectCommand(page, commandName); + await expect(page.getByTestId(expectedTestId)).toBeVisible({ timeout: 10000 }); + }); + } + + test('Go to Profile', async ({ page }) => { + await goToDashboard(page); + await openCommandPalette(page); + await searchInCommandPalette(page, 'Profile'); + await selectCommand(page, 'Go to Profile'); + // Profile page doesn't have a testId, so check for a unique element + await expect(page.getByRole('heading', { name: 'Profile Information' })).toBeVisible({ + timeout: 10000, + }); + }); + + test('Go to Reporting Overview', async ({ page }) => { + await goToDashboard(page); + await openCommandPalette(page); + await searchInCommandPalette(page, 'Reporting Overview'); + await selectCommand(page, 'Go to Reporting Overview'); + await expect(page.getByTestId('reporting_view')).toBeVisible({ timeout: 10000 }); + }); + + test('Go to Settings', async ({ page }) => { + await goToDashboard(page); + await openCommandPalette(page); + await searchInCommandPalette(page, 'Settings'); + await selectCommand(page, 'Go to Settings'); + // Settings page uses team settings which has an h3 heading + await expect( + page.getByRole('heading', { name: 'Organization Name', level: 3 }) + ).toBeVisible({ + timeout: 10000, + }); + }); + }); + + test.describe('Search and Filtering', () => { + test('filters commands when searching', async ({ page }) => { + await goToDashboard(page); + await openCommandPalette(page); + + await searchInCommandPalette(page, 'dashboard'); + await expect(page.getByRole('option', { name: 'Go to Dashboard' })).toBeVisible(); + + await searchInCommandPalette(page, 'calendar'); + await expect(page.getByRole('option', { name: 'Go to Calendar' })).toBeVisible(); + }); + + test('search is case insensitive', async ({ page }) => { + await goToDashboard(page); + await openCommandPalette(page); + + await searchInCommandPalette(page, 'DASHBOARD'); + await expect(page.getByRole('option', { name: 'Go to Dashboard' })).toBeVisible(); + }); + + test('partial word search works', async ({ page }) => { + await goToDashboard(page); + await openCommandPalette(page); + + await searchInCommandPalette(page, 'proj'); + await expect(page.getByRole('option', { name: 'Go to Projects' })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Create Project' })).toBeVisible(); + }); + + test('keyboard navigation and selection works', async ({ page }) => { + await goToDashboard(page); + await openCommandPalette(page); + + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('Enter'); + + await expect(page.locator('[role="dialog"]')).not.toBeVisible(); + }); + }); + + test.describe('Theme Commands', () => { + test('switches to dark theme', async ({ page }) => { + await goToDashboard(page); + await openCommandPalette(page); + await searchInCommandPalette(page, 'Dark Theme'); + await selectCommand(page, 'Switch to Dark Theme'); + await expect(page.locator('html')).toHaveClass(/dark/); + }); + + test('switches to light theme', async ({ page }) => { + await goToDashboard(page); + await openCommandPalette(page); + await searchInCommandPalette(page, 'Light Theme'); + await selectCommand(page, 'Switch to Light Theme'); + await expect(page.locator('html')).toHaveClass(/light/); + }); + }); + + test.describe('Timer Commands', () => { + test('starts and stops timer', async ({ page }) => { + await goToDashboard(page); + + // Start timer + await openCommandPalette(page); + await searchInCommandPalette(page, 'Start Timer'); + await selectCommand(page, 'Start Timer'); + await assertTimerIsRunning(page); + + // Stop timer + await openCommandPalette(page); + await searchInCommandPalette(page, 'Stop Timer'); + await selectCommand(page, 'Stop Timer'); + await assertTimerIsStopped(page); + }); + + test('shows active timer commands when running', async ({ page }) => { + await goToDashboard(page); + + // Start timer + await openCommandPalette(page); + await searchInCommandPalette(page, 'Start Timer'); + await selectCommand(page, 'Start Timer'); + await assertTimerIsRunning(page); + + // Check active timer commands - search for them to ensure visibility + await openCommandPalette(page); + await searchInCommandPalette(page, 'Set Project'); + await expect(page.getByRole('option', { name: 'Set Project' })).toBeVisible(); + }); + }); + + test.describe('Create Commands', () => { + test('opens create time entry modal', async ({ page }) => { + await goToDashboard(page); + await openCommandPalette(page); + await searchInCommandPalette(page, 'Create Time Entry'); + await selectCommand(page, 'Create Time Entry'); + await expect( + page.locator('[role="dialog"]').getByText('Create manual time entry') + ).toBeVisible(); + }); + + test('opens create project modal', async ({ page }) => { + await goToDashboard(page); + await openCommandPalette(page); + await searchInCommandPalette(page, 'Create Project'); + await selectCommand(page, 'Create Project'); + await expect( + page.locator('[role="dialog"]').getByRole('heading', { name: 'Create Project' }) + ).toBeVisible(); + }); + + test('opens create client modal', async ({ page }) => { + await goToDashboard(page); + await openCommandPalette(page); + await searchInCommandPalette(page, 'Create Client'); + await selectCommand(page, 'Create Client'); + await expect( + page.locator('[role="dialog"]').getByRole('heading', { name: 'Create Client' }) + ).toBeVisible(); + }); + + test('opens create tag modal', async ({ page }) => { + await goToDashboard(page); + await openCommandPalette(page); + await searchInCommandPalette(page, 'Create Tag'); + await selectCommand(page, 'Create Tag'); + await expect(page.locator('[role="dialog"]').getByText('Create Tags')).toBeVisible(); + }); + + test('opens invite member modal', async ({ page }) => { + await goToDashboard(page); + await openCommandPalette(page); + await searchInCommandPalette(page, 'Invite Member'); + await selectCommand(page, 'Invite Member'); + // Modal has title with "Invite Member" text - use first() to get the title span + await expect( + page.locator('[role="dialog"]').getByText('Invite Member').first() + ).toBeVisible(); + }); + }); + + test.describe('Entity Search', () => { + test('searches for projects and navigates on selection', async ({ page }) => { + const projectName = 'CmdPalette' + Math.floor(Math.random() * 10000); + + // Create project first + await page.goto(PLAYWRIGHT_BASE_URL + '/projects'); + await page.getByRole('button', { name: 'Create Project' }).click(); + await page.getByPlaceholder('The next big thing').fill(projectName); + + await page.getByRole('button', { name: 'Create Project' }).click(); + // Wait for project to be created and page to update + await expect(page.getByText(projectName)).toBeVisible({ timeout: 10000 }); + + // Search from the projects page where the query cache now has the new project + await openCommandPalette(page); + await searchInCommandPalette(page, projectName); + + // Wait for entity search to return results + const projectOption = page.getByRole('option').filter({ hasText: projectName }); + await expect(projectOption).toBeVisible({ + timeout: 5000, + }); + + // Select the project from search results + await projectOption.click(); + }); + }); + + test.describe('Organization Switching', () => { + test('shows switch commands only when multiple organizations exist', async ({ page }) => { + await goToDashboard(page); + await openCommandPalette(page); + + // With only one org, no switch commands should appear + await searchInCommandPalette(page, 'Switch to'); + // Check that no organization switch commands appear (only theme switch commands) + const switchOptions = page.getByRole('option', { name: /^Switch to (?!.*Theme)/ }); + await expect(switchOptions).toHaveCount(0); + }); + + test('switches organization via command palette', async ({ page }) => { + const newOrgName = 'TestOrg' + Math.floor(Math.random() * 10000); + + // Create a new organization + await page.goto(PLAYWRIGHT_BASE_URL + '/teams/create'); + await page.getByLabel('Organization Name').fill(newOrgName); + await page.getByRole('button', { name: 'Create' }).click(); + + // Wait for navigation to new org's dashboard + await expect(page.getByTestId('dashboard_view')).toBeVisible({ timeout: 10000 }); + + // Use visible switcher (desktop sidebar has one, mobile header has another) + const orgSwitcher = page.locator('[data-testid="organization_switcher"]:visible'); + + // Verify we're in the new org by checking the switcher + await expect(orgSwitcher).toContainText(newOrgName); + + // Get the original org name from switcher dropdown + await orgSwitcher.click(); + await expect(page.getByText('Switch Organizations')).toBeVisible(); + + // Find the other organization button (has ArrowRightIcon, not CheckCircleIcon) + // The button contains an SVG and a div with the org name + const otherOrgItem = page.locator('form button').filter({ hasText: /.+/ }).first(); + await expect(otherOrgItem).toBeVisible(); + const originalOrgName = (await otherOrgItem.innerText()).trim(); + await page.keyboard.press('Escape'); // Close dropdown + + // Now use command palette to switch back to original org + await openCommandPalette(page); + await searchInCommandPalette(page, 'Switch to'); + + // Should see the switch command for the original org + const switchCommand = page.getByRole('option', { + name: new RegExp(`Switch to ${originalOrgName}`), + }); + await expect(switchCommand).toBeVisible(); + await switchCommand.click(); + + // Wait for organization switch to complete + await expect(orgSwitcher).toContainText(originalOrgName, { + timeout: 10000, + }); + }); + + test('organization switch commands appear in Organization group', async ({ page }) => { + const newOrgName = 'GroupTestOrg' + Math.floor(Math.random() * 10000); + + // Create a new organization to ensure we have multiple + await page.goto(PLAYWRIGHT_BASE_URL + '/teams/create'); + await page.getByLabel('Organization Name').fill(newOrgName); + await page.getByRole('button', { name: 'Create' }).click(); + await expect(page.getByTestId('dashboard_view')).toBeVisible({ timeout: 10000 }); + + // Open command palette and check for Organization group heading + await openCommandPalette(page); + + // The Organization group should be visible when there are switch commands + await expect(page.getByText('Organization', { exact: true })).toBeVisible(); + }); + }); +}); + +// ============================================= +// Employee Permission Tests +// ============================================= + +test.describe('Employee Command Palette Restrictions', () => { + test('employee command palette does not show restricted navigation commands', async ({ + employee, + }) => { + await employee.page.goto(PLAYWRIGHT_BASE_URL + '/dashboard'); + await expect(employee.page.getByTestId('dashboard_view')).toBeVisible({ + timeout: 10000, + }); + + // Open command palette + await employee.page.getByTestId('command_palette_button').click(); + await expect(employee.page.locator('[role="dialog"]')).toBeVisible({ timeout: 5000 }); + + // Available navigation commands + await expect(employee.page.getByRole('option', { name: 'Go to Dashboard' })).toBeVisible(); + await expect(employee.page.getByRole('option', { name: 'Go to Time' })).toBeVisible(); + await expect(employee.page.getByRole('option', { name: 'Go to Calendar' })).toBeVisible(); + + // Restricted commands should NOT be visible + await expect( + employee.page.getByRole('option', { name: 'Go to Members' }) + ).not.toBeVisible(); + await expect( + employee.page.getByRole('option', { name: 'Go to Settings' }) + ).not.toBeVisible(); + }); + + test('employee command palette does not show create commands for restricted entities', async ({ + employee, + }) => { + await employee.page.goto(PLAYWRIGHT_BASE_URL + '/dashboard'); + await expect(employee.page.getByTestId('dashboard_view')).toBeVisible({ + timeout: 10000, + }); + + // Open command palette + await employee.page.getByTestId('command_palette_button').click(); + await expect(employee.page.locator('[role="dialog"]')).toBeVisible({ timeout: 5000 }); + + // Search for "Create" to filter + await employee.page.locator('[role="dialog"] input').fill('Create'); + await employee.page.waitForTimeout(300); + + // Should NOT see create commands for restricted entities + await expect( + employee.page.getByRole('option', { name: 'Create Project' }) + ).not.toBeVisible(); + await expect( + employee.page.getByRole('option', { name: 'Create Client' }) + ).not.toBeVisible(); + await expect(employee.page.getByRole('option', { name: 'Create Tag' })).not.toBeVisible(); + await expect( + employee.page.getByRole('option', { name: 'Invite Member' }) + ).not.toBeVisible(); + + // Should still see Create Time Entry (employees can create time entries) + await expect( + employee.page.getByRole('option', { name: 'Create Time Entry' }) + ).toBeVisible(); + }); +}); diff --git a/e2e/dashboard.spec.ts b/e2e/dashboard.spec.ts new file mode 100644 index 00000000..2799a3f0 --- /dev/null +++ b/e2e/dashboard.spec.ts @@ -0,0 +1,198 @@ +import { expect, test } from '../playwright/fixtures'; +import { PLAYWRIGHT_BASE_URL } from '../playwright/config'; +import type { Page } from '@playwright/test'; +import { + assertThatTimerHasStarted, + assertThatTimerIsStopped, + newTimeEntryResponse, + startOrStopTimerWithButton, + stoppedTimeEntryResponse, +} from './utils/currentTimeEntry'; +import { + createBareTimeEntryViaApi, + createPublicProjectViaApi, + createTimeEntryViaApi, + updateOrganizationSettingViaApi, +} from './utils/api'; + +async function goToDashboard(page: Page) { + await page.goto(PLAYWRIGHT_BASE_URL + '/dashboard'); +} + +test('test that dashboard loads with all expected sections', async ({ page }) => { + await goToDashboard(page); + await expect(page.getByTestId('dashboard_view')).toBeVisible({ timeout: 10000 }); + + // Timer section (scoped to dashboard_timer to avoid matching sidebar timer) + await expect(page.getByTestId('time_entry_description')).toBeVisible(); + await expect( + page + .getByTestId('dashboard_timer') + .getByTestId('timer_button') + .and(page.locator(':visible')) + ).toBeVisible(); + + // Dashboard cards + await expect(page.getByText('Recent Time Entries', { exact: true })).toBeVisible(); + await expect(page.getByText('Last 7 Days', { exact: true })).toBeVisible(); + await expect(page.getByText('Activity Graph', { exact: true })).toBeVisible(); + await expect(page.getByText('Team Activity', { exact: true })).toBeVisible(); + + // Weekly overview section + await expect(page.getByText('This Week', { exact: true })).toBeVisible(); +}); + +test('test that dashboard shows time entry data after creating entries', async ({ page, ctx }) => { + await createBareTimeEntryViaApi(ctx, 'Dashboard test entry', '1h'); + + await goToDashboard(page); + await expect(page.getByTestId('dashboard_view')).toBeVisible(); + + // The "Last 7 Days" or "This Week" section should reflect tracked time + await expect(page.getByText('This Week', { exact: true })).toBeVisible(); +}); + +test('test that timer on dashboard can start and stop', async ({ page }) => { + await goToDashboard(page); + await Promise.all([newTimeEntryResponse(page), startOrStopTimerWithButton(page)]); + await assertThatTimerHasStarted(page); + + await page.waitForTimeout(1500); + + await Promise.all([stoppedTimeEntryResponse(page), startOrStopTimerWithButton(page)]); + await assertThatTimerIsStopped(page); +}); + +test('test that weekly overview section displays stat cards', async ({ page, ctx }) => { + await createBareTimeEntryViaApi(ctx, 'Stats test entry', '2h'); + + await goToDashboard(page); + + // Verify stat card labels are visible + await expect(page.getByText('Spent Time')).toBeVisible(); + await expect(page.getByText('Billable Time')).toBeVisible(); + await expect(page.getByText('Billable Amount')).toBeVisible(); +}); + +test('test that stopping timer refreshes dashboard data', async ({ page }) => { + await goToDashboard(page); + + // Start timer + await Promise.all([newTimeEntryResponse(page), startOrStopTimerWithButton(page)]); + await assertThatTimerHasStarted(page); + await page.waitForTimeout(1500); + + // Stop timer and verify dashboard queries are refetched + await Promise.all([ + stoppedTimeEntryResponse(page), + page.waitForResponse( + (response) => + response.url().includes('/charts/') && + response.request().method() === 'GET' && + response.status() === 200 + ), + startOrStopTimerWithButton(page), + ]); + await assertThatTimerIsStopped(page); +}); + +// ============================================= +// Employee Permission Tests +// ============================================= + +test.describe('Employee Dashboard Restrictions', () => { + test('employee dashboard loads and timer is functional', async ({ employee }) => { + await employee.page.goto(PLAYWRIGHT_BASE_URL + '/dashboard'); + await expect(employee.page.getByTestId('dashboard_view')).toBeVisible({ + timeout: 10000, + }); + + // Timer should be available + await expect( + employee.page + .getByTestId('dashboard_timer') + .getByTestId('timer_button') + .and(employee.page.locator(':visible')) + ).toBeVisible(); + await expect(employee.page.getByTestId('time_entry_description')).toBeEditable(); + }); + + test('employee cannot see Team Activity card', async ({ employee }) => { + await employee.page.goto(PLAYWRIGHT_BASE_URL + '/dashboard'); + await expect(employee.page.getByTestId('dashboard_view')).toBeVisible({ + timeout: 10000, + }); + + // Other dashboard cards should be visible + await expect(employee.page.getByText('Recent Time Entries', { exact: true })).toBeVisible(); + + // Team Activity should NOT be visible for employees + await expect(employee.page.getByText('Team Activity', { exact: true })).not.toBeVisible(); + }); + + test('employee cannot see Cost column in This Week table by default', async ({ + ctx, + employee, + }) => { + const project = await createPublicProjectViaApi(ctx, { + name: 'EmpDashBillProj', + is_billable: true, + billable_rate: 10000, + }); + await createTimeEntryViaApi( + { ...ctx, memberId: employee.memberId }, + { + description: 'Emp dashboard cost entry', + duration: '1h', + projectId: project.id, + billable: true, + } + ); + + await employee.page.goto(PLAYWRIGHT_BASE_URL + '/dashboard'); + await expect(employee.page.getByTestId('dashboard_view')).toBeVisible({ + timeout: 10000, + }); + + // This Week table should be visible + await expect(employee.page.getByText('This Week', { exact: true })).toBeVisible(); + + // Duration column should be visible, but Cost column should NOT + await expect(employee.page.getByText('Duration', { exact: true })).toBeVisible(); + await expect(employee.page.getByText('Cost', { exact: true })).not.toBeVisible(); + }); + + test('employee can see Cost column in This Week table when employees_can_see_billable_rates is enabled', async ({ + ctx, + employee, + }) => { + await updateOrganizationSettingViaApi(ctx, { employees_can_see_billable_rates: true }); + + const project = await createPublicProjectViaApi(ctx, { + name: 'EmpDashBillVisProj', + is_billable: true, + billable_rate: 10000, + }); + await createTimeEntryViaApi( + { ...ctx, memberId: employee.memberId }, + { + description: 'Emp dashboard cost visible entry', + duration: '1h', + projectId: project.id, + billable: true, + } + ); + + await employee.page.goto(PLAYWRIGHT_BASE_URL + '/dashboard'); + await expect(employee.page.getByTestId('dashboard_view')).toBeVisible({ + timeout: 10000, + }); + + // Both Duration and Cost columns should be visible + await expect(employee.page.getByText('Duration', { exact: true })).toBeVisible(); + await expect(employee.page.getByText('Cost', { exact: true })).toBeVisible(); + + // 1h at 100.00/h = 100.00 EUR cost should be visible + await expect(employee.page.getByText('100,00 EUR').first()).toBeVisible(); + }); +}); diff --git a/e2e/import-export.spec.ts b/e2e/import-export.spec.ts new file mode 100644 index 00000000..740274c8 --- /dev/null +++ b/e2e/import-export.spec.ts @@ -0,0 +1,154 @@ +import { expect, test } from '../playwright/fixtures'; +import { PLAYWRIGHT_BASE_URL } from '../playwright/config'; +import type { Page } from '@playwright/test'; +import path from 'path'; + +async function goToImportExport(page: Page) { + await page.goto(PLAYWRIGHT_BASE_URL + '/import'); +} + +test('test that import page loads with type dropdown and file upload', async ({ page }) => { + await goToImportExport(page); + await expect(page.getByTestId('import_view')).toBeVisible({ timeout: 10000 }); + + // Import section + await expect(page.getByRole('heading', { name: 'Import Data' })).toBeVisible(); + await expect(page.locator('#importType')).toBeVisible(); + + // Export section + await expect(page.getByRole('heading', { name: 'Export Data' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Export Organization Data' })).toBeVisible(); +}); + +test('test that selecting an import type shows instructions', async ({ page }) => { + await goToImportExport(page); + + // Select a Toggl import type + await page.getByLabel('Import Type').selectOption({ index: 1 }); + + // Instructions should appear + await expect(page.getByText('Instructions:')).toBeVisible(); +}); + +test('test that importing without selecting type shows error', async ({ page }) => { + await goToImportExport(page); + + // Click Import Data without selecting a type + await page.getByRole('button', { name: 'Import Data' }).click(); + + // Should show an error notification + await expect(page.getByText('Please select the import type')).toBeVisible(); +}); + +test('test that importing without selecting file shows error', async ({ page }) => { + await goToImportExport(page); + + // Select an import type first + await page.getByLabel('Import Type').selectOption({ index: 1 }); + + // Click Import Data without selecting a file + await page.getByRole('button', { name: 'Import Data' }).click(); + + // Should show an error notification + await expect( + page.getByText('Please select the CSV or ZIP file that you want to import') + ).toBeVisible(); +}); + +test('test that export button triggers export and shows success modal', async ({ page }) => { + await goToImportExport(page); + await expect(page.getByRole('button', { name: 'Export Organization Data' })).toBeVisible(); + + // Override window.open to prevent the page from navigating away to the + // download URL (the app uses window.open(url, '_self') which would navigate + // away before we can verify the success modal) + await page.evaluate(() => { + window.open = () => null; + }); + + // Click Export Organization Data and wait for the API response + await Promise.all([ + page.waitForResponse( + (response) => + response.url().includes('/export') && + response.request().method() === 'POST' && + response.status() === 200, + { timeout: 60000 } + ), + page.getByRole('button', { name: 'Export Organization Data' }).click(), + ]); + + // Success modal should appear after export completes + await expect(page.getByText('The export was successful!')).toBeVisible(); +}); + +test('test that import type dropdown has multiple options', async ({ page }) => { + await goToImportExport(page); + + // The dropdown should load with options from the API + await page.waitForResponse( + (response) => + response.url().includes('/importers') && + response.request().method() === 'GET' && + response.status() === 200 + ); + + // Verify the select has options besides the default placeholder + const options = page.getByLabel('Import Type').locator('option'); + const count = await options.count(); + // Should have at least the placeholder + some import types + expect(count).toBeGreaterThan(1); +}); + +test('test that importing a generic time entries CSV works', async ({ page }) => { + await goToImportExport(page); + await expect(page.getByTestId('import_view')).toBeVisible({ timeout: 10000 }); + + // Select "Generic Time Entries" import type + await page.getByLabel('Import Type').selectOption({ label: 'Generic Time Entries' }); + await expect(page.getByText('Instructions:')).toBeVisible(); + + // Upload the test CSV file + const csvPath = path.resolve('resources/testfiles/generic_time_entries_import_test_1.csv'); + await page.locator('#file-upload').setInputFiles(csvPath); + + // Click Import and wait for the API response + await Promise.all([ + page.waitForResponse( + (response) => + response.url().includes('/import') && + response.request().method() === 'POST' && + response.status() === 200, + { timeout: 30000 } + ), + page.getByRole('button', { name: 'Import Data' }).click(), + ]); + + // Verify success modal with import results + await expect(page.getByRole('heading', { name: 'Import Result' })).toBeVisible(); + await expect(page.getByText('The import was successful!')).toBeVisible(); + + // The CSV has 2 time entries, 1 client, 2 projects, 1 task + await expect(page.getByText('Time entries created:').locator('..')).toContainText('2'); + await expect(page.getByText('Projects created:').locator('..')).toContainText('2'); + await expect(page.getByText('Clients created:').locator('..')).toContainText('1'); + await expect(page.getByText('Tasks created:').locator('..')).toContainText('1'); +}); + +// ============================================= +// Employee Permission Tests +// ============================================= + +test.describe('Employee Import Restrictions', () => { + test('employee does not see Import / Export link in the sidebar', async ({ employee }) => { + await employee.page.goto(PLAYWRIGHT_BASE_URL + '/dashboard'); + await expect(employee.page.getByTestId('dashboard_view')).toBeVisible({ + timeout: 10000, + }); + + // The Import / Export link should NOT be visible in the sidebar for employees + await expect( + employee.page.getByRole('link', { name: 'Import / Export' }) + ).not.toBeVisible(); + }); +}); diff --git a/e2e/members.spec.ts b/e2e/members.spec.ts index 70be0e7c..6c426c4e 100644 --- a/e2e/members.spec.ts +++ b/e2e/members.spec.ts @@ -3,53 +3,69 @@ // TODO: Remove Invitation import { expect, test } from '../playwright/fixtures'; import { PLAYWRIGHT_BASE_URL } from '../playwright/config'; +import type { Page } from '@playwright/test'; +import { inviteAndAcceptMember } from './utils/members'; +import { + createPlaceholderMemberViaImportApi, + getMembersViaApi, + updateMemberBillableRateViaApi, + updateOrganizationSettingViaApi, +} from './utils/api'; +import { getTableRowNames } from './utils/table'; -async function goToMembersPage(page) { +// Tests that invite + accept members need more time +test.describe.configure({ timeout: 45000 }); + +async function goToMembersPage(page: Page) { await page.goto(PLAYWRIGHT_BASE_URL + '/members'); } -async function openInviteMemberModal(page) { +async function openInviteMemberModal(page: Page) { await Promise.all([ page.getByRole('button', { name: 'Invite Member' }).click(), expect(page.getByPlaceholder('Member Email')).toBeVisible(), ]); } -test('test that new manager can be invited', async ({ page }) => { +test('test that new manager can be invited and accepted', async ({ page, browser }) => { + const memberId = Math.round(Math.random() * 100000); + const memberEmail = `manager+${memberId}@invite.test`; + + await inviteAndAcceptMember(page, browser, 'Invited Mgr', memberEmail, 'Manager'); + + // Verify the member appears in the members table with the correct role await goToMembersPage(page); - await openInviteMemberModal(page); - const editorId = Math.round(Math.random() * 10000); - await page.getByLabel('Email').fill(`new+${editorId}@editor.test`); - await page.getByRole('button', { name: 'Manager' }).click(); - await Promise.all([ - page.getByRole('button', { name: 'Invite Member', exact: true }).click(), - expect(page.getByRole('main')).toContainText(`new+${editorId}@editor.test`), - ]); + const memberRow = page.getByRole('row').filter({ hasText: 'Invited Mgr' }); + await expect(memberRow).toBeVisible(); + await expect(memberRow.getByText('Manager', { exact: true })).toBeVisible(); }); -test('test that new employee can be invited', async ({ page }) => { +test('test that new employee can be invited and accepted', async ({ page, browser }) => { + const memberId = Math.round(Math.random() * 100000); + const memberEmail = `employee+${memberId}@invite.test`; + + await inviteAndAcceptMember(page, browser, 'Invited Emp', memberEmail, 'Employee'); + + // Verify the member appears in the members table with the correct role await goToMembersPage(page); - await openInviteMemberModal(page); - const editorId = Math.round(Math.random() * 10000); - await page.getByLabel('Email').fill(`new+${editorId}@editor.test`); - await page.getByRole('button', { name: 'Employee' }).click(); - await Promise.all([ - page.getByRole('button', { name: 'Invite Member', exact: true }).click(), - await expect(page.getByRole('main')).toContainText(`new+${editorId}@editor.test`), - ]); + const memberRow = page.getByRole('row').filter({ hasText: 'Invited Emp' }); + await expect(memberRow).toBeVisible(); + await expect(memberRow.getByText('Employee', { exact: true })).toBeVisible(); }); -test('test that new admin can be invited', async ({ page }) => { +test('test that new admin can be invited and accepted', async ({ page, browser }) => { + const memberId = Math.round(Math.random() * 100000); + const memberEmail = `admin+${memberId}@invite.test`; + + await inviteAndAcceptMember(page, browser, 'Invited Adm', memberEmail, 'Administrator'); + + // Verify the member appears in the members table with the correct role await goToMembersPage(page); - await openInviteMemberModal(page); - const adminId = Math.round(Math.random() * 10000); - await page.getByLabel('Email').fill(`new+${adminId}@admin.test`); - await page.getByRole('button', { name: 'Administrator' }).click(); - await Promise.all([ - page.getByRole('button', { name: 'Invite Member', exact: true }).click(), - expect(page.getByRole('main')).toContainText(`new+${adminId}@admin.test`), - ]); + const memberRow = page.getByRole('row').filter({ hasText: 'Invited Adm' }); + await expect(memberRow).toBeVisible(); + await expect(memberRow.getByText('Admin', { exact: true })).toBeVisible(); }); + test('test that error shows if no role is selected', async ({ page }) => { await goToMembersPage(page); await openInviteMemberModal(page); @@ -69,8 +85,8 @@ test('test that organization billable rate can be updated with all existing time const newBillableRate = Math.round(Math.random() * 10000); await page.getByRole('row').first().getByRole('button').click(); await page.getByRole('menuitem').getByText('Edit').click(); - await page.getByText('Organization Default Rate').click(); - await page.getByText('Custom Rate').click(); + await page.getByRole('combobox').last().click(); + await page.getByRole('option', { name: 'Custom Rate' }).click(); await page.getByPlaceholder('Billable Rate').fill(newBillableRate.toString()); await page.getByRole('button', { name: 'Update Member' }).click(); @@ -91,3 +107,683 @@ test('test that organization billable rate can be updated with all existing time ), ]); }); + +test('test that switching member billable rate from custom back to default rate works', async ({ + page, + ctx, +}) => { + // Set a known org billable rate + await updateOrganizationSettingViaApi(ctx, { billable_rate: 12000 }); + + // Create a placeholder member with a custom billable rate + await createPlaceholderMemberViaImportApi(ctx, 'CustomToDefault Member'); + const members = await getMembersViaApi(ctx); + const member = members.find((m) => m.name === 'CustomToDefault Member'); + expect(member).toBeDefined(); + await updateMemberBillableRateViaApi(ctx, member!.id, 25000); + + await goToMembersPage(page); + const memberRow = page.getByRole('row').filter({ hasText: 'CustomToDefault Member' }); + await expect(memberRow).toBeVisible(); + + // Open edit modal + await memberRow.getByRole('button').click(); + await page.getByRole('menuitem').getByText('Edit').click(); + await expect(page.getByRole('heading', { name: 'Update Member' })).toBeVisible(); + + // Verify it starts on Custom Rate + const billableCombobox = page.getByRole('dialog').getByRole('combobox').last(); + await expect(billableCombobox).toContainText('Custom Rate'); + + // Switch to Default Rate + await billableCombobox.click(); + await page.getByRole('option', { name: 'Default Rate' }).click(); + await expect(billableCombobox).toContainText('Default Rate'); + + // Verify the billable rate input is disabled + await expect(page.getByPlaceholder('Billable Rate')).toBeDisabled(); + + // Submit — billable_rate changes from 25000 to null, so confirmation dialog appears + await page.getByRole('button', { name: 'Update Member' }).click(); + await expect(page.getByRole('heading', { name: 'Update Member Billable Rate' })).toBeVisible(); + await expect(page.getByText('the default rate of the organization')).toBeVisible(); + + // Confirm the update + await Promise.all([ + page.getByRole('button', { name: 'Yes, update existing time' }).click(), + page.waitForRequest( + (request) => + request.url().includes('/members/') && + request.method() === 'PUT' && + request.postDataJSON().billable_rate === null + ), + ]); + + // Verify both dialogs are closed + await expect(page.getByRole('dialog')).not.toBeVisible(); +}); + +test('test that default rate shows disabled input with organization billable rate', async ({ + page, + ctx, +}) => { + // Set a known org billable rate (150.00) + await updateOrganizationSettingViaApi(ctx, { billable_rate: 15000 }); + + await goToMembersPage(page); + + // Open edit modal for the owner (who uses default rate by default) + await page.getByRole('row').first().getByRole('button').click(); + await page.getByRole('menuitem').getByText('Edit').click(); + await expect(page.getByRole('heading', { name: 'Update Member' })).toBeVisible(); + + // Verify it's on Default Rate + const billableCombobox = page.getByRole('dialog').getByRole('combobox').last(); + await expect(billableCombobox).toContainText('Default Rate'); + + // Verify the input is disabled and shows the org rate (formatted with currency) + const billableInput = page.getByPlaceholder('Billable Rate'); + await expect(billableInput).toBeDisabled(); + await expect(billableInput).toHaveAttribute('aria-valuenow', '150'); + + // Close the dialog + await page.getByRole('button', { name: 'Cancel' }).click(); + await expect(page.getByRole('dialog')).not.toBeVisible(); +}); + +test('test that cancelling the billable rate confirmation dialog does not update the member', async ({ + page, + ctx, +}) => { + // Create a placeholder member with a custom billable rate + await createPlaceholderMemberViaImportApi(ctx, 'CancelConfirm Member'); + const members = await getMembersViaApi(ctx); + const member = members.find((m) => m.name === 'CancelConfirm Member'); + expect(member).toBeDefined(); + await updateMemberBillableRateViaApi(ctx, member!.id, 10000); + + await goToMembersPage(page); + const memberRow = page.getByRole('row').filter({ hasText: 'CancelConfirm Member' }); + await expect(memberRow).toBeVisible(); + + // Open edit modal + await memberRow.getByRole('button').click(); + await page.getByRole('menuitem').getByText('Edit').click(); + await expect(page.getByRole('heading', { name: 'Update Member' })).toBeVisible(); + + // Change the billable rate + await page.getByPlaceholder('Billable Rate').fill('200'); + + // Click Update Member — confirmation dialog should appear + await page.getByRole('button', { name: 'Update Member' }).click(); + await expect(page.getByRole('heading', { name: 'Update Member Billable Rate' })).toBeVisible(); + + // Set up listener to verify no PUT request is sent after cancel + let putRequestSent = false; + page.on('request', (request) => { + if (request.url().includes('/members/') && request.method() === 'PUT') { + putRequestSent = true; + } + }); + + // Click Cancel on the confirmation dialog + await page.getByRole('button', { name: 'Cancel' }).click(); + + // Verify confirmation dialog is closed + await expect( + page.getByRole('heading', { name: 'Update Member Billable Rate' }) + ).not.toBeVisible(); + + // Verify no API call was made + expect(putRequestSent).toBe(false); +}); + +test('test that changing role of placeholder member is rejected', async ({ page, ctx }) => { + const placeholderName = 'RoleChange ' + Math.floor(Math.random() * 10000); + + // Create a placeholder member via import + await createPlaceholderMemberViaImportApi(ctx, placeholderName); + + // Go to members page and verify placeholder exists with role "Placeholder" + await goToMembersPage(page); + const memberRow = page.getByRole('row').filter({ hasText: placeholderName }); + await expect(memberRow).toBeVisible(); + await expect(memberRow.getByText('Placeholder', { exact: true })).toBeVisible(); + + // Open the edit modal for the placeholder member + await memberRow.getByRole('button').click(); + await page.getByRole('menuitem').getByText('Edit').click(); + await expect(page.getByRole('dialog')).toBeVisible(); + await expect(page.getByRole('heading', { name: 'Update Member' })).toBeVisible(); + + // Change role to Employee + const roleSelect = page.getByRole('dialog').getByRole('combobox').first(); + await roleSelect.click(); + await expect(page.getByRole('option', { name: 'Employee' })).toBeVisible(); + await page.getByRole('option', { name: 'Employee' }).click(); + await expect(roleSelect).toContainText('Employee'); + + // Submit the change - the API should reject it with 400 + await Promise.all([ + page.getByRole('button', { name: 'Update Member' }).click(), + page.waitForResponse( + (response) => + response.url().includes('/members/') && + response.request().method() === 'PUT' && + response.status() === 400 + ), + ]); + + // Verify error notification is shown + await expect(page.getByText('Failed to update member')).toBeVisible(); +}); + +test('test that changing member role updates the role in the member table', async ({ + page, + browser, +}) => { + const memberId = Math.floor(Math.random() * 100000); + const memberEmail = `member+${memberId}@rolechange.test`; + + // Invite and accept a new Employee member + await inviteAndAcceptMember(page, browser, 'Jane Smith', memberEmail, 'Employee'); + + // Verify the new member appears with the Employee role + await goToMembersPage(page); + const memberRow = page.getByRole('row').filter({ hasText: 'Jane Smith' }); + await expect(memberRow).toBeVisible(); + await expect(memberRow.getByText('Employee', { exact: true })).toBeVisible(); + + // Open the edit modal + await memberRow.getByRole('button').click(); + await page.getByRole('menuitem').getByText('Edit').click(); + await expect(page.getByRole('dialog')).toBeVisible(); + await expect(page.getByRole('heading', { name: 'Update Member' })).toBeVisible(); + + // Change role to Manager + const roleSelect = page.getByRole('dialog').getByRole('combobox').first(); + await roleSelect.click(); + await expect(page.getByRole('option', { name: 'Manager' })).toBeVisible(); + await page.getByRole('option', { name: 'Manager' }).click(); + await expect(roleSelect).toContainText('Manager'); + + // Submit the change and verify the API call succeeds + await Promise.all([ + page.getByRole('button', { name: 'Update Member' }).click(), + page.waitForResponse( + (response) => + response.url().includes('/members/') && + response.request().method() === 'PUT' && + response.status() === 200 + ), + ]); + + // Verify dialog closed + await expect(page.getByRole('dialog')).not.toBeVisible(); + + // Verify the role updated in the table + await expect(memberRow.getByText('Manager', { exact: true })).toBeVisible(); +}); + +test('test that merging a placeholder member works', async ({ page, ctx }) => { + const placeholderName = 'Merge Target ' + Math.floor(Math.random() * 10000); + + // Create a placeholder member via import + await createPlaceholderMemberViaImportApi(ctx, placeholderName); + + // Go to members page + await goToMembersPage(page); + await expect(page.getByText(placeholderName)).toBeVisible(); + + // Find the placeholder member row and open actions menu + const placeholderRow = page.getByRole('row').filter({ hasText: placeholderName }); + await placeholderRow.getByRole('button').click(); + + // Click Merge + await page.getByTestId('member_merge').click(); + await expect(page.getByRole('dialog')).toBeVisible(); + await expect(page.getByRole('heading', { name: 'Merge Member' })).toBeVisible(); + + // Select the current user (the owner) as merge target via MemberCombobox + // The MemberCombobox renders a Button as trigger; clicking it opens the popover with the combobox input + await page.getByRole('dialog').getByRole('button', { name: 'Select a member...' }).click(); + + // Wait for dropdown options to load + const firstOption = page.getByRole('option').first(); + await expect(firstOption).toBeVisible({ timeout: 10000 }); + await firstOption.click(); + + // Submit merge + await Promise.all([ + page.getByRole('button', { name: 'Merge Member' }).click(), + page.waitForResponse( + (response) => + response.url().includes('/member/') && + response.url().includes('/merge-into') && + response.ok() + ), + ]); + + // Wait for merge dialog to close after successful merge + await expect(page.getByRole('dialog').filter({ hasText: 'Merge Member' })).not.toBeVisible(); + + // Verify placeholder member is no longer in the members table + await expect(page.getByRole('main').getByText(placeholderName)).not.toBeVisible(); +}); + +test('test that deleting a placeholder member works', async ({ page, ctx }) => { + const placeholderName = 'Delete Target ' + Math.floor(Math.random() * 10000); + + // Create a placeholder member via import + await createPlaceholderMemberViaImportApi(ctx, placeholderName); + + // Go to members page + await goToMembersPage(page); + const memberRow = page.getByRole('row').filter({ hasText: placeholderName }); + await expect(memberRow).toBeVisible(); + + // Open actions menu and click Delete + await memberRow.getByRole('button').click(); + await page.getByRole('menuitem').getByText('Delete').click(); + + // Verify delete modal is shown + await expect(page.getByRole('dialog')).toBeVisible(); + await expect(page.getByRole('heading', { name: 'Delete Member' })).toBeVisible(); + + // Try to delete without checking the confirmation checkbox + await page.getByRole('button', { name: 'Delete Member' }).click(); + + // Should show validation error + await expect( + page.getByText('You must confirm that you understand the consequences of this action') + ).toBeVisible(); + + // Check the confirmation checkbox + await page.getByRole('checkbox').click(); + + // Click Delete Member button and wait for API response + await Promise.all([ + page.getByRole('button', { name: 'Delete Member' }).click(), + page.waitForResponse( + (response) => + response.url().includes('/members/') && + response.request().method() === 'DELETE' && + response.ok() + ), + ]); + + // Verify modal is closed + await expect(page.getByRole('dialog')).not.toBeVisible(); + + // Verify member is removed from the table + await expect(page.getByRole('main').getByText(placeholderName)).not.toBeVisible(); +}); + +test('test that member delete modal can be cancelled', async ({ page, ctx }) => { + const placeholderName = 'Delete Cancel ' + Math.floor(Math.random() * 10000); + + // Create a placeholder member via import + await createPlaceholderMemberViaImportApi(ctx, placeholderName); + + // Go to members page + await goToMembersPage(page); + const memberRow = page.getByRole('row').filter({ hasText: placeholderName }); + await expect(memberRow).toBeVisible(); + + // Open actions menu and click Delete + await memberRow.getByRole('button').click(); + await page.getByRole('menuitem').getByText('Delete').click(); + + // Verify delete modal is shown + await expect(page.getByRole('dialog')).toBeVisible(); + + // Set up listener to verify no DELETE request is sent + let deleteRequestSent = false; + page.on('request', (request) => { + if (request.url().includes('/members/') && request.method() === 'DELETE') { + deleteRequestSent = true; + } + }); + + // Click Cancel + await page.getByRole('button', { name: 'Cancel' }).click(); + + // Verify modal is closed + await expect(page.getByRole('dialog')).not.toBeVisible(); + + // Verify member is still in the table + await expect(memberRow).toBeVisible(); + + // Verify no DELETE request was sent + expect(deleteRequestSent).toBe(false); +}); + +test('test that organization owner cannot be deleted', async ({ page }) => { + await goToMembersPage(page); + + // Find the owner row (John Doe with Owner role) + const ownerRow = page.getByRole('row').filter({ hasText: 'Owner' }); + await expect(ownerRow).toBeVisible(); + + // Open the actions menu for the owner + await ownerRow.getByRole('button').click(); + + // Click Delete + await page.getByRole('menuitem').getByText('Delete').click(); + + // Verify delete modal is shown + await expect(page.getByRole('dialog')).toBeVisible(); + + // Check the confirmation checkbox + await page.getByRole('checkbox').click(); + + // Try to delete - should fail with 400 error + const responsePromise = page.waitForResponse( + (response) => + response.url().includes('/members/') && response.request().method() === 'DELETE' + ); + await page.getByRole('button', { name: 'Delete Member' }).click(); + const response = await responsePromise; + + // Verify the API returned an error status + expect(response.status()).toBe(400); + + // Close the modal by pressing Escape + await page.keyboard.press('Escape'); + + // Refresh and verify the owner is still there + await goToMembersPage(page); + await expect(page.getByRole('row').filter({ hasText: 'Owner' })).toBeVisible(); +}); + +// ============================================= +// Invitations Tab Tests +// ============================================= + +test('test that invitation shows in invitations tab and can be revoked', async ({ page }) => { + const inviteEmail = `invite+${Math.floor(Math.random() * 100000)}@pending.test`; + + await goToMembersPage(page); + await openInviteMemberModal(page); + + await page.getByPlaceholder('Member Email').fill(inviteEmail); + await page.getByRole('button', { name: 'Employee' }).click(); + await Promise.all([ + page.waitForResponse( + (response) => + response.url().includes('/invitations') && + response.request().method() === 'POST' && + response.status() === 204 + ), + page.getByRole('button', { name: 'Invite Member', exact: true }).click(), + ]); + + // Wait for modal to close + await expect(page.getByPlaceholder('Member Email')).not.toBeVisible(); + + // Switch to Invitations tab and verify the invitation is visible + await page.getByText('Invitations', { exact: true }).click(); + await expect(page.getByText(inviteEmail)).toBeVisible(); + + // Find and click the actions menu for this invitation + const invitationRow = page.locator('tr, [role="row"]').filter({ hasText: inviteEmail }); + await invitationRow.getByRole('button').click(); + await Promise.all([ + page.waitForResponse( + (response) => + response.url().includes('/invitations/') && + response.request().method() === 'DELETE' && + response.status() === 204 + ), + page.getByRole('menuitem').getByText('Delete').click(), + ]); + + // Verify invitation is removed + await expect(page.getByText(inviteEmail)).not.toBeVisible(); +}); + +test('test that invitation can be resent', async ({ page }) => { + const inviteEmail = `resend+${Math.floor(Math.random() * 100000)}@invite.test`; + + await goToMembersPage(page); + await openInviteMemberModal(page); + + await page.getByPlaceholder('Member Email').fill(inviteEmail); + await page.getByRole('button', { name: 'Employee' }).click(); + await Promise.all([ + page.waitForResponse( + (response) => + response.url().includes('/invitations') && + response.request().method() === 'POST' && + response.status() === 204 + ), + page.getByRole('button', { name: 'Invite Member', exact: true }).click(), + ]); + + // Wait for modal to close + await expect(page.getByPlaceholder('Member Email')).not.toBeVisible(); + + // Switch to Invitations tab + await page.getByText('Invitations', { exact: true }).click(); + await expect(page.getByText(inviteEmail)).toBeVisible(); + + // Find and click the actions menu, then resend + const invitationRow = page.locator('tr, [role="row"]').filter({ hasText: inviteEmail }); + await invitationRow.getByRole('button').click(); + // Wait for dropdown menu to appear + await expect(page.getByRole('menuitem').getByText('Resend Invitation')).toBeVisible(); + await Promise.all([ + page.waitForResponse( + (response) => + response.url().includes('/resend') && response.request().method() === 'POST' + ), + page.getByRole('menuitem').getByText('Resend Invitation').click(), + ]); +}); + +test('test that admin user cannot transfer ownership', async ({ page, browser }) => { + const memberId = Math.floor(Math.random() * 100000); + const memberEmail = `admin+${memberId}@perms.test`; + + // Invite and accept an admin member + await inviteAndAcceptMember( + page, + browser, + 'Admin User ' + memberId, + memberEmail, + 'Administrator' + ); + + // Go to members page and verify the admin exists + await goToMembersPage(page); + const adminRow = page.getByRole('row').filter({ hasText: 'Admin User' }); + await expect(adminRow).toBeVisible(); + + // The owner should still be the owner + const ownerRow = page.getByRole('row').filter({ hasText: 'Owner' }); + await expect(ownerRow).toBeVisible(); + + // Open actions menu for the admin - should NOT have "Transfer Ownership" option + await adminRow.getByRole('button').click(); + await expect(page.getByRole('menuitem').getByText('Edit')).toBeVisible(); +}); + +test('test that accepted invitation disappears from invitations tab', async ({ page, browser }) => { + const memberId = Math.round(Math.random() * 100000); + const memberEmail = `accepted+${memberId}@invite.test`; + + // Invite and accept the member + await inviteAndAcceptMember(page, browser, 'Accepted Member', memberEmail, 'Employee'); + + // Go to members page and switch to Invitations tab + await goToMembersPage(page); + await page.getByRole('tab', { name: 'Invitations' }).click(); + + // The accepted invitation should not be visible + await expect(page.getByText(memberEmail)).not.toBeVisible(); +}); + +// ============================================= +// Sorting Tests +// ============================================= + +// Helper to clear localStorage before tests that check sorting +async function clearMemberTableState(page: Page) { + await page.evaluate(() => { + localStorage.removeItem('member-table-state'); + }); +} + +test('test that sorting members by name, role, and status works', async ({ page, ctx }) => { + // Create two placeholder members with names that sort predictably around "John Doe" + await createPlaceholderMemberViaImportApi(ctx, 'AAA SortFirst'); + await createPlaceholderMemberViaImportApi(ctx, 'ZZZ SortLast'); + + await goToMembersPage(page); + await clearMemberTableState(page); + await page.reload(); + + const table = page.getByTestId('member_table'); + await expect(table).toBeVisible(); + + // -- Name sorting (default is already name asc after clearing state) -- + const nameHeader = table.getByText('Name').first(); + let names = await getTableRowNames(table); + expect(names.indexOf('AAA SortFirst')).toBeLessThan(names.indexOf('ZZZ SortLast')); + + await nameHeader.click(); // toggle to desc + names = await getTableRowNames(table); + expect(names.indexOf('ZZZ SortLast')).toBeLessThan(names.indexOf('AAA SortFirst')); + + // -- Role sorting -- + const roleHeader = table.getByText('Role').first(); + await roleHeader.click(); // asc: Owner(0) < Placeholder(4) + names = await getTableRowNames(table); + const ownerIdx = names.indexOf('John Doe'); + const placeholderIdx = names.indexOf('AAA SortFirst'); + expect(ownerIdx).toBeLessThan(placeholderIdx); + + await roleHeader.click(); // desc: Placeholder first + names = await getTableRowNames(table); + expect(names.indexOf('AAA SortFirst')).toBeLessThan(names.indexOf('John Doe')); + + // -- Status sorting -- + const statusHeader = table.getByText('Status').first(); + await statusHeader.click(); // asc: Active(0) < Inactive(1) + names = await getTableRowNames(table); + expect(names.indexOf('John Doe')).toBeLessThan(names.indexOf('AAA SortFirst')); + + await statusHeader.click(); // desc: Inactive first + names = await getTableRowNames(table); + expect(names.indexOf('AAA SortFirst')).toBeLessThan(names.indexOf('John Doe')); + + // -- Email: just verify sort indicator appears -- + const emailHeader = table.getByText('Email').first(); + await emailHeader.click(); + await expect(emailHeader.locator('svg')).toBeVisible(); +}); + +test('test that member sort state persists after page reload', async ({ page }) => { + await goToMembersPage(page); + await clearMemberTableState(page); + await page.reload(); + + const table = page.getByTestId('member_table'); + await expect(table).toBeVisible(); + + // Click Role header twice to set descending sort + const roleHeader = table.getByText('Role').first(); + await roleHeader.click(); + await expect(roleHeader.locator('svg')).toBeVisible(); + await roleHeader.click(); + await expect(roleHeader.locator('svg')).toBeVisible(); + + // Reload the page + await page.reload(); + + // Verify the sort indicator is still visible on Role column + await expect(page.getByTestId('member_table')).toBeVisible(); + await expect( + page.getByTestId('member_table').getByText('Role').first().locator('svg') + ).toBeVisible(); +}); + +test('test that sorting members by billable rate works', async ({ page, ctx }) => { + // Create two placeholder members and set different billable rates + await createPlaceholderMemberViaImportApi(ctx, 'HighRate Member'); + await createPlaceholderMemberViaImportApi(ctx, 'LowRate Member'); + + const members = await getMembersViaApi(ctx); + const highRateMember = members.find((m) => m.name === 'HighRate Member'); + const lowRateMember = members.find((m) => m.name === 'LowRate Member'); + expect(highRateMember).toBeDefined(); + expect(lowRateMember).toBeDefined(); + + await updateMemberBillableRateViaApi(ctx, highRateMember!.id, 20000); + await updateMemberBillableRateViaApi(ctx, lowRateMember!.id, 5000); + + await goToMembersPage(page); + await clearMemberTableState(page); + await page.reload(); + + const table = page.getByTestId('member_table'); + await expect(table).toBeVisible(); + + // First click = desc (highest first), null rates last + const billableHeader = table.getByText('Billable Rate').first(); + await billableHeader.click(); + await expect(billableHeader.locator('svg')).toBeVisible(); + let names = await getTableRowNames(table); + expect(names.indexOf('HighRate Member')).toBeLessThan(names.indexOf('LowRate Member')); + + // Second click = asc (lowest first), null rates still last + await billableHeader.click(); + names = await getTableRowNames(table); + expect(names.indexOf('LowRate Member')).toBeLessThan(names.indexOf('HighRate Member')); +}); + +// ============================================= +// Employee Permission Tests +// ============================================= + +test.describe('Employee Sidebar Navigation', () => { + test('employee sidebar shows correct navigation links', async ({ employee }) => { + await employee.page.goto(PLAYWRIGHT_BASE_URL + '/dashboard'); + await expect(employee.page.getByTestId('dashboard_view')).toBeVisible({ + timeout: 10000, + }); + + // Visible links + await expect(employee.page.getByRole('link', { name: 'Dashboard' })).toBeVisible(); + await expect(employee.page.getByRole('link', { name: 'Time' })).toBeVisible(); + await expect(employee.page.getByRole('link', { name: 'Calendar' })).toBeVisible(); + await expect(employee.page.getByRole('link', { name: 'Projects' })).toBeVisible(); + await expect(employee.page.getByRole('link', { name: 'Clients' })).toBeVisible(); + await expect(employee.page.getByRole('link', { name: 'Tags' })).toBeVisible(); + + // Hidden links + await expect(employee.page.getByRole('link', { name: 'Members' })).not.toBeVisible(); + await expect( + employee.page.getByRole('link', { name: 'Settings', exact: true }) + ).not.toBeVisible(); + }); + + test('employee cannot see members list or invite members', async ({ employee }) => { + await employee.page.goto(PLAYWRIGHT_BASE_URL + '/members'); + + // Page loads but the members API returns 403 (no members:view permission) + await expect(employee.page.getByRole('heading', { name: 'Members' })).toBeVisible({ + timeout: 10000, + }); + + // Member table is empty — no rows rendered (only headers) + await expect(employee.page.getByTestId('member_table').locator('[role="row"]')).toHaveCount( + 0 + ); + + // Employee should NOT see the Invite Member button + await expect( + employee.page.getByRole('button', { name: 'Invite member' }) + ).not.toBeVisible(); + }); +}); diff --git a/e2e/organization.spec.ts b/e2e/organization.spec.ts index 78541c25..6df3ba34 100644 --- a/e2e/organization.spec.ts +++ b/e2e/organization.spec.ts @@ -223,9 +223,211 @@ test('test that format settings are reflected in the dashboard', async ({ page } // check that the current date is displayed in the dd/mm/yyyy format on the time page await page.goto(PLAYWRIGHT_BASE_URL + '/time'); + // Wait for time entries to load so organization data is available for date formatting + await page.waitForResponse( + (response) => response.url().includes('/time-entries') && response.status() === 200 + ); await expect( page.getByText(new Date().toLocaleDateString('en-GB'), { exact: true }).nth(0) - ).toBeVisible(); + ).toBeVisible({ timeout: 10000 }); }); -// TODO: Test 12-hour clock format +test('test that organization time entry settings can be toggled', async ({ page }) => { + await goToOrganizationSettings(page); + + const preventOverlappingCheckbox = page.getByLabel( + 'Prevent overlapping time entries (new entries only)' + ); + const manageTasksCheckbox = page.getByLabel('Allow Employees to manage tasks'); + + // Get current states and toggle both + const wasOverlappingChecked = await preventOverlappingCheckbox.isChecked(); + const wasManageTasksChecked = await manageTasksCheckbox.isChecked(); + + if (wasOverlappingChecked) { + await preventOverlappingCheckbox.uncheck(); + } else { + await preventOverlappingCheckbox.check(); + } + + if (wasManageTasksChecked) { + await manageTasksCheckbox.uncheck(); + } else { + await manageTasksCheckbox.check(); + } + + // Save + const settingsForm = page.locator('form').filter({ hasText: 'Prevent overlapping' }); + await Promise.all([ + settingsForm.getByRole('button', { name: 'Save' }).click(), + page.waitForResponse( + async (response) => + response.url().includes('/organizations/') && + response.request().method() === 'PUT' && + response.status() === 200 && + (await response.json()).data.prevent_overlapping_time_entries === + !wasOverlappingChecked + ), + ]); + + // Reload and verify both settings persisted + await page.reload(); + await expect(preventOverlappingCheckbox).toBeChecked({ checked: !wasOverlappingChecked }); + await expect(manageTasksCheckbox).toBeChecked({ checked: !wasManageTasksChecked }); + + // Toggle both back to restore original state + if (!wasOverlappingChecked) { + await preventOverlappingCheckbox.uncheck(); + } else { + await preventOverlappingCheckbox.check(); + } + + if (!wasManageTasksChecked) { + await manageTasksCheckbox.uncheck(); + } else { + await manageTasksCheckbox.check(); + } + + await Promise.all([ + settingsForm.getByRole('button', { name: 'Save' }).click(), + page.waitForResponse( + async (response) => + response.url().includes('/organizations/') && + response.request().method() === 'PUT' && + response.status() === 200 && + (await response.json()).data.prevent_overlapping_time_entries === + wasOverlappingChecked + ), + ]); +}); + +test('test that 12-hour clock format can be set', async ({ page }) => { + await goToOrganizationSettings(page); + + await page.getByLabel('Time Format').click(); + await page.getByRole('option', { name: '12-hour clock' }).click(); + await Promise.all([ + page + .locator('form') + .filter({ hasText: 'Time Format' }) + .getByRole('button', { name: 'Save' }) + .click(), + page.waitForResponse( + async (response) => + response.url().includes('/organizations/') && + response.request().method() === 'PUT' && + response.status() === 200 && + (await response.json()).data.time_format === '12-hours' + ), + ]); + + // Reload and verify it persisted + await page.reload(); + await expect(page.getByLabel('Time Format')).toContainText('12-hour clock'); + + // Reset back to 24-hour + await page.getByLabel('Time Format').click(); + await page.getByRole('option', { name: '24-hour clock' }).click(); + await Promise.all([ + page + .locator('form') + .filter({ hasText: 'Time Format' }) + .getByRole('button', { name: 'Save' }) + .click(), + page.waitForResponse( + async (response) => + response.url().includes('/organizations/') && + response.request().method() === 'PUT' && + response.status() === 200 && + (await response.json()).data.time_format === '24-hours' + ), + ]); +}); + +test('test that format settings persist after page reload', async ({ page }) => { + await goToOrganizationSettings(page); + + // Set a specific date format + await page.getByLabel('Date Format').click(); + await page.getByRole('option', { name: 'DD/MM/YYYY' }).click(); + await Promise.all([ + page + .locator('form') + .filter({ hasText: 'Date Format' }) + .getByRole('button', { name: 'Save' }) + .click(), + page.waitForResponse( + async (response) => + response.url().includes('/organizations/') && + response.request().method() === 'PUT' && + response.status() === 200 + ), + ]); + + // Reload and verify it persisted + await page.reload(); + await expect(page.getByLabel('Date Format')).toContainText('DD/MM/YYYY'); +}); + +// ============================================= +// Admin Permission Tests +// ============================================= + +test.describe('Admin Organization Settings Access', () => { + test('admin can see and edit organization settings', async ({ ctx, admin }) => { + await admin.page.goto(PLAYWRIGHT_BASE_URL + '/teams/' + ctx.orgId); + + // Organization Name section is visible + await expect( + admin.page.getByRole('heading', { name: 'Organization Name', level: 3 }) + ).toBeVisible({ timeout: 10000 }); + + // Editable settings sections should be visible + await expect( + admin.page.getByRole('heading', { name: 'Billable Rate', level: 3 }) + ).toBeVisible(); + await expect( + admin.page.getByRole('heading', { name: 'Format Settings', level: 3 }) + ).toBeVisible(); + await expect( + admin.page.getByRole('heading', { name: 'Organization Settings', level: 3 }) + ).toBeVisible(); + + // Save buttons should be visible (admin can update) + await expect(admin.page.getByRole('button', { name: 'Save' }).first()).toBeVisible(); + + // Delete organization should NOT be visible (owner only) + await expect( + admin.page.getByRole('heading', { name: 'Delete Organization' }) + ).not.toBeVisible(); + }); +}); + +// ============================================= +// Employee Permission Tests +// ============================================= + +test.describe('Employee Organization Settings Restrictions', () => { + test('employee can see org name but not editable settings', async ({ ctx, employee }) => { + await employee.page.goto(PLAYWRIGHT_BASE_URL + '/teams/' + ctx.orgId); + + // Organization Name section is visible (but inputs are disabled) + await expect( + employee.page.getByRole('heading', { name: 'Organization Name', level: 3 }) + ).toBeVisible({ timeout: 10000 }); + + // Editable settings sections should NOT be visible + await expect( + employee.page.getByRole('heading', { name: 'Billable Rate', level: 3 }) + ).not.toBeVisible(); + await expect( + employee.page.getByRole('heading', { name: 'Format Settings', level: 3 }) + ).not.toBeVisible(); + await expect( + employee.page.getByRole('heading', { name: 'Organization Settings', level: 3 }) + ).not.toBeVisible(); + + // Save button should not be visible (employee cannot update) + await expect(employee.page.getByRole('button', { name: 'Save' })).not.toBeVisible(); + }); +}); diff --git a/e2e/profile.spec.ts b/e2e/profile.spec.ts index a2f34388..93d9248c 100644 --- a/e2e/profile.spec.ts +++ b/e2e/profile.spec.ts @@ -1,5 +1,10 @@ import { test, expect } from '../playwright/fixtures'; -import { PLAYWRIGHT_BASE_URL } from '../playwright/config'; +import { PLAYWRIGHT_BASE_URL, TEST_USER_PASSWORD } from '../playwright/config'; +import type { Page } from '@playwright/test'; + +async function goToProfilePage(page: Page) { + await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile'); +} test('test that user name can be updated', async ({ page }) => { await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile'); @@ -39,6 +44,28 @@ test('test that user can create an API key', async ({ page }) => { await createNewApiToken(page); }); +test('test that creating an API key with empty name shows validation error', async ({ page }) => { + await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile'); + + // Wait for the API Key Name input to be visible before interacting + const nameInput = page.getByLabel('API Key Name'); + await expect(nameInput).toBeVisible(); + + // Ensure the API Key Name input is empty + await nameInput.fill(''); + + // Click the create button and wait for the 422 response + const [response] = await Promise.all([ + page.waitForResponse('**/users/me/api-tokens'), + page.getByRole('button', { name: 'Create API Key' }).click(), + ]); + + expect(response.status()).toBe(422); + + // Verify that an error notification is shown with validation message about the name field + await expect(page.getByText('name field is required')).toBeVisible({ timeout: 5000 }); +}); + test('test that user can delete an API key', async ({ page }) => { await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile'); await createNewApiToken(page); @@ -68,3 +95,254 @@ test('test that user can revoke an API key', async ({ page }) => { await expect(page.locator('body')).toContainText('NEW API KEY'); await expect(page.locator('body')).toContainText('Revoked'); }); + +// ============================================= +// Update Password Form Tests +// ============================================= + +test('test that password mismatch shows error', async ({ page }) => { + await goToProfilePage(page); + + // Fill in with mismatched passwords + await page.getByLabel('Current Password').fill(TEST_USER_PASSWORD); + await page.getByLabel('New Password').fill('newSecurePassword456'); + await page.getByLabel('Confirm Password').fill('differentPassword789'); + + // Find the form containing the Confirm Password field and click its Save button + const passwordForm = page.getByLabel('Confirm Password').locator('xpath=ancestor::form'); + await Promise.all([ + page.waitForResponse( + (response) => + response.url().includes('/user/password') && response.request().method() === 'PUT' + ), + passwordForm.getByRole('button', { name: 'Save' }).click(), + ]); + + // Verify error message about password confirmation + await expect(page.getByText('confirmation does not match')).toBeVisible(); +}); + +test('test that short password shows validation error', async ({ page }) => { + await goToProfilePage(page); + + // Fill in with a too short password + await page.getByLabel('Current Password').fill(TEST_USER_PASSWORD); + await page.getByLabel('New Password').fill('short'); + await page.getByLabel('Confirm Password').fill('short'); + + // Find the form containing the Confirm Password field and click its Save button + const passwordForm = page.getByLabel('Confirm Password').locator('xpath=ancestor::form'); + await Promise.all([ + page.waitForResponse( + (response) => + response.url().includes('/user/password') && response.request().method() === 'PUT' + ), + passwordForm.getByRole('button', { name: 'Save' }).click(), + ]); + + // Verify error message about password length + await expect(page.getByText('must be at least')).toBeVisible(); +}); + +test('test that incorrect current password shows validation error', async ({ page }) => { + await goToProfilePage(page); + + // Fill in with wrong current password + await page.getByLabel('Current Password').fill('wrongCurrentPassword123'); + await page.getByLabel('New Password').fill('newSecurePassword456'); + await page.getByLabel('Confirm Password').fill('newSecurePassword456'); + + // Find the form containing the Confirm Password field and click its Save button + const passwordForm = page.getByLabel('Confirm Password').locator('xpath=ancestor::form'); + await Promise.all([ + page.waitForResponse( + (response) => + response.url().includes('/user/password') && response.request().method() === 'PUT' + ), + passwordForm.getByRole('button', { name: 'Save' }).click(), + ]); + + // Verify error message about incorrect password + await expect(page.getByText('does not match')).toBeVisible(); +}); + +test('test that password can be updated successfully', async ({ page }) => { + await goToProfilePage(page); + const newPassword = 'newSecurePassword456'; + + // Change password to new password + await page.getByLabel('Current Password').fill(TEST_USER_PASSWORD); + await page.getByLabel('New Password').fill(newPassword); + await page.getByLabel('Confirm Password').fill(newPassword); + + const passwordForm = page.getByLabel('Confirm Password').locator('xpath=ancestor::form'); + const responsePromise = page.waitForResponse( + (response) => + response.url().includes('/user/password') && response.request().method() === 'PUT' + ); + await passwordForm.getByRole('button', { name: 'Save' }).click(); + const response = await responsePromise; + + // Verify successful response (303 is Inertia redirect on success, means password was updated) + expect(response.status()).toBe(303); + + // Verify no error messages are displayed + await expect(page.getByText('does not match')).not.toBeVisible(); + await expect(page.getByText('must be at least')).not.toBeVisible(); +}); + +// ============================================= +// Theme Selection Tests +// ============================================= + +test('test that theme can be changed to dark and light', async ({ page }) => { + await goToProfilePage(page); + + // The theme select is a Reka UI combobox (button), not a native