diff --git a/.env.ci b/.env.ci index 986ec588..d1ef4db2 100644 --- a/.env.ci +++ b/.env.ci @@ -1,19 +1,22 @@ -APP_NAME=Laravel +APP_NAME=solidtime APP_ENV=local APP_KEY= APP_DEBUG=true APP_URL=http://localhost +APP_FORCE_HTTPS=false +SESSION_SECURE_COOKIE=false LOG_CHANNEL=stack LOG_DEPRECATIONS_CHANNEL=null LOG_LEVEL=debug -DB_CONNECTION=sqlite -DB_HOST=127.0.0.1 -DB_PORT=5432 -DB_DATABASE=laravel -DB_USERNAME=root -DB_PASSWORD=root +DB_CONNECTION=pgsql_test + +DB_TEST_HOST=127.0.0.1 +DB_TEST_PORT=5432 +DB_TEST_DATABASE=laravel +DB_TEST_USERNAME=root +DB_TEST_PASSWORD=root BROADCAST_DRIVER=log CACHE_DRIVER=file diff --git a/.env.example b/.env.example index d3e03c26..64023886 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,7 @@ APP_KEY=base64:UNQNf1SXeASNkWux01Rj8EnHYx8FO0kAxWNDwktclkk= APP_DEBUG=true APP_URL=https://solidtime.test APP_FORCE_HTTPS=true +SESSION_SECURE_COOKIE=true SUPER_ADMINS=admin@example.com @@ -12,12 +13,19 @@ LOG_DEPRECATIONS_CHANNEL=null LOG_LEVEL=debug DB_CONNECTION=pgsql + DB_HOST=pgsql DB_PORT=5432 DB_DATABASE=laravel DB_USERNAME=root DB_PASSWORD=root +DB_TEST_HOST=pgsql_test +DB_TEST_PORT=5432 +DB_TEST_DATABASE=laravel +DB_TEST_USERNAME=root +DB_TEST_PASSWORD=root + BROADCAST_DRIVER=log CACHE_DRIVER=file FILESYSTEM_DISK=local @@ -37,7 +45,7 @@ MAIL_PORT=1025 MAIL_USERNAME=null MAIL_PASSWORD=null MAIL_ENCRYPTION=null -MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_ADDRESS="no-reply@solidtime.test" MAIL_FROM_NAME="${APP_NAME}" AWS_ACCESS_KEY_ID= diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 93cf0f8a..2bb9b68b 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -2,11 +2,7 @@ require("@rushstack/eslint-patch/modern-module-resolution") module.exports = { - extends: [ - 'plugin:vue/vue3-essential', - '@vue/eslint-config-typescript/recommended', - '@vue/eslint-config-prettier' - ], + extends: ['plugin:vue/vue3-essential', '@vue/eslint-config-typescript/recommended', '@vue/eslint-config-prettier'], rules: { 'vue/multi-word-component-names': 'off', } diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index a5d7dd47..740d2d53 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -5,7 +5,7 @@ jobs: runs-on: ubuntu-latest services: - pgsql: + pgsql_test: image: postgres:15 env: PGPASSWORD: 'root' diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 7c12dcf9..56c2afd9 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -11,6 +11,20 @@ jobs: services: mailpit: image: 'axllent/mailpit:latest' + pgsql_test: + image: postgres:15 + env: + PGPASSWORD: 'root' + POSTGRES_DB: 'laravel' + POSTGRES_USER: 'root' + POSTGRES_PASSWORD: 'root' + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 steps: - name: "Checkout code" @@ -24,20 +38,18 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: '8.3' - extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv coverage: none - name: Run composer install run: composer install -n --prefer-dist - - name: Create SQLite database - run: touch database/database.sqlite - - name: Prepare Laravel Application run: | cp .env.ci .env php artisan key:generate php artisan migrate --seed + php artisan passport:keys - name: Install dependencies run: npm ci @@ -59,7 +71,7 @@ jobs: - uses: actions/upload-artifact@v3 if: always() with: - name: playwright-report - path: playwright-report/ + name: test-results + path: test-results/ retention-days: 30 diff --git a/.gitignore b/.gitignore index e6028879..ffdee140 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ yarn-error.log /blob-report/ /playwright/.cache/ /coverage +/extensions/* diff --git a/README.md b/README.md index eb0c4796..50d0e4c1 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ cp .env.example .env ./vendor/bin/sail artisan migrate:fresh --seed +./vendor/bin/sail php artisan passport:install + ./vendor/bin/sail npm install ./vendor/bin/sail npm run build @@ -36,6 +38,7 @@ Add the following entry to your `/etc/hosts` ``` 127.0.0.1 solidtime.test 127.0.0.1 playwright.solidtime.test +127.0.0.1 mail.solidtime.test ``` ## Running E2E Tests @@ -52,6 +55,19 @@ npx playwright install npx playwright codegen solidtime.test ``` +## E2E Troubleshooting + +If the E2E tests are not working consistently and fail with a timeout during the authentication, you might want to delete the `test-results/.auth` directory to force new test accounts to be created. + +## Generate ZOD Client + +The Zodius HTTP client is generated using the following command: + +```bash + +npm run generate:zod +``` + ## Contributing This project is in a very early stage. The structure and APIs are still subject to change and not stable. diff --git a/app/Actions/Fortify/CreateNewUser.php b/app/Actions/Fortify/CreateNewUser.php index 292463ef..3702cf5a 100644 --- a/app/Actions/Fortify/CreateNewUser.php +++ b/app/Actions/Fortify/CreateNewUser.php @@ -6,9 +6,12 @@ use App\Models\Organization; use App\Models\User; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Validator; +use Illuminate\Validation\ValidationException; +use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent; use Laravel\Fortify\Contracts\CreatesNewUsers; use Laravel\Jetstream\Jetstream; @@ -20,12 +23,27 @@ class CreateNewUser implements CreatesNewUsers * Create a newly registered user. * * @param array $input + * + * @throws ValidationException */ public function create(array $input): User { Validator::make($input, [ - 'name' => ['required', 'string', 'max:255'], - 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], + 'name' => [ + 'required', + 'string', + 'max:255', + ], + 'email' => [ + 'required', + 'string', + 'email', + 'max:255', + new UniqueEloquent(User::class, 'email', function (Builder $builder): Builder { + /** @var Builder $builder */ + return $builder->where('is_placeholder', '=', false); + }), + ], 'password' => $this->passwordRules(), 'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature() ? ['accepted', 'required'] : '', ])->validate(); diff --git a/app/Actions/Jetstream/AddOrganizationMember.php b/app/Actions/Jetstream/AddOrganizationMember.php index 47f9ccfa..dd43ee5d 100644 --- a/app/Actions/Jetstream/AddOrganizationMember.php +++ b/app/Actions/Jetstream/AddOrganizationMember.php @@ -8,8 +8,11 @@ use App\Models\User; use Closure; use Illuminate\Contracts\Validation\Rule; +use Illuminate\Contracts\Validation\ValidationRule; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Validator; +use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent; use Laravel\Jetstream\Contracts\AddsTeamMembers; use Laravel\Jetstream\Events\AddingTeamMember; use Laravel\Jetstream\Events\TeamMemberAdded; @@ -21,21 +24,24 @@ class AddOrganizationMember implements AddsTeamMembers /** * Add a new team member to the given team. */ - public function add(User $user, Organization $organization, string $email, ?string $role = null): void + public function add(User $owner, Organization $organization, string $email, ?string $role = null): void { - Gate::forUser($user)->authorize('addTeamMember', $organization); + Gate::forUser($owner)->authorize('addTeamMember', $organization); $this->validate($organization, $email, $role); - $newTeamMember = Jetstream::findUserByEmailOrFail($email); + $newOrganizationMember = User::query() + ->where('email', $email) + ->where('is_placeholder', '=', false) + ->firstOrFail(); - AddingTeamMember::dispatch($organization, $newTeamMember); + AddingTeamMember::dispatch($organization, $newOrganizationMember); $organization->users()->attach( - $newTeamMember, ['role' => $role] + $newOrganizationMember, ['role' => $role] ); - TeamMemberAdded::dispatch($organization, $newTeamMember); + TeamMemberAdded::dispatch($organization, $newOrganizationMember); } /** @@ -46,9 +52,7 @@ protected function validate(Organization $organization, string $email, ?string $ Validator::make([ 'email' => $email, 'role' => $role, - ], $this->rules(), [ - 'email.exists' => __('We were unable to find a registered user with this email address.'), - ])->after( + ], $this->rules())->after( $this->ensureUserIsNotAlreadyOnTeam($organization, $email) )->validateWithBag('addTeamMember'); } @@ -56,12 +60,18 @@ protected function validate(Organization $organization, string $email, ?string $ /** * Get the validation rules for adding a team member. * - * @return array> + * @return array> */ protected function rules(): array { return array_filter([ - 'email' => ['required', 'email', 'exists:users'], + 'email' => [ + 'required', + 'email', + (new ExistsEloquent(User::class, 'email', function (Builder $builder) { + return $builder->where('is_placeholder', '=', false); + }))->withMessage(__('We were unable to find a registered user with this email address.')), + ], 'role' => Jetstream::hasRoles() ? ['required', 'string', new Role] : null, @@ -75,7 +85,7 @@ protected function ensureUserIsNotAlreadyOnTeam(Organization $team, string $emai { return function ($validator) use ($team, $email) { $validator->errors()->addIf( - $team->hasUserWithEmail($email), + $team->hasRealUserWithEmail($email), 'email', __('This user already belongs to the team.') ); diff --git a/app/Actions/Jetstream/InviteOrganizationMember.php b/app/Actions/Jetstream/InviteOrganizationMember.php index a688fffa..a73ebac2 100644 --- a/app/Actions/Jetstream/InviteOrganizationMember.php +++ b/app/Actions/Jetstream/InviteOrganizationMember.php @@ -34,6 +34,7 @@ public function invite(User $user, Organization $organization, string $email, ?s InvitingTeamMember::dispatch($organization, $email, $role); + /** @var OrganizationInvitation $invitation */ $invitation = $organization->teamInvitations()->create([ 'email' => $email, 'role' => $role, @@ -50,9 +51,7 @@ protected function validate(Organization $organization, string $email, ?string $ Validator::make([ 'email' => $email, 'role' => $role, - ], $this->rules($organization), [ - 'email.unique' => __('This user has already been invited to the team.'), - ])->after( + ], $this->rules($organization))->after( $this->ensureUserIsNotAlreadyOnTeam($organization, $email) )->validateWithBag('addTeamMember'); } @@ -68,10 +67,10 @@ protected function rules(Organization $organization): array 'email' => [ 'required', 'email', - new UniqueEloquent(OrganizationInvitation::class, 'email', function (Builder $builder) use ($organization) { + (new UniqueEloquent(OrganizationInvitation::class, 'email', function (Builder $builder) use ($organization) { /** @var Builder $builder */ return $builder->whereBelongsTo($organization, 'organization'); - }), + }))->withMessage(__('This user has already been invited to the team.')), ], 'role' => Jetstream::hasRoles() ? ['required', 'string', new Role] @@ -86,7 +85,7 @@ protected function ensureUserIsNotAlreadyOnTeam(Organization $organization, stri { return function ($validator) use ($organization, $email) { $validator->errors()->addIf( - $organization->hasUserWithEmail($email), + $organization->hasRealUserWithEmail($email), 'email', __('This user already belongs to the team.') ); diff --git a/app/Exceptions/Api/ApiException.php b/app/Exceptions/Api/ApiException.php new file mode 100644 index 00000000..e68bcc50 --- /dev/null +++ b/app/Exceptions/Api/ApiException.php @@ -0,0 +1,50 @@ +json([ + 'error' => true, + 'key' => $this->getKey(), + 'message' => $this->getTranslatedMessage(), + ], 400); + } + + /** + * Get the key for the exception. + */ + public function getKey(): string + { + $key = static::KEY; + + if ($key === ApiException::KEY) { + throw new LogicException('API exceptions need the KEY constant defined.'); + } + + return $key; + } + + /** + * Get the translated message for the exception. + */ + public function getTranslatedMessage(): string + { + return __('exceptions.api.'.$this->getKey()); + } +} diff --git a/app/Exceptions/Api/TimeEntryStillRunningApiException.php b/app/Exceptions/Api/TimeEntryStillRunningApiException.php new file mode 100644 index 00000000..c153534c --- /dev/null +++ b/app/Exceptions/Api/TimeEntryStillRunningApiException.php @@ -0,0 +1,10 @@ +json([ - 'error' => true, - 'message' => $this->getMessage(), - ], 400); - } -} diff --git a/app/Exceptions/TimeEntryStillRunning.php b/app/Exceptions/TimeEntryStillRunning.php deleted file mode 100644 index a4ee00a2..00000000 --- a/app/Exceptions/TimeEntryStillRunning.php +++ /dev/null @@ -1,9 +0,0 @@ -schema([ - // + TextInput::make('name') + ->label('Name') + ->required(), + Select::make('organization_id') + ->relationship(name: 'organization', titleAttribute: 'name') + ->label('Organization') + ->searchable(['name']) + ->required(), ]); } diff --git a/app/Filament/Resources/OrganizationResource.php b/app/Filament/Resources/OrganizationResource.php index 07664a41..080a5993 100644 --- a/app/Filament/Resources/OrganizationResource.php +++ b/app/Filament/Resources/OrganizationResource.php @@ -5,12 +5,21 @@ namespace App\Filament\Resources; use App\Filament\Resources\OrganizationResource\Pages; +use App\Filament\Resources\OrganizationResource\RelationManagers\UsersRelationManager; use App\Models\Organization; +use App\Service\Import\Importers\ImporterProvider; +use App\Service\Import\Importers\ImportException; +use App\Service\Import\Importers\ReportDto; +use App\Service\Import\ImportService; use Filament\Forms; +use Filament\Forms\Components\Select; use Filament\Forms\Form; +use Filament\Notifications\Notification; use Filament\Resources\Resource; use Filament\Tables; +use Filament\Tables\Actions\Action; use Filament\Tables\Table; +use Illuminate\Support\Facades\Storage; class OrganizationResource extends Resource { @@ -60,6 +69,55 @@ public static function table(Table $table): Table ]) ->actions([ Tables\Actions\EditAction::make(), + Action::make('Import') + ->icon('heroicon-o-inbox-arrow-down') + ->action(function (Organization $record, array $data) { + try { + /** @var ReportDto $report */ + $report = app(ImportService::class)->import( + $record, + $data['type'], + Storage::disk(config('filament.default_filesystem_disk'))->get($data['file']) + ); + Notification::make() + ->title('Import successful') + ->success() + ->body( + 'Imported time entries: '.$report->timeEntriesCreated.'
'. + 'Imported clients: '.$report->clientsCreated.'
'. + 'Imported projects: '.$report->projectsCreated.'
'. + 'Imported tasks: '.$report->tasksCreated.'
'. + 'Imported tags: '.$report->tagsCreated.'
'. + 'Imported users: '.$report->usersCreated + ) + ->persistent() + ->send(); + } catch (ImportException $exception) { + report($exception); + Notification::make() + ->title('Import failed, changes rolled back') + ->danger() + ->body('Message: '.$exception->getMessage()) + ->persistent() + ->send(); + } + }) + ->tooltip(fn (Organization $record): string => 'Import into '.$record->name) + ->form([ + Forms\Components\FileUpload::make('file') + ->label('File') + ->required(), + Select::make('type') + ->required() + ->options(function (): array { + $select = []; + foreach (app(ImporterProvider::class)->getImporterKeys() as $key) { + $select[$key] = $key; + } + + return $select; + }), + ]), ]) ->bulkActions([ Tables\Actions\BulkActionGroup::make([ @@ -71,7 +129,7 @@ public static function table(Table $table): Table public static function getRelations(): array { return [ - // + UsersRelationManager::class, ]; } diff --git a/app/Filament/Resources/OrganizationResource/RelationManagers/UsersRelationManager.php b/app/Filament/Resources/OrganizationResource/RelationManagers/UsersRelationManager.php new file mode 100644 index 00000000..a923e49e --- /dev/null +++ b/app/Filament/Resources/OrganizationResource/RelationManagers/UsersRelationManager.php @@ -0,0 +1,51 @@ +schema([ + Forms\Components\TextInput::make('name') + ->required() + ->maxLength(255), + ]); + } + + public function table(Table $table): Table + { + return $table + ->recordTitleAttribute('name') + ->columns([ + Tables\Columns\TextColumn::make('name'), + Tables\Columns\TextColumn::make('role'), + ]) + ->filters([ + // + ]) + ->headerActions([ + Tables\Actions\CreateAction::make(), + ]) + ->actions([ + Tables\Actions\EditAction::make(), + Tables\Actions\DeleteAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } +} diff --git a/app/Filament/Resources/TaskResource.php b/app/Filament/Resources/TaskResource.php index 0ed06bd6..d4be3651 100644 --- a/app/Filament/Resources/TaskResource.php +++ b/app/Filament/Resources/TaskResource.php @@ -6,9 +6,12 @@ use App\Filament\Resources\TaskResource\Pages; use App\Models\Task; +use Filament\Forms; +use Filament\Forms\Components\Select; use Filament\Forms\Form; use Filament\Resources\Resource; use Filament\Tables; +use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Table; class TaskResource extends Resource @@ -25,7 +28,18 @@ public static function form(Form $form): Form { return $form ->schema([ - // + Forms\Components\TextInput::make('name') + ->label('Name') + ->required() + ->maxLength(255), + Select::make('project_id') + ->relationship(name: 'project', titleAttribute: 'name') + ->searchable(['name']) + ->required(), + Select::make('organization_id') + ->relationship(name: 'organization', titleAttribute: 'name') + ->searchable(['name']) + ->required(), ]); } @@ -46,7 +60,9 @@ public static function table(Table $table): Table ->sortable(), ]) ->filters([ - // + SelectFilter::make('organization') + ->relationship('organization', 'name') + ->searchable(), ]) ->defaultSort('created_at', 'desc') ->actions([ diff --git a/app/Filament/Resources/TimeEntryResource.php b/app/Filament/Resources/TimeEntryResource.php index ca70c0db..56ac3898 100644 --- a/app/Filament/Resources/TimeEntryResource.php +++ b/app/Filament/Resources/TimeEntryResource.php @@ -14,6 +14,7 @@ use Filament\Resources\Resource; use Filament\Tables; use Filament\Tables\Columns\TextColumn; +use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Table; class TimeEntryResource extends Resource @@ -67,6 +68,7 @@ public static function table(Table $table): Table return $table ->columns([ TextColumn::make('description') + ->searchable() ->label('Description'), TextColumn::make('user.email') ->label('User'), @@ -89,7 +91,9 @@ public static function table(Table $table): Table ->sortable(), ]) ->filters([ - // + SelectFilter::make('organization') + ->relationship('organization', 'name') + ->searchable(), ]) ->defaultSort('created_at', 'desc') ->actions([ diff --git a/app/Filament/Resources/UserResource.php b/app/Filament/Resources/UserResource.php index a5d3681b..c1568b10 100644 --- a/app/Filament/Resources/UserResource.php +++ b/app/Filament/Resources/UserResource.php @@ -5,12 +5,16 @@ namespace App\Filament\Resources; use App\Filament\Resources\UserResource\Pages; +use App\Filament\Resources\UserResource\RelationManagers\OrganizationsRelationManager; +use App\Filament\Resources\UserResource\RelationManagers\OwnedOrganizationsRelationManager; use App\Models\User; use Filament\Forms; +use Filament\Forms\Components\TextInput; use Filament\Forms\Form; use Filament\Resources\Resource; use Filament\Tables; use Filament\Tables\Table; +use Illuminate\Support\Facades\Hash; class UserResource extends Resource { @@ -41,10 +45,11 @@ public static function form(Form $form): Form ->label('Email') ->required() ->maxLength(255), - Forms\Components\TextInput::make('password') - ->label('Password') - ->required() + TextInput::make('password') ->password() + ->dehydrateStateUsing(fn ($state) => Hash::make($state)) + ->dehydrated(fn ($state) => filled($state)) + ->required(fn (string $context): bool => $context === 'create') ->maxLength(255), ]); } @@ -77,7 +82,8 @@ public static function table(Table $table): Table public static function getRelations(): array { return [ - // + OwnedOrganizationsRelationManager::class, + OrganizationsRelationManager::class, ]; } diff --git a/app/Filament/Resources/UserResource/Pages/CreateUser.php b/app/Filament/Resources/UserResource/Pages/CreateUser.php index 72b81bb0..b5cac367 100644 --- a/app/Filament/Resources/UserResource/Pages/CreateUser.php +++ b/app/Filament/Resources/UserResource/Pages/CreateUser.php @@ -5,9 +5,23 @@ namespace App\Filament\Resources\UserResource\Pages; use App\Filament\Resources\UserResource; +use App\Models\Organization; +use App\Models\User; use Filament\Resources\Pages\CreateRecord; class CreateUser extends CreateRecord { protected static string $resource = UserResource::class; + + protected function afterCreate(): void + { + /** @var User $user */ + $user = $this->record; + + $user->ownedTeams()->save(Organization::forceCreate([ + 'user_id' => $user->id, + 'name' => explode(' ', $user->name, 2)[0]."'s Organization", + 'personal_team' => true, + ])); + } } diff --git a/app/Filament/Resources/UserResource/RelationManagers/OrganizationsRelationManager.php b/app/Filament/Resources/UserResource/RelationManagers/OrganizationsRelationManager.php new file mode 100644 index 00000000..ac67f248 --- /dev/null +++ b/app/Filament/Resources/UserResource/RelationManagers/OrganizationsRelationManager.php @@ -0,0 +1,50 @@ +schema([ + Forms\Components\TextInput::make('name') + ->required() + ->maxLength(255), + ]); + } + + public function table(Table $table): Table + { + return $table + ->recordTitleAttribute('name') + ->columns([ + Tables\Columns\TextColumn::make('name'), + ]) + ->filters([ + // + ]) + ->headerActions([ + Tables\Actions\CreateAction::make(), + ]) + ->actions([ + Tables\Actions\EditAction::make(), + Tables\Actions\DeleteAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } +} diff --git a/app/Filament/Resources/UserResource/RelationManagers/OwnedOrganizationsRelationManager.php b/app/Filament/Resources/UserResource/RelationManagers/OwnedOrganizationsRelationManager.php new file mode 100644 index 00000000..7630ec26 --- /dev/null +++ b/app/Filament/Resources/UserResource/RelationManagers/OwnedOrganizationsRelationManager.php @@ -0,0 +1,47 @@ +schema([ + Forms\Components\TextInput::make('name') + ->required() + ->maxLength(255), + ]); + } + + public function table(Table $table): Table + { + return $table + ->recordTitleAttribute('name') + ->columns([ + Tables\Columns\TextColumn::make('name'), + ]) + ->filters([ + // + ]) + ->headerActions([ + ]) + ->actions([ + Tables\Actions\EditAction::make(), + ]) + ->bulkActions([ + ]); + } +} diff --git a/app/Http/Controllers/Api/V1/ImportController.php b/app/Http/Controllers/Api/V1/ImportController.php new file mode 100644 index 00000000..a3bd4040 --- /dev/null +++ b/app/Http/Controllers/Api/V1/ImportController.php @@ -0,0 +1,63 @@ +checkPermission($organization, 'import'); + + try { + $report = $importService->import( + $organization, + $request->input('type'), + $request->input('data') + ); + + return new JsonResponse([ + /** @var array{ + * clients: array{ + * created: int, + * }, + * projects: array{ + * created: int, + * }, + * tasks: array{ + * created: int, + * }, + * time-entries: array{ + * created: int, + * }, + * tags: array{ + * created: int, + * }, + * users: array{ + * created: int, + * } + * } $report Import report */ + 'report' => $report->toArray(), + ], 200); + } catch (ImportException $exception) { + report($exception); + + return new JsonResponse([ + 'message' => $exception->getMessage(), + ], 400); + } + } +} diff --git a/app/Http/Controllers/Api/V1/ProjectController.php b/app/Http/Controllers/Api/V1/ProjectController.php index 5ab9ceeb..bbb40d48 100644 --- a/app/Http/Controllers/Api/V1/ProjectController.php +++ b/app/Http/Controllers/Api/V1/ProjectController.php @@ -28,6 +28,8 @@ protected function checkPermission(Organization $organization, string $permissio * Get projects * * @throws AuthorizationException + * + * @operationId getProjects */ public function index(Organization $organization): JsonResource { @@ -43,6 +45,8 @@ public function index(Organization $organization): JsonResource * Get project * * @throws AuthorizationException + * + * @operationId getProject */ public function show(Organization $organization, Project $project): JsonResource { @@ -57,6 +61,8 @@ public function show(Organization $organization, Project $project): JsonResource * Create project * * @throws AuthorizationException + * + * @operationId createProject */ public function store(Organization $organization, ProjectStoreRequest $request): JsonResource { @@ -75,6 +81,8 @@ public function store(Organization $organization, ProjectStoreRequest $request): * Update project * * @throws AuthorizationException + * + * @operationId updateProject */ public function update(Organization $organization, Project $project, ProjectUpdateRequest $request): JsonResource { @@ -90,6 +98,8 @@ public function update(Organization $organization, Project $project, ProjectUpda * Delete project * * @throws AuthorizationException + * + * @operationId deleteProject */ public function destroy(Organization $organization, Project $project): JsonResponse { diff --git a/app/Http/Controllers/Api/V1/TagController.php b/app/Http/Controllers/Api/V1/TagController.php index 58db6784..4e5bcfaa 100644 --- a/app/Http/Controllers/Api/V1/TagController.php +++ b/app/Http/Controllers/Api/V1/TagController.php @@ -27,6 +27,8 @@ protected function checkPermission(Organization $organization, string $permissio * Get tags * * @throws AuthorizationException + * + * @operationId getTags */ public function index(Organization $organization): TagCollection { @@ -44,6 +46,8 @@ public function index(Organization $organization): TagCollection * Create tag * * @throws AuthorizationException + * + * @operationId createTag */ public function store(Organization $organization, TagStoreRequest $request): TagResource { @@ -61,6 +65,8 @@ public function store(Organization $organization, TagStoreRequest $request): Tag * Update tag * * @throws AuthorizationException + * + * @operationId updateTag */ public function update(Organization $organization, Tag $tag, TagUpdateRequest $request): TagResource { @@ -76,6 +82,8 @@ public function update(Organization $organization, Tag $tag, TagUpdateRequest $r * Delete tag * * @throws AuthorizationException + * + * @operationId deleteTag */ public function destroy(Organization $organization, Tag $tag): JsonResponse { diff --git a/app/Http/Controllers/Api/V1/TimeEntryController.php b/app/Http/Controllers/Api/V1/TimeEntryController.php index 0b627b1d..a7a66d37 100644 --- a/app/Http/Controllers/Api/V1/TimeEntryController.php +++ b/app/Http/Controllers/Api/V1/TimeEntryController.php @@ -4,7 +4,7 @@ namespace App\Http\Controllers\Api\V1; -use App\Exceptions\TimeEntryStillRunning; +use App\Exceptions\Api\TimeEntryStillRunningApiException; use App\Http\Requests\V1\TimeEntry\TimeEntryIndexRequest; use App\Http\Requests\V1\TimeEntry\TimeEntryStoreRequest; use App\Http\Requests\V1\TimeEntry\TimeEntryUpdateRequest; @@ -32,6 +32,8 @@ protected function checkPermission(Organization $organization, string $permissio * Get time entries * * @throws AuthorizationException + * + * @operationId getTimeEntries */ public function index(Organization $organization, TimeEntryIndexRequest $request): JsonResource { @@ -102,7 +104,9 @@ public function index(Organization $organization, TimeEntryIndexRequest $request /** * Create time entry * - * @throws AuthorizationException|TimeEntryStillRunning + * @throws AuthorizationException|TimeEntryStillRunningApiException + * + * @operationId createTimeEntry */ public function store(Organization $organization, TimeEntryStoreRequest $request): JsonResource { @@ -114,13 +118,12 @@ public function store(Organization $organization, TimeEntryStoreRequest $request if ($request->get('end') === null && TimeEntry::query()->where('user_id', $request->get('user_id'))->where('end', null)->exists()) { // TODO: API documentation - // TODO: Create concept for api exceptions - throw new TimeEntryStillRunning('User already has an active time entry'); + throw new TimeEntryStillRunningApiException(); } $timeEntry = new TimeEntry(); $timeEntry->fill($request->validated()); - $timeEntry->description = $request->get('description', ''); + $timeEntry->description = $request->get('description') ?? ''; $timeEntry->organization()->associate($organization); $timeEntry->save(); @@ -131,6 +134,8 @@ public function store(Organization $organization, TimeEntryStoreRequest $request * Update time entry * * @throws AuthorizationException + * + * @operationId updateTimeEntry */ public function update(Organization $organization, TimeEntry $timeEntry, TimeEntryUpdateRequest $request): JsonResource { @@ -141,6 +146,7 @@ public function update(Organization $organization, TimeEntry $timeEntry, TimeEnt } $timeEntry->fill($request->validated()); + $timeEntry->description = $request->get('description', $timeEntry->description) ?? ''; $timeEntry->save(); return new TimeEntryResource($timeEntry); @@ -150,6 +156,8 @@ public function update(Organization $organization, TimeEntry $timeEntry, TimeEnt * Delete time entry * * @throws AuthorizationException + * + * @operationId deleteTimeEntry */ public function destroy(Organization $organization, TimeEntry $timeEntry): JsonResponse { diff --git a/app/Http/Controllers/Api/V1/UserController.php b/app/Http/Controllers/Api/V1/UserController.php new file mode 100644 index 00000000..94ff6f2e --- /dev/null +++ b/app/Http/Controllers/Api/V1/UserController.php @@ -0,0 +1,56 @@ +checkPermission($organization, 'users:view'); + + $users = $organization->users() + ->paginate(); + + return UserCollection::make($users); + } + + /** + * Invite a placeholder user to become a real user in the organization + * + * @throws AuthorizationException|UserNotPlaceholderApiException + */ + public function invitePlaceholder(Organization $organization, User $user, Request $request): JsonResponse + { + $this->checkPermission($organization, 'users:invite-placeholder'); + + if (! $user->is_placeholder) { + throw new UserNotPlaceholderApiException(); + } + + app(InvitesTeamMembers::class)->invite( + $request->user(), + $organization, + $user->email, + 'employee' + ); + + return response()->json($user); + } +} diff --git a/app/Http/Middleware/ValidateSignature.php b/app/Http/Middleware/ValidateSignature.php index 0b3a971f..d979b895 100644 --- a/app/Http/Middleware/ValidateSignature.php +++ b/app/Http/Middleware/ValidateSignature.php @@ -13,7 +13,7 @@ class ValidateSignature extends Middleware * * @var array */ - protected $except = [ + protected array $except = [ // 'fbclid', // 'utm_campaign', // 'utm_content', diff --git a/app/Http/Requests/V1/Import/ImportRequest.php b/app/Http/Requests/V1/Import/ImportRequest.php new file mode 100644 index 00000000..523324b7 --- /dev/null +++ b/app/Http/Requests/V1/Import/ImportRequest.php @@ -0,0 +1,30 @@ +> + */ + public function rules(): array + { + return [ + 'type' => [ + 'required', + 'string', + ], + 'data' => [ + 'required', + 'string', + ], + ]; + } +} diff --git a/app/Http/Requests/V1/Project/ProjectStoreRequest.php b/app/Http/Requests/V1/Project/ProjectStoreRequest.php index d608e0c4..ca57a537 100644 --- a/app/Http/Requests/V1/Project/ProjectStoreRequest.php +++ b/app/Http/Requests/V1/Project/ProjectStoreRequest.php @@ -6,6 +6,7 @@ use App\Models\Client; use App\Models\Organization; +use App\Rules\ColorRule; use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Database\Eloquent\Builder; use Illuminate\Foundation\Http\FormRequest; @@ -25,6 +26,7 @@ public function rules(): array { return [ 'name' => [ + // TODO: unique 'required', 'string', 'min:1', @@ -34,6 +36,7 @@ public function rules(): array 'required', 'string', 'max:255', + new ColorRule(), ], 'client_id' => [ 'nullable', diff --git a/app/Http/Requests/V1/Project/ProjectUpdateRequest.php b/app/Http/Requests/V1/Project/ProjectUpdateRequest.php index 69b6661e..82259815 100644 --- a/app/Http/Requests/V1/Project/ProjectUpdateRequest.php +++ b/app/Http/Requests/V1/Project/ProjectUpdateRequest.php @@ -6,6 +6,7 @@ use App\Models\Client; use App\Models\Organization; +use App\Rules\ColorRule; use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Database\Eloquent\Builder; use Illuminate\Foundation\Http\FormRequest; @@ -25,6 +26,7 @@ public function rules(): array { return [ 'name' => [ + // TODO: unique 'required', 'string', 'max:255', @@ -33,6 +35,7 @@ public function rules(): array 'required', 'string', 'max:255', + new ColorRule(), ], 'client_id' => [ 'nullable', diff --git a/app/Http/Requests/V1/Tag/TagStoreRequest.php b/app/Http/Requests/V1/Tag/TagStoreRequest.php index 27955e19..07373f3e 100644 --- a/app/Http/Requests/V1/Tag/TagStoreRequest.php +++ b/app/Http/Requests/V1/Tag/TagStoreRequest.php @@ -18,6 +18,7 @@ public function rules(): array { return [ 'name' => [ + // TODO: unique 'required', 'string', 'min:1', diff --git a/app/Http/Requests/V1/Tag/TagUpdateRequest.php b/app/Http/Requests/V1/Tag/TagUpdateRequest.php index a0f5a1db..a54a31ee 100644 --- a/app/Http/Requests/V1/Tag/TagUpdateRequest.php +++ b/app/Http/Requests/V1/Tag/TagUpdateRequest.php @@ -18,6 +18,7 @@ public function rules(): array { return [ 'name' => [ + // TODO: unique 'required', 'string', 'min:1', diff --git a/app/Http/Requests/V1/TimeEntry/TimeEntryIndexRequest.php b/app/Http/Requests/V1/TimeEntry/TimeEntryIndexRequest.php index 0d3bb8be..de310a2d 100644 --- a/app/Http/Requests/V1/TimeEntry/TimeEntryIndexRequest.php +++ b/app/Http/Requests/V1/TimeEntry/TimeEntryIndexRequest.php @@ -30,10 +30,7 @@ public function rules(): array 'uuid', new ExistsEloquent(User::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ - return $builder->whereHas('organizations', function (Builder $builder) { - /** @var Builder $builder */ - return $builder->whereKey($this->organization->getKey()); - }); + return $builder->belongsToOrganization($this->organization); }), ], // Filter only time entries that have a start date before (not including) the given date (example: 2021-12-31) @@ -51,7 +48,8 @@ public function rules(): array ], // Filter only time entries that are active (have no end date, are still running) 'active' => [ - 'boolean', + 'string', + 'in:true,false', ], // Limit the number of returned time entries 'limit' => [ diff --git a/app/Http/Requests/V1/TimeEntry/TimeEntryStoreRequest.php b/app/Http/Requests/V1/TimeEntry/TimeEntryStoreRequest.php index b2441819..1a7850bd 100644 --- a/app/Http/Requests/V1/TimeEntry/TimeEntryStoreRequest.php +++ b/app/Http/Requests/V1/TimeEntry/TimeEntryStoreRequest.php @@ -33,10 +33,7 @@ public function rules(): array 'uuid', new ExistsEloquent(User::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ - return $builder->whereHas('organizations', function (Builder $builder) { - /** @var Builder $builder */ - return $builder->whereKey($this->organization->getKey()); - }); + return $builder->belongsToOrganization($this->organization); }), ], // ID of the task that the time entry should belong to @@ -64,7 +61,7 @@ public function rules(): array 'description' => [ 'nullable', 'string', - 'max:255', + 'max:500', ], // List of tag IDs 'tags' => [ diff --git a/app/Http/Requests/V1/TimeEntry/TimeEntryUpdateRequest.php b/app/Http/Requests/V1/TimeEntry/TimeEntryUpdateRequest.php index f044ce72..7e55ded5 100644 --- a/app/Http/Requests/V1/TimeEntry/TimeEntryUpdateRequest.php +++ b/app/Http/Requests/V1/TimeEntry/TimeEntryUpdateRequest.php @@ -42,7 +42,7 @@ public function rules(): array ], // End of time entry (ISO 8601 format, UTC timezone) 'end' => [ - 'required', + 'present', 'nullable', 'date', // TODO 'after:start', @@ -51,7 +51,7 @@ public function rules(): array 'description' => [ 'nullable', 'string', - 'max:255', + 'max:500', ], // List of tag IDs 'tags' => [ diff --git a/app/Http/Requests/V1/User/UserIndexRequest.php b/app/Http/Requests/V1/User/UserIndexRequest.php new file mode 100644 index 00000000..f600d01b --- /dev/null +++ b/app/Http/Requests/V1/User/UserIndexRequest.php @@ -0,0 +1,26 @@ +> + */ + public function rules(): array + { + return [ + ]; + } +} diff --git a/app/Http/Resources/V1/TimeEntry/TimeEntryResource.php b/app/Http/Resources/V1/TimeEntry/TimeEntryResource.php index 9003dae1..146ba44f 100644 --- a/app/Http/Resources/V1/TimeEntry/TimeEntryResource.php +++ b/app/Http/Resources/V1/TimeEntry/TimeEntryResource.php @@ -30,8 +30,8 @@ public function toArray(Request $request): array /** * @var string|null $end End of time entry (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z) */ - 'end' => $this->formatDateTime($this->resource->start), - /** @var int $duration Duration of time entry in seconds */ + 'end' => $this->formatDateTime($this->resource->end), + /** @var int|null $duration Duration of time entry in seconds */ 'duration' => $this->resource->getDuration()?->seconds, /** @var string|null $description Description of time entry */ 'description' => $this->resource->description, @@ -42,7 +42,7 @@ public function toArray(Request $request): array /** @var string $user_id ID of user */ 'user_id' => $this->resource->user_id, /** @var array $tags List of tag IDs */ - 'tags' => $this->resource->tags, + 'tags' => $this->resource->tags ?? [], ]; } } diff --git a/app/Http/Resources/V1/User/UserCollection.php b/app/Http/Resources/V1/User/UserCollection.php new file mode 100644 index 00000000..e9461a8f --- /dev/null +++ b/app/Http/Resources/V1/User/UserCollection.php @@ -0,0 +1,17 @@ +> + */ + public function toArray(Request $request): array + { + /** @var Membership $membership */ + $membership = $this->resource->getRelationValue('membership'); + + return [ + /** @var string $id ID */ + 'id' => $this->resource->id, + /** @var string $name Name */ + 'name' => $this->resource->name, + /** @var string $email Email */ + 'email' => $this->resource->email, + /** @var string $role Role */ + 'role' => $membership->role, + /** @var bool $is_placeholder Placeholder user for imports, user might not really exist and does not know about this placeholder membership */ + 'is_placeholder' => $this->resource->is_placeholder, + ]; + } +} diff --git a/app/Listeners/RemovePlaceholder.php b/app/Listeners/RemovePlaceholder.php new file mode 100644 index 00000000..4ba70a9d --- /dev/null +++ b/app/Listeners/RemovePlaceholder.php @@ -0,0 +1,30 @@ +where('is_placeholder', '=', true) + ->where('email', '=', $event->user->email) + ->belongsToOrganization($event->team) + ->get(); + + foreach ($placeholders as $placeholder) { + $userService->assignOrganizationEntitiesToDifferentUser($event->team, $placeholder, $event->user); + } + } +} diff --git a/app/Models/Organization.php b/app/Models/Organization.php index c44a485f..5e6a99c3 100644 --- a/app/Models/Organization.php +++ b/app/Models/Organization.php @@ -5,12 +5,15 @@ namespace App\Models; use Database\Factories\OrganizationFactory; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Laravel\Jetstream\Events\TeamCreated; use Laravel\Jetstream\Events\TeamDeleted; use Laravel\Jetstream\Events\TeamUpdated; +use Laravel\Jetstream\Jetstream; use Laravel\Jetstream\Team as JetstreamTeam; /** @@ -18,6 +21,8 @@ * @property string $name * @property bool $personal_team * @property User $owner + * @property Collection $users + * @property Collection $realUsers * * @method HasMany teamInvitations() * @method static OrganizationFactory factory() @@ -57,4 +62,43 @@ class Organization extends JetstreamTeam 'updated' => TeamUpdated::class, 'deleted' => TeamDeleted::class, ]; + + /** + * Get all the non-placeholder users of the organization including its owner. + * + * @return Collection + */ + public function allRealUsers(): Collection + { + return $this->realUsers->merge([$this->owner]); + } + + public function hasRealUserWithEmail(string $email): bool + { + return $this->allRealUsers()->contains(function (User $user) use ($email): bool { + return $user->email === $email; + }); + } + + /** + * Get all the users that belong to the team. + * + * @return BelongsToMany + */ + public function users(): BelongsToMany + { + return $this->belongsToMany(Jetstream::userModel(), Jetstream::membershipModel()) + ->withPivot('role') + ->withTimestamps() + ->as('membership'); + } + + /** + * @return BelongsToMany + */ + public function realUsers(): BelongsToMany + { + return $this->users() + ->where('is_placeholder', false); + } } diff --git a/app/Models/User.php b/app/Models/User.php index 891b3be0..a58a737f 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -6,6 +6,8 @@ use Database\Factories\UserFactory; use Filament\Panel; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsToMany; @@ -21,9 +23,16 @@ * @property string $id * @property string $name * @property string $email + * @property string|null $email_verified_at + * @property string|null $password + * @property bool $is_placeholder + * @property Collection $organizations + * @property Collection $timeEntries * * @method HasMany ownedTeams() * @method static UserFactory factory() + * @method static Builder query() + * @method Builder belongsToOrganization(Organization $organization) */ class User extends Authenticatable { @@ -64,8 +73,11 @@ class User extends Authenticatable * @var array */ protected $casts = [ + 'name' => 'string', + 'email' => 'string', 'email_verified_at' => 'datetime', 'is_admin' => 'boolean', + 'is_placeholder' => 'boolean', ]; /** @@ -94,4 +106,27 @@ public function organizations(): BelongsToMany ->withTimestamps() ->as('membership'); } + + /** + * @return HasMany + */ + public function timeEntries(): HasMany + { + return $this->hasMany(TimeEntry::class); + } + + /** + * @param Builder $builder + * @return Builder + */ + public function scopeBelongsToOrganization(Builder $builder, Organization $organization): Builder + { + return $builder->where(function (Builder $builder) use ($organization): Builder { + return $builder->whereHas('organizations', function (Builder $query) use ($organization): void { + $query->whereKey($organization->getKey()); + })->orWhereHas('ownedTeams', function (Builder $query) use ($organization): void { + $query->whereKey($organization->getKey()); + }); + }); + } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index b027f23f..2674c1bf 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -46,6 +46,7 @@ public function boot(): void Model::preventLazyLoading(! $this->app->isProduction()); Model::preventSilentlyDiscardingAttributes(! $this->app->isProduction()); + Model::preventAccessingMissingAttributes(! $this->app->isProduction()); Relation::enforceMorphMap([ 'membership' => Membership::class, 'organization' => Organization::class, @@ -74,6 +75,7 @@ public function boot(): void if (config('app.force_https', false) || App::isProduction()) { URL::forceScheme('https'); + request()->server->set('HTTPS', request()->header('X-Forwarded-Proto', 'https') === 'https' ? 'on' : 'off'); } } } diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index ee09f108..4dc848b5 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -4,10 +4,11 @@ namespace App\Providers; +use App\Listeners\RemovePlaceholder; use Illuminate\Auth\Events\Registered; use Illuminate\Auth\Listeners\SendEmailVerificationNotification; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; -use Illuminate\Support\Facades\Event; +use Laravel\Jetstream\Events\TeamMemberAdded; class EventServiceProvider extends ServiceProvider { @@ -20,6 +21,9 @@ class EventServiceProvider extends ServiceProvider Registered::class => [ SendEmailVerificationNotification::class, ], + TeamMemberAdded::class => [ + RemovePlaceholder::class, + ], ]; /** diff --git a/app/Providers/JetstreamServiceProvider.php b/app/Providers/JetstreamServiceProvider.php index e8e5bfb0..c9d6d59c 100644 --- a/app/Providers/JetstreamServiceProvider.php +++ b/app/Providers/JetstreamServiceProvider.php @@ -74,6 +74,9 @@ protected function configurePermissions(): void 'clients:delete', 'organizations:view', 'organizations:update', + 'import', + 'users:invite-placeholder', + 'users:view', ])->description('Administrator users can perform any action.'); Jetstream::role('manager', 'Manager', [ @@ -94,7 +97,8 @@ protected function configurePermissions(): void 'tags:update', 'tags:delete', 'organizations:view', - ])->description('Editor users have the ability to read, create, and update.'); + 'users:view', + ])->description('Managers have the ability to read, create, and update their own time entries as well as those of their team.'); Jetstream::role('employee', 'Employee', [ 'projects:view', @@ -104,6 +108,9 @@ protected function configurePermissions(): void 'time-entries:update:own', 'time-entries:delete:own', 'organizations:view', - ])->description('Editor users have the ability to read, create, and update.'); + ])->description('Employees have the ability to read, create, and update their own time entries.'); + + Jetstream::role('placeholder', 'Placeholder', [ + ])->description('Placeholders are used for importing data. They cannot log in and have no permissions.'); } } diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index d7e3c8fa..4ebf1e55 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -27,6 +27,10 @@ class RouteServiceProvider extends ServiceProvider public function boot(): void { RateLimiter::for('api', function (Request $request) { + if (! $this->app->isProduction()) { + return Limit::none(); + } + return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()); }); diff --git a/app/Rules/ColorRule.php b/app/Rules/ColorRule.php new file mode 100644 index 00000000..68dda354 --- /dev/null +++ b/app/Rules/ColorRule.php @@ -0,0 +1,32 @@ +isValid($value)) { + $fail(__('validation.color')); + + return; + } + } +} diff --git a/app/Service/ColorService.php b/app/Service/ColorService.php new file mode 100644 index 00000000..e9ff97c0 --- /dev/null +++ b/app/Service/ColorService.php @@ -0,0 +1,45 @@ + + */ + private const array COLORS = [ + '#ef5350', + '#ec407a', + '#ab47bc', + '#7e57c2', + '#5c6bc0', + '#42a5f5', + '#29b6f6', + '#26c6da', + '#26a69a', + '#66bb6a', + '#9ccc65', + '#d4e157', + '#ffee58', + '#ffca28', + '#ffa726', + '#ff7043', + '#8d6e63', + '#bdbdbd', + '#78909c', + ]; + + private const string VALID_REGEX = '/^#[0-9a-f]{6}$/'; + + public function getRandomColor(): string + { + return self::COLORS[array_rand(self::COLORS)]; + } + + public function isValid(string $color): bool + { + return preg_match(self::VALID_REGEX, $color) === 1; + } +} diff --git a/app/Service/Import/ImportDatabaseHelper.php b/app/Service/Import/ImportDatabaseHelper.php new file mode 100644 index 00000000..3c02285a --- /dev/null +++ b/app/Service/Import/ImportDatabaseHelper.php @@ -0,0 +1,211 @@ + + */ + private string $model; + + /** + * @var string[] + */ + private array $identifiers; + + /** + * @var array|null + */ + private ?array $mapIdentifierToKey = null; + + /** + * @var array + */ + private array $mapExternalIdentifierToInternalIdentifier = []; + + private bool $attachToExisting; + + private ?Closure $queryModifier; + + private ?Closure $afterCreate; + + private int $createdCount; + + /** + * @var array> + */ + private array $validate; + + /** + * @param class-string $model + * @param array $identifiers + * @param array> $validate + */ + public function __construct(string $model, array $identifiers, bool $attachToExisting = false, ?Closure $queryModifier = null, ?Closure $afterCreate = null, array $validate = []) + { + $this->model = $model; + $this->identifiers = $identifiers; + $this->attachToExisting = $attachToExisting; + $this->queryModifier = $queryModifier; + $this->afterCreate = $afterCreate; + $this->createdCount = 0; + $this->validate = $validate; + } + + /** + * @return Builder + */ + private function getModelInstance(): Builder + { + return (new $this->model)->query(); + } + + /** + * @param array $identifierData + * @param array $createValues + */ + private function createEntity(array $identifierData, array $createValues, ?string $externalIdentifier): string + { + $data = array_merge($identifierData, $createValues); + + $validator = Validator::make($data, $this->validate); + if ($validator->fails()) { + throw new ImportException('Invalid data: '.implode(', ', $validator->errors()->all())); + } + + $model = new $this->model(); + foreach ($data as $key => $value) { + $model->{$key} = $value; + } + $model->save(); + + if ($this->afterCreate !== null) { + ($this->afterCreate)($model); + } + + $hash = $this->getHash($identifierData); + $this->mapIdentifierToKey[$hash] = $model->getKey(); + $this->createdCount++; + + if ($externalIdentifier !== null) { + $this->mapExternalIdentifierToInternalIdentifier[$externalIdentifier] = $hash; + } + + return $model->getKey(); + } + + /** + * @param array $data + */ + private function getHash(array $data): string + { + $jsonData = json_encode($data); + if ($jsonData === false) { + throw new \RuntimeException('Failed to encode data to JSON'); + } + + return md5($jsonData); + } + + /** + * @param array $identifierData + * @param array $createValues + * + * @throws ImportException + */ + public function getKey(array $identifierData, array $createValues = [], ?string $externalIdentifier = null): string + { + $this->checkMap(); + + $this->validateIdentifierData($identifierData); + + $hash = $this->getHash($identifierData); + if ($this->attachToExisting) { + $key = $this->mapIdentifierToKey[$hash] ?? null; + if ($key !== null) { + if ($externalIdentifier !== null) { + $this->mapExternalIdentifierToInternalIdentifier[$externalIdentifier] = $hash; + } + + return $key; + } + + return $this->createEntity($identifierData, $createValues, $externalIdentifier); + } else { + throw new \RuntimeException('Not implemented'); + } + } + + /** + * @param array $identifierData + * + * @throws ImportException + */ + private function validateIdentifierData(array $identifierData): void + { + if (array_keys($identifierData) !== $this->identifiers) { + throw new ImportException('Invalid identifier data'); + } + } + + public function getKeyByExternalIdentifier(string $externalIdentifier): ?string + { + $hash = $this->mapExternalIdentifierToInternalIdentifier[$externalIdentifier] ?? null; + if ($hash === null) { + return null; + } + + return $this->mapIdentifierToKey[$hash] ?? null; + } + + /** + * @return array + */ + public function getExternalIds(): array + { + // Note: Otherwise the external ids are integers + return array_map(fn ($value) => (string) $value, array_keys($this->mapExternalIdentifierToInternalIdentifier)); + } + + private function checkMap(): void + { + if ($this->mapIdentifierToKey === null) { + $select = $this->identifiers; + $select[] = (new $this->model())->getKeyName(); + $builder = $this->getModelInstance(); + + if ($this->queryModifier !== null) { + $builder = ($this->queryModifier)($builder); + } + + $databaseEntries = $builder->select($select) + ->get(); + $this->mapIdentifierToKey = []; + foreach ($databaseEntries as $databaseEntry) { + $identifierData = []; + foreach ($this->identifiers as $identifier) { + $identifierData[$identifier] = $databaseEntry->{$identifier}; + } + $hash = $this->getHash($identifierData); + $this->mapIdentifierToKey[$hash] = $databaseEntry->getKey(); + } + } + } + + public function getCreatedCount(): int + { + return $this->createdCount; + } +} diff --git a/app/Service/Import/ImportService.php b/app/Service/Import/ImportService.php new file mode 100644 index 00000000..8f42a756 --- /dev/null +++ b/app/Service/Import/ImportService.php @@ -0,0 +1,30 @@ +getImporter($importerType); + $importer->init($organization); + DB::transaction(function () use (&$importer, &$data) { + $importer->importData($data); + }); + + return $importer->getReport(); + } +} diff --git a/app/Service/Import/Importers/ClockifyProjectsImporter.php b/app/Service/Import/Importers/ClockifyProjectsImporter.php new file mode 100644 index 00000000..d71c6cd9 --- /dev/null +++ b/app/Service/Import/Importers/ClockifyProjectsImporter.php @@ -0,0 +1,87 @@ +setHeaderOffset(0); + $reader->setDelimiter(','); + $header = $reader->getHeader(); + $this->validateHeader($header); + $records = $reader->getRecords(); + foreach ($records as $record) { + $clientId = null; + if ($record['Client'] !== '') { + $clientId = $this->clientImportHelper->getKey([ + 'name' => $record['Client'], + 'organization_id' => $this->organization->id, + ]); + } + $projectId = null; + if ($record['Name'] !== '') { + $projectId = $this->projectImportHelper->getKey([ + 'name' => $record['Name'], + 'organization_id' => $this->organization->id, + ], [ + 'client_id' => $clientId, + 'color' => $this->colorService->getRandomColor(), + ]); + } + + if ($record['Tasks'] !== '') { + $tasks = explode(', ', $record['Tasks']); + foreach ($tasks as $task) { + $this->taskImportHelper->getKey([ + 'name' => $task, + 'project_id' => $projectId, + 'organization_id' => $this->organization->id, + ]); + } + } + } + } catch (ImportException $exception) { + throw $exception; + } catch (CsvException $exception) { + throw new ImportException('Invalid CSV data'); + } catch (Exception $exception) { + report($exception); + throw new ImportException('Unknown error'); + } + } + + /** + * @param array $header + * + * @throws ImportException + */ + private function validateHeader(array $header): void + { + $requiredFields = [ + 'Name', + 'Client', + 'Status', + 'Visibility', + 'Billability', + 'Tasks', + ]; + foreach ($requiredFields as $requiredField) { + if (! in_array($requiredField, $header, true)) { + throw new ImportException('Invalid CSV header, missing field: '.$requiredField); + } + } + } +} diff --git a/app/Service/Import/Importers/ClockifyTimeEntriesImporter.php b/app/Service/Import/Importers/ClockifyTimeEntriesImporter.php new file mode 100644 index 00000000..228b54df --- /dev/null +++ b/app/Service/Import/Importers/ClockifyTimeEntriesImporter.php @@ -0,0 +1,160 @@ + + * + * @throws ImportException + */ + private function getTags(string $tags): array + { + if (trim($tags) === '') { + return []; + } + $tagsParsed = explode(', ', $tags); + $tagIds = []; + foreach ($tagsParsed as $tagParsed) { + $tagId = $this->tagImportHelper->getKey([ + 'name' => $tagParsed, + 'organization_id' => $this->organization->id, + ]); + $tagIds[] = $tagId; + } + + return $tagIds; + } + + /** + * @throws ImportException + */ + #[\Override] + public function importData(string $data): void + { + try { + $reader = Reader::createFromString($data); + $reader->setHeaderOffset(0); + $reader->setDelimiter(','); + $header = $reader->getHeader(); + $this->validateHeader($header); + $records = $reader->getRecords(); + foreach ($records as $record) { + $userId = $this->userImportHelper->getKey([ + 'email' => $record['Email'], + ], [ + 'name' => $record['User'], + 'is_placeholder' => true, + ]); + $clientId = null; + if ($record['Client'] !== '') { + $clientId = $this->clientImportHelper->getKey([ + 'name' => $record['Client'], + 'organization_id' => $this->organization->id, + ]); + } + $projectId = null; + if ($record['Project'] !== '') { + $projectId = $this->projectImportHelper->getKey([ + 'name' => $record['Project'], + 'organization_id' => $this->organization->id, + ], [ + 'client_id' => $clientId, + 'color' => $this->colorService->getRandomColor(), + ]); + } + $taskId = null; + if ($record['Task'] !== '') { + $taskId = $this->taskImportHelper->getKey([ + 'name' => $record['Task'], + 'project_id' => $projectId, + 'organization_id' => $this->organization->id, + ]); + } + $timeEntry = new TimeEntry(); + $timeEntry->user_id = $userId; + $timeEntry->task_id = $taskId; + $timeEntry->project_id = $projectId; + $timeEntry->organization_id = $this->organization->id; + if (strlen($record['Description']) > 500) { + throw new ImportException('Time entry description is too long'); + } + $timeEntry->description = $record['Description']; + if (! in_array($record['Billable'], ['Yes', 'No'], true)) { + throw new ImportException('Invalid billable value'); + } + $timeEntry->billable = $record['Billable'] === 'Yes'; + $timeEntry->tags = $this->getTags($record['Tags']); + + // Start + if (preg_match('/^[0-9]{1,2}:[0-9]{1,2} (AM|PM)$/', $record['Start Time']) === 1) { + $start = Carbon::createFromFormat('m/d/Y h:i A', $record['Start Date'].' '.$record['Start Time'], 'UTC'); + } else { + $start = Carbon::createFromFormat('m/d/Y H:i:s A', $record['Start Date'].' '.$record['Start Time'], 'UTC'); + } + if ($start === false) { + throw new ImportException('Start date ("'.$record['Start Date'].'") or time ("'.$record['Start Time'].'") are invalid'); + } + $timeEntry->start = $start; + + // End + if (preg_match('/^[0-9]{1,2}:[0-9]{1,2} (AM|PM)$/', $record['End Time']) === 1) { + $end = Carbon::createFromFormat('m/d/Y h:i A', $record['End Date'].' '.$record['End Time'], 'UTC'); + } else { + $end = Carbon::createFromFormat('m/d/Y H:i:s A', $record['End Date'].' '.$record['End Time'], 'UTC'); + } + if ($end === false) { + throw new ImportException('End date ("'.$record['End Date'].'") or time ("'.$record['End Time'].'") are invalid'); + } + $timeEntry->end = $end; + $timeEntry->save(); + $this->timeEntriesCreated++; + } + } catch (ImportException $exception) { + throw $exception; + } catch (CsvException $exception) { + throw new ImportException('Invalid CSV data'); + } catch (Exception $exception) { + report($exception); + throw new ImportException('Unknown error'); + } + } + + /** + * @param array $header + * + * @throws ImportException + */ + private function validateHeader(array $header): void + { + $requiredFields = [ + 'Project', + 'Client', + 'Description', + 'Task', + 'User', + 'Group', + 'Email', + 'Tags', + 'Billable', + 'Start Date', + 'Start Time', + 'End Date', + 'End Time', + ]; + foreach ($requiredFields as $requiredField) { + if (! in_array($requiredField, $header, true)) { + throw new ImportException('Invalid CSV header, missing field: '.$requiredField); + } + } + } +} diff --git a/app/Service/Import/Importers/DefaultImporter.php b/app/Service/Import/Importers/DefaultImporter.php new file mode 100644 index 00000000..e60cef51 --- /dev/null +++ b/app/Service/Import/Importers/DefaultImporter.php @@ -0,0 +1,114 @@ + + */ + protected ImportDatabaseHelper $userImportHelper; + + /** + * @var ImportDatabaseHelper + */ + protected ImportDatabaseHelper $projectImportHelper; + + /** + * @var ImportDatabaseHelper + */ + protected ImportDatabaseHelper $tagImportHelper; + + /** + * @var ImportDatabaseHelper + */ + protected ImportDatabaseHelper $clientImportHelper; + + /** + * @var ImportDatabaseHelper + */ + protected ImportDatabaseHelper $taskImportHelper; + + protected int $timeEntriesCreated; + + protected ColorService $colorService; + + public function init(Organization $organization): void + { + $this->organization = $organization; + $this->userImportHelper = new ImportDatabaseHelper(User::class, ['email'], true, function (Builder $builder) { + /** @var Builder $builder */ + return $builder->belongsToOrganization($this->organization); + }, function (User $user) { + $user->organizations()->attach($this->organization, [ + 'role' => 'placeholder', + ]); + }, validate: [ + 'name' => [ + 'required', + 'max:255', + ], + ]); + $this->projectImportHelper = new ImportDatabaseHelper(Project::class, ['name', 'organization_id'], true, function (Builder $builder) { + return $builder->where('organization_id', $this->organization->id); + }, validate: [ + 'name' => [ + 'required', + 'max:255', + ], + ]); + $this->tagImportHelper = new ImportDatabaseHelper(Tag::class, ['name', 'organization_id'], true, function (Builder $builder) { + return $builder->where('organization_id', $this->organization->id); + }, validate: [ + 'name' => [ + 'required', + 'max:255', + ], + ]); + $this->clientImportHelper = new ImportDatabaseHelper(Client::class, ['name', 'organization_id'], true, function (Builder $builder) { + return $builder->where('organization_id', $this->organization->id); + }, validate: [ + 'name' => [ + 'required', + 'max:255', + ], + ]); + $this->taskImportHelper = new ImportDatabaseHelper(Task::class, ['name', 'project_id', 'organization_id'], true, function (Builder $builder) { + return $builder->where('organization_id', $this->organization->id); + }, validate: [ + 'name' => [ + 'required', + 'max:500', + ], + ]); + $this->timeEntriesCreated = 0; + $this->colorService = app(ColorService::class); + } + + #[\Override] + public function getReport(): ReportDto + { + return new ReportDto( + clientsCreated: $this->clientImportHelper->getCreatedCount(), + projectsCreated: $this->projectImportHelper->getCreatedCount(), + tasksCreated: $this->taskImportHelper->getCreatedCount(), + timeEntriesCreated: $this->timeEntriesCreated, + tagsCreated: $this->tagImportHelper->getCreatedCount(), + usersCreated: $this->userImportHelper->getCreatedCount(), + ); + } +} diff --git a/app/Service/Import/Importers/ImportException.php b/app/Service/Import/Importers/ImportException.php new file mode 100644 index 00000000..00588701 --- /dev/null +++ b/app/Service/Import/Importers/ImportException.php @@ -0,0 +1,9 @@ +> + */ + private array $importers = [ + 'toggl_time_entries' => TogglTimeEntriesImporter::class, + 'toggl_data_importer' => TogglDataImporter::class, + 'clockify_time_entries' => ClockifyTimeEntriesImporter::class, + 'clockify_projects' => ClockifyProjectsImporter::class, + ]; + + /** + * @param class-string $importer + */ + public function registerImporter(string $type, string $importer): void + { + $this->importers[$type] = $importer; + } + + /** + * @return array + */ + public function getImporterKeys(): array + { + return array_keys($this->importers); + } + + public function getImporter(string $type): ImporterContract + { + if (! array_key_exists($type, $this->importers)) { + throw new \InvalidArgumentException('Invalid importer type'); + } + + return new $this->importers[$type]; + } +} diff --git a/app/Service/Import/Importers/ReportDto.php b/app/Service/Import/Importers/ReportDto.php new file mode 100644 index 00000000..5f4748af --- /dev/null +++ b/app/Service/Import/Importers/ReportDto.php @@ -0,0 +1,76 @@ +clientsCreated = $clientsCreated; + $this->projectsCreated = $projectsCreated; + $this->tasksCreated = $tasksCreated; + $this->timeEntriesCreated = $timeEntriesCreated; + $this->tagsCreated = $tagsCreated; + $this->usersCreated = $usersCreated; + } + + /** + * @return array{ + * clients: array{ + * created: int, + * }, + * projects: array{ + * created: int, + * }, + * tasks: array{ + * created: int, + * }, + * time-entries: array{ + * created: int, + * }, + * tags: array{ + * created: int, + * }, + * users: array{ + * created: int, + * } + * } + */ + public function toArray(): array + { + return [ + 'clients' => [ + 'created' => $this->clientsCreated, + ], + 'projects' => [ + 'created' => $this->projectsCreated, + ], + 'tasks' => [ + 'created' => $this->tasksCreated, + ], + 'time-entries' => [ + 'created' => $this->timeEntriesCreated, + ], + 'tags' => [ + 'created' => $this->tagsCreated, + ], + 'users' => [ + 'created' => $this->usersCreated, + ], + ]; + } +} diff --git a/app/Service/Import/Importers/TogglDataImporter.php b/app/Service/Import/Importers/TogglDataImporter.php new file mode 100644 index 00000000..d6c8415e --- /dev/null +++ b/app/Service/Import/Importers/TogglDataImporter.php @@ -0,0 +1,116 @@ +path('import.zip'), $data); + $zip->open($temporaryDirectory->path('import.zip'), ZipArchive::RDONLY); + $temporaryDirectory = TemporaryDirectory::make(); + $zip->extractTo($temporaryDirectory->path()); + $zip->close(); + $clientsFileContent = file_get_contents($temporaryDirectory->path('clients.json')); + if ($clientsFileContent === false) { + throw new ImportException('File clients.json missing in ZIP'); + } + $clients = json_decode($clientsFileContent); + $projectsFileContent = file_get_contents($temporaryDirectory->path('projects.json')); + if ($projectsFileContent === false) { + throw new ImportException('File projects.json missing in ZIP'); + } + $projects = json_decode($projectsFileContent); + $tagsFileContent = file_get_contents($temporaryDirectory->path('tags.json')); + if ($tagsFileContent === false) { + throw new ImportException('File tags.json missing in ZIP'); + } + $tags = json_decode($tagsFileContent); + $workspaceUsersFileContent = file_get_contents($temporaryDirectory->path('workspace_users.json')); + if ($workspaceUsersFileContent === false) { + throw new ImportException('File workspace_users.json missing in ZIP'); + } + $workspaceUsers = json_decode($workspaceUsersFileContent); + foreach ($clients as $client) { + $this->clientImportHelper->getKey([ + 'name' => $client->name, + 'organization_id' => $this->organization->id, + ], [], (string) $client->id); + } + foreach ($tags as $tag) { + $this->tagImportHelper->getKey([ + 'name' => $tag->name, + 'organization_id' => $this->organization->id, + ], [], (string) $tag->id); + } + + foreach ($projects as $project) { + $clientId = null; + if ($project->client_id !== null) { + $clientId = $this->clientImportHelper->getKeyByExternalIdentifier((string) $project->client_id); + if ($clientId === null) { + throw new Exception('Client does not exist'); + } + } + + if (! $this->colorService->isValid($project->color)) { + throw new ImportException('Invalid color'); + } + + $this->projectImportHelper->getKey([ + 'name' => $project->name, + 'organization_id' => $this->organization->getKey(), + ], [ + 'client_id' => $clientId, + 'color' => $project->color, + ], (string) $project->id); + } + foreach ($workspaceUsers as $workspaceUser) { + $this->userImportHelper->getKey([ + 'email' => $workspaceUser->email, + ], [ + 'name' => $workspaceUser->name, + 'is_placeholder' => true, + ], (string) $workspaceUser->id); + } + $projectIds = $this->projectImportHelper->getExternalIds(); + foreach ($projectIds as $projectIdExternal) { + $tasksFileContent = file_get_contents($temporaryDirectory->path('tasks/'.$projectIdExternal.'.json')); + if ($tasksFileContent === false) { + throw new ImportException('File tasks/'.$projectIdExternal.'.json missing in ZIP'); + } + $tasks = json_decode($tasksFileContent); + foreach ($tasks as $task) { + $projectId = $this->projectImportHelper->getKeyByExternalIdentifier((string) $projectIdExternal); + + if ($projectId === null) { + throw new Exception('Project does not exist'); + } + $this->taskImportHelper->getKey([ + 'name' => $task->name, + 'project_id' => $projectId, + 'organization_id' => $this->organization->getKey(), + ], [], (string) $task->id); + } + } + } catch (ImportException $exception) { + throw $exception; + } catch (Exception $exception) { + report($exception); + throw new ImportException('Unknown error'); + } + } +} diff --git a/app/Service/Import/Importers/TogglTimeEntriesImporter.php b/app/Service/Import/Importers/TogglTimeEntriesImporter.php new file mode 100644 index 00000000..99f301f5 --- /dev/null +++ b/app/Service/Import/Importers/TogglTimeEntriesImporter.php @@ -0,0 +1,144 @@ + + * + * @throws ImportException + */ + private function getTags(string $tags): array + { + if (trim($tags) === '') { + return []; + } + $tagsParsed = explode(', ', $tags); + $tagIds = []; + foreach ($tagsParsed as $tagParsed) { + $tagId = $this->tagImportHelper->getKey([ + 'name' => $tagParsed, + 'organization_id' => $this->organization->id, + ]); + $tagIds[] = $tagId; + } + + return $tagIds; + } + + /** + * @throws ImportException + */ + #[\Override] + public function importData(string $data): void + { + try { + $reader = Reader::createFromString($data); + $reader->setHeaderOffset(0); + $reader->setDelimiter(','); + $header = $reader->getHeader(); + $this->validateHeader($header); + $records = $reader->getRecords(); + foreach ($records as $record) { + $userId = $this->userImportHelper->getKey([ + 'email' => $record['Email'], + ], [ + 'name' => $record['User'], + 'is_placeholder' => true, + ]); + $clientId = null; + if ($record['Client'] !== '') { + $clientId = $this->clientImportHelper->getKey([ + 'name' => $record['Client'], + 'organization_id' => $this->organization->id, + ]); + } + $projectId = null; + if ($record['Project'] !== '') { + $projectId = $this->projectImportHelper->getKey([ + 'name' => $record['Project'], + 'organization_id' => $this->organization->id, + ], [ + 'client_id' => $clientId, + 'color' => $this->colorService->getRandomColor(), + ]); + } + $taskId = null; + if ($record['Task'] !== '') { + $taskId = $this->taskImportHelper->getKey([ + 'name' => $record['Task'], + 'project_id' => $projectId, + 'organization_id' => $this->organization->id, + ]); + } + $timeEntry = new TimeEntry(); + $timeEntry->user_id = $userId; + $timeEntry->task_id = $taskId; + $timeEntry->project_id = $projectId; + $timeEntry->organization_id = $this->organization->id; + $timeEntry->description = $record['Description']; + if (! in_array($record['Billable'], ['Yes', 'No'], true)) { + throw new ImportException('Invalid billable value'); + } + $timeEntry->billable = $record['Billable'] === 'Yes'; + $timeEntry->tags = $this->getTags($record['Tags']); + $start = Carbon::createFromFormat('Y-m-d H:i:s', $record['Start date'].' '.$record['Start time'], 'UTC'); + if ($start === false) { + throw new ImportException('Start date ("'.$record['Start date'].'") or time ("'.$record['Start time'].'") are invalid'); + } + $timeEntry->start = $start; + $end = Carbon::createFromFormat('Y-m-d H:i:s', $record['End date'].' '.$record['End time'], 'UTC'); + if ($end === false) { + throw new ImportException('End date ("'.$record['End date'].'") or time ("'.$record['End time'].'") are invalid'); + } + $timeEntry->end = $end; + $timeEntry->save(); + $this->timeEntriesCreated++; + } + } catch (ImportException $exception) { + throw $exception; + } catch (CsvException $exception) { + throw new ImportException('Invalid CSV data'); + } catch (Exception $exception) { + report($exception); + throw new ImportException('Unknown error'); + } + } + + /** + * @param array $header + * + * @throws ImportException + */ + private function validateHeader(array $header): void + { + $requiredFields = [ + 'User', + 'Email', + 'Client', + 'Project', + 'Task', + 'Description', + 'Billable', + 'Start date', + 'Start time', + 'End date', + 'End time', + 'Tags', + ]; + foreach ($requiredFields as $requiredField) { + if (! in_array($requiredField, $header, true)) { + throw new ImportException('Invalid CSV header, missing field: '.$requiredField); + } + } + } +} diff --git a/app/Service/UserService.php b/app/Service/UserService.php new file mode 100644 index 00000000..87be9978 --- /dev/null +++ b/app/Service/UserService.php @@ -0,0 +1,23 @@ +whereBelongsTo($organization, 'organization') + ->whereBelongsTo($fromUser, 'user') + ->update([ + 'user_id' => $toUser->getKey(), + ]); + } +} diff --git a/composer.json b/composer.json index 8b895a38..8b84fdf8 100644 --- a/composer.json +++ b/composer.json @@ -6,6 +6,7 @@ "license": "AGPL-3.0-or-later", "require": { "php": "8.3.*", + "ext-zip": "*", "dedoc/scramble": "^0.8.5", "filament/filament": "^3.2", "guzzlehttp/guzzle": "^7.2", @@ -16,6 +17,7 @@ "laravel/passport": "^11.10.2", "laravel/tinker": "^2.8", "pxlrbt/filament-environment-indicator": "^2.0", + "spatie/temporary-directory": "^2.2", "tightenco/ziggy": "^1.0", "tpetry/laravel-postgresql-enhanced": "^0.33.0" }, diff --git a/composer.lock b/composer.lock index d39541ae..f3065194 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e83929e68d256367652d91e43a79288e", + "content-hash": "9e9c41ae5787e1aa711b04cc019cb7e7", "packages": [ { "name": "anourvalar/eloquent-serialize", @@ -6542,6 +6542,67 @@ ], "time": "2024-01-11T08:43:00+00:00" }, + { + "name": "spatie/temporary-directory", + "version": "2.2.1", + "source": { + "type": "git", + "url": "https://github.com/spatie/temporary-directory.git", + "reference": "76949fa18f8e1a7f663fd2eaa1d00e0bcea0752a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/temporary-directory/zipball/76949fa18f8e1a7f663fd2eaa1d00e0bcea0752a", + "reference": "76949fa18f8e1a7f663fd2eaa1d00e0bcea0752a", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\TemporaryDirectory\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alex Vanderbist", + "email": "alex@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Easily create, use and destroy temporary directories", + "homepage": "https://github.com/spatie/temporary-directory", + "keywords": [ + "php", + "spatie", + "temporary-directory" + ], + "support": { + "issues": "https://github.com/spatie/temporary-directory/issues", + "source": "https://github.com/spatie/temporary-directory/tree/2.2.1" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2023-12-25T11:46:58+00:00" + }, { "name": "symfony/console", "version": "v6.4.4", @@ -12424,7 +12485,8 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "8.3.*" + "php": "8.3.*", + "ext-zip": "*" }, "platform-dev": [], "plugin-api-version": "2.6.0" diff --git a/config/database.php b/config/database.php index f75f0d9f..5ac598cc 100644 --- a/config/database.php +++ b/config/database.php @@ -80,6 +80,21 @@ 'sslmode' => 'prefer', ], + 'pgsql_test' => [ + 'driver' => 'pgsql', + 'url' => env('DATABASE_URL'), + 'host' => env('DB_TEST_HOST', '127.0.0.1'), + 'port' => env('DB_TEST_PORT', '5432'), + 'database' => env('DB_TEST_DATABASE', 'forge'), + 'username' => env('DB_TEST_USERNAME', 'forge'), + 'password' => env('DB_TEST_PASSWORD', ''), + 'charset' => 'utf8', + 'prefix' => '', + 'prefix_indexes' => true, + 'search_path' => 'public', + 'sslmode' => 'prefer', + ], + 'sqlsrv' => [ 'driver' => 'sqlsrv', 'url' => env('DATABASE_URL'), diff --git a/config/filesystems.php b/config/filesystems.php index d307268d..508cc76c 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -58,6 +58,12 @@ 'throw' => false, ], + 'testfiles' => [ + 'driver' => 'local', + 'root' => storage_path('tests'), + 'throw' => false, + ], + ], /* diff --git a/config/scramble.php b/config/scramble.php index 6bc087ff..eb01031c 100644 --- a/config/scramble.php +++ b/config/scramble.php @@ -64,9 +64,9 @@ * ``` */ 'servers' => [ - 'Production' => 'https://app.solidtime.io', - 'Staging' => 'https://app.staging.solidtime.io', - 'Local' => 'https://soldtime.test', + 'Production' => 'https://app.solidtime.io/api', + 'Staging' => 'https://app.staging.solidtime.io/api', + 'Local' => 'https://soldtime.test/api', ], 'middleware' => [ diff --git a/config/telescope.php b/config/telescope.php index 312fdf26..d4057545 100644 --- a/config/telescope.php +++ b/config/telescope.php @@ -98,7 +98,6 @@ ], 'ignore_paths' => [ - 'livewire*', 'nova-api*', 'pulse*', ], @@ -156,7 +155,7 @@ Watchers\LogWatcher::class => [ 'enabled' => env('TELESCOPE_LOG_WATCHER', true), - 'level' => 'error', + 'level' => 'debug', ], Watchers\MailWatcher::class => env('TELESCOPE_MAIL_WATCHER', true), diff --git a/database/factories/OrganizationFactory.php b/database/factories/OrganizationFactory.php index e0d629e4..e1efdea6 100644 --- a/database/factories/OrganizationFactory.php +++ b/database/factories/OrganizationFactory.php @@ -27,10 +27,10 @@ public function definition(): array ]; } - public function withOwner(): self + public function withOwner(?User $owner = null): self { return $this->state(fn (array $attributes) => [ - 'user_id' => User::factory(), + 'user_id' => $owner === null ? User::factory() : $owner, ]); } } diff --git a/database/factories/ProjectFactory.php b/database/factories/ProjectFactory.php index c05a79ff..f8654731 100644 --- a/database/factories/ProjectFactory.php +++ b/database/factories/ProjectFactory.php @@ -7,6 +7,7 @@ use App\Models\Client; use App\Models\Organization; use App\Models\Project; +use App\Service\ColorService; use Illuminate\Database\Eloquent\Factories\Factory; /** @@ -23,7 +24,7 @@ public function definition(): array { return [ 'name' => $this->faker->company(), - 'color' => $this->faker->hexColor(), + 'color' => app(ColorService::class)->getRandomColor(), 'organization_id' => Organization::factory(), 'client_id' => null, ]; diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 60f31ffb..893c7e65 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -31,9 +31,19 @@ public function definition(): array 'remember_token' => Str::random(10), 'profile_photo_path' => null, 'current_team_id' => null, + 'is_placeholder' => false, ]; } + public function placeholder(bool $placeholder = true): static + { + return $this->state(function (array $attributes) use ($placeholder): array { + return [ + 'is_placeholder' => $placeholder, + ]; + }); + } + /** * Indicate that the model's email address should be unverified. */ diff --git a/database/migrations/2014_10_12_000000_create_users_table.php b/database/migrations/2014_10_12_000000_create_users_table.php index 490d4d25..ffa23ea9 100644 --- a/database/migrations/2014_10_12_000000_create_users_table.php +++ b/database/migrations/2014_10_12_000000_create_users_table.php @@ -16,13 +16,17 @@ public function up(): void Schema::create('users', function (Blueprint $table) { $table->uuid('id')->primary(); $table->string('name'); - $table->string('email')->unique(); + $table->string('email'); $table->timestamp('email_verified_at')->nullable(); - $table->string('password'); + $table->string('password')->nullable(); $table->rememberToken(); + $table->boolean('is_placeholder')->default(false); $table->foreignUuid('current_team_id')->nullable(); $table->string('profile_photo_path', 2048)->nullable(); $table->timestamps(); + + $table->uniqueIndex('email') + ->where('is_placeholder = false'); }); } diff --git a/database/migrations/2024_01_20_110444_create_tasks_table.php b/database/migrations/2024_01_20_110444_create_tasks_table.php index 876508f2..4a0a4848 100644 --- a/database/migrations/2024_01_20_110444_create_tasks_table.php +++ b/database/migrations/2024_01_20_110444_create_tasks_table.php @@ -15,7 +15,7 @@ public function up(): void { Schema::create('tasks', function (Blueprint $table) { $table->uuid('id')->primary(); - $table->string('name', 255); + $table->string('name', 500); $table->uuid('project_id'); $table->foreign('project_id') ->references('id') diff --git a/database/migrations/2024_01_20_110837_create_time_entries_table.php b/database/migrations/2024_01_20_110837_create_time_entries_table.php index ed5ef5fb..5fa5e9fb 100644 --- a/database/migrations/2024_01_20_110837_create_time_entries_table.php +++ b/database/migrations/2024_01_20_110837_create_time_entries_table.php @@ -15,7 +15,7 @@ public function up(): void { Schema::create('time_entries', function (Blueprint $table) { $table->uuid('id')->primary(); - $table->string('description', 255); + $table->string('description', 500); $table->dateTime('start'); $table->dateTime('end')->nullable(); $table->boolean('billable')->default(false); diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 7dd88fa8..2e057b52 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -8,6 +8,7 @@ use App\Models\Client; use App\Models\Organization; use App\Models\Project; +use App\Models\Tag; use App\Models\Task; use App\Models\TimeEntry; use App\Models\User; @@ -22,31 +23,57 @@ class DatabaseSeeder extends Seeder public function run(): void { $this->deleteAll(); - $organization1 = Organization::factory()->create([ + $userAcmeOwner = User::factory()->create([ + 'name' => 'ACME Admin', + 'email' => 'owner@acme.test', + ]); + $organizationAcme = Organization::factory()->withOwner($userAcmeOwner)->create([ 'name' => 'ACME Corp', ]); - $user1 = User::factory()->withPersonalOrganization()->create([ + $userAcmeManager = User::factory()->withPersonalOrganization()->create([ 'name' => 'Test User', 'email' => 'test@example.com', ]); - $employee1 = User::factory()->withPersonalOrganization()->create([ - 'name' => 'Test User', - 'email' => 'employee@example.com', - ]); - $userAcmeAdmin = User::factory()->create([ + $userAcmeAdmin = User::factory()->withPersonalOrganization()->create([ 'name' => 'ACME Admin', 'email' => 'admin@acme.test', ]); - $user1->organizations()->attach($organization1, [ + $userAcmeEmployee = User::factory()->withPersonalOrganization()->create([ + 'name' => 'Max Mustermann', + 'email' => 'max.mustermann@acme.test', + ]); + $userAcmePlaceholder = User::factory()->placeholder()->create([ + 'name' => 'Old Employee', + 'email' => 'old.employee@acme.test', + 'password' => null, + ]); + $userAcmeManager->organizations()->attach($organizationAcme, [ 'role' => 'manager', ]); - $userAcmeAdmin->organizations()->attach($organization1, [ + $userAcmeAdmin->organizations()->attach($organizationAcme, [ 'role' => 'admin', ]); - $timeEntriesEmployees = TimeEntry::factory() + $userAcmeEmployee->organizations()->attach($organizationAcme, [ + 'role' => 'employee', + ]); + $userAcmePlaceholder->organizations()->attach($organizationAcme, [ + 'role' => 'employee', + ]); + + $timeEntriesAcmeAdmin = TimeEntry::factory() + ->count(10) + ->forUser($userAcmeAdmin) + ->forOrganization($organizationAcme) + ->create(); + $timeEntriesAcmePlaceholder = TimeEntry::factory() + ->count(10) + ->forUser($userAcmePlaceholder) + ->forOrganization($organizationAcme) + ->create(); + $timeEntriesAcmePlaceholder = TimeEntry::factory() ->count(10) - ->forUser($employee1) - ->forOrganization($organization1) + ->forUser($userAcmeEmployee) + ->forOrganization($organizationAcme) ->create(); $client = Client::factory()->create([ 'name' => 'Big Company', @@ -63,11 +90,11 @@ public function run(): void $organization2 = Organization::factory()->create([ 'name' => 'Rival Corp', ]); - $user1 = User::factory()->withPersonalOrganization()->create([ + $userAcmeManager = User::factory()->withPersonalOrganization()->create([ 'name' => 'Other User', 'email' => 'test@rival-company.test', ]); - $user1->organizations()->attach($organization2, [ + $userAcmeManager->organizations()->attach($organization2, [ 'role' => 'admin', ]); $otherCompanyProject = Project::factory()->forClient($client)->create([ @@ -83,6 +110,7 @@ private function deleteAll(): void { DB::table((new TimeEntry())->getTable())->delete(); DB::table((new Task())->getTable())->delete(); + DB::table((new Tag())->getTable())->delete(); DB::table((new Project())->getTable())->delete(); DB::table((new Client())->getTable())->delete(); DB::table((new User())->getTable())->delete(); diff --git a/docker-compose.yml b/docker-compose.yml index 876bcfe0..0ccaecc6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -57,14 +57,49 @@ services: - '${DB_USERNAME}' retries: 3 timeout: 5s + pgsql_test: + image: 'postgres:15' + environment: + PGPASSWORD: '${DB_PASSWORD:-secret}' + POSTGRES_DB: '${DB_DATABASE}' + POSTGRES_USER: '${DB_USERNAME}' + POSTGRES_PASSWORD: '${DB_PASSWORD:-secret}' + volumes: + - 'sail-pgsql-test:/var/lib/postgresql/data' + - './vendor/laravel/sail/database/pgsql/create-testing-database.sql:/docker-entrypoint-initdb.d/10-create-testing-database.sql' + networks: + - sail + healthcheck: + test: + - CMD + - pg_isready + - '-q' + - '-d' + - '${DB_DATABASE}' + - '-U' + - '${DB_USERNAME}' + retries: 3 + timeout: 5s mailpit: image: 'axllent/mailpit:latest' + labels: + - "traefik.enable=true" + - "traefik.docker.network=${NETWORK_NAME}" + - "traefik.http.routers.solidtime-mailpit.rule=Host(`mail.${NGINX_HOST_NAME}`)" + - "traefik.http.routers.solidtime-mailpit.entrypoints=web" + - "traefik.http.services.solidtime-mailpit.loadbalancer.server.port=8025" + - "traefik.http.routers.solidtime-mailpit-https.rule=Host(`mail.${NGINX_HOST_NAME}`)" + - "traefik.http.routers.solidtime-mailpit-https.entrypoints=websecure" + - "traefik.http.routers.solidtime-mailpit-https.tls=true" networks: - sail + - reverse-proxy playwright: image: mcr.microsoft.com/playwright:v1.41.1-jammy command: ['npx', 'playwright', 'test', '--ui-port=8080', '--ui-host=0.0.0.0'] working_dir: /src + extra_hosts: + - "solidtime.test:${REVERSE_PROXY_IP:-10.100.100.10}" labels: - "traefik.enable=true" - "traefik.docker.network=${NETWORK_NAME}" @@ -88,3 +123,5 @@ networks: volumes: sail-pgsql: driver: local + sail-pgsql-test: + driver: local diff --git a/e2e/auth.spec.ts b/e2e/auth.spec.ts index d3837545..2051647e 100644 --- a/e2e/auth.spec.ts +++ b/e2e/auth.spec.ts @@ -8,9 +8,7 @@ async function registerNewUser(page, email, password) { await page.getByLabel('Password', { exact: true }).fill(password); await page.getByLabel('Confirm Password').fill(password); await page.getByRole('button', { name: 'Register' }).click(); - await expect( - page.getByRole('heading', { name: 'Dashboard' }) - ).toBeVisible(); + await expect(page.getByTestId('dashboard_view')).toBeVisible(); } test('can register, logout and log back in', async ({ page }) => { @@ -18,20 +16,15 @@ test('can register, logout and log back in', async ({ page }) => { const email = `john+${Math.round(Math.random() * 10000)}@doe.com`; const password = 'suchagreatpassword123'; await registerNewUser(page, email, password); - await expect( - page.getByRole('button', { name: "John's Organization" }) - ).toBeVisible(); - await page.locator('#currentUserButton').click(); - - await page.getByRole('button', { name: 'Log Out' }).click(); + await expect(page.getByTestId('dashboard_view')).toBeVisible(); + await page.getByTestId('current_user_button').click(); + await page.getByText('Log Out').click(); await page.waitForURL(PLAYWRIGHT_BASE_URL + '/'); await page.goto(PLAYWRIGHT_BASE_URL + '/login'); await page.getByLabel('Email').fill(email); await page.getByLabel('Password').fill(password); await page.getByRole('button', { name: 'Log in' }).click(); - await expect( - page.getByRole('heading', { name: 'Dashboard' }) - ).toBeVisible(); + await expect(page.getByTestId('dashboard_view')).toBeVisible(); }); test('can register and delete account', async ({ page }) => { diff --git a/e2e/organization.spec.ts b/e2e/organization.spec.ts index a61fb985..d079f8e6 100644 --- a/e2e/organization.spec.ts +++ b/e2e/organization.spec.ts @@ -3,8 +3,8 @@ import { PLAYWRIGHT_BASE_URL } from '../playwright/config'; async function goToOrganizationSettings(page) { await page.goto(PLAYWRIGHT_BASE_URL + '/dashboard'); - await page.locator('#currentTeamButton').click(); - await page.getByRole('link', { name: 'Team Settings' }).click(); + await page.getByTestId('organization_switcher').click(); + await page.getByText('Team Settings').click(); } test('test that organization name can be updated', async ({ page }) => { @@ -12,14 +12,28 @@ test('test that organization name can be updated', async ({ page }) => { await page.getByLabel('Team Name').fill('NEW ORG NAME'); await page.getByLabel('Team Name').press('Enter'); await page.getByLabel('Team Name').press('Meta+r'); - await expect(page.getByRole('navigation')).toContainText('NEW ORG NAME'); + await expect(page.getByTestId('organization_switcher')).toContainText( + 'NEW ORG NAME' + ); +}); + +test('test that new manager can be invited', async ({ page }) => { + await goToOrganizationSettings(page); + const editorId = Math.round(Math.random() * 10000); + await page.getByLabel('Email').fill(`new+${editorId}@editor.test`); + await page.getByRole('button', { name: 'Manager' }).click(); + await page.getByRole('button', { name: 'Add' }).click(); + await page.reload(); + await expect(page.getByRole('main')).toContainText( + `new+${editorId}@editor.test` + ); }); -test('test that new editor can be invited', async ({ page }) => { +test('test that new employee can be invited', async ({ page }) => { await goToOrganizationSettings(page); const editorId = Math.round(Math.random() * 10000); await page.getByLabel('Email').fill(`new+${editorId}@editor.test`); - await page.getByRole('button', { name: 'Editor' }).click(); + await page.getByRole('button', { name: 'Employee' }).click(); await page.getByRole('button', { name: 'Add' }).click(); await page.reload(); await expect(page.getByRole('main')).toContainText( diff --git a/e2e/timetracker.spec.ts b/e2e/timetracker.spec.ts new file mode 100644 index 00000000..f7efd2ed --- /dev/null +++ b/e2e/timetracker.spec.ts @@ -0,0 +1,435 @@ +import { expect, test } from '../playwright/fixtures'; +import { PLAYWRIGHT_BASE_URL } from '../playwright/config'; + +async function goToDashboard(page) { + await page.goto(PLAYWRIGHT_BASE_URL + '/dashboard'); +} + +async function startOrStopTimerWithButton(page) { + await page + .locator('[data-testid="dashboard_timer"] [data-testid="timer_button"]') + .click(); +} + +async function assertThatTimerHasStarted(page) { + await page.locator( + '[data-testid="dashboard_timer"] [data-testid="timer_button"].bg-red-400/80' + ); +} + +async function assertNewTimeEntryResponse(page) { + await page.waitForResponse(async (response) => { + return ( + response.status() === 201 && + (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 && + (await response.json()).data.project_id === null && + (await response.json()).data.description === '' && + (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([]) + ); + }); +} + +async function assertThatTimerIsStoped(page) { + await page.locator( + '[data-testid="dashboard_timer"] [data-testid="timer_button"].bg-accent-300/70' + ); +} + +test('test that starting and stopping a timer without description and project works', async ({ + page, +}) => { + await goToDashboard(page); + await startOrStopTimerWithButton(page); + await assertNewTimeEntryResponse(page); + await assertThatTimerHasStarted(page); + await page.waitForTimeout(1500); + await startOrStopTimerWithButton(page); + await page.waitForResponse(async (response) => { + return ( + response.status() === 200 && + (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 && + (await response.json()).data.project_id === null && + (await response.json()).data.description === '' && + (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([]) + ); + }); + await assertThatTimerIsStoped(page); +}); + +test('test that starting and stopping a timer with a description works', async ({ + page, +}) => { + await goToDashboard(page); + await page + .getByTestId('time_entry_description') + .fill('New Time Entry Description'); + await startOrStopTimerWithButton(page); + await page.waitForResponse(async (response) => { + return ( + response.status() === 201 && + (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 && + (await response.json()).data.project_id === null && + (await response.json()).data.description === + 'New Time Entry Description' && + (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([]) + ); + }); + await assertThatTimerHasStarted(page); + await page.waitForTimeout(500); + await startOrStopTimerWithButton(page); + await page.waitForResponse(async (response) => { + return ( + response.status() === 200 && + (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 && + (await response.json()).data.project_id === null && + (await response.json()).data.description === + 'New Time Entry Description' && + (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([]) + ); + }); + await assertThatTimerIsStoped(page); +}); + +test('test that starting and updating the description while running works', async ({ + page, +}) => { + await goToDashboard(page); + await startOrStopTimerWithButton(page); + await page.waitForResponse(async (response) => { + return ( + response.status() === 201 && + (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 && + (await response.json()).data.project_id === null && + (await response.json()).data.description === '' && + (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([]) + ); + }); + await assertThatTimerHasStarted(page); + await page.waitForTimeout(500); + await page + .getByTestId('time_entry_description') + .fill('New Time Entry Description'); + await page.getByTestId('time_entry_description').press('Tab'); + await page.waitForResponse(async (response) => { + return ( + response.status() === 200 && + (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 && + (await response.json()).data.project_id === null && + (await response.json()).data.description === + 'New Time Entry Description' && + (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([]) + ); + }); + await page.waitForTimeout(500); + await startOrStopTimerWithButton(page); + await page.waitForResponse(async (response) => { + return ( + response.status() === 200 && + (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 && + (await response.json()).data.project_id === null && + (await response.json()).data.description === + 'New Time Entry Description' && + (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([]) + ); + }); + await assertThatTimerIsStoped(page); +}); + +test('test that starting and updating the time while running works', async ({ + page, +}) => { + await goToDashboard(page); + await startOrStopTimerWithButton(page); + const createResponse = await page.waitForResponse(async (response) => { + return ( + response.status() === 201 && + (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 && + (await response.json()).data.project_id === null && + (await response.json()).data.description === '' && + (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([]) + ); + }); + await assertThatTimerHasStarted(page); + await page.waitForTimeout(500); + await page.getByTestId('time_entry_time').fill('20min'); + await page.getByTestId('time_entry_time').press('Tab'); + await page.waitForResponse(async (response) => { + return ( + response.status() === 200 && + (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.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.duration === null && + (await response.json()).data.user_id !== null && + JSON.stringify((await response.json()).data.tags) === + JSON.stringify([]) + ); + }); + await expect(page.getByTestId('time_entry_time')).toHaveValue(/00:20/); + await page.waitForTimeout(500); + await startOrStopTimerWithButton(page); + await page.waitForResponse(async (response) => { + return ( + response.status() === 200 && + (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 && + (await response.json()).data.project_id === null && + (await response.json()).data.description === '' && + (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([]) + ); + }); + await assertThatTimerIsStoped(page); +}); + +test('test that entering a time starts the timer on blur', async ({ page }) => { + await goToDashboard(page); + await page.getByTestId('time_entry_time').fill('20min'); + await page.getByTestId('time_entry_time').press('Tab'); + await page.waitForResponse(async (response) => { + return ( + response.status() === 201 && + (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 && + (await response.json()).data.project_id === null && + (await response.json()).data.description === '' && + (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([]) + ); + }); + await assertThatTimerHasStarted(page); + await startOrStopTimerWithButton(page); + await page.waitForResponse(async (response) => { + return ( + response.status() === 200 && + (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 && + (await response.json()).data.project_id === null && + (await response.json()).data.description === '' && + (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([]) + ); + }); + await page.locator( + '[data-testid="dashboard_timer"] [data-testid="timer_button"].bg-accent-300/70' + ); +}); + +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 page.getByTestId('time_entry_time').press('Enter'); + await page.waitForResponse(async (response) => { + return ( + response.status() === 201 && + (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 && + (await response.json()).data.project_id === null && + (await response.json()).data.description === '' && + (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([]) + ); + }); + await assertThatTimerHasStarted(page); + await startOrStopTimerWithButton(page); + await page.waitForResponse(async (response) => { + return ( + response.status() === 200 && + (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 && + (await response.json()).data.project_id === null && + (await response.json()).data.description === '' && + (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([]) + ); + }); + await assertThatTimerIsStoped(page); +}); + +test('test that adding a new tag works', async ({ page }) => { + const newTagName = 'New Tag' + Math.floor(Math.random() * 10000); + await goToDashboard(page); + await page.getByTestId('tag_dropdown').click(); + await page.getByTestId('tag_dropdown_search').fill(newTagName); + await page.getByTestId('tag_dropdown_search').press('Enter'); + await page.waitForResponse(async (response) => { + return ( + response.status() === 201 && + (await response.headerValue('Content-Type')) === + 'application/json' && + (await response.json()).data.name === newTagName + ); + }); + await expect(page.getByTestId('tag_dropdown_search')).toHaveValue(''); + await expect(page.getByRole('option', { name: newTagName })).toBeVisible(); +}); + +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 startOrStopTimerWithButton(page); + await assertNewTimeEntryResponse(page); + await assertThatTimerHasStarted(page); + await page.getByTestId('tag_dropdown').click(); + await page.getByTestId('tag_dropdown_search').fill(newTagName); + await page.getByTestId('tag_dropdown_search').press('Enter'); + const tagCreateResponse = await page.waitForResponse(async (response) => { + return ( + response.status() === 201 && + (await response.headerValue('Content-Type')) === + 'application/json' && + (await response.json()).data.name === newTagName + ); + }); + await expect(page.getByTestId('tag_dropdown_search')).toHaveValue(''); + + await expect(page.getByRole('option', { name: newTagName })).toBeVisible(); + + await page.waitForResponse(async (response) => { + return ( + response.status() === 200 && + (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 && + (await response.json()).data.project_id === null && + (await response.json()).data.description === '' && + (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([(await tagCreateResponse.json()).data.id]) + ); + }); + await page.getByTestId('tag_dropdown_search').press('Escape'); + await page.waitForTimeout(1000); + await startOrStopTimerWithButton(page); + await page.waitForResponse(async (response) => { + return ( + response.status() === 200 && + (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 && + (await response.json()).data.project_id === null && + (await response.json()).data.description === '' && + (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([(await tagCreateResponse.json()).data.id]) + ); + }); + await assertThatTimerIsStoped(page); +}); + +// test that adding a new tag when the timer is running + +// test that search is working diff --git a/lang/en/auth.php b/lang/en/auth.php new file mode 100644 index 00000000..e2de2aca --- /dev/null +++ b/lang/en/auth.php @@ -0,0 +1,22 @@ + 'These credentials do not match our records.', + 'password' => 'The provided password is incorrect.', + 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', + +]; diff --git a/lang/en/exceptions.php b/lang/en/exceptions.php new file mode 100644 index 00000000..706574fb --- /dev/null +++ b/lang/en/exceptions.php @@ -0,0 +1,13 @@ + [ + TimeEntryStillRunningApiException::KEY => 'Time entry is still running', + UserNotPlaceholderApiException::KEY => 'The given user is not a placeholder', + ], +]; diff --git a/lang/en/pagination.php b/lang/en/pagination.php new file mode 100644 index 00000000..f03c42c6 --- /dev/null +++ b/lang/en/pagination.php @@ -0,0 +1,21 @@ + '« Previous', + 'next' => 'Next »', + +]; diff --git a/lang/en/passwords.php b/lang/en/passwords.php new file mode 100644 index 00000000..43092321 --- /dev/null +++ b/lang/en/passwords.php @@ -0,0 +1,24 @@ + 'Your password has been reset.', + 'sent' => 'We have emailed your password reset link.', + 'throttled' => 'Please wait before retrying.', + 'token' => 'This password reset token is invalid.', + 'user' => "We can't find a user with that email address.", + +]; diff --git a/lang/en/validation.php b/lang/en/validation.php new file mode 100644 index 00000000..9813d38a --- /dev/null +++ b/lang/en/validation.php @@ -0,0 +1,199 @@ + 'The :attribute field must be accepted.', + 'accepted_if' => 'The :attribute field must be accepted when :other is :value.', + 'active_url' => 'The :attribute field must be a valid URL.', + 'after' => 'The :attribute field must be a date after :date.', + 'after_or_equal' => 'The :attribute field must be a date after or equal to :date.', + 'alpha' => 'The :attribute field must only contain letters.', + 'alpha_dash' => 'The :attribute field must only contain letters, numbers, dashes, and underscores.', + 'alpha_num' => 'The :attribute field must only contain letters and numbers.', + 'array' => 'The :attribute field must be an array.', + 'ascii' => 'The :attribute field must only contain single-byte alphanumeric characters and symbols.', + 'before' => 'The :attribute field must be a date before :date.', + 'before_or_equal' => 'The :attribute field must be a date before or equal to :date.', + 'between' => [ + 'array' => 'The :attribute field must have between :min and :max items.', + 'file' => 'The :attribute field must be between :min and :max kilobytes.', + 'numeric' => 'The :attribute field must be between :min and :max.', + 'string' => 'The :attribute field must be between :min and :max characters.', + ], + 'boolean' => 'The :attribute field must be true or false.', + 'can' => 'The :attribute field contains an unauthorized value.', + 'confirmed' => 'The :attribute field confirmation does not match.', + 'current_password' => 'The password is incorrect.', + 'date' => 'The :attribute field must be a valid date.', + 'date_equals' => 'The :attribute field must be a date equal to :date.', + 'date_format' => 'The :attribute field must match the format :format.', + 'decimal' => 'The :attribute field must have :decimal decimal places.', + 'declined' => 'The :attribute field must be declined.', + 'declined_if' => 'The :attribute field must be declined when :other is :value.', + 'different' => 'The :attribute field and :other must be different.', + 'digits' => 'The :attribute field must be :digits digits.', + 'digits_between' => 'The :attribute field must be between :min and :max digits.', + 'dimensions' => 'The :attribute field has invalid image dimensions.', + 'distinct' => 'The :attribute field has a duplicate value.', + 'doesnt_end_with' => 'The :attribute field must not end with one of the following: :values.', + 'doesnt_start_with' => 'The :attribute field must not start with one of the following: :values.', + 'email' => 'The :attribute field must be a valid email address.', + 'ends_with' => 'The :attribute field must end with one of the following: :values.', + 'enum' => 'The selected :attribute is invalid.', + 'exists' => 'The selected :attribute is invalid.', + 'extensions' => 'The :attribute field must have one of the following extensions: :values.', + 'file' => 'The :attribute field must be a file.', + 'filled' => 'The :attribute field must have a value.', + 'gt' => [ + 'array' => 'The :attribute field must have more than :value items.', + 'file' => 'The :attribute field must be greater than :value kilobytes.', + 'numeric' => 'The :attribute field must be greater than :value.', + 'string' => 'The :attribute field must be greater than :value characters.', + ], + 'gte' => [ + 'array' => 'The :attribute field must have :value items or more.', + 'file' => 'The :attribute field must be greater than or equal to :value kilobytes.', + 'numeric' => 'The :attribute field must be greater than or equal to :value.', + 'string' => 'The :attribute field must be greater than or equal to :value characters.', + ], + 'hex_color' => 'The :attribute field must be a valid hexadecimal color.', + 'image' => 'The :attribute field must be an image.', + 'in' => 'The selected :attribute is invalid.', + 'in_array' => 'The :attribute field must exist in :other.', + 'integer' => 'The :attribute field must be an integer.', + 'ip' => 'The :attribute field must be a valid IP address.', + 'ipv4' => 'The :attribute field must be a valid IPv4 address.', + 'ipv6' => 'The :attribute field must be a valid IPv6 address.', + 'json' => 'The :attribute field must be a valid JSON string.', + 'lowercase' => 'The :attribute field must be lowercase.', + 'lt' => [ + 'array' => 'The :attribute field must have less than :value items.', + 'file' => 'The :attribute field must be less than :value kilobytes.', + 'numeric' => 'The :attribute field must be less than :value.', + 'string' => 'The :attribute field must be less than :value characters.', + ], + 'lte' => [ + 'array' => 'The :attribute field must not have more than :value items.', + 'file' => 'The :attribute field must be less than or equal to :value kilobytes.', + 'numeric' => 'The :attribute field must be less than or equal to :value.', + 'string' => 'The :attribute field must be less than or equal to :value characters.', + ], + 'mac_address' => 'The :attribute field must be a valid MAC address.', + 'max' => [ + 'array' => 'The :attribute field must not have more than :max items.', + 'file' => 'The :attribute field must not be greater than :max kilobytes.', + 'numeric' => 'The :attribute field must not be greater than :max.', + 'string' => 'The :attribute field must not be greater than :max characters.', + ], + 'max_digits' => 'The :attribute field must not have more than :max digits.', + 'mimes' => 'The :attribute field must be a file of type: :values.', + 'mimetypes' => 'The :attribute field must be a file of type: :values.', + 'min' => [ + 'array' => 'The :attribute field must have at least :min items.', + 'file' => 'The :attribute field must be at least :min kilobytes.', + 'numeric' => 'The :attribute field must be at least :min.', + 'string' => 'The :attribute field must be at least :min characters.', + ], + 'min_digits' => 'The :attribute field must have at least :min digits.', + 'missing' => 'The :attribute field must be missing.', + 'missing_if' => 'The :attribute field must be missing when :other is :value.', + 'missing_unless' => 'The :attribute field must be missing unless :other is :value.', + 'missing_with' => 'The :attribute field must be missing when :values is present.', + 'missing_with_all' => 'The :attribute field must be missing when :values are present.', + 'multiple_of' => 'The :attribute field must be a multiple of :value.', + 'not_in' => 'The selected :attribute is invalid.', + 'not_regex' => 'The :attribute field format is invalid.', + 'numeric' => 'The :attribute field must be a number.', + 'password' => [ + 'letters' => 'The :attribute field must contain at least one letter.', + 'mixed' => 'The :attribute field must contain at least one uppercase and one lowercase letter.', + 'numbers' => 'The :attribute field must contain at least one number.', + 'symbols' => 'The :attribute field must contain at least one symbol.', + 'uncompromised' => 'The given :attribute has appeared in a data leak. Please choose a different :attribute.', + ], + 'present' => 'The :attribute field must be present.', + 'present_if' => 'The :attribute field must be present when :other is :value.', + 'present_unless' => 'The :attribute field must be present unless :other is :value.', + 'present_with' => 'The :attribute field must be present when :values is present.', + 'present_with_all' => 'The :attribute field must be present when :values are present.', + 'prohibited' => 'The :attribute field is prohibited.', + 'prohibited_if' => 'The :attribute field is prohibited when :other is :value.', + 'prohibited_unless' => 'The :attribute field is prohibited unless :other is in :values.', + 'prohibits' => 'The :attribute field prohibits :other from being present.', + 'regex' => 'The :attribute field format is invalid.', + 'required' => 'The :attribute field is required.', + 'required_array_keys' => 'The :attribute field must contain entries for: :values.', + 'required_if' => 'The :attribute field is required when :other is :value.', + 'required_if_accepted' => 'The :attribute field is required when :other is accepted.', + 'required_unless' => 'The :attribute field is required unless :other is in :values.', + 'required_with' => 'The :attribute field is required when :values is present.', + 'required_with_all' => 'The :attribute field is required when :values are present.', + 'required_without' => 'The :attribute field is required when :values is not present.', + 'required_without_all' => 'The :attribute field is required when none of :values are present.', + 'same' => 'The :attribute field must match :other.', + 'size' => [ + 'array' => 'The :attribute field must contain :size items.', + 'file' => 'The :attribute field must be :size kilobytes.', + 'numeric' => 'The :attribute field must be :size.', + 'string' => 'The :attribute field must be :size characters.', + ], + 'starts_with' => 'The :attribute field must start with one of the following: :values.', + 'string' => 'The :attribute field must be a string.', + 'timezone' => 'The :attribute field must be a valid timezone.', + 'unique' => 'The :attribute has already been taken.', + 'uploaded' => 'The :attribute failed to upload.', + 'uppercase' => 'The :attribute field must be uppercase.', + 'url' => 'The :attribute field must be a valid URL.', + 'ulid' => 'The :attribute field must be a valid ULID.', + 'uuid' => 'The :attribute field must be a valid UUID.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap our attribute placeholder + | with something more reader friendly such as "E-Mail Address" instead + | of "email". This simply helps us make our message more expressive. + | + */ + + 'attributes' => [], + + /* + * Custom validation rules + */ + + 'color' => 'The :attribute field must be a valid color.', + +]; diff --git a/openapi.json b/openapi.json new file mode 100644 index 00000000..0d424c08 --- /dev/null +++ b/openapi.json @@ -0,0 +1 @@ +{"openapi":"3.1.0","info":{"title":"Laravel","version":"0.0.1"},"servers":[{"url":"https:\/\/app.solidtime.io\/api","description":"Production"},{"url":"https:\/\/app.staging.solidtime.io\/api","description":"Staging"},{"url":"https:\/\/soldtime.test\/api","description":"Local"}],"security":[{"oauth2":[]}],"paths":{"\/v1\/organization\/{organization}\/projects":{"get":{"operationId":"getProjects","summary":"Get projects","tags":["Project"],"parameters":[{"name":"organization","in":"path","required":true,"description":"The organization ID","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"`ProjectCollection`","content":{"application\/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#\/components\/schemas\/ProjectCollection"}},"required":["data"]}}}},"403":{"$ref":"#\/components\/responses\/AuthorizationException"},"404":{"$ref":"#\/components\/responses\/ModelNotFoundException"}}},"post":{"operationId":"createProject","summary":"Create project","tags":["Project"],"parameters":[{"name":"organization","in":"path","required":true,"description":"The organization ID","schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application\/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"color":{"type":"string"}},"required":["name","color"]}}}},"responses":{"200":{"description":"`ProjectResource`","content":{"application\/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#\/components\/schemas\/ProjectResource"}},"required":["data"]}}}},"403":{"$ref":"#\/components\/responses\/AuthorizationException"},"404":{"$ref":"#\/components\/responses\/ModelNotFoundException"},"422":{"$ref":"#\/components\/responses\/ValidationException"}}}},"\/v1\/organization\/{organization}\/projects\/{project}":{"get":{"operationId":"getProject","summary":"Get project","tags":["Project"],"parameters":[{"name":"organization","in":"path","required":true,"description":"The organization ID","schema":{"type":"string","format":"uuid"}},{"name":"project","in":"path","required":true,"description":"The project ID","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"`ProjectResource`","content":{"application\/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#\/components\/schemas\/ProjectResource"}},"required":["data"]}}}},"403":{"$ref":"#\/components\/responses\/AuthorizationException"},"404":{"$ref":"#\/components\/responses\/ModelNotFoundException"}}},"put":{"operationId":"updateProject","summary":"Update project","tags":["Project"],"parameters":[{"name":"organization","in":"path","required":true,"description":"The organization ID","schema":{"type":"string","format":"uuid"}},{"name":"project","in":"path","required":true,"description":"The project ID","schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application\/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"color":{"type":"string"}},"required":["name","color"]}}}},"responses":{"200":{"description":"`ProjectResource`","content":{"application\/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#\/components\/schemas\/ProjectResource"}},"required":["data"]}}}},"403":{"$ref":"#\/components\/responses\/AuthorizationException"},"404":{"$ref":"#\/components\/responses\/ModelNotFoundException"},"422":{"$ref":"#\/components\/responses\/ValidationException"}}},"delete":{"operationId":"v1.projects.destroy","summary":"Delete project","tags":["Project"],"parameters":[{"name":"organization","in":"path","required":true,"description":"The organization ID","schema":{"type":"string","format":"uuid"}},{"name":"project","in":"path","required":true,"description":"The project ID","schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application\/json":{"schema":{"type":"object"}}}},"responses":{"204":{"description":"No content","content":{"application\/json":{"schema":{"type":"null"}}}},"403":{"$ref":"#\/components\/responses\/AuthorizationException"},"404":{"$ref":"#\/components\/responses\/ModelNotFoundException"}}}},"\/v1\/organization\/{organization}\/time-entries":{"get":{"operationId":"v1.time-entries.index","summary":"Get time entries","tags":["TimeEntry"],"parameters":[{"name":"organization","in":"path","required":true,"description":"The organization ID","schema":{"type":"string","format":"uuid"}},{"name":"user_id","in":"query","description":"Filter by user ID","schema":{"type":"string","format":"uuid"}},{"name":"before","in":"query","description":"Filter only time entries that have a start date before (not including) the given date (example: 2021-12-31)","schema":{"type":["string","null"]}},{"name":"after","in":"query","description":"Filter only time entries that have a start date after (not including) the given date (example: 2021-12-31)","schema":{"type":["string","null"]}},{"name":"active","in":"query","description":"Filter only time entries that are active (have no end date, are still running)","schema":{"type":"boolean"}},{"name":"limit","in":"query","description":"Limit the number of returned time entries","schema":{"type":"integer","minimum":1,"maximum":500}},{"name":"only_full_dates","in":"query","description":"Filter makes sure that only time entries of a whole date are returned","schema":{"type":"boolean"}}],"responses":{"200":{"description":"`TimeEntryCollection`","content":{"application\/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#\/components\/schemas\/TimeEntryCollection"}},"required":["data"]}}}},"403":{"$ref":"#\/components\/responses\/AuthorizationException"},"404":{"$ref":"#\/components\/responses\/ModelNotFoundException"},"422":{"$ref":"#\/components\/responses\/ValidationException"}}},"post":{"operationId":"v1.time-entries.store","summary":"Create time entry","tags":["TimeEntry"],"parameters":[{"name":"organization","in":"path","required":true,"description":"The organization ID","schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application\/json":{"schema":{"type":"object","properties":{"user_id":{"type":"string","format":"uuid","description":"ID of the user that the time entry should belong to"},"task_id":{"type":["string","null"],"format":"uuid","description":"ID of the task that the time entry should belong to"},"start":{"type":"string","description":"Start of time entry (ISO 8601 format, UTC timezone)"},"end":{"type":["string","null"],"description":"End of time entry (ISO 8601 format, UTC timezone)"},"description":{"type":["string","null"],"description":"Description of time entry"},"tags":{"type":["array","null"],"description":"List of tag IDs","items":{"type":"string","format":"uuid"}}},"required":["user_id","start","end"]}}}},"responses":{"200":{"description":"`TimeEntryResource`","content":{"application\/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#\/components\/schemas\/TimeEntryResource"}},"required":["data"]}}}},"403":{"$ref":"#\/components\/responses\/AuthorizationException"},"404":{"$ref":"#\/components\/responses\/ModelNotFoundException"},"422":{"$ref":"#\/components\/responses\/ValidationException"}}}},"\/v1\/organization\/{organization}\/time-entries\/{timeEntry}":{"put":{"operationId":"v1.time-entries.update","summary":"Update time entry","tags":["TimeEntry"],"parameters":[{"name":"organization","in":"path","required":true,"description":"The organization ID","schema":{"type":"string","format":"uuid"}},{"name":"timeEntry","in":"path","required":true,"description":"The time entry ID","schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application\/json":{"schema":{"type":"object","properties":{"task_id":{"type":["string","null"],"format":"uuid","description":"ID of the task that the time entry should belong to"},"start":{"type":"string","description":"Start of time entry (ISO 8601 format, UTC timezone)"},"end":{"type":["string","null"],"description":"End of time entry (ISO 8601 format, UTC timezone)"},"description":{"type":["string","null"],"description":"Description of time entry"},"tags":{"type":["array","null"],"description":"List of tag IDs","items":{"type":"string","format":"uuid"}}},"required":["start","end"]}}}},"responses":{"200":{"description":"`TimeEntryResource`","content":{"application\/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#\/components\/schemas\/TimeEntryResource"}},"required":["data"]}}}},"403":{"$ref":"#\/components\/responses\/AuthorizationException"},"404":{"$ref":"#\/components\/responses\/ModelNotFoundException"},"422":{"$ref":"#\/components\/responses\/ValidationException"}}},"delete":{"operationId":"v1.time-entries.destroy","summary":"Delete time entry","tags":["TimeEntry"],"parameters":[{"name":"organization","in":"path","required":true,"description":"The organization ID","schema":{"type":"string","format":"uuid"}},{"name":"timeEntry","in":"path","required":true,"description":"The time entry ID","schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application\/json":{"schema":{"type":"object"}}}},"responses":{"204":{"description":"No content","content":{"application\/json":{"schema":{"type":"null"}}}},"403":{"$ref":"#\/components\/responses\/AuthorizationException"},"404":{"$ref":"#\/components\/responses\/ModelNotFoundException"}}}}},"components":{"securitySchemes":{"oauth2":{"type":"oauth2","flows":{"authorizationCode":{"authorizationUrl":"https:\/\/solidtime.test\/oauth\/authorize"}}}},"schemas":{"ProjectCollection":{"type":"array","items":{"$ref":"#\/components\/schemas\/ProjectResource"},"title":"ProjectCollection"},"ProjectResource":{"type":"object","properties":{"id":{"type":"string","description":"ID of project"},"name":{"type":"string","description":"Name of project"},"color":{"type":"string","description":"Color of project"},"client_id":{"type":["string","null"],"description":"ID of client"}},"required":["id","name","color","client_id"],"title":"ProjectResource"},"TimeEntryCollection":{"type":"array","items":{"$ref":"#\/components\/schemas\/TimeEntryResource"},"title":"TimeEntryCollection"},"TimeEntryResource":{"type":"object","properties":{"id":{"type":"string","description":"ID of time entry"},"start":{"type":"string","description":"Start of time entry (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z)"},"end":{"type":["string","null"],"description":"End of time entry (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z)"},"duration":{"type":"integer","description":"Duration of time entry in seconds"},"description":{"type":["string","null"],"description":"Description of time entry"},"task_id":{"type":["string","null"],"description":"ID of task"},"project_id":{"type":["string","null"],"description":"ID of project"},"user_id":{"type":"string","description":"ID of user"},"tags":{"type":"array","description":"List of tag IDs","items":{"type":"string"}}},"required":["id","start","end","duration","description","task_id","project_id","user_id","tags"],"title":"TimeEntryResource"}},"responses":{"AuthorizationException":{"description":"Authorization error","content":{"application\/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Error overview."}},"required":["message"]}}}},"ModelNotFoundException":{"description":"Not found","content":{"application\/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Error overview."}},"required":["message"]}}}},"ValidationException":{"description":"Validation error","content":{"application\/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Errors overview."},"errors":{"type":"object","description":"A detailed description of each field that failed validation.","additionalProperties":{"type":"array","items":{"type":"string"}}}},"required":["message","errors"]}}}}}}} diff --git a/openapi.json.client.ts b/openapi.json.client.ts new file mode 100644 index 00000000..5b7b3add --- /dev/null +++ b/openapi.json.client.ts @@ -0,0 +1,991 @@ +import { makeApi, Zodios, type ZodiosOptions } from '@zodios/core'; +import { z } from 'zod'; + +const ClientResource = z + .object({ + id: z.string(), + name: z.string(), + created_at: z.string(), + updated_at: z.string(), + }) + .passthrough(); +const ClientCollection = z.array(ClientResource); +const v1_import_import_Body = z + .object({ type: z.string(), data: z.string() }) + .passthrough(); +const OrganizationResource = z + .object({ id: z.string(), name: z.string(), is_personal: z.string() }) + .passthrough(); +const ProjectResource = z + .object({ + id: z.string(), + name: z.string(), + color: z.string(), + client_id: z.union([z.string(), z.null()]), + }) + .passthrough(); +const ProjectCollection = z.array(ProjectResource); +const createProject_Body = z + .object({ + name: z.string(), + color: z.string(), + client_id: z.union([z.string(), z.null()]).optional(), + }) + .passthrough(); +const TagResource = z + .object({ + id: z.string(), + name: z.string(), + created_at: z.string(), + updated_at: z.string(), + }) + .passthrough(); +const TagCollection = z.array(TagResource); +const before = z.union([z.string(), z.null()]).optional(); +const TimeEntryResource = z + .object({ + id: z.string(), + start: z.string(), + end: z.union([z.string(), z.null()]), + duration: z.union([z.number(), z.null()]), + description: z.union([z.string(), z.null()]), + task_id: z.union([z.string(), z.null()]), + project_id: z.union([z.string(), z.null()]), + user_id: z.string(), + tags: z.array(z.string()), + }) + .passthrough(); +const TimeEntryCollection = z.array(TimeEntryResource); +const createTimeEntry_Body = z + .object({ + user_id: z.string().uuid(), + task_id: z.union([z.string(), z.null()]).optional(), + start: z.string(), + end: z.union([z.string(), z.null()]).optional(), + description: z.union([z.string(), z.null()]).optional(), + tags: z.union([z.array(z.string()), z.null()]).optional(), + }) + .passthrough(); +const updateTimeEntry_Body = z + .object({ + task_id: z.union([z.string(), z.null()]).optional(), + start: z.string(), + end: z.union([z.string(), z.null()]).optional(), + description: z.union([z.string(), z.null()]).optional(), + tags: z.union([z.array(z.string()), z.null()]).optional(), + }) + .passthrough(); +const UserResource = z + .object({ + id: z.string(), + name: z.string(), + email: z.string(), + role: z.string(), + is_placeholder: z.boolean(), + }) + .passthrough(); +const UserCollection = z.array(UserResource); + +export const schemas = { + ClientResource, + ClientCollection, + v1_import_import_Body, + OrganizationResource, + ProjectResource, + ProjectCollection, + createProject_Body, + TagResource, + TagCollection, + before, + TimeEntryResource, + TimeEntryCollection, + createTimeEntry_Body, + updateTimeEntry_Body, + UserResource, + UserCollection, +}; + +const endpoints = makeApi([ + { + method: 'get', + path: '/v1/organizations/:organization', + alias: 'v1.organizations.show', + requestFormat: 'json', + parameters: [ + { + name: 'organization', + type: 'Path', + schema: z.string().uuid(), + }, + ], + response: z.object({ data: OrganizationResource }).passthrough(), + errors: [ + { + status: 403, + description: `Authorization error`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 404, + description: `Not found`, + schema: z.object({ message: z.string() }).passthrough(), + }, + ], + }, + { + method: 'put', + path: '/v1/organizations/:organization', + alias: 'v1.organizations.update', + requestFormat: 'json', + parameters: [ + { + name: 'body', + type: 'Body', + schema: z.object({ name: z.string() }).passthrough(), + }, + { + name: 'organization', + type: 'Path', + schema: z.string().uuid(), + }, + ], + response: z.object({ data: OrganizationResource }).passthrough(), + errors: [ + { + status: 403, + description: `Authorization error`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 404, + description: `Not found`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 422, + description: `Validation error`, + schema: z + .object({ + message: z.string(), + errors: z.record(z.array(z.string())), + }) + .passthrough(), + }, + ], + }, + { + method: 'get', + path: '/v1/organizations/:organization/clients', + alias: 'v1.clients.index', + requestFormat: 'json', + parameters: [ + { + name: 'organization', + type: 'Path', + schema: z.string().uuid(), + }, + ], + response: z.object({ data: ClientCollection }).passthrough(), + errors: [ + { + status: 403, + description: `Authorization error`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 404, + description: `Not found`, + schema: z.object({ message: z.string() }).passthrough(), + }, + ], + }, + { + method: 'post', + path: '/v1/organizations/:organization/clients', + alias: 'v1.clients.store', + requestFormat: 'json', + parameters: [ + { + name: 'body', + type: 'Body', + schema: z.object({ name: z.string() }).passthrough(), + }, + { + name: 'organization', + type: 'Path', + schema: z.string().uuid(), + }, + ], + response: z.object({ data: ClientResource }).passthrough(), + errors: [ + { + status: 403, + description: `Authorization error`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 404, + description: `Not found`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 422, + description: `Validation error`, + schema: z + .object({ + message: z.string(), + errors: z.record(z.array(z.string())), + }) + .passthrough(), + }, + ], + }, + { + method: 'put', + path: '/v1/organizations/:organization/clients/:client', + alias: 'v1.clients.update', + requestFormat: 'json', + parameters: [ + { + name: 'body', + type: 'Body', + schema: z.object({ name: z.string() }).passthrough(), + }, + { + name: 'organization', + type: 'Path', + schema: z.string().uuid(), + }, + { + name: 'client', + type: 'Path', + schema: z.string().uuid(), + }, + ], + response: z.object({ data: ClientResource }).passthrough(), + errors: [ + { + status: 403, + description: `Authorization error`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 404, + description: `Not found`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 422, + description: `Validation error`, + schema: z + .object({ + message: z.string(), + errors: z.record(z.array(z.string())), + }) + .passthrough(), + }, + ], + }, + { + method: 'delete', + path: '/v1/organizations/:organization/clients/:client', + alias: 'v1.clients.destroy', + requestFormat: 'json', + parameters: [ + { + name: 'body', + type: 'Body', + schema: z.object({}).partial().passthrough(), + }, + { + name: 'organization', + type: 'Path', + schema: z.string().uuid(), + }, + { + name: 'client', + type: 'Path', + schema: z.string().uuid(), + }, + ], + response: z.null(), + errors: [ + { + status: 403, + description: `Authorization error`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 404, + description: `Not found`, + schema: z.object({ message: z.string() }).passthrough(), + }, + ], + }, + { + method: 'post', + path: '/v1/organizations/:organization/import', + alias: 'v1.import.import', + requestFormat: 'json', + parameters: [ + { + name: 'body', + type: 'Body', + schema: v1_import_import_Body, + }, + { + name: 'organization', + type: 'Path', + schema: z.string().uuid(), + }, + ], + response: z + .object({ + report: z + .object({ + clients: z + .object({ created: z.number().int() }) + .passthrough(), + projects: z + .object({ created: z.number().int() }) + .passthrough(), + tasks: z + .object({ created: z.number().int() }) + .passthrough(), + 'time-entries': z + .object({ created: z.number().int() }) + .passthrough(), + tags: z + .object({ created: z.number().int() }) + .passthrough(), + users: z + .object({ created: z.number().int() }) + .passthrough(), + }) + .passthrough(), + }) + .passthrough(), + errors: [ + { + status: 400, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 403, + description: `Authorization error`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 404, + description: `Not found`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 422, + description: `Validation error`, + schema: z + .object({ + message: z.string(), + errors: z.record(z.array(z.string())), + }) + .passthrough(), + }, + ], + }, + { + method: 'get', + path: '/v1/organizations/:organization/projects', + alias: 'getProjects', + requestFormat: 'json', + parameters: [ + { + name: 'organization', + type: 'Path', + schema: z.string().uuid(), + }, + ], + response: z.object({ data: ProjectCollection }).passthrough(), + errors: [ + { + status: 403, + description: `Authorization error`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 404, + description: `Not found`, + schema: z.object({ message: z.string() }).passthrough(), + }, + ], + }, + { + method: 'post', + path: '/v1/organizations/:organization/projects', + alias: 'createProject', + requestFormat: 'json', + parameters: [ + { + name: 'body', + type: 'Body', + schema: createProject_Body, + }, + { + name: 'organization', + type: 'Path', + schema: z.string().uuid(), + }, + ], + response: z.object({ data: ProjectResource }).passthrough(), + errors: [ + { + status: 403, + description: `Authorization error`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 404, + description: `Not found`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 422, + description: `Validation error`, + schema: z + .object({ + message: z.string(), + errors: z.record(z.array(z.string())), + }) + .passthrough(), + }, + ], + }, + { + method: 'get', + path: '/v1/organizations/:organization/projects/:project', + alias: 'getProject', + requestFormat: 'json', + parameters: [ + { + name: 'organization', + type: 'Path', + schema: z.string().uuid(), + }, + { + name: 'project', + type: 'Path', + schema: z.string().uuid(), + }, + ], + response: z.object({ data: ProjectResource }).passthrough(), + errors: [ + { + status: 403, + description: `Authorization error`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 404, + description: `Not found`, + schema: z.object({ message: z.string() }).passthrough(), + }, + ], + }, + { + method: 'put', + path: '/v1/organizations/:organization/projects/:project', + alias: 'updateProject', + requestFormat: 'json', + parameters: [ + { + name: 'body', + type: 'Body', + schema: createProject_Body, + }, + { + name: 'organization', + type: 'Path', + schema: z.string().uuid(), + }, + { + name: 'project', + type: 'Path', + schema: z.string().uuid(), + }, + ], + response: z.object({ data: ProjectResource }).passthrough(), + errors: [ + { + status: 403, + description: `Authorization error`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 404, + description: `Not found`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 422, + description: `Validation error`, + schema: z + .object({ + message: z.string(), + errors: z.record(z.array(z.string())), + }) + .passthrough(), + }, + ], + }, + { + method: 'delete', + path: '/v1/organizations/:organization/projects/:project', + alias: 'deleteProject', + requestFormat: 'json', + parameters: [ + { + name: 'body', + type: 'Body', + schema: z.object({}).partial().passthrough(), + }, + { + name: 'organization', + type: 'Path', + schema: z.string().uuid(), + }, + { + name: 'project', + type: 'Path', + schema: z.string().uuid(), + }, + ], + response: z.null(), + errors: [ + { + status: 403, + description: `Authorization error`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 404, + description: `Not found`, + schema: z.object({ message: z.string() }).passthrough(), + }, + ], + }, + { + method: 'get', + path: '/v1/organizations/:organization/tags', + alias: 'getTags', + requestFormat: 'json', + parameters: [ + { + name: 'organization', + type: 'Path', + schema: z.string().uuid(), + }, + ], + response: z.object({ data: TagCollection }).passthrough(), + errors: [ + { + status: 403, + description: `Authorization error`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 404, + description: `Not found`, + schema: z.object({ message: z.string() }).passthrough(), + }, + ], + }, + { + method: 'post', + path: '/v1/organizations/:organization/tags', + alias: 'createTag', + requestFormat: 'json', + parameters: [ + { + name: 'body', + type: 'Body', + schema: z.object({ name: z.string() }).passthrough(), + }, + { + name: 'organization', + type: 'Path', + schema: z.string().uuid(), + }, + ], + response: z.object({ data: TagResource }).passthrough(), + errors: [ + { + status: 403, + description: `Authorization error`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 404, + description: `Not found`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 422, + description: `Validation error`, + schema: z + .object({ + message: z.string(), + errors: z.record(z.array(z.string())), + }) + .passthrough(), + }, + ], + }, + { + method: 'put', + path: '/v1/organizations/:organization/tags/:tag', + alias: 'updateTag', + requestFormat: 'json', + parameters: [ + { + name: 'body', + type: 'Body', + schema: z.object({ name: z.string() }).passthrough(), + }, + { + name: 'organization', + type: 'Path', + schema: z.string().uuid(), + }, + { + name: 'tag', + type: 'Path', + schema: z.string().uuid(), + }, + ], + response: z.object({ data: TagResource }).passthrough(), + errors: [ + { + status: 403, + description: `Authorization error`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 404, + description: `Not found`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 422, + description: `Validation error`, + schema: z + .object({ + message: z.string(), + errors: z.record(z.array(z.string())), + }) + .passthrough(), + }, + ], + }, + { + method: 'delete', + path: '/v1/organizations/:organization/tags/:tag', + alias: 'deleteTag', + requestFormat: 'json', + parameters: [ + { + name: 'body', + type: 'Body', + schema: z.object({}).partial().passthrough(), + }, + { + name: 'organization', + type: 'Path', + schema: z.string().uuid(), + }, + { + name: 'tag', + type: 'Path', + schema: z.string().uuid(), + }, + ], + response: z.null(), + errors: [ + { + status: 403, + description: `Authorization error`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 404, + description: `Not found`, + schema: z.object({ message: z.string() }).passthrough(), + }, + ], + }, + { + method: 'get', + path: '/v1/organizations/:organization/time-entries', + alias: 'getTimeEntries', + requestFormat: 'json', + parameters: [ + { + name: 'organization', + type: 'Path', + schema: z.string().uuid(), + }, + { + name: 'user_id', + type: 'Query', + schema: z.string().uuid().optional(), + }, + { + name: 'before', + type: 'Query', + schema: before, + }, + { + name: 'after', + type: 'Query', + schema: before, + }, + { + name: 'active', + type: 'Query', + schema: z.enum(['true', 'false']).optional(), + }, + { + name: 'limit', + type: 'Query', + schema: z.number().int().gte(1).lte(500).optional(), + }, + { + name: 'only_full_dates', + type: 'Query', + schema: z.boolean().optional(), + }, + ], + response: z.object({ data: TimeEntryCollection }).passthrough(), + errors: [ + { + status: 403, + description: `Authorization error`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 404, + description: `Not found`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 422, + description: `Validation error`, + schema: z + .object({ + message: z.string(), + errors: z.record(z.array(z.string())), + }) + .passthrough(), + }, + ], + }, + { + method: 'post', + path: '/v1/organizations/:organization/time-entries', + alias: 'createTimeEntry', + requestFormat: 'json', + parameters: [ + { + name: 'body', + type: 'Body', + schema: createTimeEntry_Body, + }, + { + name: 'organization', + type: 'Path', + schema: z.string().uuid(), + }, + ], + response: z.object({ data: TimeEntryResource }).passthrough(), + errors: [ + { + status: 403, + description: `Authorization error`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 404, + description: `Not found`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 422, + description: `Validation error`, + schema: z + .object({ + message: z.string(), + errors: z.record(z.array(z.string())), + }) + .passthrough(), + }, + ], + }, + { + method: 'put', + path: '/v1/organizations/:organization/time-entries/:timeEntry', + alias: 'updateTimeEntry', + requestFormat: 'json', + parameters: [ + { + name: 'body', + type: 'Body', + schema: updateTimeEntry_Body, + }, + { + name: 'organization', + type: 'Path', + schema: z.string().uuid(), + }, + { + name: 'timeEntry', + type: 'Path', + schema: z.string().uuid(), + }, + ], + response: z.object({ data: TimeEntryResource }).passthrough(), + errors: [ + { + status: 403, + description: `Authorization error`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 404, + description: `Not found`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 422, + description: `Validation error`, + schema: z + .object({ + message: z.string(), + errors: z.record(z.array(z.string())), + }) + .passthrough(), + }, + ], + }, + { + method: 'delete', + path: '/v1/organizations/:organization/time-entries/:timeEntry', + alias: 'deleteTimeEntry', + requestFormat: 'json', + parameters: [ + { + name: 'body', + type: 'Body', + schema: z.object({}).partial().passthrough(), + }, + { + name: 'organization', + type: 'Path', + schema: z.string().uuid(), + }, + { + name: 'timeEntry', + type: 'Path', + schema: z.string().uuid(), + }, + ], + response: z.null(), + errors: [ + { + status: 403, + description: `Authorization error`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 404, + description: `Not found`, + schema: z.object({ message: z.string() }).passthrough(), + }, + ], + }, + { + method: 'get', + path: '/v1/organizations/:organization/users', + alias: 'v1.users.index', + requestFormat: 'json', + parameters: [ + { + name: 'organization', + type: 'Path', + schema: z.string().uuid(), + }, + ], + response: z.object({ data: UserCollection }).passthrough(), + errors: [ + { + status: 403, + description: `Authorization error`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 404, + description: `Not found`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 422, + description: `Validation error`, + schema: z + .object({ + message: z.string(), + errors: z.record(z.array(z.string())), + }) + .passthrough(), + }, + ], + }, + { + method: 'post', + path: '/v1/organizations/:organization/users/:user/invite-placeholder', + alias: 'v1.users.invite-placeholder', + requestFormat: 'json', + parameters: [ + { + name: 'body', + type: 'Body', + schema: z.object({}).partial().passthrough(), + }, + { + name: 'organization', + type: 'Path', + schema: z.string().uuid(), + }, + { + name: 'user', + type: 'Path', + schema: z.string().uuid(), + }, + ], + response: z.string(), + errors: [ + { + status: 403, + description: `Authorization error`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 404, + description: `Not found`, + schema: z.object({ message: z.string() }).passthrough(), + }, + ], + }, +]); + +export const api = new Zodios('/api', endpoints); + +export function createApiClient(baseUrl: string, options?: ZodiosOptions) { + return new Zodios(baseUrl, endpoints, options); +} diff --git a/package-lock.json b/package-lock.json index a5469820..496c0b7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,9 +5,17 @@ "packages": { "": { "dependencies": { + "@heroicons/vue": "^2.1.1", "@rushstack/eslint-patch": "^1.7.0", "@vue/eslint-config-prettier": "^9.0.0", - "@vue/eslint-config-typescript": "^12.0.0" + "@vue/eslint-config-typescript": "^12.0.0", + "dayjs": "^1.11.10", + "echarts": "^5.5.0", + "parse-duration": "^1.1.0", + "pinia": "^2.1.7", + "radix-vue": "^1.4.9", + "tailwind-merge": "^2.2.1", + "vue-echarts": "^6.6.9" }, "devDependencies": { "@inertiajs/vue3": "^1.0.0", @@ -20,6 +28,7 @@ "autoprefixer": "^10.4.7", "axios": "^1.6.4", "laravel-vite-plugin": "^1.0.0", + "openapi-zod-client": "^1.16.2", "postcss": "^8.4.14", "tailwindcss": "^3.1.0", "typescript": "^5.3.3", @@ -51,6 +60,121 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.6.tgz", + "integrity": "sha512-M3YgsLjI0lZxvrpeGVk9Ap032W6TPQkH6pRAZz81Ac3WUNF79VQooAFnp8umjvVzUmD93NkogxEwbSce7qMsUg==", + "dev": true, + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "js-yaml": "^3.13.1" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", + "dev": true + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.1.0.tgz", + "integrity": "sha512-9Kt7EuS/7WbMAUv2gSziqjvxwDbFSg3Xeyfuj5laUODX8o/k/CpsAKiQ8W7/R88eXFTMbJYg6+7uAmOWNKmwnw==", + "dev": true, + "dependencies": { + "@apidevtools/json-schema-ref-parser": "9.0.6", + "@apidevtools/openapi-schemas": "^2.1.0", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "ajv": "^8.6.3", + "ajv-draft-04": "^1.0.0", + "call-me-maybe": "^1.0.1" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, + "node_modules/@apidevtools/swagger-parser/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@apidevtools/swagger-parser/node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "dev": true, + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@apidevtools/swagger-parser/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, "node_modules/@babel/code-frame": { "version": "7.23.5", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", @@ -135,6 +259,192 @@ "node": ">=4" } }, + "node_modules/@babel/compat-data": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", + "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.24.0", + "@babel/parser": "^7.24.0", + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.0", + "@babel/types": "^7.24.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.23.6", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-validator-identifier": { "version": "7.22.20", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", @@ -144,6 +454,29 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-validator-option": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.0.tgz", + "integrity": "sha512-ulDZdc0Aj5uLc5nETsa7EPx2L7rM0YJM8r7ck7U73AXi7qOV44IHHRAYZHY6iU1rr3C5N4NtTmMRUJP6kwCWeA==", + "dev": true, + "dependencies": { + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.0", + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/highlight": { "version": "7.23.4", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", @@ -230,10 +563,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", - "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", - "dev": true, + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz", + "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==", "bin": { "parser": "bin/babel-parser.js" }, @@ -241,10 +573,79 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", + "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.0.tgz", + "integrity": "sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz", - "integrity": "sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", "cpu": [ "ppc64" ], @@ -258,9 +659,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.11.tgz", - "integrity": "sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", "cpu": [ "arm" ], @@ -274,9 +675,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz", - "integrity": "sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", "cpu": [ "arm64" ], @@ -290,9 +691,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.11.tgz", - "integrity": "sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", "cpu": [ "x64" ], @@ -306,9 +707,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz", - "integrity": "sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", "cpu": [ "arm64" ], @@ -322,9 +723,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz", - "integrity": "sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", "cpu": [ "x64" ], @@ -338,9 +739,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz", - "integrity": "sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", "cpu": [ "arm64" ], @@ -354,9 +755,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz", - "integrity": "sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", "cpu": [ "x64" ], @@ -370,9 +771,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz", - "integrity": "sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", "cpu": [ "arm" ], @@ -386,9 +787,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz", - "integrity": "sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", "cpu": [ "arm64" ], @@ -402,9 +803,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz", - "integrity": "sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", "cpu": [ "ia32" ], @@ -418,9 +819,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz", - "integrity": "sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", "cpu": [ "loong64" ], @@ -434,9 +835,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz", - "integrity": "sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", "cpu": [ "mips64el" ], @@ -450,9 +851,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz", - "integrity": "sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", "cpu": [ "ppc64" ], @@ -466,9 +867,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz", - "integrity": "sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", "cpu": [ "riscv64" ], @@ -482,9 +883,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz", - "integrity": "sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", "cpu": [ "s390x" ], @@ -498,9 +899,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz", - "integrity": "sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", "cpu": [ "x64" ], @@ -514,9 +915,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz", - "integrity": "sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", "cpu": [ "x64" ], @@ -530,9 +931,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz", - "integrity": "sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", "cpu": [ "x64" ], @@ -546,9 +947,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz", - "integrity": "sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", "cpu": [ "x64" ], @@ -562,9 +963,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz", - "integrity": "sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", "cpu": [ "arm64" ], @@ -578,9 +979,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz", - "integrity": "sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", "cpu": [ "ia32" ], @@ -594,9 +995,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz", - "integrity": "sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", "cpu": [ "x64" ], @@ -677,14 +1078,79 @@ } }, "node_modules/@eslint/js": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", - "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "peer": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", + "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", + "dependencies": { + "@floating-ui/utils": "^0.2.1" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.3.tgz", + "integrity": "sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==", + "dependencies": { + "@floating-ui/core": "^1.0.0", + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", + "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" + }, + "node_modules/@floating-ui/vue": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@floating-ui/vue/-/vue-1.0.6.tgz", + "integrity": "sha512-EdrOljjkpkkqZnrpqUcPoz9NvHxuTjUtSInh6GMv3+Mcy+giY2cE2pHh9rpacRcZ2eMSCxel9jWkWXTjLmY55w==", + "dependencies": { + "@floating-ui/dom": "^1.6.1", + "@floating-ui/utils": "^0.2.1", + "vue-demi": ">=0.13.0" + } + }, + "node_modules/@floating-ui/vue/node_modules/vue-demi": { + "version": "0.14.7", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.7.tgz", + "integrity": "sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@heroicons/vue": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@heroicons/vue/-/vue-2.1.1.tgz", + "integrity": "sha512-Yi5nh/89L193ALgHyJUQUdNLsKXPrrE3yj5yiR8WAlo7nZyXGxGauQcEAmBsa2XJGMhBMuEdoOiuZ8wEwTBxVQ==", + "peerDependencies": { + "vue": ">= 3" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -741,24 +1207,24 @@ "peer": true }, "node_modules/@inertiajs/core": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/@inertiajs/core/-/core-1.0.14.tgz", - "integrity": "sha512-S33PU6mWEYbn/s2Op+CJ6MN7ON354vWw8Y+UvtQzPt0r7pVgOuIArrqqsoulf9oQz9sbP1+vp/tCvyBzm4XmpA==", + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inertiajs/core/-/core-1.0.15.tgz", + "integrity": "sha512-7h73+manxkpXdSPlVa2uSjo10AbSS0z9Q1jV7r/otqHfTYCNp7JJxHUX4QpKq/3Z88U8bDUe1RRix1o76pyd5Q==", "dev": true, "dependencies": { - "axios": "^1.2.0", + "axios": "^1.6.0", "deepmerge": "^4.0.0", "nprogress": "^0.2.0", "qs": "^6.9.0" } }, "node_modules/@inertiajs/vue3": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/@inertiajs/vue3/-/vue3-1.0.14.tgz", - "integrity": "sha512-lKL3Bm9k95Gw1GAq4RxgjfwSMfklkeMbvEfzwmsEBsZ4BbbWwfpC/+KS+4O4faTjjijczvkDPhMKv4duzFxtGw==", + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inertiajs/vue3/-/vue3-1.0.15.tgz", + "integrity": "sha512-9Im1DAei5OrMDGu3XkVBE45hb5KEyujqGq9D3eE/Va1/NMHF76+SpGUQc+A5cJcBDjo3JG31Fl2R4li7WiIecw==", "dev": true, "dependencies": { - "@inertiajs/core": "1.0.14", + "@inertiajs/core": "1.0.15", "lodash.clonedeep": "^4.5.0", "lodash.isequal": "^4.5.0" }, @@ -766,6 +1232,14 @@ "vue": "^3.0.0" } }, + "node_modules/@internationalized/date": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.5.2.tgz", + "integrity": "sha512-vo1yOMUt2hzp63IutEaTUxROdvQg1qlMRsbCvbay2AK2Gai7wIgCyK5weEX3nHkiLgo4qCXHijFNC/ILhlRpOQ==", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -783,33 +1257,60 @@ "node": ">=12" } }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, "engines": { "node": ">=6.0.0" @@ -818,19 +1319,34 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.21", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.21.tgz", - "integrity": "sha512-SRfKmRe1KvYnxjEMtxEr+J4HIeMX5YBg/qhRHpxEIGjhX1rshcHlnFUE9K0GazhVKWM7B+nARSkV8LuvJdJ5/g==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "dev": true + }, + "node_modules/@liuli-util/fs-extra": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@liuli-util/fs-extra/-/fs-extra-0.1.0.tgz", + "integrity": "sha512-eaAyDyMGT23QuRGbITVY3SOJff3G9ekAAyGqB9joAnTBmqvFN+9a1FazOdO70G6IUqgpKV451eBHYSRcOJ/FNQ==", + "dev": true, + "dependencies": { + "@types/fs-extra": "^9.0.13", + "fs-extra": "^10.1.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -885,12 +1401,12 @@ } }, "node_modules/@playwright/test": { - "version": "1.41.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.1.tgz", - "integrity": "sha512-9g8EWTjiQ9yFBXc6HjCWe41msLpxEX0KhmfmPl9RPLJdfzL4F0lg2BdJ91O9azFdl11y1pmpwdjBiSxvqc+btw==", + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.42.1.tgz", + "integrity": "sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==", "dev": true, "dependencies": { - "playwright": "1.41.1" + "playwright": "1.42.1" }, "bin": { "playwright": "cli.js" @@ -900,9 +1416,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.5.tgz", - "integrity": "sha512-idWaG8xeSRCfRq9KpRysDHJ/rEHBEXcHuJ82XY0yYFIWnLMjZv9vF/7DOq8djQ2n3Lk6+3qfSH8AqlmHlmi1MA==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz", + "integrity": "sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==", "cpu": [ "arm" ], @@ -913,9 +1429,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.5.tgz", - "integrity": "sha512-f14d7uhAMtsCGjAYwZGv6TwuS3IFaM4ZnGMUn3aCBgkcHAYErhV1Ad97WzBvS2o0aaDv4mVz+syiN0ElMyfBPg==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz", + "integrity": "sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==", "cpu": [ "arm64" ], @@ -926,9 +1442,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.5.tgz", - "integrity": "sha512-ndoXeLx455FffL68OIUrVr89Xu1WLzAG4n65R8roDlCoYiQcGGg6MALvs2Ap9zs7AHg8mpHtMpwC8jBBjZrT/w==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz", + "integrity": "sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==", "cpu": [ "arm64" ], @@ -939,9 +1455,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.5.tgz", - "integrity": "sha512-UmElV1OY2m/1KEEqTlIjieKfVwRg0Zwg4PLgNf0s3glAHXBN99KLpw5A5lrSYCa1Kp63czTpVll2MAqbZYIHoA==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz", + "integrity": "sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==", "cpu": [ "x64" ], @@ -952,9 +1468,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.5.tgz", - "integrity": "sha512-Q0LcU61v92tQB6ae+udZvOyZ0wfpGojtAKrrpAaIqmJ7+psq4cMIhT/9lfV6UQIpeItnq/2QDROhNLo00lOD1g==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz", + "integrity": "sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==", "cpu": [ "arm" ], @@ -965,9 +1481,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.5.tgz", - "integrity": "sha512-dkRscpM+RrR2Ee3eOQmRWFjmV/payHEOrjyq1VZegRUa5OrZJ2MAxBNs05bZuY0YCtpqETDy1Ix4i/hRqX98cA==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz", + "integrity": "sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==", "cpu": [ "arm64" ], @@ -978,9 +1494,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.5.tgz", - "integrity": "sha512-QaKFVOzzST2xzY4MAmiDmURagWLFh+zZtttuEnuNn19AiZ0T3fhPyjPPGwLNdiDT82ZE91hnfJsUiDwF9DClIQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz", + "integrity": "sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==", "cpu": [ "arm64" ], @@ -991,9 +1507,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.5.tgz", - "integrity": "sha512-HeGqmRJuyVg6/X6MpE2ur7GbymBPS8Np0S/vQFHDmocfORT+Zt76qu+69NUoxXzGqVP1pzaY6QIi0FJWLC3OPA==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz", + "integrity": "sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==", "cpu": [ "riscv64" ], @@ -1004,9 +1520,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.5.tgz", - "integrity": "sha512-Dq1bqBdLaZ1Gb/l2e5/+o3B18+8TI9ANlA1SkejZqDgdU/jK/ThYaMPMJpVMMXy2uRHvGKbkz9vheVGdq3cJfA==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz", + "integrity": "sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==", "cpu": [ "x64" ], @@ -1017,9 +1533,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.5.tgz", - "integrity": "sha512-ezyFUOwldYpj7AbkwyW9AJ203peub81CaAIVvckdkyH8EvhEIoKzaMFJj0G4qYJ5sw3BpqhFrsCc30t54HV8vg==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz", + "integrity": "sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==", "cpu": [ "x64" ], @@ -1030,9 +1546,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.5.tgz", - "integrity": "sha512-aHSsMnUw+0UETB0Hlv7B/ZHOGY5bQdwMKJSzGfDfvyhnpmVxLMGnQPGNE9wgqkLUs3+gbG1Qx02S2LLfJ5GaRQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz", + "integrity": "sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==", "cpu": [ "arm64" ], @@ -1043,9 +1559,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.5.tgz", - "integrity": "sha512-AiqiLkb9KSf7Lj/o1U3SEP9Zn+5NuVKgFdRIZkvd4N0+bYrTOovVd0+LmYCPQGbocT4kvFyK+LXCDiXPBF3fyA==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz", + "integrity": "sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==", "cpu": [ "ia32" ], @@ -1056,9 +1572,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.5.tgz", - "integrity": "sha512-1q+mykKE3Vot1kaFJIDoUFv5TuW+QQVaf2FmTT9krg86pQrGStOSJJ0Zil7CFagyxDuouTepzt5Y5TVzyajOdQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz", + "integrity": "sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==", "cpu": [ "x64" ], @@ -1069,9 +1585,22 @@ ] }, "node_modules/@rushstack/eslint-patch": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.7.0.tgz", - "integrity": "sha512-Jh4t/593gxs0lJZ/z3NnasKlplXT2f+4y/LZYuaKZW5KAaiVFL/fThhs+17EbUd53jUVJ0QudYCBGbN/psvaqg==" + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.7.2.tgz", + "integrity": "sha512-RbhOOTCNoCrbfkRyoXODZp75MlpiHMgbE5MEBZAnnnLyQNgrigEj4p0lzsMDyc1zVsJDLrivB58tgg3emX0eEA==" + }, + "node_modules/@swc/helpers": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.6.tgz", + "integrity": "sha512-aYX01Ke9hunpoCexYAgQucEpARGQ5w/cqHFrIR+e9gdKb1QWTsVJuTJ2ozQzIAxLyRQe/m+2RqzkyOOGiMKRQA==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@swc/helpers/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/@tailwindcss/forms": { "version": "0.5.7", @@ -1106,35 +1635,44 @@ "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, "node_modules/@types/node": { - "version": "20.11.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.5.tgz", - "integrity": "sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==", + "version": "20.11.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.27.tgz", + "integrity": "sha512-qyUZfMnCg1KEz57r7pzFtSGt49f6RPkPBis3Vo4PbS7roQEDn22hiHzl/Lo1q4i4hDEgBJmBF/NTNg2XR0HbFg==", "dev": true, "dependencies": { "undici-types": "~5.26.4" } }, "node_modules/@types/semver": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", - "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==" + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.19.0.tgz", - "integrity": "sha512-DUCUkQNklCQYnrBSSikjVChdc84/vMPDQSgJTHBZ64G9bA9w0Crc0rd2diujKbTdp6w2J47qkeHQLoi0rpLCdg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.19.0", - "@typescript-eslint/type-utils": "6.19.0", - "@typescript-eslint/utils": "6.19.0", - "@typescript-eslint/visitor-keys": "6.19.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -1160,14 +1698,14 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.19.0.tgz", - "integrity": "sha512-1DyBLG5SH7PYCd00QlroiW60YJ4rWMuUGa/JBV0iZuqi4l4IK3twKPq5ZkEebmGqRjXWVgsUzfd3+nZveewgow==", - "dependencies": { - "@typescript-eslint/scope-manager": "6.19.0", - "@typescript-eslint/types": "6.19.0", - "@typescript-eslint/typescript-estree": "6.19.0", - "@typescript-eslint/visitor-keys": "6.19.0", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4" }, "engines": { @@ -1187,12 +1725,12 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.19.0.tgz", - "integrity": "sha512-dO1XMhV2ehBI6QN8Ufi7I10wmUovmLU0Oru3n5LVlM2JuzB4M+dVphCPLkVpKvGij2j/pHBWuJ9piuXx+BhzxQ==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", "dependencies": { - "@typescript-eslint/types": "6.19.0", - "@typescript-eslint/visitor-keys": "6.19.0" + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -1203,12 +1741,12 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.19.0.tgz", - "integrity": "sha512-mcvS6WSWbjiSxKCwBcXtOM5pRkPQ6kcDds/juxcy/727IQr3xMEcwr/YLHW2A2+Fp5ql6khjbKBzOyjuPqGi/w==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", "dependencies": { - "@typescript-eslint/typescript-estree": "6.19.0", - "@typescript-eslint/utils": "6.19.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -1229,9 +1767,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.19.0.tgz", - "integrity": "sha512-lFviGV/vYhOy3m8BJ/nAKoAyNhInTdXpftonhWle66XHAtT1ouBlkjL496b5H5hb8dWXHwtypTqgtb/DEa+j5A==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", "engines": { "node": "^16.0.0 || >=18.0.0" }, @@ -1241,12 +1779,12 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.19.0.tgz", - "integrity": "sha512-o/zefXIbbLBZ8YJ51NlkSAt2BamrK6XOmuxSR3hynMIzzyMY33KuJ9vuMdFSXW+H0tVvdF9qBPTHA91HDb4BIQ==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", "dependencies": { - "@typescript-eslint/types": "6.19.0", - "@typescript-eslint/visitor-keys": "6.19.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1268,16 +1806,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.19.0.tgz", - "integrity": "sha512-QR41YXySiuN++/dC9UArYOg4X86OAYP83OWTewpVx5ct1IZhjjgTLocj7QNxGhWoTqknsgpl7L+hGygCO+sdYw==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.19.0", - "@typescript-eslint/types": "6.19.0", - "@typescript-eslint/typescript-estree": "6.19.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", "semver": "^7.5.4" }, "engines": { @@ -1292,11 +1830,11 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.19.0.tgz", - "integrity": "sha512-hZaUCORLgubBvtGpp1JEFEazcuEdfxta9j4iUwdSAr7mEsYYAp3EAUyCZk3VEEqGj6W+AV4uWyrDGtrlawAsgQ==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", "dependencies": { - "@typescript-eslint/types": "6.19.0", + "@typescript-eslint/types": "6.21.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -1355,55 +1893,56 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.4.14", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.14.tgz", - "integrity": "sha512-ro4Zzl/MPdWs7XwxT7omHRxAjMbDFRZEEjD+2m3NBf8YzAe3HuoSEZosXQo+m1GQ1G3LQ1LdmNh1RKTYe+ssEg==", - "dev": true, + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.21.tgz", + "integrity": "sha512-MjXawxZf2SbZszLPYxaFCjxfibYrzr3eYbKxwpLR9EQN+oaziSu3qKVbwBERj1IFIB8OLUewxB5m/BFzi613og==", "dependencies": { - "@babel/parser": "^7.23.6", - "@vue/shared": "3.4.14", + "@babel/parser": "^7.23.9", + "@vue/shared": "3.4.21", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.0.2" } }, "node_modules/@vue/compiler-dom": { - "version": "3.4.14", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.14.tgz", - "integrity": "sha512-nOZTY+veWNa0DKAceNWxorAbWm0INHdQq7cejFaWM1WYnoNSJbSEKYtE7Ir6lR/+mo9fttZpPVI9ZFGJ1juUEQ==", - "dev": true, + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.21.tgz", + "integrity": "sha512-IZC6FKowtT1sl0CR5DpXSiEB5ayw75oT2bma1BEhV7RRR1+cfwLrxc2Z8Zq/RGFzJ8w5r9QtCOvTjQgdn0IKmA==", "dependencies": { - "@vue/compiler-core": "3.4.14", - "@vue/shared": "3.4.14" + "@vue/compiler-core": "3.4.21", + "@vue/shared": "3.4.21" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.4.14", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.14.tgz", - "integrity": "sha512-1vHc9Kv1jV+YBZC/RJxQJ9JCxildTI+qrhtDh6tPkR1O8S+olBUekimY0km0ZNn8nG1wjtFAe9XHij+YLR8cRQ==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.23.6", - "@vue/compiler-core": "3.4.14", - "@vue/compiler-dom": "3.4.14", - "@vue/compiler-ssr": "3.4.14", - "@vue/shared": "3.4.14", + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.21.tgz", + "integrity": "sha512-me7epoTxYlY+2CUM7hy9PCDdpMPfIwrOvAXud2Upk10g4YLv9UBW7kL798TvMeDhPthkZ0CONNrK2GoeI1ODiQ==", + "dependencies": { + "@babel/parser": "^7.23.9", + "@vue/compiler-core": "3.4.21", + "@vue/compiler-dom": "3.4.21", + "@vue/compiler-ssr": "3.4.21", + "@vue/shared": "3.4.21", "estree-walker": "^2.0.2", - "magic-string": "^0.30.5", - "postcss": "^8.4.33", + "magic-string": "^0.30.7", + "postcss": "^8.4.35", "source-map-js": "^1.0.2" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.4.14", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.14.tgz", - "integrity": "sha512-bXT6+oAGlFjTYVOTtFJ4l4Jab1wjsC0cfSfOe2B4Z0N2vD2zOBSQ9w694RsCfhjk+bC2DY5Gubb1rHZVii107Q==", - "dev": true, + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.21.tgz", + "integrity": "sha512-M5+9nI2lPpAsgXOGQobnIueVqc9sisBFexh5yMIMRAPYLa7+5wEJs8iqOZc1WAa9WQbx9GR2twgznU8LTIiZ4Q==", "dependencies": { - "@vue/compiler-dom": "3.4.14", - "@vue/shared": "3.4.14" + "@vue/compiler-dom": "3.4.21", + "@vue/shared": "3.4.21" } }, + "node_modules/@vue/devtools-api": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.1.tgz", + "integrity": "sha512-LgPscpE3Vs0x96PzSSB4IGVSZXZBZHpfxs+ZA1d+VEPwHdOXowy/Y2CsvCAIFrf+ssVU1pD1jidj505EpUnfbA==" + }, "node_modules/@vue/eslint-config-prettier": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/@vue/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", @@ -1466,53 +2005,48 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.4.14", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.14.tgz", - "integrity": "sha512-xRYwze5Q4tK7tT2J4uy4XLhK/AIXdU5EBUu9PLnIHcOKXO0uyXpNNMzlQKuq7B+zwtq6K2wuUL39pHA6ZQzObw==", - "dev": true, + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.21.tgz", + "integrity": "sha512-UhenImdc0L0/4ahGCyEzc/pZNwVgcglGy9HVzJ1Bq2Mm9qXOpP8RyNTjookw/gOCUlXSEtuZ2fUg5nrHcoqJcw==", "dependencies": { - "@vue/shared": "3.4.14" + "@vue/shared": "3.4.21" } }, "node_modules/@vue/runtime-core": { - "version": "3.4.14", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.14.tgz", - "integrity": "sha512-qu+NMkfujCoZL6cfqK5NOfxgXJROSlP2ZPs4CTcVR+mLrwl4TtycF5Tgo0QupkdBL+2kigc6EsJlTcuuZC1NaQ==", - "dev": true, + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.21.tgz", + "integrity": "sha512-pQthsuYzE1XcGZznTKn73G0s14eCJcjaLvp3/DKeYWoFacD9glJoqlNBxt3W2c5S40t6CCcpPf+jG01N3ULyrA==", "dependencies": { - "@vue/reactivity": "3.4.14", - "@vue/shared": "3.4.14" + "@vue/reactivity": "3.4.21", + "@vue/shared": "3.4.21" } }, "node_modules/@vue/runtime-dom": { - "version": "3.4.14", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.14.tgz", - "integrity": "sha512-B85XmcR4E7XsirEHVqhmy4HPbRT9WLFWV9Uhie3OapV9m1MEN9+Er6hmUIE6d8/l2sUygpK9RstFM2bmHEUigA==", - "dev": true, + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.21.tgz", + "integrity": "sha512-gvf+C9cFpevsQxbkRBS1NpU8CqxKw0ebqMvLwcGQrNpx6gqRDodqKqA+A2VZZpQ9RpK2f9yfg8VbW/EpdFUOJw==", "dependencies": { - "@vue/runtime-core": "3.4.14", - "@vue/shared": "3.4.14", + "@vue/runtime-core": "3.4.21", + "@vue/shared": "3.4.21", "csstype": "^3.1.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.4.14", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.14.tgz", - "integrity": "sha512-pwSKXQfYdJBTpvWHGEYI+akDE18TXAiLcGn+Q/2Fj8wQSHWztoo7PSvfMNqu6NDhp309QXXbPFEGCU5p85HqkA==", - "dev": true, + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.21.tgz", + "integrity": "sha512-aV1gXyKSN6Rz+6kZ6kr5+Ll14YzmIbeuWe7ryJl5muJ4uwSwY/aStXTixx76TwkZFJLm1aAlA/HSWEJ4EyiMkg==", "dependencies": { - "@vue/compiler-ssr": "3.4.14", - "@vue/shared": "3.4.14" + "@vue/compiler-ssr": "3.4.21", + "@vue/shared": "3.4.21" }, "peerDependencies": { - "vue": "3.4.14" + "vue": "3.4.21" } }, "node_modules/@vue/shared": { - "version": "3.4.14", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.14.tgz", - "integrity": "sha512-nmi3BtLpvqXAWoRZ6HQ+pFJOHBU4UnH3vD3opgmwXac7vhaHKA9nj1VeGjMggdB9eLtW83eHyPCmOU1qzdsC7Q==", - "dev": true + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.21.tgz", + "integrity": "sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==" }, "node_modules/@vue/tsconfig": { "version": "0.5.1", @@ -1520,6 +2054,16 @@ "integrity": "sha512-VcZK7MvpjuTPx2w6blwnwZAu5/LgBUtejFOi3pPGQFXQN5Ela03FUtd2Qtg4yWGGissVL0dr6Ro1LfOFh+PCuQ==", "dev": true }, + "node_modules/@zodios/core": { + "version": "10.9.6", + "resolved": "https://registry.npmjs.org/@zodios/core/-/core-10.9.6.tgz", + "integrity": "sha512-aH4rOdb3AcezN7ws8vDgBfGboZMk2JGGzEq/DtW65MhnRxyTGRuLJRWVQ/2KxDgWvV2F5oTkAS+5pnjKbl0n+A==", + "dev": true, + "peerDependencies": { + "axios": "^0.x || ^1.0.0", + "zod": "^3.x" + } + }, "node_modules/acorn": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", @@ -1583,24 +2127,22 @@ } }, "node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "node": ">=8" } }, "node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">=12" + "node": ">=8" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" @@ -1652,9 +2194,9 @@ "dev": true }, "node_modules/autoprefixer": { - "version": "10.4.16", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", - "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==", + "version": "10.4.18", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.18.tgz", + "integrity": "sha512-1DKbDfsr6KUElM6wg+0zRNkB/Q7WcKYAaK+pzXn+Xqmszm/5Xa9coeNdtP88Vi+dPzZnMjhge8GIV49ZQkDa+g==", "dev": true, "funding": [ { @@ -1671,9 +2213,9 @@ } ], "dependencies": { - "browserslist": "^4.21.10", - "caniuse-lite": "^1.0.30001538", - "fraction.js": "^4.3.6", + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001591", + "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.0", "postcss-value-parser": "^4.2.0" @@ -1689,9 +2231,9 @@ } }, "node_modules/axios": { - "version": "1.6.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", - "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", + "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", "dev": true, "dependencies": { "follow-redirects": "^1.15.4", @@ -1739,9 +2281,9 @@ } }, "node_modules/browserslist": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", - "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", "dev": true, "funding": [ { @@ -1758,8 +2300,8 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001565", - "electron-to-chromium": "^1.4.601", + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", "node-releases": "^2.0.14", "update-browserslist-db": "^1.0.13" }, @@ -1770,20 +2312,40 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dev": true, "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "dev": true + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1803,9 +2365,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001577", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001577.tgz", - "integrity": "sha512-rs2ZygrG1PNXMfmncM0B5H1hndY5ZCC9b5TkFaVNfZ+AUlyqcMyVIQtc3fsezi0NUCk5XZfDf9WS6WxMxnfdrg==", + "version": "1.0.30001597", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz", + "integrity": "sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==", "dev": true, "funding": [ { @@ -1837,31 +2399,11 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chalk/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -1874,6 +2416,9 @@ "engines": { "node": ">= 8.10.0" }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, "optionalDependencies": { "fsevents": "~2.3.2" } @@ -1938,6 +2483,12 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1965,8 +2516,12 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/dayjs": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" }, "node_modules/de-indent": { "version": "1.0.2", @@ -2006,17 +2561,20 @@ } }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/delayed-stream": { @@ -2069,10 +2627,19 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/echarts": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.5.0.tgz", + "integrity": "sha512-rNYnNCzqDAPCr4m/fqyUFv7fD9qIsd50S6GDFgO1DxZhncCsNsG7IfUlAlvZe5oSEQxtsjnHiUuppzccry93Xw==", + "dependencies": { + "tslib": "2.3.0", + "zrender": "5.5.0" + } + }, "node_modules/electron-to-chromium": { - "version": "1.4.632", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.632.tgz", - "integrity": "sha512-JGmudTwg7yxMYvR/gWbalqqQiyu7WTFv2Xu3vw4cJHXPFxNgAk0oy8UHaer8nLF4lZJa+rNoj6GsrKIVJTV6Tw==", + "version": "1.4.703", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.703.tgz", + "integrity": "sha512-094ZZC4nHXPKl/OwPinSMtLN9+hoFkdfQGKnvXbY+3WEAYtVDpz9UhJIViiY6Zb8agvqxiaJzNG9M+pRZWvSZw==", "dev": true }, "node_modules/emoji-regex": { @@ -2085,7 +2652,6 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, "engines": { "node": ">=0.12" }, @@ -2093,10 +2659,31 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "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==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.11.tgz", - "integrity": "sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", "dev": true, "hasInstallScript": true, "bin": { @@ -2106,35 +2693,35 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.19.11", - "@esbuild/android-arm": "0.19.11", - "@esbuild/android-arm64": "0.19.11", - "@esbuild/android-x64": "0.19.11", - "@esbuild/darwin-arm64": "0.19.11", - "@esbuild/darwin-x64": "0.19.11", - "@esbuild/freebsd-arm64": "0.19.11", - "@esbuild/freebsd-x64": "0.19.11", - "@esbuild/linux-arm": "0.19.11", - "@esbuild/linux-arm64": "0.19.11", - "@esbuild/linux-ia32": "0.19.11", - "@esbuild/linux-loong64": "0.19.11", - "@esbuild/linux-mips64el": "0.19.11", - "@esbuild/linux-ppc64": "0.19.11", - "@esbuild/linux-riscv64": "0.19.11", - "@esbuild/linux-s390x": "0.19.11", - "@esbuild/linux-x64": "0.19.11", - "@esbuild/netbsd-x64": "0.19.11", - "@esbuild/openbsd-x64": "0.19.11", - "@esbuild/sunos-x64": "0.19.11", - "@esbuild/win32-arm64": "0.19.11", - "@esbuild/win32-ia32": "0.19.11", - "@esbuild/win32-x64": "0.19.11" + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" } }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "dev": true, "engines": { "node": ">=6" @@ -2153,16 +2740,16 @@ } }, "node_modules/eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", - "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.56.0", - "@humanwhocodes/config-array": "^0.11.13", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -2248,17 +2835,17 @@ } }, "node_modules/eslint-plugin-vue": { - "version": "9.20.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.20.1.tgz", - "integrity": "sha512-GyCs8K3lkEvoyC1VV97GJhP1SvqsKCiWGHnbn0gVUYiUhaH2+nB+Dv1uekv1THFMPbBfYxukrzQdltw950k+LQ==", + "version": "9.23.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.23.0.tgz", + "integrity": "sha512-Bqd/b7hGYGrlV+wP/g77tjyFmp81lh5TMw0be9093X02SyelxRRfCI6/IsGq/J7Um0YwB9s0Ry0wlFyjPdmtUw==", "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "natural-compare": "^1.4.0", "nth-check": "^2.1.1", - "postcss-selector-parser": "^6.0.13", - "semver": "^7.5.4", - "vue-eslint-parser": "^9.4.0", + "postcss-selector-parser": "^6.0.15", + "semver": "^7.6.0", + "vue-eslint-parser": "^9.4.2", "xml-name-validator": "^4.0.0" }, "engines": { @@ -2269,9 +2856,9 @@ } }, "node_modules/eslint-plugin-vue/node_modules/postcss-selector-parser": { - "version": "6.0.15", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz", - "integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==", + "version": "6.0.16", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", + "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", "peer": true, "dependencies": { "cssesc": "^3.0.0", @@ -2307,15 +2894,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "peer": true, - "engines": { - "node": ">=8" - } - }, "node_modules/eslint/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2338,18 +2916,6 @@ "node": "*" } }, - "node_modules/eslint/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "peer": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -2366,6 +2932,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", @@ -2399,8 +2978,7 @@ "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" }, "node_modules/esutils": { "version": "2.0.3", @@ -2411,11 +2989,16 @@ "node": ">=0.10.0" } }, + "node_modules/eval-estree-expression": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eval-estree-expression/-/eval-estree-expression-1.1.0.tgz", + "integrity": "sha512-6ZAHSb0wsqxutjk2lXZcW7btSc51I8BhlIetit0wIf5sOb5xDNBrIqe0g8RFyQ/EW6Xwn1szrtButztU7Vdj1Q==", + "dev": true + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "peer": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-diff": { "version": "1.3.0", @@ -2461,9 +3044,9 @@ "peer": true }, "node_modules/fastq": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz", - "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dependencies": { "reusify": "^1.0.4" } @@ -2522,9 +3105,9 @@ } }, "node_modules/flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "peer": true }, "node_modules/follow-redirects": { @@ -2591,9 +3174,9 @@ } }, "node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "dependencies": { "graceful-fs": "^4.2.0", @@ -2601,7 +3184,7 @@ "universalify": "^2.0.0" }, "engines": { - "node": ">=14.14" + "node": ">=12" } }, "node_modules/fs.realpath": { @@ -2611,9 +3194,9 @@ "peer": true }, "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, "hasInstallScript": true, "optional": true, @@ -2633,38 +3216,49 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dev": true, "dependencies": { + "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dev": true, + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "peer": true, "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "*" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -2681,6 +3275,28 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", @@ -2738,6 +3354,27 @@ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2747,21 +3384,21 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.2" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", "dev": true, "engines": { "node": ">= 0.4" @@ -2783,9 +3420,9 @@ } }, "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, "dependencies": { "function-bind": "^1.1.2" @@ -2804,9 +3441,9 @@ } }, "node_modules/ignore": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", - "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "engines": { "node": ">= 4" } @@ -2971,6 +3608,18 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -2989,6 +3638,18 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "peer": true }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -3011,9 +3672,9 @@ } }, "node_modules/laravel-vite-plugin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.0.1.tgz", - "integrity": "sha512-laLEZUnSskIDZtLb2FNRdcjsRUhh1VOVvapbVGVTeaBPJTCF/b6gbPiX2dZdcH1RKoOE0an7L+2gnInk6K33Zw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.0.2.tgz", + "integrity": "sha512-Mcclml10khYzBVxDwJro8wnVDwD4i7XOSEMACQNnarvTnHjrjXLLL+B/Snif2wYAyElsOqagJZ7VAinb/2vF5g==", "dev": true, "dependencies": { "picocolors": "^1.0.0", @@ -3089,12 +3750,6 @@ "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", "dev": true }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true - }, "node_modules/lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", @@ -3112,26 +3767,19 @@ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, - "node_modules/lodash.pick": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", - "integrity": "sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==", - "dev": true - }, "node_modules/lru-cache": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", - "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, - "engines": { - "node": "14 || >=16.14" + "dependencies": { + "yallist": "^3.0.2" } }, "node_modules/magic-string": { - "version": "0.30.5", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", - "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", - "dev": true, + "version": "0.30.8", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", + "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, @@ -3203,6 +3851,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", @@ -3238,7 +3895,6 @@ "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true, "funding": [ { "type": "github", @@ -3257,6 +3913,12 @@ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, "node_modules/node-releases": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", @@ -3347,6 +4009,61 @@ "wrappy": "1" } }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "dev": true + }, + "node_modules/openapi-zod-client": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/openapi-zod-client/-/openapi-zod-client-1.16.2.tgz", + "integrity": "sha512-cN8rb7bzWqvsJzzDGoi9sUJJNid8triuH5ry2dYhJ++Xw/TStkCaOwe2NaNnIrLp7uVgIu+cEIE608Y9YS3/aw==", + "dev": true, + "dependencies": { + "@apidevtools/swagger-parser": "^10.1.0", + "@liuli-util/fs-extra": "^0.1.0", + "@zodios/core": "^10.3.1", + "axios": "^1.6.0", + "cac": "^6.7.14", + "handlebars": "^4.7.7", + "openapi-types": "^12.0.2", + "openapi3-ts": "3.1.0", + "pastable": "^2.2.1", + "prettier": "^2.7.1", + "tanu": "^0.1.13", + "ts-pattern": "^5.0.1", + "whence": "^2.0.0", + "zod": "^3.19.1" + }, + "bin": { + "openapi-zod-client": "bin.js" + } + }, + "node_modules/openapi-zod-client/node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/openapi3-ts": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-3.1.0.tgz", + "integrity": "sha512-1qKTvCCVoV0rkwUh1zq5o8QyghmwYPuhdvtjv1rFjuOnJToXhQyF8eGjNETQ8QmGjr9Jz/tkAKLITIl2s7dw3A==", + "dev": true, + "dependencies": { + "yaml": "^2.1.3" + } + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -3406,6 +4123,49 @@ "node": ">=6" } }, + "node_modules/parse-duration": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-1.1.0.tgz", + "integrity": "sha512-z6t9dvSJYaPoQq7quMzdEagSFtpGu+utzHqqxmpVWNNZRIXnvqyCvn9XsTdh7c/w0Bqmdz3RB3YnRaKtpRtEXQ==" + }, + "node_modules/pastable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/pastable/-/pastable-2.2.1.tgz", + "integrity": "sha512-K4ClMxRKpgN4sXj6VIPPrvor/TMp2yPNCGtfhvV106C73SwefQ3FuegURsH7AQHpqu0WwbvKXRl1HQxF6qax9w==", + "dev": true, + "dependencies": { + "@babel/core": "^7.20.12", + "ts-toolbelt": "^9.6.0", + "type-fest": "^3.5.3" + }, + "engines": { + "node": ">=14.x" + }, + "peerDependencies": { + "react": ">=17", + "xstate": ">=4.32.1" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "xstate": { + "optional": true + } + } + }, + "node_modules/pastable/node_modules/type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -3460,6 +4220,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -3471,8 +4240,7 @@ "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -3494,6 +4262,56 @@ "node": ">=0.10.0" } }, + "node_modules/pinia": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.1.7.tgz", + "integrity": "sha512-+C2AHFtcFqjPih0zpYuvof37SFxMQ7OEG2zV9jRI12i9BOy3YQVAHwdKtyyc8pDcDyIc33WCIsZaCFWU7WWxGQ==", + "dependencies": { + "@vue/devtools-api": "^6.5.0", + "vue-demi": ">=0.14.5" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "@vue/composition-api": "^1.4.0", + "typescript": ">=4.4.4", + "vue": "^2.6.14 || ^3.3.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/pinia/node_modules/vue-demi": { + "version": "0.14.7", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.7.tgz", + "integrity": "sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, "node_modules/pirates": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", @@ -3504,12 +4322,12 @@ } }, "node_modules/playwright": { - "version": "1.41.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.1.tgz", - "integrity": "sha512-gdZAWG97oUnbBdRL3GuBvX3nDDmUOuqzV/D24dytqlKt+eI5KbwusluZRGljx1YoJKZ2NRPaeWiFTeGZO7SosQ==", + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.1.tgz", + "integrity": "sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==", "dev": true, "dependencies": { - "playwright-core": "1.41.1" + "playwright-core": "1.42.1" }, "bin": { "playwright": "cli.js" @@ -3522,9 +4340,9 @@ } }, "node_modules/playwright-core": { - "version": "1.41.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.1.tgz", - "integrity": "sha512-/KPO5DzXSMlxSX77wy+HihKGOunh3hqndhqeo/nMxfigiKzogn8kfL0ZBDu0L1RKgan5XHCPmn6zXd2NUJgjhg==", + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.1.tgz", + "integrity": "sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==", "dev": true, "bin": { "playwright-core": "cli.js" @@ -3533,25 +4351,10 @@ "node": ">=16" } }, - "node_modules/playwright/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/postcss": { - "version": "8.4.33", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", - "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", - "dev": true, + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", "funding": [ { "type": "opencollective", @@ -3647,12 +4450,15 @@ } }, "node_modules/postcss-load-config/node_modules/lilconfig": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", - "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", + "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", "dev": true, "engines": { "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, "node_modules/postcss-nested": { @@ -3675,9 +4481,9 @@ } }, "node_modules/postcss-nested/node_modules/postcss-selector-parser": { - "version": "6.0.15", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz", - "integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==", + "version": "6.0.16", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", + "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", "dev": true, "dependencies": { "cssesc": "^3.0.0", @@ -3716,9 +4522,9 @@ } }, "node_modules/prettier": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz", - "integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", "peer": true, "bin": { "prettier": "bin/prettier.cjs" @@ -3751,18 +4557,17 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "peer": true, "engines": { "node": ">=6" } }, "node_modules/qs": { - "version": "6.11.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", - "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.0.tgz", + "integrity": "sha512-trVZiI6RMOkO476zLGaBIzszOdFPnCCXHPG9kn0yuS1uz6xdVxPfZdB3vUig9pxPFDM9BRAgz/YUIVQ1/vuiUg==", "dev": true, "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -3790,6 +4595,35 @@ } ] }, + "node_modules/radix-vue": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/radix-vue/-/radix-vue-1.5.2.tgz", + "integrity": "sha512-XyXB6mYm7dthW56LDHG4ttR3x+XtspTi48nSq4vHoHldgGZzAEa5VXlqUCr2J21fNKrt3NIYhIIRLB6kKwWwrA==", + "dependencies": { + "@floating-ui/dom": "^1.5.4", + "@floating-ui/vue": "^1.0.4", + "@internationalized/date": "^3.5.2", + "fast-deep-equal": "^3.1.3", + "nanoid": "^5.0.6" + } + }, + "node_modules/radix-vue/node_modules/nanoid": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.6.tgz", + "integrity": "sha512-rRq0eMHoGZxlvaFOUdK1Ev83Bd1IgzzR+WJ3IbDJ7QOSdAxYjlurSPqFs9s4lJg29RT6nPwizFtJhQS6V5xgiA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -3811,6 +4645,25 @@ "node": ">=8.10.0" } }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resize-detector": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/resize-detector/-/resize-detector-0.3.0.tgz", + "integrity": "sha512-R/tCuvuOHQ8o2boRP6vgx8hXCCy87H1eY9V5imBYeVNyNVpuL9ciReSccLj2gDcax9+2weXy3bc8Vv+NRXeEvQ==" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -3861,52 +4714,10 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "peer": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "peer": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/rollup": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.5.tgz", - "integrity": "sha512-E4vQW0H/mbNMw2yLSqJyjtkHY9dslf/p0zuT1xehNRqUTBOFMqEjguDvqhXr7N7r/4ttb2jr4T41d3dncmIgbQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz", + "integrity": "sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==", "dev": true, "dependencies": { "@types/estree": "1.0.5" @@ -3919,19 +4730,19 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.9.5", - "@rollup/rollup-android-arm64": "4.9.5", - "@rollup/rollup-darwin-arm64": "4.9.5", - "@rollup/rollup-darwin-x64": "4.9.5", - "@rollup/rollup-linux-arm-gnueabihf": "4.9.5", - "@rollup/rollup-linux-arm64-gnu": "4.9.5", - "@rollup/rollup-linux-arm64-musl": "4.9.5", - "@rollup/rollup-linux-riscv64-gnu": "4.9.5", - "@rollup/rollup-linux-x64-gnu": "4.9.5", - "@rollup/rollup-linux-x64-musl": "4.9.5", - "@rollup/rollup-win32-arm64-msvc": "4.9.5", - "@rollup/rollup-win32-ia32-msvc": "4.9.5", - "@rollup/rollup-win32-x64-msvc": "4.9.5", + "@rollup/rollup-android-arm-eabi": "4.13.0", + "@rollup/rollup-android-arm64": "4.13.0", + "@rollup/rollup-darwin-arm64": "4.13.0", + "@rollup/rollup-darwin-x64": "4.13.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.13.0", + "@rollup/rollup-linux-arm64-gnu": "4.13.0", + "@rollup/rollup-linux-arm64-musl": "4.13.0", + "@rollup/rollup-linux-riscv64-gnu": "4.13.0", + "@rollup/rollup-linux-x64-gnu": "4.13.0", + "@rollup/rollup-linux-x64-musl": "4.13.0", + "@rollup/rollup-win32-arm64-msvc": "4.13.0", + "@rollup/rollup-win32-ia32-msvc": "4.13.0", + "@rollup/rollup-win32-x64-msvc": "4.13.0", "fsevents": "~2.3.2" } }, @@ -3958,9 +4769,9 @@ } }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -3982,17 +4793,23 @@ "node": ">=10" } }, + "node_modules/semver/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "node_modules/set-function-length": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", - "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, "dependencies": { - "define-data-property": "^1.1.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.2", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -4018,14 +4835,18 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dev": true, "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4051,15 +4872,29 @@ "node": ">=8" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true, "engines": { "node": ">=0.10.0" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -4092,34 +4927,25 @@ "node": ">=8" } }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/string-width-cjs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { + "node_modules/string-width/node_modules/ansi-regex": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/strip-ansi": { + "node_modules/string-width/node_modules/strip-ansi": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", @@ -4134,12 +4960,10 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", + "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -4147,11 +4971,15 @@ "node": ">=8" } }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, "engines": { "node": ">=8" } @@ -4190,6 +5018,28 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/sucrase/node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -4228,6 +5078,23 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/synckit/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/tailwind-merge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.2.1.tgz", + "integrity": "sha512-o+2GTLkthfa5YUt4JxPfzMIpQzZ3adD1vLVkvKE1Twl9UAhGsEbIZhHHZVRttyW177S8PDJI3bTQNaebyofK3Q==", + "dependencies": { + "@babel/runtime": "^7.23.7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", @@ -4266,9 +5133,9 @@ } }, "node_modules/tailwindcss/node_modules/postcss-selector-parser": { - "version": "6.0.15", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz", - "integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==", + "version": "6.0.16", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", + "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", "dev": true, "dependencies": { "cssesc": "^3.0.0", @@ -4278,6 +5145,35 @@ "node": ">=4" } }, + "node_modules/tanu": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/tanu/-/tanu-0.1.13.tgz", + "integrity": "sha512-UbRmX7ccZ4wMVOY/Uw+7ji4VOkEYSYJG1+I4qzbnn4qh/jtvVbrm6BFnF12NQQ4+jGv21wKmjb1iFyUSVnBWcQ==", + "dev": true, + "dependencies": { + "tslib": "^2.4.0", + "typescript": "^4.7.4" + } + }, + "node_modules/tanu/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/tanu/node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -4306,11 +5202,20 @@ } }, "node_modules/tiny-invariant": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz", - "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "dev": true }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -4323,11 +5228,11 @@ } }, "node_modules/ts-api-utils": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", - "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", "engines": { - "node": ">=16.13.0" + "node": ">=16" }, "peerDependencies": { "typescript": ">=4.2.0" @@ -4339,10 +5244,22 @@ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "dev": true }, + "node_modules/ts-pattern": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/ts-pattern/-/ts-pattern-5.0.8.tgz", + "integrity": "sha512-aafbuAQOTEeWmA7wtcL94w6I89EgLD7F+IlWkr596wYxeb0oveWDO5dQpv85YP0CGbxXT/qXBIeV6IYLcoZ2uA==", + "dev": true + }, + "node_modules/ts-toolbelt": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz", + "integrity": "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==", + "dev": true + }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" }, "node_modules/type-check": { "version": "0.4.0", @@ -4369,9 +5286,9 @@ } }, "node_modules/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", + "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4380,6 +5297,19 @@ "node": ">=14.17" } }, + "node_modules/uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "dev": true, + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -4429,7 +5359,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "peer": true, "dependencies": { "punycode": "^2.1.0" } @@ -4440,13 +5369,13 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/vite": { - "version": "5.0.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.11.tgz", - "integrity": "sha512-XBMnDjZcNAw/G1gEiskiM1v6yzM4GE5aMGvhWTlHAYYhxb7S3/V1s3m2LDHa8Vh6yIWYYB0iJwsEaS523c4oYA==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.6.tgz", + "integrity": "sha512-yYIAZs9nVfRJ/AiOLCA91zzhjsHUgMjB+EigzFb6W2XTLO8JixBCKCjvhKZaye+NKYHCrkv3Oh50dH9EdLU2RA==", "dev": true, "dependencies": { "esbuild": "^0.19.3", - "postcss": "^8.4.32", + "postcss": "^8.4.35", "rollup": "^4.2.0" }, "bin": { @@ -4495,9 +5424,9 @@ } }, "node_modules/vite-plugin-checker": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/vite-plugin-checker/-/vite-plugin-checker-0.6.2.tgz", - "integrity": "sha512-YvvvQ+IjY09BX7Ab+1pjxkELQsBd4rPhWNw8WLBeFVxu/E7O+n6VYAqNsKdK/a2luFlX/sMpoWdGFfg4HvwdJQ==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/vite-plugin-checker/-/vite-plugin-checker-0.6.4.tgz", + "integrity": "sha512-2zKHH5oxr+ye43nReRbC2fny1nyARwhxdm0uNYp/ERy4YvU9iZpNOsueoi/luXw5gnpqRSvjcEPxXbS153O2wA==", "dev": true, "dependencies": { "@babel/code-frame": "^7.12.13", @@ -4507,8 +5436,6 @@ "commander": "^8.0.0", "fast-glob": "^3.2.7", "fs-extra": "^11.1.0", - "lodash.debounce": "^4.0.8", - "lodash.pick": "^4.4.0", "npm-run-path": "^4.0.1", "semver": "^7.5.0", "strip-ansi": "^6.0.0", @@ -4559,15 +5486,6 @@ } } }, - "node_modules/vite-plugin-checker/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/vite-plugin-checker/node_modules/commander": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", @@ -4577,16 +5495,18 @@ "node": ">= 12" } }, - "node_modules/vite-plugin-checker/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/vite-plugin-checker/node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", "dev": true, "dependencies": { - "ansi-regex": "^5.0.1" + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, "engines": { - "node": ">=8" + "node": ">=14.14" } }, "node_modules/vite-plugin-full-reload": { @@ -4599,6 +5519,20 @@ "picomatch": "^2.3.1" } }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/vscode-jsonrpc": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz", @@ -4685,16 +5619,15 @@ "dev": true }, "node_modules/vue": { - "version": "3.4.14", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.14.tgz", - "integrity": "sha512-Rop5Al/ZcBbBz+KjPZaZDgHDX0kUP4duEzDbm+1o91uxYUNmJrZSBuegsNIJvUGy+epLevNRNhLjm08VKTgGyw==", - "dev": true, + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.21.tgz", + "integrity": "sha512-5hjyV/jLEIKD/jYl4cavMcnzKwjMKohureP8ejn3hhEjwhWIhWeuzL2kJAjzl/WyVsgPY56Sy4Z40C3lVshxXA==", "dependencies": { - "@vue/compiler-dom": "3.4.14", - "@vue/compiler-sfc": "3.4.14", - "@vue/runtime-dom": "3.4.14", - "@vue/server-renderer": "3.4.14", - "@vue/shared": "3.4.14" + "@vue/compiler-dom": "3.4.21", + "@vue/compiler-sfc": "3.4.21", + "@vue/runtime-dom": "3.4.21", + "@vue/server-renderer": "3.4.21", + "@vue/shared": "3.4.21" }, "peerDependencies": { "typescript": "*" @@ -4705,10 +5638,59 @@ } } }, + "node_modules/vue-echarts": { + "version": "6.6.9", + "resolved": "https://registry.npmjs.org/vue-echarts/-/vue-echarts-6.6.9.tgz", + "integrity": "sha512-mojIq3ZvsjabeVmDthhAUDV8Kgf2Rr/X4lV4da7gEFd1fP05gcSJ0j7wa7HQkW5LlFmF2gdCJ8p4Chas6NNIQQ==", + "hasInstallScript": true, + "dependencies": { + "resize-detector": "^0.3.0", + "vue-demi": "^0.13.11" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.5", + "@vue/runtime-core": "^3.0.0", + "echarts": "^5.4.1", + "vue": "^2.6.12 || ^3.1.1" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + }, + "@vue/runtime-core": { + "optional": true + } + } + }, + "node_modules/vue-echarts/node_modules/vue-demi": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz", + "integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, "node_modules/vue-eslint-parser": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.0.tgz", - "integrity": "sha512-7KsNBb6gHFA75BtneJsoK/dbZ281whUIwFYdQxA68QrCrGMXYzUMbPDHGcOQ0OocIVKrWSKWXZ4mL7tonCXoUw==", + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.2.tgz", + "integrity": "sha512-Ry9oiGmCAK91HrKMtCrKFWmSFWvYkpGglCeFAIqDdr9zdXmMMpJOmUJS7WWsW7fX81h6mwHmUZCQQ1E0PkSwYQ==", "dependencies": { "debug": "^4.3.4", "eslint-scope": "^7.1.1", @@ -4755,6 +5737,19 @@ "typescript": "*" } }, + "node_modules/whence": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whence/-/whence-2.0.0.tgz", + "integrity": "sha512-exmM13v2lg8juBbfS2tao/alV68jyryPXS+jf29NBNGLzE2hRgmzvQFQGX5CxNfH4Ag9qRqd6gGpXTH2JxqKHg==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.15.7", + "eval-estree-expression": "^1.0.1" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4769,6 +5764,12 @@ "node": ">= 8" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -4804,30 +5805,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -4848,16 +5825,43 @@ "node": ">=8" } }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/wrappy": { @@ -4876,15 +5880,19 @@ } }, "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true }, "node_modules/yaml": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", - "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", + "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==", "dev": true, + "bin": { + "yaml": "bin.mjs" + }, "engines": { "node": ">= 14" } @@ -4902,9 +5910,9 @@ } }, "node_modules/ziggy-js": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/ziggy-js/-/ziggy-js-1.8.1.tgz", - "integrity": "sha512-fnf30uG0yvUQBPL4T8YPgmkBHUdjYaOFgUb1K1gj0+rclnLTNr9/K/cxC3xkCZyYCZz8oTnXkdf3oJXRPSzavw==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/ziggy-js/-/ziggy-js-1.8.2.tgz", + "integrity": "sha512-lCtJkecWwAIinVs2aQgKMSheFbZ60VHnEBXMdJ5I/n7C8kFt2NNd0iuFCc0fLYMBzLl25qhVeYTK+1aZr3jGVA==", "dev": true, "dependencies": { "qs": "~6.9.7" @@ -4921,6 +5929,23 @@ "funding": { "url": "https://github.com/sponsors/ljharb" } + }, + "node_modules/zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zrender": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.5.0.tgz", + "integrity": "sha512-O3MilSi/9mwoovx77m6ROZM7sXShR/O/JIanvzTwjN3FORfLSr81PsUGd7jlaYOeds9d8tw82oP44+3YucVo+w==", + "dependencies": { + "tslib": "2.3.0" + } } } } diff --git a/package.json b/package.json index 1e06d963..cb10fff8 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "lint": "eslint --ext .js,.vue,.ts --ignore-path .gitignore .", "lint:fix": "eslint --fix --ext .js,.vue,.ts --ignore-path .gitignore .", "type-check": "vue-tsc --noEmit", - "test:e2e": "npx playwright test" + "test:e2e": "rm -rf test-results/.auth && npx playwright test", + "generate:zod": "npx openapi-zod-client http://localhost:80/docs/api.json --output openapi.json.client.ts --base-url /api" }, "devDependencies": { "@inertiajs/vue3": "^1.0.0", @@ -20,6 +21,7 @@ "autoprefixer": "^10.4.7", "axios": "^1.6.4", "laravel-vite-plugin": "^1.0.0", + "openapi-zod-client": "^1.16.2", "postcss": "^8.4.14", "tailwindcss": "^3.1.0", "typescript": "^5.3.3", @@ -30,8 +32,16 @@ "ziggy-js": "^1.8.1" }, "dependencies": { + "@heroicons/vue": "^2.1.1", "@rushstack/eslint-patch": "^1.7.0", "@vue/eslint-config-prettier": "^9.0.0", - "@vue/eslint-config-typescript": "^12.0.0" + "@vue/eslint-config-typescript": "^12.0.0", + "dayjs": "^1.11.10", + "echarts": "^5.5.0", + "parse-duration": "^1.1.0", + "pinia": "^2.1.7", + "radix-vue": "^1.4.9", + "tailwind-merge": "^2.2.1", + "vue-echarts": "^6.6.9" } } diff --git a/phpunit.xml b/phpunit.xml index 82235621..d5881ea7 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -21,7 +21,7 @@ - + diff --git a/playwright.config.ts b/playwright.config.ts index 5a1b230e..507e1c49 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -16,20 +16,22 @@ export default defineConfig({ /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, + retries: process.env.CI ? 1 : 0, /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'html', + reporter: process.env.CI ? 'line' : 'html', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ // baseURL: 'http://127.0.0.1:3000', /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', + trace: process.env.CI ? 'on-first-retry' : 'on', }, + timeout: 10 * 1000, + /* Configure projects for major browsers */ projects: [ { diff --git a/playwright/config.ts b/playwright/config.ts index 9942dfd7..06ada1f8 100644 --- a/playwright/config.ts +++ b/playwright/config.ts @@ -1,2 +1,2 @@ export const PLAYWRIGHT_BASE_URL = - process.env.PLAYWRIGHT_BASE_URL ?? 'http://laravel.test'; + process.env.PLAYWRIGHT_BASE_URL ?? 'http://solidtime.test'; diff --git a/playwright/fixtures.ts b/playwright/fixtures.ts index 7d34ba60..92bcf4ce 100644 --- a/playwright/fixtures.ts +++ b/playwright/fixtures.ts @@ -1,4 +1,4 @@ -import { expect, test as baseTest } from '@playwright/test'; +import { test as baseTest } from '@playwright/test'; import fs from 'fs'; import path from 'path'; import { PLAYWRIGHT_BASE_URL } from './config'; @@ -55,11 +55,6 @@ export const test = baseTest.extend({ // Wait for the final URL to ensure that the cookies are actually set. await page.waitForURL(PLAYWRIGHT_BASE_URL + '/dashboard'); - // Alternatively, you can wait until the page reaches a state where all cookies are set. - await expect( - page.getByRole('heading', { name: 'Dashboard' }) - ).toBeVisible(); - // End of authentication steps. await page.context().storageState({ path: fileName }); diff --git a/public/fonts/Outfit-VariableFont_wght.ttf b/public/fonts/Outfit-VariableFont_wght.ttf new file mode 100644 index 00000000..96106f09 Binary files /dev/null and b/public/fonts/Outfit-VariableFont_wght.ttf differ diff --git a/resources/css/app.css b/resources/css/app.css index 0de21207..6697dea9 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -2,6 +2,18 @@ @tailwind components; @tailwind utilities; + +:root{ + --theme-color-icon-default: #42466C; + --theme-color-card-background: #13152B; +} + + [x-cloak] { display: none; } + +@font-face { + font-family: 'Outfit'; + src: url('/fonts/Outfit-VariableFont_wght.ttf'); +} diff --git a/resources/js/Components/ActionMessage.vue b/resources/js/Components/ActionMessage.vue index ef7a286e..a0becbe8 100644 --- a/resources/js/Components/ActionMessage.vue +++ b/resources/js/Components/ActionMessage.vue @@ -10,7 +10,7 @@ defineProps({ leave-active-class="transition ease-in duration-1000" leave-from-class="opacity-100" leave-to-class="opacity-0"> -
+
diff --git a/resources/js/Components/ActionSection.vue b/resources/js/Components/ActionSection.vue index f54f18be..6d07781e 100644 --- a/resources/js/Components/ActionSection.vue +++ b/resources/js/Components/ActionSection.vue @@ -15,7 +15,7 @@ import SectionTitle from './SectionTitle.vue';
+ class="px-4 py-5 sm:p-6 bg-card-background shadow sm:rounded-lg">
diff --git a/resources/js/Components/ApplicationLogo.vue b/resources/js/Components/ApplicationLogo.vue index b8a554d0..bc90424a 100644 --- a/resources/js/Components/ApplicationLogo.vue +++ b/resources/js/Components/ApplicationLogo.vue @@ -2,7 +2,7 @@ + class="fill-white" /> diff --git a/resources/js/Components/AuthenticationCard.vue b/resources/js/Components/AuthenticationCard.vue index ee11110f..80c8a3d4 100644 --- a/resources/js/Components/AuthenticationCard.vue +++ b/resources/js/Components/AuthenticationCard.vue @@ -1,12 +1,12 @@
+ class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-card-background">
+ class="w-full sm:max-w-md mt-6 px-6 py-4 bg-card-background shadow-md overflow-hidden sm:rounded-lg">
diff --git a/resources/js/Components/Checkbox.vue b/resources/js/Components/Checkbox.vue index e5ffd9a7..e2d0bda0 100644 --- a/resources/js/Components/Checkbox.vue +++ b/resources/js/Components/Checkbox.vue @@ -30,5 +30,5 @@ const proxyChecked = computed({ v-model="proxyChecked" type="checkbox" :value="value" - class="rounded dark:bg-gray-900 border-gray-300 dark:border-gray-700 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:focus:ring-offset-gray-800" /> + class="rounded bg-input-background border-input-border text-indigo-600 shadow-sm focus:ring-indigo-500" /> diff --git a/resources/js/Components/ConfirmationModal.vue b/resources/js/Components/ConfirmationModal.vue index a771b506..e5f730db 100644 --- a/resources/js/Components/ConfirmationModal.vue +++ b/resources/js/Components/ConfirmationModal.vue @@ -29,12 +29,12 @@ const close = () => { :max-width="maxWidth" :closeable="closeable" @close="close"> -
+
{
-

+

-
+
@@ -61,7 +60,7 @@ const close = () => {
+ class="flex flex-row justify-end px-6 py-4 bg-card-background text-end">
diff --git a/resources/js/Components/CurrentSidebarTimer.vue b/resources/js/Components/CurrentSidebarTimer.vue new file mode 100644 index 00000000..da07403d --- /dev/null +++ b/resources/js/Components/CurrentSidebarTimer.vue @@ -0,0 +1,13 @@ + + + diff --git a/resources/js/Components/DangerButton.vue b/resources/js/Components/DangerButton.vue index af1283bd..e2c756ac 100644 --- a/resources/js/Components/DangerButton.vue +++ b/resources/js/Components/DangerButton.vue @@ -14,7 +14,7 @@ Modal.vue diff --git a/resources/js/Components/Dashboard/ActivityGraphCard.vue b/resources/js/Components/Dashboard/ActivityGraphCard.vue new file mode 100644 index 00000000..066ae6e8 --- /dev/null +++ b/resources/js/Components/Dashboard/ActivityGraphCard.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/resources/js/Components/Dashboard/DashboardCard.vue b/resources/js/Components/Dashboard/DashboardCard.vue new file mode 100644 index 00000000..b07d2724 --- /dev/null +++ b/resources/js/Components/Dashboard/DashboardCard.vue @@ -0,0 +1,25 @@ + + + diff --git a/resources/js/Components/Dashboard/DayOverviewCardEntry.vue b/resources/js/Components/Dashboard/DayOverviewCardEntry.vue new file mode 100644 index 00000000..9d9b5c96 --- /dev/null +++ b/resources/js/Components/Dashboard/DayOverviewCardEntry.vue @@ -0,0 +1,103 @@ + + + diff --git a/resources/js/Components/Dashboard/LastSevenDaysCard.vue b/resources/js/Components/Dashboard/LastSevenDaysCard.vue new file mode 100644 index 00000000..123f00a8 --- /dev/null +++ b/resources/js/Components/Dashboard/LastSevenDaysCard.vue @@ -0,0 +1,22 @@ + + + diff --git a/resources/js/Components/Dashboard/ProjectsChartCard.vue b/resources/js/Components/Dashboard/ProjectsChartCard.vue new file mode 100644 index 00000000..6d3221c5 --- /dev/null +++ b/resources/js/Components/Dashboard/ProjectsChartCard.vue @@ -0,0 +1,105 @@ + + + + + diff --git a/resources/js/Components/Dashboard/RecentlyTrackedTasksCard.vue b/resources/js/Components/Dashboard/RecentlyTrackedTasksCard.vue new file mode 100644 index 00000000..35195a24 --- /dev/null +++ b/resources/js/Components/Dashboard/RecentlyTrackedTasksCard.vue @@ -0,0 +1,23 @@ + + + diff --git a/resources/js/Components/Dashboard/RecentlyTrackedTasksCardEntry.vue b/resources/js/Components/Dashboard/RecentlyTrackedTasksCardEntry.vue new file mode 100644 index 00000000..65c5c830 --- /dev/null +++ b/resources/js/Components/Dashboard/RecentlyTrackedTasksCardEntry.vue @@ -0,0 +1,23 @@ + + + diff --git a/resources/js/Components/Dashboard/TeamActivityCard.vue b/resources/js/Components/Dashboard/TeamActivityCard.vue new file mode 100644 index 00000000..9f262cd7 --- /dev/null +++ b/resources/js/Components/Dashboard/TeamActivityCard.vue @@ -0,0 +1,27 @@ + + + diff --git a/resources/js/Components/Dashboard/TeamActivityCardEntry.vue b/resources/js/Components/Dashboard/TeamActivityCardEntry.vue new file mode 100644 index 00000000..a359d475 --- /dev/null +++ b/resources/js/Components/Dashboard/TeamActivityCardEntry.vue @@ -0,0 +1,31 @@ + + + diff --git a/resources/js/Components/Dashboard/ThisWeekOverview.vue b/resources/js/Components/Dashboard/ThisWeekOverview.vue new file mode 100644 index 00000000..75a191ce --- /dev/null +++ b/resources/js/Components/Dashboard/ThisWeekOverview.vue @@ -0,0 +1,171 @@ + + + + + diff --git a/resources/js/Components/DialogModal.vue b/resources/js/Components/DialogModal.vue index 06534946..94b37f37 100644 --- a/resources/js/Components/DialogModal.vue +++ b/resources/js/Components/DialogModal.vue @@ -30,17 +30,17 @@ const close = () => { :closeable="closeable" @close="close">
-
+
-
+
+ class="flex flex-row justify-end px-6 py-4 bg-gray-100 text-end">
diff --git a/resources/js/Components/Dropdown.vue b/resources/js/Components/Dropdown.vue index f2470927..fb5709c9 100644 --- a/resources/js/Components/Dropdown.vue +++ b/resources/js/Components/Dropdown.vue @@ -1,22 +1,28 @@