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/README.md b/README.md index eb0c4796..08b42048 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 @@ -52,6 +54,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/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..6919faad 100644 --- a/app/Http/Controllers/Api/V1/TimeEntryController.php +++ b/app/Http/Controllers/Api/V1/TimeEntryController.php @@ -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 { @@ -103,6 +105,8 @@ public function index(Organization $organization, TimeEntryIndexRequest $request * Create time entry * * @throws AuthorizationException|TimeEntryStillRunning + * + * @operationId createTimeEntry */ public function store(Organization $organization, TimeEntryStoreRequest $request): JsonResource { @@ -120,7 +124,7 @@ public function store(Organization $organization, TimeEntryStoreRequest $request $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 +135,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 +147,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 +157,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/Requests/V1/TimeEntry/TimeEntryUpdateRequest.php b/app/Http/Requests/V1/TimeEntry/TimeEntryUpdateRequest.php index f044ce72..f9c1bf4c 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', 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/Providers/JetstreamServiceProvider.php b/app/Providers/JetstreamServiceProvider.php index e8e5bfb0..f1762c10 100644 --- a/app/Providers/JetstreamServiceProvider.php +++ b/app/Providers/JetstreamServiceProvider.php @@ -94,7 +94,7 @@ protected function configurePermissions(): void 'tags:update', 'tags:delete', 'organizations:view', - ])->description('Editor users have the ability to read, create, and update.'); + ])->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 +104,6 @@ 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.'); } } 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/docker-compose.yml b/docker-compose.yml index 876bcfe0..3874cd43 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -65,6 +65,8 @@ services: 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}" 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..ec0789b0 --- /dev/null +++ b/e2e/timetracker.spec.ts @@ -0,0 +1,507 @@ +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 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.getByTestId('tag_dropdown_entries')).toHaveText( + newTagName + ); +}); + +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.getByTestId('tag_dropdown_entries')).toHaveText( + newTagName + ); + + 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/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..5366233e --- /dev/null +++ b/openapi.json.client.ts @@ -0,0 +1,833 @@ +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 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(); + +export const schemas = { + ClientResource, + ClientCollection, + OrganizationResource, + ProjectResource, + ProjectCollection, + createProject_Body, + TagResource, + TagCollection, + before, + TimeEntryResource, + TimeEntryCollection, + createTimeEntry_Body, + updateTimeEntry_Body, +}; + +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: '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.string().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(), + }, + ], + }, +]); + +export const api = new Zodios('http://solidtime.test/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..f1d717a5 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,207 @@ "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/lru-cache": { + "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, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "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-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "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 +469,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 +578,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,6 +588,75 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", + "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", + "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", @@ -685,6 +1101,71 @@ "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", @@ -784,14 +1265,14 @@ } }, "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" @@ -807,9 +1288,9 @@ } }, "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 +1299,48 @@ "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.24", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.24.tgz", + "integrity": "sha512-+VaWXDa6+l6MhflBvVXjIEAzb59nQ2JUK3bwRp2zRpPtU+8TFRy9Gg/5oIcNlkEL5PGlBFGfemUVvIgLnTzq7Q==", "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/@liuli-util/fs-extra/node_modules/fs-extra": { + "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", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1106,6 +1616,15 @@ "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", @@ -1358,7 +1877,6 @@ "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, "dependencies": { "@babel/parser": "^7.23.6", "@vue/shared": "3.4.14", @@ -1371,7 +1889,6 @@ "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, "dependencies": { "@vue/compiler-core": "3.4.14", "@vue/shared": "3.4.14" @@ -1381,7 +1898,6 @@ "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", @@ -1398,12 +1914,16 @@ "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, "dependencies": { "@vue/compiler-dom": "3.4.14", "@vue/shared": "3.4.14" } }, + "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", @@ -1469,7 +1989,6 @@ "version": "3.4.14", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.14.tgz", "integrity": "sha512-xRYwze5Q4tK7tT2J4uy4XLhK/AIXdU5EBUu9PLnIHcOKXO0uyXpNNMzlQKuq7B+zwtq6K2wuUL39pHA6ZQzObw==", - "dev": true, "dependencies": { "@vue/shared": "3.4.14" } @@ -1478,7 +1997,6 @@ "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, "dependencies": { "@vue/reactivity": "3.4.14", "@vue/shared": "3.4.14" @@ -1488,7 +2006,6 @@ "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, "dependencies": { "@vue/runtime-core": "3.4.14", "@vue/shared": "3.4.14", @@ -1499,7 +2016,6 @@ "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, "dependencies": { "@vue/compiler-ssr": "3.4.14", "@vue/shared": "3.4.14" @@ -1511,8 +2027,7 @@ "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 + "integrity": "sha512-nmi3BtLpvqXAWoRZ6HQ+pFJOHBU4UnH3vD3opgmwXac7vhaHKA9nj1VeGjMggdB9eLtW83eHyPCmOU1qzdsC7Q==" }, "node_modules/@vue/tsconfig": { "version": "0.5.1", @@ -1520,6 +2035,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", @@ -1767,7 +2292,16 @@ "browserslist": "cli.js" }, "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + "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": { @@ -1784,6 +2318,12 @@ "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", @@ -1938,6 +2478,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 +2511,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", @@ -2069,6 +2619,20 @@ "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/echarts/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + }, "node_modules/electron-to-chromium": { "version": "1.4.632", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.632.tgz", @@ -2085,7 +2649,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" }, @@ -2366,6 +2929,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 +2975,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 +2986,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", @@ -2633,6 +3213,15 @@ "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", @@ -2738,6 +3327,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", @@ -2971,6 +3581,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 +3611,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", @@ -3131,7 +3765,6 @@ "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, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, @@ -3203,6 +3836,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 +3880,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 +3898,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 +3994,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 +4108,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", @@ -3471,8 +4216,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 +4238,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", @@ -3551,7 +4345,6 @@ "version": "8.4.33", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -3751,7 +4544,6 @@ "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" } @@ -3790,6 +4582,16 @@ } ] }, + "node_modules/radix-vue": { + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/radix-vue/-/radix-vue-1.4.9.tgz", + "integrity": "sha512-xGY29nUqaAJTncubdhevwGuv5ZSHGvZjUinWBXVrwHvo6oeJ/SLudxYuc3qRcAU+DK+OcthEQFq255wLJJe4Rw==", + "dependencies": { + "@floating-ui/dom": "^1.5.4", + "@floating-ui/vue": "^1.0.4", + "fast-deep-equal": "^3.1.3" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -3811,6 +4613,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", @@ -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", @@ -4228,6 +5063,18 @@ "url": "https://opencollective.com/unts" } }, + "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", @@ -4278,6 +5125,29 @@ "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/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", @@ -4311,6 +5181,15 @@ "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==", "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", @@ -4339,6 +5218,18 @@ "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", @@ -4380,6 +5271,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 +5333,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" } @@ -4688,7 +5591,6 @@ "version": "3.4.14", "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.14.tgz", "integrity": "sha512-Rop5Al/ZcBbBz+KjPZaZDgHDX0kUP4duEzDbm+1o91uxYUNmJrZSBuegsNIJvUGy+epLevNRNhLjm08VKTgGyw==", - "dev": true, "dependencies": { "@vue/compiler-dom": "3.4.14", "@vue/compiler-sfc": "3.4.14", @@ -4705,6 +5607,55 @@ } } }, + "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", @@ -4755,6 +5706,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 +5733,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", @@ -4921,6 +5891,28 @@ "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" + } + }, + "node_modules/zrender/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" } } } diff --git a/package.json b/package.json index 1e06d963..9ecfa5fa 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": "npx playwright test", + "generate:zod": "npx openapi-zod-client http://localhost:80/docs/api.json --output openapi.json.client.ts --base-url http://solidtime.test/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/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/ConfirmationModal.vue b/resources/js/Components/ConfirmationModal.vue index a771b506..abea8f8f 100644 --- a/resources/js/Components/ConfirmationModal.vue +++ b/resources/js/Components/ConfirmationModal.vue @@ -48,12 +48,11 @@ const close = () => {
-

+

-
+
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/Dashboard/ActivityGraphCard.vue b/resources/js/Components/Dashboard/ActivityGraphCard.vue new file mode 100644 index 00000000..e0f9fb95 --- /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..df8be660 --- /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..2356acfd --- /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..eeb5fb36 --- /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..ad4fefaa --- /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..0d2468fe 100644 --- a/resources/js/Components/DialogModal.vue +++ b/resources/js/Components/DialogModal.vue @@ -30,11 +30,11 @@ const close = () => { :closeable="closeable" @close="close">
-
+
-
+
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 @@