Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
47c2d8e
upgrade inertia v2; add prefetching; migrate queries to tanstack query
Onatcer Jan 9, 2026
6f68bbb
refactor timeentries queries and mutations, improve activitygraph, ad…
Onatcer Jan 14, 2026
ebbc4e6
use tanstack query in ProjectMultiselectDropdown, ClientTableRow and …
Onatcer Jan 14, 2026
900ee29
fix e2e project filtering in reporting e2e test
Onatcer Jan 14, 2026
b2a04c8
load time entries above pagination limit for calendar, fixes #995
Onatcer Jan 14, 2026
79999fd
remove redundant projects pinia store after tanstack query migration
Onatcer Jan 14, 2026
3fb75ec
add outline and secondary variants to TimeTrackerStartStop button to …
Onatcer Jan 15, 2026
672c243
add command palette
Onatcer Jan 27, 2026
99400ca
fix: display custom billable rate correctly on project detail page
Onatcer Jan 27, 2026
44bcce9
fix styling inconsistencies
Onatcer Jan 27, 2026
bca1e8b
migrate select/multiselect components to Radix Vue primitives
Onatcer Feb 1, 2026
8524e01
refactor: extract ReportingFilterBar and migrate reporting to TanStac…
Onatcer Feb 2, 2026
66dfc51
add no project, no task, no client, no task, no tag support to the API
Onatcer Feb 2, 2026
fe8c7e9
Update openapi api client spec
Onatcer Feb 2, 2026
0d3978a
Add reporting e2e helpers and detailed tests
Onatcer Feb 2, 2026
7266272
Add client_ids filter to time entry export
Onatcer Feb 2, 2026
1597b54
Enable npm workspaces and update dependencies
Onatcer Feb 2, 2026
98634f4
fix Y-Label ui regression from echarts update
Onatcer Feb 2, 2026
18989a9
Add Mailpit SMTP and refine Playwright tests
Onatcer Feb 2, 2026
09c3205
Allow NONE filter value to shared reports and add shared-report tests
Onatcer Feb 2, 2026
a58becc
Add calendar query prefetch
Onatcer Feb 3, 2026
03e0377
fix responsive issues in timetracker recently tracked entries dropdown
Onatcer Feb 3, 2026
9be97a8
Improve Time page responsiveness and compact tags, fixes #896
Onatcer Feb 3, 2026
7d068fe
fix admin panel time entry save and update, fixes #997
Onatcer Feb 4, 2026
22f3af2
Make sure that time entry billable status updates when project changes,
Onatcer Feb 4, 2026
f82f5e7
fix desync of checkboxes on the reporting detailed page, fixes #892
Onatcer Feb 4, 2026
6668106
migrate datepickers to shadcn, Fixes #877, #807
Onatcer Feb 5, 2026
d264411
Allow updating public_until on already-public reports
Onatcer Feb 5, 2026
bbe05ca
improve time estimate input, responsive time entry create modal fixes,
Onatcer Feb 6, 2026
1ecb332
Expand e2e test coverage migrate to API-based data setup
Onatcer Feb 9, 2026
fd012e7
Add Field component system and migrate UI
Onatcer Feb 10, 2026
2159571
Add Euro Symbol as Billable Icon when EUR is the organization currency.
Onatcer Feb 10, 2026
2c4af95
Add Tag Edit Modal and UI
Onatcer Feb 10, 2026
d06b063
add sharding for e2e tests in CI
Onatcer Feb 10, 2026
6c319fa
add e2e tests for employee restrictions
Onatcer Feb 10, 2026
6f8f46f
use frankenphp in the playwright tests CI to handle parallel requests
Onatcer Feb 10, 2026
e038870
fix reporting tab selectors in e2e test
Onatcer Feb 10, 2026
dc98151
responsive time entry modal fixes
Onatcer Feb 10, 2026
bfc33b4
make sure that 404 current time entry requests do not override local
Onatcer Feb 10, 2026
abfa7ce
improve format settings e2e test consistency; improve euro icon sizing
Onatcer Feb 10, 2026
3c9159f
Conditionally show cost column in report tables; Task/Project Modal
Onatcer Feb 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .env.ci
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ SESSION_DRIVER=database
SESSION_LIFETIME=120

# Mail
MAIL_MAILER=log
MAIL_MAILER=smtp
MAIL_HOST=localhost
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="no-reply@solidtime.test"
MAIL_FROM_NAME="solidtime"
MAIL_REPLY_TO_ADDRESS="hello@solidtime.test"
Expand All @@ -56,3 +61,6 @@ TELESCOPE_ENABLED=false

# Services
GOTENBERG_URL=http://0.0.0.0:3000

# Octane
OCTANE_SERVER=frankenphp
63 changes: 56 additions & 7 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,18 @@ jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4]
shardTotal: [4]

services:
mailpit:
image: 'axllent/mailpit:latest'
ports:
- 1025:1025
- 8025:8025
pgsql_test:
image: postgres:15
env:
Expand Down Expand Up @@ -57,22 +65,63 @@ jobs:
- name: "Build Frontend"
run: npm run build

- name: "Run Laravel Server"
run: php artisan serve > /dev/null 2>&1 &
- name: "Install FrankenPHP"
run: |
ARCH="$(uname -m)"
curl -fsSL "https://github.com/dunglas/frankenphp/releases/latest/download/frankenphp-linux-${ARCH}" -o /usr/local/bin/frankenphp
chmod +x /usr/local/bin/frankenphp

- name: "Run Laravel Octane Server"
run: php artisan octane:start --server=frankenphp --host=127.0.0.1 --port=8000 --workers=4 --max-requests=500 > /dev/null 2>&1 &
env:
OCTANE_SERVER: frankenphp

- name: "Install Playwright Browsers"
run: npx playwright install --with-deps

- name: "Run Playwright tests"
run: npx playwright test
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
env:
PLAYWRIGHT_BASE_URL: 'http://127.0.0.1:8000'
MAILPIT_BASE_URL: 'http://localhost:8025'

- name: "Upload test results"
- name: "Upload blob report"
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: test-results/
retention-days: 30
name: blob-report-${{ matrix.shardIndex }}
path: blob-report/
retention-days: 7

merge-reports:
if: always()
needs: [test]
runs-on: ubuntu-latest
steps:
- name: "Checkout code"
uses: actions/checkout@v4

- name: "Setup node"
uses: actions/setup-node@v4
with:
node-version: '20.x'

- name: "Install dependencies"
run: npm ci

- name: "Download blob reports"
uses: actions/download-artifact@v4
with:
path: all-blob-reports
pattern: blob-report-*
merge-multiple: true

- name: "Merge reports"
run: npx playwright merge-reports --reporter html ./all-blob-reports

- name: "Upload merged HTML report"
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 30
18 changes: 14 additions & 4 deletions app/Filament/Resources/TimeEntryResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace App\Filament\Resources;

use App\Filament\Resources\TimeEntryResource\Pages;
use App\Models\Member;
use App\Models\TimeEntry;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\Select;
Expand All @@ -16,6 +17,7 @@
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;

class TimeEntryResource extends Resource
{
Expand Down Expand Up @@ -51,15 +53,23 @@ public static function form(Form $form): Form
->rules([
'after_or_equal:start',
]),
Select::make('user_id')
->relationship(name: 'user', titleAttribute: 'email')
->searchable(['name', 'email'])
Select::make('member_id')
->relationship(
name: 'member',
titleAttribute: 'id',
modifyQueryUsing: fn (Builder $query) => $query->with(['user', 'organization'])
)
->getOptionLabelFromRecordUsing(fn (Member $record): string => $record->user->email.' ('.$record->organization->name.')')
->searchable()
->required(),
Select::make('project_id')
->relationship(name: 'project', titleAttribute: 'name')
->searchable(['name'])
->nullable(),
// TODO
Select::make('task_id')
->relationship(name: 'task', titleAttribute: 'name')
->searchable(['name'])
->nullable(),
]);
}

Expand Down
19 changes: 19 additions & 0 deletions app/Filament/Resources/TimeEntryResource/Pages/CreateTimeEntry.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,28 @@
namespace App\Filament\Resources\TimeEntryResource\Pages;

use App\Filament\Resources\TimeEntryResource;
use App\Models\Member;
use Filament\Resources\Pages\CreateRecord;

class CreateTimeEntry extends CreateRecord
{
protected static string $resource = TimeEntryResource::class;

/**
* @param array<string, mixed> $data
* @return array<string, mixed>
*/
protected function mutateFormDataBeforeCreate(array $data): array
{
if (isset($data['member_id'])) {
/** @var Member|null $member */
$member = Member::query()->find($data['member_id']);
if ($member !== null) {
$data['user_id'] = $member->user_id;
$data['organization_id'] = $member->organization_id;
}
}

return $data;
}
}
19 changes: 19 additions & 0 deletions app/Filament/Resources/TimeEntryResource/Pages/EditTimeEntry.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace App\Filament\Resources\TimeEntryResource\Pages;

use App\Filament\Resources\TimeEntryResource;
use App\Models\Member;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;

Expand All @@ -19,4 +20,22 @@ protected function getHeaderActions(): array
->icon('heroicon-m-trash'),
];
}

/**
* @param array<string, mixed> $data
* @return array<string, mixed>
*/
protected function mutateFormDataBeforeSave(array $data): array
{
if (isset($data['member_id'])) {
/** @var Member|null $member */
$member = Member::query()->find($data['member_id']);
if ($member !== null) {
$data['user_id'] = $member->user_id;
$data['organization_id'] = $member->organization_id;
}
}

return $data;
}
}
2 changes: 1 addition & 1 deletion app/Http/Controllers/Api/V1/ChartController.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public function dailyTrackedHours(Organization $organization, DashboardService $
$this->checkPermission($organization, 'charts:view:own');
$user = $this->user();

$dailyTrackedHours = $dashboardService->getDailyTrackedHours($user, $organization, 60);
$dailyTrackedHours = $dashboardService->getDailyTrackedHours($user, $organization, 100);

return response()->json($dailyTrackedHours);
}
Expand Down
3 changes: 3 additions & 0 deletions app/Http/Controllers/Api/V1/ReportController.php
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ public function update(Organization $organization, Report $report, ReportUpdateR
$report->share_secret = null;
$report->public_until = null;
}
} elseif ($report->is_public && $request->has('public_until')) {
// Allow updating expiration date on already-public reports
$report->public_until = $request->getPublicUntil();
}
$report->save();

Expand Down
40 changes: 35 additions & 5 deletions app/Http/Requests/V1/Report/ReportStoreRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
use App\Enums\Weekday;
use App\Http\Requests\V1\BaseFormRequest;
use App\Models\Organization;
use App\Service\TimeEntryFilter;
use Illuminate\Contracts\Validation\Rule as LegacyValidationRule;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;

/**
Expand All @@ -23,7 +25,7 @@ class ReportStoreRequest extends BaseFormRequest
/**
* Get the validation rules that apply to the request.
*
* @return array<string, array<string|ValidationRule|LegacyValidationRule>>
* @return array<string, array<string|ValidationRule|LegacyValidationRule|\Closure>>
*/
public function rules(): array
{
Expand Down Expand Up @@ -81,7 +83,14 @@ public function rules(): array
],
'properties.client_ids.*' => [
'string',
'uuid',
function (string $attribute, mixed $value, \Closure $fail): void {
if ($value === TimeEntryFilter::NONE_VALUE) {
return;
}
if (! Str::isUuid($value)) {
$fail('The '.$attribute.' must be a valid UUID.');
}
},
],
// Filter by project IDs, project IDs are OR combined
'properties.project_ids' => [
Expand All @@ -90,7 +99,14 @@ public function rules(): array
],
'properties.project_ids.*' => [
'string',
'uuid',
function (string $attribute, mixed $value, \Closure $fail): void {
if ($value === TimeEntryFilter::NONE_VALUE) {
return;
}
if (! Str::isUuid($value)) {
$fail('The '.$attribute.' must be a valid UUID.');
}
},
],
// Filter by tag IDs, tag IDs are OR combined
'properties.tag_ids' => [
Expand All @@ -99,15 +115,29 @@ public function rules(): array
],
'properties.tag_ids.*' => [
'string',
'uuid',
function (string $attribute, mixed $value, \Closure $fail): void {
if ($value === TimeEntryFilter::NONE_VALUE) {
return;
}
if (! Str::isUuid($value)) {
$fail('The '.$attribute.' must be a valid UUID.');
}
},
],
'properties.task_ids' => [
'nullable',
'array',
],
'properties.task_ids.*' => [
'string',
'uuid',
function (string $attribute, mixed $value, \Closure $fail): void {
if ($value === TimeEntryFilter::NONE_VALUE) {
return;
}
if (! Str::isUuid($value)) {
$fail('The '.$attribute.' must be a valid UUID.');
}
},
],
'properties.group' => [
'required',
Expand Down
Loading
Loading