From d464bbb0ece1a38e6243b8475a8240e7fdfd44ff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Jul 2025 06:34:17 +0000 Subject: [PATCH 01/25] Bump form-data from 4.0.0 to 4.0.4 in /resources/js/packages/api Bumps [form-data](https://github.com/form-data/form-data) from 4.0.0 to 4.0.4. - [Release notes](https://github.com/form-data/form-data/releases) - [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md) - [Commits](https://github.com/form-data/form-data/compare/v4.0.0...v4.0.4) --- updated-dependencies: - dependency-name: form-data dependency-version: 4.0.4 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- resources/js/packages/api/package-lock.json | 179 +++++++++++++++++++- 1 file changed, 174 insertions(+), 5 deletions(-) diff --git a/resources/js/packages/api/package-lock.json b/resources/js/packages/api/package-lock.json index 6788b024..81130191 100644 --- a/resources/js/packages/api/package-lock.json +++ b/resources/js/packages/api/package-lock.json @@ -1217,6 +1217,20 @@ "concat-map": "0.0.1" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1293,6 +1307,21 @@ "node": ">=0.4.0" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -1306,6 +1335,55 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", @@ -1383,14 +1461,16 @@ } }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "license": "MIT", "peer": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -1431,12 +1511,63 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "peer": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -1454,11 +1585,39 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "peer": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -1581,6 +1740,16 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", From e7fa414c06189ac30b455c1aa48cdf3f93dda5f9 Mon Sep 17 00:00:00 2001 From: Constantin Graf Date: Wed, 23 Jul 2025 10:50:10 +0200 Subject: [PATCH 02/25] Restrict rounding to premium users --- .../Api/V1/TimeEntryController.php | 32 +++++++----- .../Endpoint/Api/V1/TimeEntryEndpointTest.php | 49 +++++++++++++++++++ 2 files changed, 68 insertions(+), 13 deletions(-) diff --git a/app/Http/Controllers/Api/V1/TimeEntryController.php b/app/Http/Controllers/Api/V1/TimeEntryController.php index a2bb23ac..a36c9f71 100644 --- a/app/Http/Controllers/Api/V1/TimeEntryController.php +++ b/app/Http/Controllers/Api/V1/TimeEntryController.php @@ -86,7 +86,8 @@ public function index(Organization $organization, TimeEntryIndexRequest $request $this->checkPermission($organization, 'time-entries:view:all'); } - $timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $member); + $canAccessPremiumFeatures = $this->canAccessPremiumFeatures($organization); + $timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $member, $canAccessPremiumFeatures); $totalCount = $timeEntriesQuery->count(); @@ -140,13 +141,15 @@ public function index(Organization $organization, TimeEntryIndexRequest $request /** * @return Builder */ - private function getTimeEntriesQuery(Organization $organization, TimeEntryIndexRequest|TimeEntryIndexExportRequest $request, ?Member $member): Builder + private function getTimeEntriesQuery(Organization $organization, TimeEntryIndexRequest|TimeEntryIndexExportRequest $request, ?Member $member, bool $canAccessPremiumFeatures): Builder { $select = TimeEntry::SELECT_COLUMNS; - if ($request->getRoundingType() !== null && $request->getRoundingMinutes() !== null) { + $roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null; + $roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null; + if ($roundingType !== null && $roundingMinutes !== null) { $select = array_diff($select, ['start', 'end']); - $select[] = DB::raw(app(TimeEntryService::class)->getStartSelectRawForRounding($request->getRoundingType(), $request->getRoundingMinutes()).' as start'); - $select[] = DB::raw(app(TimeEntryService::class)->getEndSelectRawForRounding($request->getRoundingType(), $request->getRoundingMinutes()).' as end'); + $select[] = DB::raw(app(TimeEntryService::class)->getStartSelectRawForRounding($roundingType, $roundingMinutes).' as start'); + $select[] = DB::raw(app(TimeEntryService::class)->getEndSelectRawForRounding($roundingType, $roundingMinutes).' as end'); } $timeEntriesQuery = TimeEntry::query() ->whereBelongsTo($organization, 'organization') @@ -184,18 +187,19 @@ public function indexExport(Organization $organization, TimeEntryIndexExportRequ } else { $this->checkPermission($organization, 'time-entries:view:all'); } + $canAccessPremiumFeatures = $this->canAccessPremiumFeatures($organization); $debug = $request->getDebug(); $format = $request->getFormatValue(); - if ($format === ExportFormat::PDF && ! $this->canAccessPremiumFeatures($organization)) { + if ($format === ExportFormat::PDF && ! $canAccessPremiumFeatures) { throw new FeatureIsNotAvailableInFreePlanApiException; } $user = $this->user(); $timezone = $user->timezone; $showBillableRate = $this->member($organization)->role !== Role::Employee->value || $organization->employees_can_see_billable_rates; - $roundingType = $request->getRoundingType(); - $roundingMinutes = $request->getRoundingMinutes(); + $roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null; + $roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null; - $timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $member); + $timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $member, $canAccessPremiumFeatures); $timeEntriesQuery->with([ 'task', 'client', @@ -332,14 +336,15 @@ public function aggregate(Organization $organization, TimeEntryAggregateRequest } else { $this->checkPermission($organization, 'time-entries:view:all'); } + $canAccessPremiumFeatures = $this->canAccessPremiumFeatures($organization); $user = $this->user(); $showBillableRate = $this->member($organization)->role !== Role::Employee->value || $organization->employees_can_see_billable_rates; $group1Type = $request->getGroup(); $group2Type = $request->getSubGroup(); $timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $member); - $roundingType = $request->getRoundingType(); - $roundingMinutes = $request->getRoundingMinutes(); + $roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null; + $roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null; $aggregatedData = $timeEntryAggregationService->getAggregatedTimeEntries( $timeEntriesAggregateQuery, @@ -380,6 +385,7 @@ public function aggregateExport(Organization $organization, TimeEntryAggregateEx } else { $this->checkPermission($organization, 'time-entries:view:all'); } + $canAccessPremiumFeatures = $this->canAccessPremiumFeatures($organization); $format = $request->getFormatValue(); if ($format === ExportFormat::PDF && ! $this->canAccessPremiumFeatures($organization)) { throw new FeatureIsNotAvailableInFreePlanApiException; @@ -391,8 +397,8 @@ public function aggregateExport(Organization $organization, TimeEntryAggregateEx $group = $request->getGroup(); $subGroup = $request->getSubGroup(); $timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $member); - $roundingType = $request->getRoundingType(); - $roundingMinutes = $request->getRoundingMinutes(); + $roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null; + $roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null; $aggregatedData = $timeEntryAggregationService->getAggregatedTimeEntriesWithDescriptions( $timeEntriesAggregateQuery->clone(), diff --git a/tests/Unit/Endpoint/Api/V1/TimeEntryEndpointTest.php b/tests/Unit/Endpoint/Api/V1/TimeEntryEndpointTest.php index c1ba8a95..6e7e6cdc 100644 --- a/tests/Unit/Endpoint/Api/V1/TimeEntryEndpointTest.php +++ b/tests/Unit/Endpoint/Api/V1/TimeEntryEndpointTest.php @@ -409,6 +409,7 @@ public function test_index_endpoint_can_round_up(): void 'start' => Carbon::createFromFormat('Y-m-d H:i:s', '2020-01-01 00:00:07'), 'end' => null, ]); + $this->actAsOrganizationWithSubscription(); Passport::actingAs($data->user); // Act @@ -435,6 +436,52 @@ public function test_index_endpoint_can_round_up(): void ); } + public function test_index_endpoint_ignores_rounding_if_organization_has_no_premium_features(): void + { + // Arrange + $this->travelTo(Carbon::createFromFormat('Y-m-d H:i:s', '2020-01-01 00:15:04')); + $data = $this->createUserWithPermission([ + 'time-entries:view:own', + ]); + $timeEntry1 = TimeEntry::factory()->forOrganization($data->organization) + ->forMember($data->member) + ->create([ + 'start' => Carbon::createFromFormat('Y-m-d H:i:s', '2020-01-01 00:00:08'), + 'end' => Carbon::createFromFormat('Y-m-d H:i:s', '2020-01-01 00:00:01'), + ]); + $timeEntry2 = TimeEntry::factory()->forOrganization($data->organization) + ->forMember($data->member) + ->create([ + 'start' => Carbon::createFromFormat('Y-m-d H:i:s', '2020-01-01 00:00:07'), + 'end' => null, + ]); + $this->actAsOrganizationWithoutSubscriptionAndWithoutTrial(); + Passport::actingAs($data->user); + + // Act + $response = $this->getJson(route('api.v1.time-entries.index', [ + $data->organization->getKey(), + 'member_id' => $data->member->getKey(), + 'rounding_type' => TimeEntryRoundingType::Up, + 'rounding_minutes' => 6, + ])); + + // Assert + $this->assertResponseCode($response, 200); + $response->assertJson(fn (AssertableJson $json) => $json + ->has('data') + ->has('meta') + ->where('meta.total', 2) + ->count('data', 2) + ->where('data.0.id', $timeEntry1->getKey()) + ->where('data.0.start', '2020-01-01T00:00:08Z') + ->where('data.0.end', '2020-01-01T00:00:01Z') + ->where('data.1.id', $timeEntry2->getKey()) + ->where('data.1.start', '2020-01-01T00:00:07Z') + ->where('data.1.end', null) + ); + } + public function test_index_endpoint_can_round_down(): void { // Arrange @@ -454,6 +501,7 @@ public function test_index_endpoint_can_round_down(): void 'start' => Carbon::createFromFormat('Y-m-d H:i:s', '2020-01-01 00:00:07'), 'end' => null, ]); + $this->actAsOrganizationWithSubscription(); Passport::actingAs($data->user); // Act @@ -499,6 +547,7 @@ public function test_index_endpoint_can_round_nearest(): void 'start' => Carbon::createFromFormat('Y-m-d H:i:s', '2020-01-01 00:00:07'), 'end' => null, ]); + $this->actAsOrganizationWithSubscription(); Passport::actingAs($data->user); // Act From 97dcadc7959accbcad105a717fea79c192a07964 Mon Sep 17 00:00:00 2001 From: Gregor Vostrak Date: Wed, 23 Jul 2025 14:20:32 +0200 Subject: [PATCH 03/25] add frontend blocking for rounding for non-premium users --- .../Reporting/ReportingRoundingControls.vue | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/resources/js/Components/Common/Reporting/ReportingRoundingControls.vue b/resources/js/Components/Common/Reporting/ReportingRoundingControls.vue index ba9e1d68..7ca82430 100644 --- a/resources/js/Components/Common/Reporting/ReportingRoundingControls.vue +++ b/resources/js/Components/Common/Reporting/ReportingRoundingControls.vue @@ -20,6 +20,9 @@ import { import { ArrowsUpDownIcon } from '@heroicons/vue/20/solid'; import { computed, ref, watch } from 'vue'; import { twMerge } from 'tailwind-merge'; +import { isAllowedToPerformPremiumAction } from '@/utils/billing'; +import { Link } from '@inertiajs/vue3'; +import { CreditCardIcon } from '@heroicons/vue/20/solid'; // TimeEntryRoundingType definition const TimeEntryRoundingType = { Up: 'up' as const, @@ -150,7 +153,17 @@ const iconClass = computed(() => { -
+
+ Premium + Rounding is a premium feature. Upgrade to unlock this feature. + + + +
+
From b11672732bd6af3043bdc9da2e5b39476c5e9a59 Mon Sep 17 00:00:00 2001 From: Constantin Graf Date: Wed, 23 Jul 2025 15:52:31 +0200 Subject: [PATCH 04/25] Fixed modules service providers --- composer.json | 3 ++- config/app.php | 2 ++ tests/TestCase.php | 6 +++++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 10af10bf..53c40842 100644 --- a/composer.json +++ b/composer.json @@ -118,7 +118,8 @@ "extra": { "laravel": { "dont-discover": [ - "laravel/telescope" + "laravel/telescope", + "nwidart/laravel-modules" ] } }, diff --git a/config/app.php b/config/app.php index 8acf6252..a15c3759 100644 --- a/config/app.php +++ b/config/app.php @@ -9,6 +9,7 @@ use App\Enums\TimeFormat; use Illuminate\Support\Facades\Facade; use Illuminate\Support\ServiceProvider; +use Nwidart\Modules\LaravelModulesServiceProvider; return [ @@ -197,6 +198,7 @@ App\Providers\FortifyServiceProvider::class, App\Providers\JetstreamServiceProvider::class, // Warning: Do not add TelescopeServiceProvider here since it is already conditionally registered in AppServiceProvider + LaravelModulesServiceProvider::class, ])->toArray(), /* diff --git a/tests/TestCase.php b/tests/TestCase.php index cc158eca..95b21835 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -21,13 +21,17 @@ abstract class TestCase extends BaseTestCase { use CreatesApplication; + protected bool $mockBillingContract = true; + protected function setUp(): void { parent::setUp(); Mail::fake(); LogFake::bind(); Http::preventStrayRequests(); - $this->actAsOrganizationWithoutSubscriptionAndWithoutTrial(); + if ($this->mockBillingContract) { + $this->actAsOrganizationWithoutSubscriptionAndWithoutTrial(); + } // Note: The following line can be used to test timezone edge cases. // $this->travelTo(Carbon::now()->timezone('Europe/Vienna')->setHour(0)->setMinute(59)->setSecond(0)); } From 204698ef9ba9bbc1b3679db41529511422de05a9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Jul 2025 17:21:19 +0000 Subject: [PATCH 05/25] Bump axios from 1.10.0 to 1.11.0 Bumps [axios](https://github.com/axios/axios) from 1.10.0 to 1.11.0. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.10.0...v1.11.0) --- updated-dependencies: - dependency-name: axios dependency-version: 1.11.0 dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- package-lock.json | 11 ++++++----- package.json | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5a6091e3..b895f7df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,7 +43,7 @@ "@vitejs/plugin-vue": "^5.2.1", "@vue/tsconfig": "^0.5.1", "autoprefixer": "^10.4.20", - "axios": "^1.6.4", + "axios": "^1.11.0", "eslint-plugin-unused-imports": "^4.1.4", "laravel-vite-plugin": "^1.0.0", "openapi-zod-client": "^1.16.2", @@ -2502,13 +2502,14 @@ } }, "node_modules/axios": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", - "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", "devOptional": true, + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, diff --git a/package.json b/package.json index 38d2cef4..389fece5 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "@vitejs/plugin-vue": "^5.2.1", "@vue/tsconfig": "^0.5.1", "autoprefixer": "^10.4.20", - "axios": "^1.6.4", + "axios": "^1.11.0", "eslint-plugin-unused-imports": "^4.1.4", "laravel-vite-plugin": "^1.0.0", "openapi-zod-client": "^1.16.2", From 36a327ca1597033a1815ede5bd311e13c6f55b0c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Jul 2025 17:53:56 +0000 Subject: [PATCH 06/25] Bump form-data from 4.0.1 to 4.0.4 in /resources/js/packages/ui --- updated-dependencies: - dependency-name: form-data dependency-version: 4.0.4 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- resources/js/packages/ui/package-lock.json | 177 ++++++++++++++++++++- 1 file changed, 174 insertions(+), 3 deletions(-) diff --git a/resources/js/packages/ui/package-lock.json b/resources/js/packages/ui/package-lock.json index 5fc7459a..8033d5f1 100644 --- a/resources/js/packages/ui/package-lock.json +++ b/resources/js/packages/ui/package-lock.json @@ -1739,6 +1739,20 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -1961,6 +1975,21 @@ "license": "MIT", "peer": true }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -1994,6 +2023,55 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -2148,14 +2226,16 @@ } }, "node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "license": "MIT", "peer": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -2214,6 +2294,45 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "peer": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -2248,6 +2367,19 @@ "node": ">=10.13.0" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -2265,6 +2397,35 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "peer": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -2491,6 +2652,16 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", From 2da11f8fc03561b52c1144f4a2db2ba83e7ee9ff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Jul 2025 09:20:49 +0000 Subject: [PATCH 07/25] Bump @eslint/plugin-kit from 0.3.3 to 0.3.4 Bumps [@eslint/plugin-kit](https://github.com/eslint/rewrite/tree/HEAD/packages/plugin-kit) from 0.3.3 to 0.3.4. - [Release notes](https://github.com/eslint/rewrite/releases) - [Changelog](https://github.com/eslint/rewrite/blob/main/packages/plugin-kit/CHANGELOG.md) - [Commits](https://github.com/eslint/rewrite/commits/plugin-kit-v0.3.4/packages/plugin-kit) --- updated-dependencies: - dependency-name: "@eslint/plugin-kit" dependency-version: 0.3.4 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b895f7df..6d95ff79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -924,9 +924,10 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz", - "integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", + "license": "Apache-2.0", "peer": true, "dependencies": { "@eslint/core": "^0.15.1", From cb30487a21c831c7f2f70301d8d56db6c291f417 Mon Sep 17 00:00:00 2001 From: Gregor Vostrak Date: Tue, 29 Jul 2025 18:58:31 +0200 Subject: [PATCH 08/25] add format check, update prettier rules, apply rules consistently --- .github/workflows/npm-format-check.yml | 23 ++ .prettierignore | 27 +++ .prettierrc.json | 3 +- e2e/clients.spec.ts | 19 +- e2e/members.spec.ts | 35 +-- e2e/organization.spec.ts | 52 ++--- e2e/profile.spec.ts | 49 ++-- e2e/project-members.spec.ts | 10 +- e2e/projects.spec.ts | 44 +--- e2e/reporting.spec.ts | 43 +++- e2e/tags.spec.ts | 12 +- e2e/tasks.spec.ts | 34 +-- e2e/time.spec.ts | 88 +++----- e2e/timetracker.spec.ts | 106 +++------ e2e/utils/currentTimeEntry.ts | 25 +-- e2e/utils/money.ts | 2 +- e2e/utils/tags.ts | 3 +- eslint.config.mjs | 25 ++- package.json | 4 +- playwright/config.ts | 3 +- playwright/fixtures.ts | 8 +- resources/js/Components/ActionSection.vue | 3 +- .../js/Components/AuthenticationCard.vue | 6 +- resources/js/Components/Banner.vue | 4 +- .../js/Components/Billing/BillingBanner.vue | 65 ++---- resources/js/Components/Common/Card.vue | 3 +- .../Client/ClientMoreOptionsDropdown.vue | 6 +- .../Components/Common/Client/ClientTable.vue | 15 +- .../Common/Client/ClientTableRow.vue | 8 +- .../Common/Invitation/InvitationTable.vue | 7 +- .../Invitation/InvitationTableHeading.vue | 3 +- .../Common/Invitation/InvitationTableRow.vue | 19 +- .../Common/Member/MemberBillableRateModal.vue | 4 +- .../Common/Member/MemberCombobox.vue | 8 +- .../Common/Member/MemberDeleteModal.vue | 106 ++++----- .../Common/Member/MemberEditModal.vue | 26 +-- .../Common/Member/MemberInviteModal.vue | 36 +-- .../Member/MemberMakePlaceholderModal.vue | 29 ++- .../Common/Member/MemberMergeModal.vue | 56 +++-- .../Member/MemberMoreOptionsDropdown.vue | 14 +- .../MemberOwnershipTransferConfirmModal.vue | 4 +- .../Common/Member/MemberRoleSelect.vue | 4 +- .../Common/Member/MemberTableHeading.vue | 7 +- .../Common/Member/MemberTableRow.vue | 16 +- .../OrganizationBillableRateModal.vue | 4 +- .../Common/Project/ProjectDropdown.vue | 70 +++--- .../Common/Project/ProjectEditModal.vue | 20 +- .../Project/ProjectMoreOptionsDropdown.vue | 6 +- .../Common/Project/ProjectTable.vue | 31 +-- .../Common/Project/ProjectTableHeading.vue | 12 +- .../Common/Project/ProjectTableRow.vue | 25 +-- .../ProjectMemberBillableRateModal.vue | 4 +- .../ProjectMemberCreateModal.vue | 9 +- .../ProjectMember/ProjectMemberEditModal.vue | 22 +- .../ProjectMemberMoreOptionsDropdown.vue | 4 +- .../ProjectMember/ProjectMemberTable.vue | 18 +- .../ProjectMemberTableHeading.vue | 4 +- .../ProjectMember/ProjectMemberTableRow.vue | 4 +- .../Common/Report/ReportCreateModal.vue | 22 +- .../Common/Report/ReportEditModal.vue | 13 +- .../Common/Report/ReportSaveButton.vue | 8 +- .../Components/Common/Report/ReportTable.vue | 20 +- .../Common/Report/ReportTableHeading.vue | 12 +- .../Common/Report/ReportTableRow.vue | 15 +- .../Common/Reporting/ReportingChart.vue | 19 +- .../Reporting/ReportingExportButton.vue | 23 +- .../Common/Reporting/ReportingExportModal.vue | 27 +-- .../Common/Reporting/ReportingFilterBadge.vue | 14 +- .../Reporting/ReportingGroupBySelect.vue | 6 +- .../Common/Reporting/ReportingOverview.vue | 144 +++--------- .../Common/Reporting/ReportingPieChart.vue | 9 +- .../Reporting/ReportingRoundingControls.vue | 108 +++++---- .../Common/Reporting/ReportingRow.vue | 23 +- .../Common/Reporting/ReportingTabNavbar.vue | 16 +- resources/js/Components/Common/StatCard.vue | 3 +- .../js/Components/Common/TabBar/TabBar.vue | 6 +- .../Components/Common/TabBar/TabBarItem.vue | 21 +- .../js/Components/Common/Tag/TagTable.vue | 15 +- .../Common/Task/TaskCreateModal.vue | 11 +- .../Common/Task/TaskMoreOptionsDropdown.vue | 6 +- .../js/Components/Common/Task/TaskTable.vue | 20 +- .../Common/Task/TaskTableHeading.vue | 8 +- .../Components/Common/Task/TaskTableRow.vue | 10 +- .../js/Components/Common/UpgradeBadge.vue | 3 +- .../js/Components/Common/UpgradeModal.vue | 8 +- resources/js/Components/ConfirmationModal.vue | 9 +- .../js/Components/CurrentSidebarTimer.vue | 4 +- .../Dashboard/ActivityGraphCard.vue | 18 +- .../Dashboard/DayOverviewCardChart.vue | 26 ++- .../Dashboard/DayOverviewCardEntry.vue | 8 +- .../Dashboard/LastSevenDaysCard.vue | 34 ++- .../Dashboard/ProjectsChartCard.vue | 17 +- .../Dashboard/RecentlyTrackedTasksCard.vue | 89 ++++---- .../RecentlyTrackedTasksCardEntry.vue | 28 +-- .../Components/Dashboard/TeamActivityCard.vue | 32 ++- .../Dashboard/TeamActivityCardEntry.vue | 15 +- .../Components/Dashboard/ThisWeekOverview.vue | 24 +- resources/js/Components/FormSection.vue | 6 +- .../js/Components/NavigationSidebarItem.vue | 12 +- .../js/Components/NavigationSidebarLink.vue | 4 +- .../js/Components/NotificationContainer.vue | 25 +-- .../js/Components/OrganizationSwitcher.vue | 42 +--- resources/js/Components/ResponsiveNavLink.vue | 5 +- resources/js/Components/TimeTracker.vue | 18 +- .../Components/UpdateSidebarNotification.vue | 12 +- resources/js/Components/UserSettingsIcon.vue | 12 +- .../js/Components/ui/accordion/Accordion.vue | 22 +- .../ui/accordion/AccordionContent.vue | 29 ++- .../Components/ui/accordion/AccordionItem.vue | 25 +-- .../ui/accordion/AccordionTrigger.vue | 54 ++--- resources/js/Components/ui/accordion/index.ts | 8 +- .../ui/alert-dialog/AlertDialog.vue | 19 +- .../ui/alert-dialog/AlertDialogAction.vue | 22 +- .../ui/alert-dialog/AlertDialogCancel.vue | 31 ++- .../ui/alert-dialog/AlertDialogContent.vue | 60 +++-- .../alert-dialog/AlertDialogDescription.vue | 28 +-- .../ui/alert-dialog/AlertDialogFooter.vue | 21 +- .../ui/alert-dialog/AlertDialogHeader.vue | 16 +- .../ui/alert-dialog/AlertDialogTitle.vue | 23 +- .../ui/alert-dialog/AlertDialogTrigger.vue | 10 +- .../js/Components/ui/alert-dialog/index.ts | 18 +- resources/js/Components/ui/button/Button.vue | 31 ++- resources/js/Components/ui/button/index.ts | 65 +++--- .../js/Components/ui/calendar/Calendar.vue | 106 +++++---- .../Components/ui/calendar/CalendarCell.vue | 32 +-- .../ui/calendar/CalendarCellTrigger.vue | 57 ++--- .../ui/calendar/CalendarDateInput.vue | 19 +- .../Components/ui/calendar/CalendarGrid.vue | 27 ++- .../ui/calendar/CalendarGridBody.vue | 10 +- .../ui/calendar/CalendarGridHead.vue | 12 +- .../ui/calendar/CalendarGridRow.vue | 22 +- .../ui/calendar/CalendarHeadCell.vue | 24 +- .../Components/ui/calendar/CalendarHeader.vue | 24 +- .../ui/calendar/CalendarHeading.vue | 37 ++- .../ui/calendar/CalendarNextButton.vue | 45 ++-- .../ui/calendar/CalendarPrevButton.vue | 45 ++-- resources/js/Components/ui/calendar/index.ts | 24 +- resources/js/Components/ui/dialog/Dialog.vue | 19 +- .../js/Components/ui/dialog/DialogClose.vue | 10 +- .../js/Components/ui/dialog/DialogContent.vue | 77 ++++--- .../ui/dialog/DialogDescription.vue | 27 ++- .../js/Components/ui/dialog/DialogFooter.vue | 19 +- .../js/Components/ui/dialog/DialogHeader.vue | 16 +- .../ui/dialog/DialogScrollContent.vue | 96 ++++---- .../js/Components/ui/dialog/DialogTitle.vue | 32 ++- .../js/Components/ui/dialog/DialogTrigger.vue | 10 +- resources/js/Components/ui/dialog/index.ts | 18 +- .../ui/dropdown-menu/DropdownMenu.vue | 19 +- .../DropdownMenuCheckboxItem.vue | 59 ++--- .../ui/dropdown-menu/DropdownMenuContent.vue | 56 ++--- .../ui/dropdown-menu/DropdownMenuGroup.vue | 10 +- .../ui/dropdown-menu/DropdownMenuItem.vue | 39 ++-- .../ui/dropdown-menu/DropdownMenuLabel.vue | 29 +-- .../dropdown-menu/DropdownMenuRadioGroup.vue | 22 +- .../dropdown-menu/DropdownMenuRadioItem.vue | 59 ++--- .../dropdown-menu/DropdownMenuSeparator.vue | 27 +-- .../ui/dropdown-menu/DropdownMenuShortcut.vue | 14 +- .../ui/dropdown-menu/DropdownMenuSub.vue | 22 +- .../dropdown-menu/DropdownMenuSubContent.vue | 42 ++-- .../dropdown-menu/DropdownMenuSubTrigger.vue | 43 ++-- .../ui/dropdown-menu/DropdownMenuTrigger.vue | 12 +- .../js/Components/ui/dropdown-menu/index.ts | 30 +-- .../ui/number-field/NumberField.vue | 96 ++++---- .../ui/number-field/NumberFieldContent.vue | 20 +- .../ui/number-field/NumberFieldDecrement.vue | 38 ++-- .../ui/number-field/NumberFieldIncrement.vue | 38 ++-- .../ui/number-field/NumberFieldInput.vue | 22 +- .../js/Components/ui/number-field/index.ts | 10 +- .../js/Components/ui/popover/Popover.vue | 16 +- .../Components/ui/popover/PopoverContent.vue | 67 +++--- .../Components/ui/popover/PopoverTrigger.vue | 10 +- resources/js/Components/ui/popover/index.ts | 8 +- .../ui/range-calendar/RangeCalendar.vue | 109 +++++---- .../ui/range-calendar/RangeCalendarCell.vue | 32 +-- .../RangeCalendarCellTrigger.vue | 65 +++--- .../ui/range-calendar/RangeCalendarGrid.vue | 27 ++- .../range-calendar/RangeCalendarGridBody.vue | 10 +- .../range-calendar/RangeCalendarGridHead.vue | 10 +- .../range-calendar/RangeCalendarGridRow.vue | 22 +- .../range-calendar/RangeCalendarHeadCell.vue | 27 ++- .../ui/range-calendar/RangeCalendarHeader.vue | 24 +- .../range-calendar/RangeCalendarHeading.vue | 37 ++- .../RangeCalendarNextButton.vue | 45 ++-- .../RangeCalendarPrevButton.vue | 45 ++-- .../js/Components/ui/range-calendar/index.ts | 24 +- resources/js/Components/ui/select/Select.vue | 16 +- .../js/Components/ui/select/SelectContent.vue | 86 +++---- .../js/Components/ui/select/SelectGroup.vue | 20 +- .../js/Components/ui/select/SelectItem.vue | 63 +++--- .../Components/ui/select/SelectItemText.vue | 10 +- .../js/Components/ui/select/SelectLabel.vue | 14 +- .../ui/select/SelectScrollDownButton.vue | 30 +-- .../ui/select/SelectScrollUpButton.vue | 30 +-- .../Components/ui/select/SelectSeparator.vue | 16 +- .../js/Components/ui/select/SelectTrigger.vue | 52 ++--- .../js/Components/ui/select/SelectValue.vue | 10 +- resources/js/Components/ui/select/index.ts | 22 +- resources/js/Components/ui/switch/Switch.vue | 58 ++--- resources/js/Components/ui/switch/index.ts | 2 +- resources/js/Components/ui/table/Table.vue | 18 +- .../js/Components/ui/table/TableBody.vue | 14 +- .../js/Components/ui/table/TableCaption.vue | 14 +- .../js/Components/ui/table/TableCell.vue | 27 ++- .../js/Components/ui/table/TableEmpty.vue | 53 +++-- .../js/Components/ui/table/TableFooter.vue | 14 +- .../js/Components/ui/table/TableHead.vue | 20 +- .../js/Components/ui/table/TableHeader.vue | 14 +- resources/js/Components/ui/table/TableRow.vue | 20 +- resources/js/Components/ui/table/index.ts | 18 +- resources/js/Components/ui/tabs/Tabs.vue | 16 +- .../js/Components/ui/tabs/TabsContent.vue | 30 +-- resources/js/Components/ui/tabs/TabsList.vue | 30 ++- .../js/Components/ui/tabs/TabsTrigger.vue | 47 ++-- resources/js/Components/ui/tabs/index.ts | 8 +- resources/js/Layouts/AppLayout.vue | 54 ++--- resources/js/Pages/API/Index.vue | 4 +- .../js/Pages/API/Partials/ApiTokenManager.vue | 88 +++----- resources/js/Pages/Auth/ConfirmPassword.vue | 4 +- resources/js/Pages/Auth/ForgotPassword.vue | 5 +- resources/js/Pages/Auth/Register.vue | 20 +- resources/js/Pages/Auth/ResetPassword.vue | 8 +- .../js/Pages/Auth/TwoFactorChallenge.vue | 8 +- resources/js/Pages/Auth/VerifyEmail.vue | 17 +- resources/js/Pages/Clients.vue | 20 +- resources/js/Pages/Dashboard.vue | 73 +++--- resources/js/Pages/Import.vue | 3 +- resources/js/Pages/Members.vue | 14 +- .../Pages/Profile/Partials/ApiTokensForm.vue | 163 +++++++------- .../Pages/Profile/Partials/DeleteUserForm.vue | 25 +-- .../LogoutOtherBrowserSessionsForm.vue | 50 ++--- .../js/Pages/Profile/Partials/ThemeForm.vue | 24 +- .../Partials/TwoFactorAuthenticationForm.vue | 66 ++---- .../Profile/Partials/UpdatePasswordForm.vue | 20 +- .../Partials/UpdateProfileInformationForm.vue | 39 +--- resources/js/Pages/Profile/Show.vue | 23 +- resources/js/Pages/ProjectShow.vue | 45 +--- resources/js/Pages/Projects.vue | 31 +-- resources/js/Pages/Reporting.vue | 9 +- resources/js/Pages/ReportingDetailed.vue | 83 ++----- resources/js/Pages/ReportingShared.vue | 34 +-- resources/js/Pages/SharedReport.vue | 41 +--- .../Pages/Teams/Partials/CreateTeamForm.vue | 4 +- .../Pages/Teams/Partials/DeleteTeamForm.vue | 20 +- .../js/Pages/Teams/Partials/ExportData.vue | 43 ++-- .../js/Pages/Teams/Partials/ImportData.vue | 77 ++----- .../Partials/OrganizationBillableRate.vue | 13 +- .../Partials/OrganizationFormatSettings.vue | 119 +++------- .../Teams/Partials/TeamMemberManager.vue | 107 +++------ .../Teams/Partials/UpdateTeamNameForm.vue | 19 +- resources/js/Pages/Teams/Show.vue | 11 +- resources/js/Pages/Time.vue | 29 +-- resources/js/Pages/Welcome.vue | 51 ++--- resources/js/app.ts | 42 ++-- resources/js/lib/utils.ts | 7 +- resources/js/packages/api/src/index.ts | 142 +++--------- .../packages/api/src/openapi.json.client.ts | 212 ++++-------------- .../packages/ui/src/Buttons/PrimaryButton.vue | 5 +- .../ui/src/Buttons/SecondaryButton.vue | 5 +- resources/js/packages/ui/src/CardTitle.vue | 5 +- .../packages/ui/src/Client/ClientDropdown.vue | 25 +-- resources/js/packages/ui/src/DialogModal.vue | 6 +- .../ui/src/Input/BillableToggleButton.vue | 3 +- .../js/packages/ui/src/Input/DatePicker.vue | 5 +- .../packages/ui/src/Input/DateRangePicker.vue | 164 +++++--------- .../js/packages/ui/src/Input/Dropdown.vue | 3 +- .../ui/src/Input/DurationHumanInput.vue | 5 +- .../packages/ui/src/Input/DurationInput.vue | 3 +- .../js/packages/ui/src/Input/InputLabel.vue | 3 +- .../ui/src/Input/MultiselectDropdown.vue | 36 +-- .../ui/src/Input/MultiselectDropdownItem.vue | 4 +- .../packages/ui/src/Input/SelectDropdown.vue | 37 +-- .../ui/src/Input/SelectDropdownItem.vue | 4 +- .../packages/ui/src/Input/TextareaInput.vue | 36 +-- .../js/packages/ui/src/Input/TimePicker.vue | 30 +-- .../ui/src/Input/TimePickerSimple.vue | 18 +- .../ui/src/Input/TimeRangeSelector.vue | 17 +- .../js/packages/ui/src/LoadingSpinner.vue | 4 +- resources/js/packages/ui/src/Modal.vue | 9 +- .../packages/ui/src/Project/ProjectBadge.vue | 7 +- .../src/Project/ProjectBillableRateModal.vue | 4 +- .../ui/src/Project/ProjectBillableSelect.vue | 5 +- .../ui/src/Project/ProjectCreateModal.vue | 20 +- .../ui/src/Project/ProjectDropdownItem.vue | 4 +- .../Project/ProjectEditBillableSection.vue | 6 +- .../js/packages/ui/src/Tag/TagDropdown.vue | 42 ++-- .../src/TimeEntry/TimeEntryAggregateRow.vue | 63 +++--- .../ui/src/TimeEntry/TimeEntryCreateModal.vue | 65 ++---- .../TimeEntry/TimeEntryDescriptionInput.vue | 3 +- .../src/TimeEntry/TimeEntryGroupedTable.vue | 52 ++--- .../src/TimeEntry/TimeEntryMassActionRow.vue | 13 +- .../TimeEntry/TimeEntryMassUpdateModal.vue | 34 +-- .../src/TimeEntry/TimeEntryRangeSelector.vue | 9 +- .../ui/src/TimeEntry/TimeEntryRow.vue | 44 ++-- .../TimeEntry/TimeEntryRowDurationInput.vue | 9 +- .../src/TimeEntry/TimeEntryRowTagDropdown.vue | 4 +- .../src/TimeTracker/TimeTrackerControls.vue | 125 +++++------ .../TimeTrackerProjectTaskDropdown.vue | 149 ++++-------- .../TimeTracker/TimeTrackerRangeSelector.vue | 25 +-- .../TimeTrackerRecentlyTrackedEntry.vue | 60 +++-- ...rRunningInDifferentOrganizationOverlay.vue | 3 +- .../TimeTracker/TimeTrackerTagDropdown.vue | 6 +- resources/js/packages/ui/src/utils/money.ts | 8 +- resources/js/packages/ui/src/utils/random.ts | 11 +- resources/js/packages/ui/src/utils/select.ts | 42 ++-- resources/js/packages/ui/src/utils/time.ts | 32 ++- resources/js/utils/billing.ts | 5 +- resources/js/utils/notification.ts | 20 +- resources/js/utils/theme.ts | 12 +- resources/js/utils/useClients.ts | 9 +- resources/js/utils/useCssVariable.ts | 92 ++++---- resources/js/utils/useCurrentTimeEntry.ts | 6 +- resources/js/utils/useInvitations.ts | 4 +- resources/js/utils/useMembers.ts | 11 +- resources/js/utils/useOrganization.ts | 4 +- resources/js/utils/useProjectMembers.ts | 9 +- resources/js/utils/useProjects.ts | 9 +- resources/js/utils/useReporting.ts | 33 +-- resources/js/utils/useTags.ts | 8 +- resources/js/utils/useTimeEntries.ts | 35 +-- resources/js/utils/useUser.ts | 10 +- tailwind.config.js | 204 ++++++++--------- vite-module-loader.js | 20 +- vite.config.js | 5 +- 323 files changed, 3729 insertions(+), 5580 deletions(-) create mode 100644 .github/workflows/npm-format-check.yml create mode 100644 .prettierignore diff --git a/.github/workflows/npm-format-check.yml b/.github/workflows/npm-format-check.yml new file mode 100644 index 00000000..088e5211 --- /dev/null +++ b/.github/workflows/npm-format-check.yml @@ -0,0 +1,23 @@ +name: NPM Format Check + +on: [push] + +jobs: + format-check: + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: "Checkout code" + uses: actions/checkout@v4 + + - name: "Use Node.js" + uses: actions/setup-node@v4 + with: + node-version: '20.x' + + - name: "Install npm dependencies" + run: npm ci + + - name: "Check code formatting" + run: npm run format:check \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..6157e53f --- /dev/null +++ b/.prettierignore @@ -0,0 +1,27 @@ +# Ignore build outputs +node_modules/ +vendor/ +storage/ +bootstrap/cache/ +public/build/ +public/hot/ + +# Ignore lock files +package-lock.json +composer.lock + +# Ignore generated files +*.min.js +*.min.css + +# Ignore test results +test-results/ +playwright-report/ + +# Ignore IDE files +.idea/ +.vscode/ + +# Ignore OS files +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json index 62762f5c..d689166c 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -3,5 +3,6 @@ "tabWidth": 4, "singleQuote": true, "bracketSameLine": true, - "quoteProps": "preserve" + "quoteProps": "preserve", + "printWidth": 100 } diff --git a/e2e/clients.spec.ts b/e2e/clients.spec.ts index 9ca98d5d..dab9c81f 100644 --- a/e2e/clients.spec.ts +++ b/e2e/clients.spec.ts @@ -7,11 +7,8 @@ async function goToProjectsOverview(page: Page) { } // Create new project 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); +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 page.getByRole('button', { name: 'Create Client' }).click(); await page.getByPlaceholder('Client Name').fill(newClientName); @@ -28,13 +25,9 @@ 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 + "']" - ); + const moreButton = page.locator("[aria-label='Actions for Client " + newClientName + "']"); moreButton.click(); - const deleteButton = page.locator( - "[aria-label='Delete Client " + newClientName + "']" - ); + const deleteButton = page.locator("[aria-label='Delete Client " + newClientName + "']"); await Promise.all([ deleteButton.click(), @@ -45,9 +38,7 @@ test('test that creating and deleting a new client via the modal works', async ( response.status() === 204 ), ]); - await expect(page.getByTestId('client_table')).not.toContainText( - newClientName - ); + await expect(page.getByTestId('client_table')).not.toContainText(newClientName); }); test('test that archiving and unarchiving clients works', async ({ page }) => { diff --git a/e2e/members.spec.ts b/e2e/members.spec.ts index 253acf79..70be0e7c 100644 --- a/e2e/members.spec.ts +++ b/e2e/members.spec.ts @@ -22,12 +22,8 @@ test('test that new manager can be invited', async ({ page }) => { 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` - ), + page.getByRole('button', { name: 'Invite Member', exact: true }).click(), + expect(page.getByRole('main')).toContainText(`new+${editorId}@editor.test`), ]); }); @@ -38,12 +34,8 @@ test('test that new employee can be invited', async ({ page }) => { 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` - ), + page.getByRole('button', { name: 'Invite Member', exact: true }).click(), + await expect(page.getByRole('main')).toContainText(`new+${editorId}@editor.test`), ]); }); @@ -54,12 +46,8 @@ test('test that new admin can be invited', async ({ page }) => { 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` - ), + page.getByRole('button', { name: 'Invite Member', exact: true }).click(), + expect(page.getByRole('main')).toContainText(`new+${adminId}@admin.test`), ]); }); test('test that error shows if no role is selected', async ({ page }) => { @@ -69,9 +57,7 @@ test('test that error shows if no role is selected', async ({ page }) => { await page.getByLabel('Email').fill(`new+${noRoleId}@norole.test`); await Promise.all([ - page - .getByRole('button', { name: 'Invite Member', exact: true }) - .click(), + page.getByRole('button', { name: 'Invite Member', exact: true }).click(), expect(page.getByText('Please select a role')).toBeVisible(), ]); }); @@ -85,9 +71,7 @@ test('test that organization billable rate can be updated with all existing time await page.getByRole('menuitem').getByText('Edit').click(); await page.getByText('Organization Default Rate').click(); await page.getByText('Custom Rate').click(); - await page - .getByPlaceholder('Billable Rate') - .fill(newBillableRate.toString()); + await page.getByPlaceholder('Billable Rate').fill(newBillableRate.toString()); await page.getByRole('button', { name: 'Update Member' }).click(); await Promise.all([ @@ -103,8 +87,7 @@ test('test that organization billable rate can be updated with all existing time response.url().includes('/organizations/') && response.request().method() === 'PUT' && response.status() === 200 && - (await response.json()).data.billable_rate === - newBillableRate * 100 + (await response.json()).data.billable_rate === newBillableRate * 100 ), ]); }); diff --git a/e2e/organization.spec.ts b/e2e/organization.spec.ts index ebb4a539..d2386577 100644 --- a/e2e/organization.spec.ts +++ b/e2e/organization.spec.ts @@ -35,9 +35,9 @@ test('test that organization name can be updated', async ({ page }) => { await page.getByLabel('Organization Name').fill('NEW ORG NAME'); await page.getByLabel('Organization Name').press('Enter'); await page.getByLabel('Organization Name').press('Meta+r'); - await expect( - page.locator('[data-testid="organization_switcher"]:visible') - ).toContainText('NEW ORG NAME'); + await expect(page.locator('[data-testid="organization_switcher"]:visible')).toContainText( + 'NEW ORG NAME' + ); }); test('test that organization billable rate can be updated with all existing time entries', async ({ @@ -46,9 +46,7 @@ test('test that organization billable rate can be updated with all existing time await goToOrganizationSettings(page); const newBillableRate = Math.round(Math.random() * 10000); await page.getByLabel('Organization Billable Rate').click(); - await page - .getByLabel('Organization Billable Rate') - .fill(newBillableRate.toString()); + await page.getByLabel('Organization Billable Rate').fill(newBillableRate.toString()); await page .locator('form') .filter({ hasText: 'Organization Billable' }) @@ -56,9 +54,7 @@ test('test that organization billable rate can be updated with all existing time .click(); await Promise.all([ - page - .getByRole('button', { name: 'Yes, update existing time entries' }) - .click(), + page.getByRole('button', { name: 'Yes, update existing time entries' }).click(), page.waitForRequest( async (request) => request.url().includes('/organizations/') && @@ -70,15 +66,12 @@ test('test that organization billable rate can be updated with all existing time response.url().includes('/organizations/') && response.request().method() === 'PUT' && response.status() === 200 && - (await response.json()).data.billable_rate === - newBillableRate * 100 + (await response.json()).data.billable_rate === newBillableRate * 100 ), ]); }); -test('test that organization format settings can be updated', async ({ - page, -}) => { +test('test that organization format settings can be updated', async ({ page }) => { await goToOrganizationSettings(page); // Test number format @@ -113,8 +106,7 @@ test('test that organization format settings can be updated', async ({ response.url().includes('/organizations/') && response.request().method() === 'PUT' && response.status() === 200 && - (await response.json()).data.currency_format === - 'iso-code-after-with-space' + (await response.json()).data.currency_format === 'iso-code-after-with-space' ), ]); @@ -132,8 +124,7 @@ test('test that organization format settings can be updated', async ({ response.url().includes('/organizations/') && response.request().method() === 'PUT' && response.status() === 200 && - (await response.json()).data.date_format === - 'slash-separated-dd-mm-yyyy' + (await response.json()).data.date_format === 'slash-separated-dd-mm-yyyy' ), ]); @@ -169,19 +160,14 @@ test('test that organization format settings can be updated', async ({ response.url().includes('/organizations/') && response.request().method() === 'PUT' && response.status() === 200 && - (await response.json()).data.interval_format === - 'hours-minutes-colon-separated' + (await response.json()).data.interval_format === 'hours-minutes-colon-separated' ), ]); }); -test('test that format settings are reflected in the dashboard', async ({ - page, -}) => { +test('test that format settings are reflected in the dashboard', async ({ page }) => { // check that 0h 00min is displayed - await expect( - page.getByText('0h 00min', { exact: true }).nth(0) - ).toBeVisible(); + await expect(page.getByText('0h 00min', { exact: true }).nth(0)).toBeVisible(); // First set the format settings await goToOrganizationSettings(page); @@ -213,10 +199,8 @@ test('test that format settings are reflected in the dashboard', async ({ response.url().includes('/organizations/') && response.request().method() === 'PUT' && response.status() === 200 && - (await response.json()).data.interval_format === - 'hours-minutes-colon-separated' && - (await response.json()).data.currency_format === - 'symbol-after' && + (await response.json()).data.interval_format === 'hours-minutes-colon-separated' && + (await response.json()).data.currency_format === 'symbol-after' && (await response.json()).data.number_format === 'comma-point' ), ]); @@ -232,16 +216,12 @@ test('test that format settings are reflected in the dashboard', async ({ // check that 00:00 is displayed await expect(page.getByText('0:00', { exact: true }).nth(0)).toBeVisible(); // check that 0h 00min is not displayed - await expect( - page.getByText('0h 00min', { exact: true }).nth(0) - ).not.toBeVisible(); + await expect(page.getByText('0h 00min', { exact: true }).nth(0)).not.toBeVisible(); // check that the current date is displayed in the dd/mm/yyyy format on the time page await page.goto(PLAYWRIGHT_BASE_URL + '/time'); await expect( - page - .getByText(new Date().toLocaleDateString('en-GB'), { exact: true }) - .nth(0) + page.getByText(new Date().toLocaleDateString('en-GB'), { exact: true }).nth(0) ).toBeVisible(); }); diff --git a/e2e/profile.spec.ts b/e2e/profile.spec.ts index 2428af9c..a2f34388 100644 --- a/e2e/profile.spec.ts +++ b/e2e/profile.spec.ts @@ -1,34 +1,32 @@ -import {test, expect} from '../playwright/fixtures'; -import {PLAYWRIGHT_BASE_URL} from '../playwright/config'; +import { test, expect } from '../playwright/fixtures'; +import { PLAYWRIGHT_BASE_URL } from '../playwright/config'; -test('test that user name can be updated', async ({page}) => { +test('test that user name can be updated', async ({ page }) => { await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile'); - await page.getByLabel('Name', {exact: true} ).fill('NEW NAME'); + await page.getByLabel('Name', { exact: true }).fill('NEW NAME'); await Promise.all([ - page.getByRole('button', {name: 'Save'}).first().click(), + page.getByRole('button', { name: 'Save' }).first().click(), page.waitForResponse('**/user/profile-information'), ]); await page.reload(); - await expect(page.getByLabel('Name', {exact: true})).toHaveValue('NEW NAME'); + await expect(page.getByLabel('Name', { exact: true })).toHaveValue('NEW NAME'); }); -test.skip('test that user email can be updated', async ({page}) => { +test.skip('test that user email can be updated', async ({ page }) => { // this does not work because of email verification currently await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile'); const emailId = Math.round(Math.random() * 10000); await page.getByLabel('Email').fill(`newemail+${emailId}@test.com`); - await page.getByRole('button', {name: 'Save'}).first().click(); + await page.getByRole('button', { name: 'Save' }).first().click(); await page.reload(); - await expect(page.getByLabel('Email')).toHaveValue( - `newemail+${emailId}@test.com` - ); + await expect(page.getByLabel('Email')).toHaveValue(`newemail+${emailId}@test.com`); }); async function createNewApiToken(page) { await page.getByLabel('API Key Name').fill('NEW API KEY'); await Promise.all([ - page.getByRole('button', {name: 'Create API Key'}).click(), - page.waitForResponse('**/users/me/api-tokens') + page.getByRole('button', { name: 'Create API Key' }).click(), + page.waitForResponse('**/users/me/api-tokens'), ]); await expect(page.locator('body')).toContainText('API Token created successfully'); @@ -36,34 +34,37 @@ async function createNewApiToken(page) { await expect(page.locator('body')).toContainText('NEW API KEY'); } -test('test that user can create an API key', async ({page}) => { +test('test that user can create an API key', async ({ page }) => { await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile'); await createNewApiToken(page); }); -test('test that user can delete an API key', async ({page}) => { +test('test that user can delete an API key', async ({ page }) => { await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile'); await createNewApiToken(page); page.getByLabel('Delete API Token NEW API KEY').click(); - await expect(page.getByRole('dialog')).toContainText('Are you sure you would like to delete this API token?'); + await expect(page.getByRole('dialog')).toContainText( + 'Are you sure you would like to delete this API token?' + ); await Promise.all([ - page.getByRole('dialog').getByRole('button', {name: 'Delete'}).click(), - page.waitForResponse('**/users/me/api-tokens') + page.getByRole('dialog').getByRole('button', { name: 'Delete' }).click(), + page.waitForResponse('**/users/me/api-tokens'), ]); await expect(page.locator('body')).not.toContainText('NEW API KEY'); }); - -test('test that user can revoke an API key', async ({page}) => { +test('test that user can revoke an API key', async ({ page }) => { await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile'); await createNewApiToken(page); page.getByLabel('Revoke API Token NEW API KEY').click(); - await expect(page.getByRole('dialog')).toContainText('Are you sure you would like to revoke this API token?'); + await expect(page.getByRole('dialog')).toContainText( + 'Are you sure you would like to revoke this API token?' + ); await Promise.all([ - page.getByRole('dialog').getByRole('button', {name: 'Revoke'}).click(), - page.waitForResponse('**/users/me/api-tokens') + page.getByRole('dialog').getByRole('button', { name: 'Revoke' }).click(), + page.waitForResponse('**/users/me/api-tokens'), ]); - await expect(page.getByRole('button', {name: 'Revoke'})).toBeHidden(); + await expect(page.getByRole('button', { name: 'Revoke' })).toBeHidden(); await expect(page.locator('body')).toContainText('NEW API KEY'); await expect(page.locator('body')).toContainText('Revoked'); }); diff --git a/e2e/project-members.spec.ts b/e2e/project-members.spec.ts index 66434eab..36ee7f2b 100644 --- a/e2e/project-members.spec.ts +++ b/e2e/project-members.spec.ts @@ -12,8 +12,7 @@ async function goToProjectsOverview(page: Page) { test('test that updating project member billable rate works for existing time entries', async ({ page, }) => { - const newProjectName = - 'New Project ' + Math.floor(1 + Math.random() * 10000); + const newProjectName = 'New Project ' + Math.floor(1 + Math.random() * 10000); const newBillableRate = Math.round(Math.random() * 10000); await goToProjectsOverview(page); await page.getByRole('button', { name: 'Create Project' }).click(); @@ -36,9 +35,7 @@ test('test that updating project member billable rate works for existing time en .first() .getByRole('button') .click(); - await page - .getByRole('menuitem', { name: 'Edit Project Member' }) - .click(); + await page.getByRole('menuitem', { name: 'Edit Project Member' }).click(); await page.getByLabel('Billable Rate').fill(newBillableRate.toString()); await page.getByRole('button', { name: 'Update Project Member' }).click(); @@ -55,8 +52,7 @@ test('test that updating project member billable rate works for existing time en response.url().includes('/project-members/') && response.request().method() === 'PUT' && response.status() === 200 && - (await response.json()).data.billable_rate === - newBillableRate * 100 + (await response.json()).data.billable_rate === newBillableRate * 100 ), ]); await expect( diff --git a/e2e/projects.spec.ts b/e2e/projects.spec.ts index b751601a..9dd51c5d 100644 --- a/e2e/projects.spec.ts +++ b/e2e/projects.spec.ts @@ -9,11 +9,8 @@ async function goToProjectsOverview(page: Page) { } // Create new project via modal -test('test that creating and deleting a new project via the modal works', async ({ - page, -}) => { - const newProjectName = - 'New Project ' + Math.floor(1 + Math.random() * 10000); +test('test that creating and deleting a new project via the modal works', async ({ page }) => { + const newProjectName = 'New Project ' + Math.floor(1 + Math.random() * 10000); await goToProjectsOverview(page); await page.getByRole('button', { name: 'Create Project' }).click(); await page.getByLabel('Project Name').fill(newProjectName); @@ -31,16 +28,10 @@ test('test that creating and deleting a new project via the modal works', async ), ]); - await expect(page.getByTestId('project_table')).toContainText( - newProjectName - ); - const moreButton = page.locator( - "[aria-label='Actions for Project " + newProjectName + "']" - ); + await expect(page.getByTestId('project_table')).toContainText(newProjectName); + const moreButton = page.locator("[aria-label='Actions for Project " + newProjectName + "']"); moreButton.click(); - const deleteButton = page.locator( - "[aria-label='Delete Project " + newProjectName + "']" - ); + const deleteButton = page.locator("[aria-label='Delete Project " + newProjectName + "']"); await Promise.all([ deleteButton.click(), @@ -51,14 +42,11 @@ test('test that creating and deleting a new project via the modal works', async response.status() === 204 ), ]); - await expect(page.getByTestId('project_table')).not.toContainText( - newProjectName - ); + await expect(page.getByTestId('project_table')).not.toContainText(newProjectName); }); test('test that archiving and unarchiving projects works', async ({ page }) => { - const newProjectName = - 'New Project ' + Math.floor(1 + Math.random() * 10000); + const newProjectName = 'New Project ' + Math.floor(1 + Math.random() * 10000); await goToProjectsOverview(page); await page.getByRole('button', { name: 'Create Project' }).click(); await page.getByLabel('Project Name').fill(newProjectName); @@ -87,11 +75,8 @@ test('test that archiving and unarchiving projects works', async ({ page }) => { ]); }); -test('test that updating billable rate works with existing time entries', async ({ - page, -}) => { - const newProjectName = - 'New Project ' + Math.floor(1 + Math.random() * 10000); +test('test that updating billable rate works with existing time entries', async ({ page }) => { + const newProjectName = 'New Project ' + Math.floor(1 + Math.random() * 10000); const newBillableRate = Math.round(Math.random() * 10000); await goToProjectsOverview(page); await page.getByRole('button', { name: 'Create Project' }).click(); @@ -104,15 +89,11 @@ test('test that updating billable rate works with existing time entries', async await page.getByRole('menuitem').getByText('Edit').first().click(); await page.getByText('Non-Billable').click(); await page.getByText('Custom Rate').click(); - await page - .getByPlaceholder('Billable Rate') - .fill(newBillableRate.toString()); + await page.getByPlaceholder('Billable Rate').fill(newBillableRate.toString()); await page.getByRole('button', { name: 'Update Project' }).click(); await Promise.all([ - page - .locator('button').filter({ hasText: 'Yes, update existing time' }) - .click(), + page.locator('button').filter({ hasText: 'Yes, update existing time' }).click(), page.waitForRequest( async (request) => request.url().includes('/projects/') && @@ -124,8 +105,7 @@ test('test that updating billable rate works with existing time entries', async response.url().includes('/projects/') && response.request().method() === 'PUT' && response.status() === 200 && - (await response.json()).data.billable_rate === - newBillableRate * 100 + (await response.json()).data.billable_rate === newBillableRate * 100 ), ]); await expect( diff --git a/e2e/reporting.spec.ts b/e2e/reporting.spec.ts index 6b2f59c2..0cfa872c 100644 --- a/e2e/reporting.spec.ts +++ b/e2e/reporting.spec.ts @@ -2,8 +2,6 @@ import { expect, Page } from '@playwright/test'; import { PLAYWRIGHT_BASE_URL } from '../playwright/config'; import { test } from '../playwright/fixtures'; - - async function goToTimeOverview(page: Page) { await page.goto(PLAYWRIGHT_BASE_URL + '/time'); } @@ -31,7 +29,10 @@ async function createTimeEntryWithProject(page: Page, projectName: string, durat await page.getByRole('button', { name: 'Manual time entry' }).click(); // Fill in the time entry details - await page.getByRole('dialog').getByRole('textbox', { name: 'Description' }).fill(`Time entry for ${projectName}`); + await page + .getByRole('dialog') + .getByRole('textbox', { name: 'Description' }) + .fill(`Time entry for ${projectName}`); await page.getByRole('button', { name: 'No Project' }).click(); await page.getByText(projectName).click(); @@ -43,7 +44,9 @@ async function createTimeEntryWithProject(page: Page, projectName: string, durat // Submit the time entry await Promise.all([ page.getByRole('button', { name: 'Create Time Entry' }).click(), - page.waitForResponse(response => response.url().includes('/time-entries') && response.status() === 201) + page.waitForResponse( + (response) => response.url().includes('/time-entries') && response.status() === 201 + ), ]); } @@ -52,7 +55,10 @@ async function createTimeEntryWithTag(page: Page, tagName: string, duration: str await page.getByRole('button', { name: 'Manual time entry' }).click(); // Fill in the time entry details - await page.getByRole('dialog').getByRole('textbox', { name: 'Description' }).fill(`Time entry with tag ${tagName}`); + await page + .getByRole('dialog') + .getByRole('textbox', { name: 'Description' }) + .fill(`Time entry with tag ${tagName}`); // Add tag await page.getByRole('button', { name: 'Tags' }).click(); @@ -69,12 +75,19 @@ async function createTimeEntryWithTag(page: Page, tagName: string, duration: str await page.getByRole('button', { name: 'Create Time Entry' }).click(); } -async function createTimeEntryWithBillableStatus(page: Page, isBillable: boolean, duration: string) { +async function createTimeEntryWithBillableStatus( + page: Page, + isBillable: boolean, + duration: string +) { await goToTimeOverview(page); await page.getByRole('button', { name: 'Manual time entry' }).click(); // Fill in the time entry details - await page.getByRole('dialog').getByRole('textbox', { name: 'Description' }).fill(`Time entry ${isBillable ? 'billable' : 'non-billable'}`); + await page + .getByRole('dialog') + .getByRole('textbox', { name: 'Description' }) + .fill(`Time entry ${isBillable ? 'billable' : 'non-billable'}`); // Set billable status await page.getByRole('button', { name: 'Non-Billable' }).click(); @@ -109,7 +122,10 @@ test('test that project filtering works in reporting', async ({ page }) => { // escape page.keyboard.press('Escape'), // wait for API request to finish - page.waitForResponse(response => response.url().includes('/time-entries/aggregate') && response.status() === 200) + page.waitForResponse( + (response) => + response.url().includes('/time-entries/aggregate') && response.status() === 200 + ), ]); await page.waitForLoadState('networkidle'); @@ -138,7 +154,10 @@ test('test that tag filtering works in reporting', async ({ page }) => { // escape page.keyboard.press('Escape'), // wait for API request to finish - page.waitForResponse(response => response.url().includes('/time-entries/aggregate') && response.status() === 200) + page.waitForResponse( + (response) => + response.url().includes('/time-entries/aggregate') && response.status() === 200 + ), ]); // Verify only time entries with tag1 are shown @@ -160,14 +179,16 @@ test('test that billable status filtering works in reporting', async ({ page }) // escape page.keyboard.press('Escape'), // wait for API request to finish - page.waitForResponse(response => response.url().includes('/time-entries/aggregate') && response.status() === 200) + page.waitForResponse( + (response) => + response.url().includes('/time-entries/aggregate') && response.status() === 200 + ), ]); await page.waitForLoadState('networkidle'); await expect(page.getByTestId('reporting_view').getByText('1h 00min').first()).toBeVisible(); }); - test('test that detailed view shows time entries correctly', async ({ page }) => { const projectName = 'Detailed View Project ' + Math.floor(Math.random() * 10000); diff --git a/e2e/tags.spec.ts b/e2e/tags.spec.ts index 4a29c3db..8b89aa2b 100644 --- a/e2e/tags.spec.ts +++ b/e2e/tags.spec.ts @@ -7,9 +7,7 @@ async function goToTagsOverview(page: Page) { } // Create new project via modal -test('test that creating and deleting a new client via the modal works', async ({ - page, -}) => { +test('test that creating and deleting a new client via the modal works', async ({ page }) => { const newTagName = 'New Tag ' + Math.floor(1 + Math.random() * 10000); await goToTagsOverview(page); await page.getByRole('button', { name: 'Create Tag' }).click(); @@ -27,13 +25,9 @@ test('test that creating and deleting a new client via the modal works', async ( ]); await expect(page.getByTestId('tag_table')).toContainText(newTagName); - const moreButton = page.locator( - "[aria-label='Actions for Tag " + newTagName + "']" - ); + const moreButton = page.locator("[aria-label='Actions for Tag " + newTagName + "']"); moreButton.click(); - const deleteButton = page.locator( - "[aria-label='Delete Tag " + newTagName + "']" - ); + const deleteButton = page.locator("[aria-label='Delete Tag " + newTagName + "']"); await Promise.all([ deleteButton.click(), diff --git a/e2e/tasks.spec.ts b/e2e/tasks.spec.ts index d5165993..8e2073db 100644 --- a/e2e/tasks.spec.ts +++ b/e2e/tasks.spec.ts @@ -7,11 +7,8 @@ async function goToProjectsOverview(page: Page) { } // Create new project via modal -test('test that creating and deleting a new tag in a new project works', async ({ - page, -}) => { - const newProjectName = - 'New Project ' + Math.floor(1 + Math.random() * 10000); +test('test that creating and deleting a new tag in a new project works', async ({ page }) => { + const newProjectName = 'New Project ' + Math.floor(1 + Math.random() * 10000); await goToProjectsOverview(page); await page.getByRole('button', { name: 'Create Project' }).click(); await page.getByLabel('Project Name').fill(newProjectName); @@ -29,9 +26,7 @@ test('test that creating and deleting a new tag in a new project works', async ( ), ]); - await expect(page.getByTestId('project_table')).toContainText( - newProjectName - ); + await expect(page.getByTestId('project_table')).toContainText(newProjectName); await page.getByText(newProjectName).click(); @@ -55,13 +50,9 @@ test('test that creating and deleting a new tag in a new project works', async ( await expect(page.getByTestId('task_table')).toContainText(newTaskName); - const taskMoreButton = page.locator( - "[aria-label='Actions for Task " + newTaskName + "']" - ); + const taskMoreButton = page.locator("[aria-label='Actions for Task " + newTaskName + "']"); taskMoreButton.click(); - const taskDeleteButton = page.locator( - "[aria-label='Delete Task " + newTaskName + "']" - ); + const taskDeleteButton = page.locator("[aria-label='Delete Task " + newTaskName + "']"); await Promise.all([ taskDeleteButton.click(), @@ -76,13 +67,9 @@ test('test that creating and deleting a new tag in a new project works', async ( await goToProjectsOverview(page); - const moreButton = page.locator( - "[aria-label='Actions for Project " + newProjectName + "']" - ); + const moreButton = page.locator("[aria-label='Actions for Project " + newProjectName + "']"); moreButton.click(); - const deleteButton = page.locator( - "[aria-label='Delete Project " + newProjectName + "']" - ); + const deleteButton = page.locator("[aria-label='Delete Project " + newProjectName + "']"); await Promise.all([ deleteButton.click(), @@ -93,14 +80,11 @@ test('test that creating and deleting a new tag in a new project works', async ( response.status() === 204 ), ]); - await expect(page.getByTestId('project_table')).not.toContainText( - newProjectName - ); + await expect(page.getByTestId('project_table')).not.toContainText(newProjectName); }); test('test that archiving and unarchiving tasks works', async ({ page }) => { - const newProjectName = - 'New Project ' + Math.floor(1 + Math.random() * 10000); + const newProjectName = 'New Project ' + Math.floor(1 + Math.random() * 10000); const newTaskName = 'New Project ' + Math.floor(1 + Math.random() * 10000); await goToProjectsOverview(page); diff --git a/e2e/time.spec.ts b/e2e/time.spec.ts index da2aa7ce..2b01ddfb 100644 --- a/e2e/time.spec.ts +++ b/e2e/time.spec.ts @@ -25,9 +25,7 @@ async function createEmptyTimeEntry(page: Page) { startOrStopTimerWithButton(page), assertThatTimerIsStopped(page), page.waitForResponse( - (response) => - response.url().includes('/time-entries') && - response.status() === 200 + (response) => response.url().includes('/time-entries') && response.status() === 200 ), ]); } @@ -38,9 +36,7 @@ test('test that starting and stopping an empty time entry shows a new time entry await Promise.all([ goToTimeOverview(page), page.waitForResponse( - (response) => - response.url().includes('/time-entries') && - response.status() === 200 + (response) => response.url().includes('/time-entries') && response.status() === 200 ), ]); await page.waitForTimeout(100); @@ -56,9 +52,7 @@ test('test that starting and stopping an empty time entry shows a new time entry // Test that description update works async function assertThatTimeEntryRowIsStopped(newTimeEntry: Locator) { - await expect(newTimeEntry.getByTestId('timer_button')).toHaveClass( - /bg-accent-300\/70/ - ); + await expect(newTimeEntry.getByTestId('timer_button')).toHaveClass(/bg-accent-300\/70/); } test('test that updating a description of a time entry in the overview works on blur', async ({ @@ -71,17 +65,14 @@ test('test that updating a description of a time entry in the overview works on await assertThatTimeEntryRowIsStopped(newTimeEntry); const newDescription = Math.floor(Math.random() * 1000000).toString(); - const descriptionElement = newTimeEntry.getByTestId( - 'time_entry_description' - ); + const descriptionElement = newTimeEntry.getByTestId('time_entry_description'); await descriptionElement.fill(newDescription); await Promise.all([ descriptionElement.press('Tab'), page.waitForResponse(async (response) => { return ( response.status() === 200 && - (await response.headerValue('Content-Type')) === - 'application/json' && + (await response.headerValue('Content-Type')) === 'application/json' && (await response.json()).data.id !== null && (await response.json()).data.start !== null && (await response.json()).data.end !== null && @@ -90,8 +81,7 @@ test('test that updating a description of a time entry in the overview works on (await response.json()).data.task_id === null && (await response.json()).data.duration !== null && (await response.json()).data.user_id !== null && - JSON.stringify((await response.json()).data.tags) === - JSON.stringify([]) + JSON.stringify((await response.json()).data.tags) === JSON.stringify([]) ); }), ]); @@ -107,17 +97,14 @@ test('test that updating a description of a time entry in the overview works on const newTimeEntry = timeEntryRows.first(); await assertThatTimeEntryRowIsStopped(newTimeEntry); const newDescription = Math.floor(Math.random() * 1000000).toString(); - const descriptionElement = newTimeEntry.getByTestId( - 'time_entry_description' - ); + const descriptionElement = newTimeEntry.getByTestId('time_entry_description'); await descriptionElement.fill(newDescription); await Promise.all([ descriptionElement.press('Enter'), page.waitForResponse(async (response) => { return ( response.status() === 200 && - (await response.headerValue('Content-Type')) === - 'application/json' && + (await response.headerValue('Content-Type')) === 'application/json' && (await response.json()).data.id !== null && (await response.json()).data.start !== null && (await response.json()).data.end !== null && @@ -126,16 +113,13 @@ test('test that updating a description of a time entry in the overview works on (await response.json()).data.task_id === null && (await response.json()).data.duration !== null && (await response.json()).data.user_id !== null && - JSON.stringify((await response.json()).data.tags) === - JSON.stringify([]) + JSON.stringify((await response.json()).data.tags) === JSON.stringify([]) ); }), ]); }); -test('test that adding a new tag to an existing time entry works', async ({ - page, -}) => { +test('test that adding a new tag to an existing time entry works', async ({ page }) => { await goToTimeOverview(page); const timeEntryRows = page.locator('[data-testid="time_entry_row"]'); await createEmptyTimeEntry(page); @@ -152,8 +136,7 @@ test('test that adding a new tag to an existing time entry works', async ({ page.waitForResponse(async (response) => { return ( response.status() === 201 && - (await response.headerValue('Content-Type')) === - 'application/json' && + (await response.headerValue('Content-Type')) === 'application/json' && (await response.json()).data.name === newTagName ); }), @@ -163,8 +146,7 @@ test('test that adding a new tag to an existing time entry works', async ({ await page.waitForResponse(async (response) => { return ( response.status() === 200 && - (await response.headerValue('Content-Type')) === - 'application/json' && + (await response.headerValue('Content-Type')) === 'application/json' && (await response.json()).data.id !== null && (await response.json()).data.start !== null && (await response.json()).data.end !== null && @@ -187,17 +169,14 @@ test('test that updating a the start of an existing time entry in the overview w const newTimeEntry = timeEntryRows.first(); await assertThatTimeEntryRowIsStopped(newTimeEntry); await page.waitForTimeout(1500); - const timeEntryRangeElement = newTimeEntry.getByTestId( - 'time_entry_range_selector' - ); + const timeEntryRangeElement = newTimeEntry.getByTestId('time_entry_range_selector'); await timeEntryRangeElement.click(); await page.getByTestId('time_entry_range_start').first().fill('1'); await Promise.all([ page.waitForResponse(async (response) => { return ( response.status() === 200 && - (await response.headerValue('Content-Type')) === - 'application/json' && + (await response.headerValue('Content-Type')) === 'application/json' && (await response.json()).data.id !== null && // TODO! Actually check the value (await response.json()).data.start !== null && @@ -208,9 +187,7 @@ test('test that updating a the start of an existing time entry in the overview w ]); }); -test('test that updating a the duration in the overview works on blur', async ({ - page, -}) => { +test('test that updating a the duration in the overview works on blur', async ({ page }) => { await goToTimeOverview(page); const timeEntryRows = page.locator('[data-testid="time_entry_row"]'); await createEmptyTimeEntry(page); @@ -225,8 +202,7 @@ test('test that updating a the duration in the overview works on blur', async ({ page.waitForResponse(async (response) => { return ( response.status() === 200 && - (await response.headerValue('Content-Type')) === - 'application/json' && + (await response.headerValue('Content-Type')) === 'application/json' && (await response.json()).data.id !== null && // TODO! Actually check the value (await response.json()).data.start !== null && @@ -240,9 +216,7 @@ test('test that updating a the duration in the overview works on blur', async ({ }); // Test that start stop button stops running timer -test('test that starting a time entry from the overview works', async ({ - page, -}) => { +test('test that starting a time entry from the overview works', async ({ page }) => { await goToTimeOverview(page); const timeEntryRows = page.locator('[data-testid="time_entry_row"]'); await createEmptyTimeEntry(page); @@ -255,8 +229,7 @@ test('test that starting a time entry from the overview works', async ({ page.waitForResponse(async (response) => { return ( response.status() === 200 && - (await response.headerValue('Content-Type')) === - 'application/json' && + (await response.headerValue('Content-Type')) === 'application/json' && (await response.json()).data.id !== null && (await response.json()).data.start !== null && (await response.json()).data.end !== null @@ -272,8 +245,7 @@ test('test that starting a time entry from the overview works', async ({ page.waitForResponse(async (response) => { return ( response.status() === 200 && - (await response.headerValue('Content-Type')) === - 'application/json' && + (await response.headerValue('Content-Type')) === 'application/json' && (await response.json()).data.id !== null && (await response.json()).data.start !== null && (await response.json()).data.end !== null @@ -284,9 +256,7 @@ test('test that starting a time entry from the overview works', async ({ ]); }); -test('test that deleting a time entry from the overview works', async ({ - page, -}) => { +test('test that deleting a time entry from the overview works', async ({ page }) => { await goToTimeOverview(page); const timeEntryRows = page.locator('[data-testid="time_entry_row"]'); await createEmptyTimeEntry(page); @@ -302,16 +272,12 @@ test('test that deleting a time entry from the overview works', async ({ await expect(timeEntryRows).toHaveCount(0); }); -test.skip('test that load more works when the end of page is reached', async ({ - page, -}) => { +test.skip('test that load more works when the end of page is reached', async ({ page }) => { // this test is flaky when you do not need to scroll await Promise.all([ goToTimeOverview(page), page.waitForResponse( - (response) => - response.url().includes('/time-entries') && - response.status() === 200 + (response) => response.url().includes('/time-entries') && response.status() === 200 ), ]); @@ -322,18 +288,14 @@ test.skip('test that load more works when the end of page is reached', async ({ return ( response.status() === 200 && response.url().includes('before') && - (await response.headerValue('Content-Type')) === - 'application/json' && - JSON.stringify((await response.json()).data) === - JSON.stringify([]) + (await response.headerValue('Content-Type')) === 'application/json' && + JSON.stringify((await response.json()).data) === JSON.stringify([]) ); }), ]); // assert that "All time entries are loaded!" is visible on page - await expect(page.locator('body')).toHaveText( - /All time entries are loaded!/ - ); + await expect(page.locator('body')).toHaveText(/All time entries are loaded!/); }); // TODO: Test that updating the time entry start / end times works while it is running diff --git a/e2e/timetracker.spec.ts b/e2e/timetracker.spec.ts index df4a210f..c7896ca1 100644 --- a/e2e/timetracker.spec.ts +++ b/e2e/timetracker.spec.ts @@ -24,22 +24,15 @@ test('test that starting and stopping a timer without description and project wo assertThatTimerHasStarted(page), ]); await page.waitForTimeout(1500); - await Promise.all([ - stoppedTimeEntryResponse(page), - startOrStopTimerWithButton(page), - ]); + await Promise.all([stoppedTimeEntryResponse(page), startOrStopTimerWithButton(page)]); await assertThatTimerIsStopped(page); }); -test('test that starting and stopping a timer with a description works', async ({ - page, -}) => { +test('test that starting and stopping a timer with a description works', async ({ page }) => { await goToDashboard(page); // TODO: Fix flakyness by disabling description input field until timer is loaded await page.waitForTimeout(500); - await page - .getByTestId('time_entry_description') - .fill('New Time Entry Description'); + await page.getByTestId('time_entry_description').fill('New Time Entry Description'); await Promise.all([ newTimeEntryResponse(page, { description: 'New Time Entry Description', @@ -62,47 +55,29 @@ test('test that starting the time entry starts the live timer and that it keeps }) => { await goToDashboard(page); - await Promise.all([ - newTimeEntryResponse(page), - startOrStopTimerWithButton(page), - ]); + await Promise.all([newTimeEntryResponse(page), startOrStopTimerWithButton(page)]); await assertThatTimerHasStarted(page); await page.waitForTimeout(500); - const beforeTimerValue = await page - .getByTestId('time_entry_time') - .inputValue(); + const beforeTimerValue = await page.getByTestId('time_entry_time').inputValue(); await page.waitForTimeout(2000); - const afterWaitTimeValue = await page - .getByTestId('time_entry_time') - .inputValue(); + const afterWaitTimeValue = await page.getByTestId('time_entry_time').inputValue(); expect(afterWaitTimeValue).not.toEqual(beforeTimerValue); await page.reload(); await page.waitForTimeout(500); - const afterReloadTimerValue = await page - .getByTestId('time_entry_time') - .inputValue(); + const afterReloadTimerValue = await page.getByTestId('time_entry_time').inputValue(); await page.waitForTimeout(2000); - const afterReloadAfterWaitTimerValue = await page - .getByTestId('time_entry_time') - .inputValue(); + const afterReloadAfterWaitTimerValue = await page.getByTestId('time_entry_time').inputValue(); expect(afterReloadTimerValue).not.toEqual(afterReloadAfterWaitTimerValue); }); -test('test that starting and updating the description while running works', async ({ - page, -}) => { +test('test that starting and updating the description while running works', async ({ page }) => { await goToDashboard(page); - await Promise.all([ - newTimeEntryResponse(page), - startOrStopTimerWithButton(page), - ]); + await Promise.all([newTimeEntryResponse(page), startOrStopTimerWithButton(page)]); await assertThatTimerHasStarted(page); await page.waitForTimeout(500); - await page - .getByTestId('time_entry_description') - .fill('New Time Entry Description'); + await page.getByTestId('time_entry_description').fill('New Time Entry Description'); await Promise.all([ newTimeEntryResponse(page, { @@ -121,9 +96,7 @@ test('test that starting and updating the description while running works', asyn await assertThatTimerIsStopped(page); }); -test('test that starting and updating the time while running works', async ({ - page, -}) => { +test('test that starting and updating the time while running works', async ({ page }) => { await goToDashboard(page); const [createResponse] = await Promise.all([ newTimeEntryResponse(page), @@ -138,19 +111,16 @@ test('test that starting and updating the time while running works', async ({ return ( response.url().includes('/time-entries') && response.status() === 200 && - (await response.headerValue('Content-Type')) === - 'application/json' && + (await response.headerValue('Content-Type')) === 'application/json' && (await response.json()).data.id !== null && (await response.json()).data.start !== null && - (await response.json()).data.start !== - (await createResponse.json()).data.start && + (await response.json()).data.start !== (await createResponse.json()).data.start && (await response.json()).data.end === null && (await response.json()).data.project_id === null && (await response.json()).data.description === '' && (await response.json()).data.task_id === null && (await response.json()).data.user_id !== null && - JSON.stringify((await response.json()).data.tags) === - JSON.stringify([]) + JSON.stringify((await response.json()).data.tags) === JSON.stringify([]) ); }), page.getByTestId('time_entry_time').press('Enter'), @@ -158,16 +128,11 @@ test('test that starting and updating the time while running works', async ({ await expect(page.getByTestId('time_entry_time')).toHaveValue(/00:20/); await page.waitForTimeout(500); - await Promise.all([ - stoppedTimeEntryResponse(page), - startOrStopTimerWithButton(page), - ]); + await Promise.all([stoppedTimeEntryResponse(page), startOrStopTimerWithButton(page)]); await assertThatTimerIsStopped(page); }); -test('test that entering a human readable time starts the timer on blur', async ({ - page, -}) => { +test('test that entering a human readable time starts the timer on blur', async ({ page }) => { await goToDashboard(page); await page.getByTestId('time_entry_time').fill('20min'); await Promise.all([ @@ -177,18 +142,13 @@ test('test that entering a human readable time starts the timer on blur', async await expect(page.getByTestId('time_entry_time')).toHaveValue(/00:20:/); await assertThatTimerHasStarted(page); - await Promise.all([ - stoppedTimeEntryResponse(page), - startOrStopTimerWithButton(page), - ]); + await Promise.all([stoppedTimeEntryResponse(page), startOrStopTimerWithButton(page)]); await page.locator( '[data-testid="dashboard_timer"] [data-testid="timer_button"].bg-accent-300/70' ); }); -test('test that entering a number in the time range starts the timer on blur', async ({ - page, -}) => { +test('test that entering a number in the time range starts the timer on blur', async ({ page }) => { await goToDashboard(page); await page.getByTestId('time_entry_time').fill('5'); await Promise.all([ @@ -198,10 +158,7 @@ test('test that entering a number in the time range starts the timer on blur', a await expect(page.getByTestId('time_entry_time')).toHaveValue(/00:05:/); await assertThatTimerHasStarted(page); - await Promise.all([ - stoppedTimeEntryResponse(page), - startOrStopTimerWithButton(page), - ]); + await Promise.all([stoppedTimeEntryResponse(page), startOrStopTimerWithButton(page)]); await page.locator( '[data-testid="dashboard_timer"] [data-testid="timer_button"].bg-accent-300/70' ); @@ -219,10 +176,7 @@ test('test that entering a value with the format hh:mm in the time range starts await expect(page.getByTestId('time_entry_time')).toHaveValue(/12:30:/); await assertThatTimerHasStarted(page); - await Promise.all([ - stoppedTimeEntryResponse(page), - startOrStopTimerWithButton(page), - ]); + await Promise.all([stoppedTimeEntryResponse(page), startOrStopTimerWithButton(page)]); await page.locator( '[data-testid="dashboard_timer"] [data-testid="timer_button"].bg-accent-300/70' ); @@ -239,9 +193,7 @@ test('test that entering a random value in the time range does not start the tim ); }); -test('test that entering a time starts the timer on enter', async ({ - page, -}) => { +test('test that entering a time starts the timer on enter', async ({ page }) => { await goToDashboard(page); await page.getByTestId('time_entry_time').fill('20min'); await Promise.all([ @@ -249,10 +201,7 @@ test('test that entering a time starts the timer on enter', async ({ page.getByTestId('time_entry_time').press('Enter'), ]); await assertThatTimerHasStarted(page); - await Promise.all([ - stoppedTimeEntryResponse(page), - startOrStopTimerWithButton(page), - ]); + await Promise.all([stoppedTimeEntryResponse(page), startOrStopTimerWithButton(page)]); await assertThatTimerIsStopped(page); }); @@ -273,15 +222,10 @@ test('test that adding a new tag works', async ({ page }) => { await expect(page.getByRole('option', { name: newTagName })).toBeVisible(); }); -test('test that adding a new tag when the timer is running', async ({ - page, -}) => { +test('test that adding a new tag when the timer is running', async ({ page }) => { const newTagName = 'New Tag' + Math.floor(Math.random() * 10000); await goToDashboard(page); - await Promise.all([ - newTimeEntryResponse(page), - startOrStopTimerWithButton(page), - ]); + await Promise.all([newTimeEntryResponse(page), startOrStopTimerWithButton(page)]); await assertThatTimerHasStarted(page); await page.getByTestId('tag_dropdown').click(); await page.getByText('Create new tag').click(); diff --git a/e2e/utils/currentTimeEntry.ts b/e2e/utils/currentTimeEntry.ts index 720fddde..162975ec 100644 --- a/e2e/utils/currentTimeEntry.ts +++ b/e2e/utils/currentTimeEntry.ts @@ -1,9 +1,7 @@ import { expect, Page } from '@playwright/test'; export async function startOrStopTimerWithButton(page: Page) { - await page - .locator('[data-testid="dashboard_timer"] [data-testid="timer_button"]') - .click(); + await page.locator('[data-testid="dashboard_timer"] [data-testid="timer_button"]').click(); } export async function assertThatTimerHasStarted(page: Page) { @@ -20,8 +18,7 @@ export function newTimeEntryResponse( return ( response.url().includes('/time-entries') && response.status() === status && - (await response.headerValue('Content-Type')) === - 'application/json' && + (await response.headerValue('Content-Type')) === 'application/json' && (await response.json()).data.id !== null && (await response.json()).data.start !== null && (await response.json()).data.end === null && @@ -29,30 +26,23 @@ export function newTimeEntryResponse( (await response.json()).data.description === description && (await response.json()).data.task_id === null && (await response.json()).data.user_id !== null && - JSON.stringify((await response.json()).data.tags) === - JSON.stringify(tags) + JSON.stringify((await response.json()).data.tags) === JSON.stringify(tags) ); }); } export async function assertThatTimerIsStopped(page: Page) { await expect( - page.locator( - '[data-testid="dashboard_timer"] [data-testid="timer_button"]' - ) + page.locator('[data-testid="dashboard_timer"] [data-testid="timer_button"]') ).toHaveClass(/bg-accent-300\/70/); } -export async function stoppedTimeEntryResponse( - page: Page, - { description = '', tags = [] } = {} -) { +export async function stoppedTimeEntryResponse(page: Page, { description = '', tags = [] } = {}) { return page.waitForResponse(async (response) => { return ( response.status() === 200 && response.url().includes('/time-entries/') && - (await response.headerValue('Content-Type')) === - 'application/json' && + (await response.headerValue('Content-Type')) === 'application/json' && (await response.json()).data.id !== null && (await response.json()).data.start !== null && (await response.json()).data.end !== null && @@ -61,8 +51,7 @@ export async function stoppedTimeEntryResponse( (await response.json()).data.task_id === null && (await response.json()).data.duration !== null && (await response.json()).data.user_id !== null && - JSON.stringify((await response.json()).data.tags) === - JSON.stringify(tags) + JSON.stringify((await response.json()).data.tags) === JSON.stringify(tags) ); }); } diff --git a/e2e/utils/money.ts b/e2e/utils/money.ts index 34bf83dd..aed8176c 100644 --- a/e2e/utils/money.ts +++ b/e2e/utils/money.ts @@ -14,4 +14,4 @@ export function formatCentsWithOrganizationDefaults( currencySymbol, 'point-comma' as NumberFormat ); -} \ No newline at end of file +} diff --git a/e2e/utils/tags.ts b/e2e/utils/tags.ts index a0a3a701..888d6794 100644 --- a/e2e/utils/tags.ts +++ b/e2e/utils/tags.ts @@ -4,8 +4,7 @@ export function newTagResponse(page: Page, { name = '' } = {}) { return page.waitForResponse(async (response) => { return ( response.status() === 201 && - (await response.headerValue('Content-Type')) === - 'application/json' && + (await response.headerValue('Content-Type')) === 'application/json' && (await response.json()).data.name === name ); }); diff --git a/eslint.config.mjs b/eslint.config.mjs index 9b1c982f..2d6a685a 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -3,7 +3,7 @@ import eslintConfigPrettier from 'eslint-config-prettier'; import eslintPluginVue from 'eslint-plugin-vue'; import globals from 'globals'; import typescriptEslint from 'typescript-eslint'; -import unusedImports from "eslint-plugin-unused-imports"; +import unusedImports from 'eslint-plugin-unused-imports'; export default typescriptEslint.config( { ignores: ['*.d.ts', '**/coverage', '**/dist'] }, @@ -23,18 +23,21 @@ export default typescriptEslint.config( }, }, plugins: { - "unused-imports": unusedImports, + 'unused-imports': unusedImports, }, rules: { - "vue/multi-word-component-names": "off", - "@typescript-eslint/no-unused-vars": "off", - "unused-imports/no-unused-imports": "error", - "unused-imports/no-unused-vars": ["error", { - "vars": "all", - "varsIgnorePattern": "^_", - "args": "after-used", - "argsIgnorePattern": "^_", - }], + 'vue/multi-word-component-names': 'off', + '@typescript-eslint/no-unused-vars': 'off', + 'unused-imports/no-unused-imports': 'error', + 'unused-imports/no-unused-vars': [ + 'error', + { + 'vars': 'all', + 'varsIgnorePattern': '^_', + 'args': 'after-used', + 'argsIgnorePattern': '^_', + }, + ], }, }, eslintConfigPrettier diff --git a/package.json b/package.json index 159f4fda..c7c2a120 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,9 @@ "lint:fix": "eslint --fix resources/js", "type-check": "vue-tsc --noEmit", "test:e2e": "rm -rf test-results/.auth && npx playwright test", - "zod:generate": "npx openapi-zod-client http://localhost:80/docs/api.json --output resources/js/packages/api/src/openapi.json.client.ts --base-url /api" + "zod:generate": "npx openapi-zod-client http://localhost:80/docs/api.json --output resources/js/packages/api/src/openapi.json.client.ts --base-url /api", + "format": "prettier --write './**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts,vue}'", + "format:check": "prettier --check './**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts,vue}'" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", diff --git a/playwright/config.ts b/playwright/config.ts index 06ada1f8..3d12852a 100644 --- a/playwright/config.ts +++ b/playwright/config.ts @@ -1,2 +1 @@ -export const PLAYWRIGHT_BASE_URL = - process.env.PLAYWRIGHT_BASE_URL ?? 'http://solidtime.test'; +export const PLAYWRIGHT_BASE_URL = process.env.PLAYWRIGHT_BASE_URL ?? 'http://solidtime.test'; diff --git a/playwright/fixtures.ts b/playwright/fixtures.ts index 80fadd92..93469ad3 100644 --- a/playwright/fixtures.ts +++ b/playwright/fixtures.ts @@ -8,12 +8,8 @@ export const test = baseTest.extend({ // Perform authentication steps. Replace these actions with your own. await page.goto(PLAYWRIGHT_BASE_URL + '/register'); await page.getByLabel('Name').fill('John Doe'); - await page - .getByLabel('Email') - .fill(`john+${Math.round(Math.random() * 1000000)}@doe.com`); - await page - .getByLabel('Password', { exact: true }) - .fill('amazingpassword123'); + await page.getByLabel('Email').fill(`john+${Math.round(Math.random() * 1000000)}@doe.com`); + await page.getByLabel('Password', { exact: true }).fill('amazingpassword123'); await page.getByLabel('Confirm Password').fill('amazingpassword123'); await page.getByLabel('I agree to the Terms of').click(); await page.getByRole('button', { name: 'Register' }).click(); diff --git a/resources/js/Components/ActionSection.vue b/resources/js/Components/ActionSection.vue index 6d07781e..6748475c 100644 --- a/resources/js/Components/ActionSection.vue +++ b/resources/js/Components/ActionSection.vue @@ -14,8 +14,7 @@ import SectionTitle from './SectionTitle.vue';
-
+
diff --git a/resources/js/Components/AuthenticationCard.vue b/resources/js/Components/AuthenticationCard.vue index 30ffea9b..4a8c6c17 100644 --- a/resources/js/Components/AuthenticationCard.vue +++ b/resources/js/Components/AuthenticationCard.vue @@ -1,9 +1,9 @@ diff --git a/resources/js/Components/Banner.vue b/resources/js/Components/Banner.vue index 4c69e0ff..475f0eae 100644 --- a/resources/js/Components/Banner.vue +++ b/resources/js/Components/Banner.vue @@ -24,9 +24,7 @@ watchEffect(async () => {