mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-10-10 06:43:18 +01:00
Compare commits
1 Commits
fix/api-do
...
feature/ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7117571f50 |
185
.github/workflows/playwright-extensions.yml
vendored
Normal file
185
.github/workflows/playwright-extensions.yml
vendored
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
name: Playwright Tests - Extensions
|
||||||
|
on: push
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: test (${{ matrix.variant }})
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 60
|
||||||
|
|
||||||
|
# Same extension sets as build-private.yml and build-onpremise.yml, so the extension specs run
|
||||||
|
# with and without Billing.
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- variant: private
|
||||||
|
billing: true
|
||||||
|
extensions: Billing Services Invoicing
|
||||||
|
- variant: onpremise
|
||||||
|
billing: false
|
||||||
|
extensions: Invoicing
|
||||||
|
|
||||||
|
services:
|
||||||
|
mailpit:
|
||||||
|
image: 'axllent/mailpit:latest'
|
||||||
|
ports:
|
||||||
|
- 1025:1025
|
||||||
|
- 8025:8025
|
||||||
|
pgsql_test:
|
||||||
|
image: postgres:15
|
||||||
|
env:
|
||||||
|
PGPASSWORD: 'root'
|
||||||
|
POSTGRES_DB: 'laravel'
|
||||||
|
POSTGRES_USER: 'root'
|
||||||
|
POSTGRES_PASSWORD: 'root'
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
options: >-
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
gotenberg:
|
||||||
|
image: gotenberg/gotenberg:8
|
||||||
|
ports:
|
||||||
|
- 3000:3000
|
||||||
|
options: >-
|
||||||
|
--health-cmd "curl --silent --fail http://localhost:3000/health"
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: "Checkout code"
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
|
- name: "Setup node"
|
||||||
|
uses: actions/setup-node@v7
|
||||||
|
with:
|
||||||
|
node-version: '20.x'
|
||||||
|
|
||||||
|
- name: "Setup PHP"
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.3'
|
||||||
|
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv
|
||||||
|
coverage: none
|
||||||
|
|
||||||
|
- name: "Read extension manifest"
|
||||||
|
id: extension-manifest
|
||||||
|
run: |
|
||||||
|
{
|
||||||
|
echo "billing_repository=$(jq -r '.Billing.repository' extensions/manifest.json)"
|
||||||
|
echo "billing_ref=$(jq -r '.Billing.ref' extensions/manifest.json)"
|
||||||
|
echo "services_repository=$(jq -r '.Services.repository' extensions/manifest.json)"
|
||||||
|
echo "services_ref=$(jq -r '.Services.ref' extensions/manifest.json)"
|
||||||
|
echo "invoicing_repository=$(jq -r '.Invoicing.repository' extensions/manifest.json)"
|
||||||
|
echo "invoicing_ref=$(jq -r '.Invoicing.ref' extensions/manifest.json)"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: "Checkout billing extension"
|
||||||
|
if: matrix.billing
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
repository: ${{ steps.extension-manifest.outputs.billing_repository }}
|
||||||
|
ref: ${{ steps.extension-manifest.outputs.billing_ref }}
|
||||||
|
path: extensions/Billing
|
||||||
|
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_BILLING_EXTENSION }}
|
||||||
|
|
||||||
|
- name: "Install composer dependencies in billing extension"
|
||||||
|
if: matrix.billing
|
||||||
|
working-directory: extensions/Billing
|
||||||
|
env:
|
||||||
|
COMPOSER_AUTH: '{"http-basic": {"spark.laravel.com": {"username": "contact@solidtime.io", "password": "${{ secrets.LARAVEL_SPARK_API_KEY }}"}}}'
|
||||||
|
run: composer install -n --prefer-dist --ignore-platform-reqs
|
||||||
|
|
||||||
|
- name: "Install npm dependencies in billing extension"
|
||||||
|
if: matrix.billing
|
||||||
|
run: cd extensions/Billing && npm ci
|
||||||
|
|
||||||
|
- name: "Checkout services extension"
|
||||||
|
if: matrix.billing
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
repository: ${{ steps.extension-manifest.outputs.services_repository }}
|
||||||
|
ref: ${{ steps.extension-manifest.outputs.services_ref }}
|
||||||
|
path: extensions/Services
|
||||||
|
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_SERVICES_EXTENSION }}
|
||||||
|
|
||||||
|
- name: "Install composer dependencies in services extension"
|
||||||
|
if: matrix.billing
|
||||||
|
working-directory: extensions/Services
|
||||||
|
run: composer install -n --prefer-dist --ignore-platform-reqs
|
||||||
|
|
||||||
|
- name: "Install npm dependencies in services extension"
|
||||||
|
if: matrix.billing
|
||||||
|
run: cd extensions/Services && npm ci
|
||||||
|
|
||||||
|
- name: "Checkout invoicing extension"
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
repository: ${{ steps.extension-manifest.outputs.invoicing_repository }}
|
||||||
|
ref: ${{ steps.extension-manifest.outputs.invoicing_ref }}
|
||||||
|
path: extensions/Invoicing
|
||||||
|
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_INVOICING_EXTENSION }}
|
||||||
|
|
||||||
|
- name: "Install composer dependencies in invoicing extension"
|
||||||
|
working-directory: extensions/Invoicing
|
||||||
|
run: composer install -n --prefer-dist --ignore-platform-reqs
|
||||||
|
|
||||||
|
- name: "Install npm dependencies in invoicing extension"
|
||||||
|
run: cd extensions/Invoicing && npm ci
|
||||||
|
|
||||||
|
- name: "Run composer install"
|
||||||
|
run: composer install -n --prefer-dist
|
||||||
|
|
||||||
|
# Must run before `migrate` so the extensions' migrations are applied, and before the
|
||||||
|
# frontend build so vite collects the extensions' assets. It also writes
|
||||||
|
# modules_statuses.json, which playwright.config.ts reads to collect the extension specs.
|
||||||
|
- name: "Activate extensions"
|
||||||
|
run: |
|
||||||
|
for extension in ${{ matrix.extensions }}; do
|
||||||
|
php artisan module:enable "$extension"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: "Prepare Laravel Application"
|
||||||
|
run: |
|
||||||
|
cp .env.ci .env
|
||||||
|
php artisan key:generate
|
||||||
|
php artisan passport:keys
|
||||||
|
php artisan migrate --seed
|
||||||
|
|
||||||
|
- name: "Install dependencies"
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: "Build Frontend"
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: "Install FrankenPHP"
|
||||||
|
run: |
|
||||||
|
ARCH="$(uname -m)"
|
||||||
|
curl -fsSL "https://github.com/dunglas/frankenphp/releases/latest/download/frankenphp-linux-${ARCH}" -o /usr/local/bin/frankenphp
|
||||||
|
chmod +x /usr/local/bin/frankenphp
|
||||||
|
|
||||||
|
- name: "Run Laravel Octane Server"
|
||||||
|
run: php artisan octane:start --server=frankenphp --host=127.0.0.1 --port=8000 --workers=4 --max-requests=500 > /dev/null 2>&1 &
|
||||||
|
env:
|
||||||
|
OCTANE_SERVER: frankenphp
|
||||||
|
|
||||||
|
- name: "Install Playwright Browsers"
|
||||||
|
run: npx playwright install --with-deps
|
||||||
|
|
||||||
|
# Only the extension specs, the core specs run without extensions in playwright.yml
|
||||||
|
- name: "Run Playwright tests"
|
||||||
|
run: npx playwright test extensions/
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_BASE_URL: 'http://127.0.0.1:8000'
|
||||||
|
MAILPIT_BASE_URL: 'http://localhost:8025'
|
||||||
|
|
||||||
|
# No artifacts are uploaded on purpose. This repository is public, so every artifact is
|
||||||
|
# downloadable by anyone, and Playwright's reports carry the source of the specs that
|
||||||
|
# produced them (traces embed whole source files, error-context.md embeds a window of the
|
||||||
|
# spec). Those specs live in the private extension repositories. Failures are diagnosed from
|
||||||
|
# the job log; to inspect a trace, reproduce the failure locally with the extension checked out.
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Extensions\Scramble;
|
||||||
|
|
||||||
|
use App\Http\Resources\PaginatedResourceCollection;
|
||||||
|
use App\Http\Resources\V1\TimeEntry\TimeEntryCollection;
|
||||||
|
use Dedoc\Scramble\Extensions\TypeToSchemaExtension;
|
||||||
|
use Dedoc\Scramble\Support\Generator\Response;
|
||||||
|
use Dedoc\Scramble\Support\Generator\Schema;
|
||||||
|
use Dedoc\Scramble\Support\Generator\Types\ArrayType;
|
||||||
|
use Dedoc\Scramble\Support\Generator\Types\BooleanType;
|
||||||
|
use Dedoc\Scramble\Support\Generator\Types\IntegerType;
|
||||||
|
use Dedoc\Scramble\Support\Generator\Types\ObjectType as OpenApiObjectType;
|
||||||
|
use Dedoc\Scramble\Support\Generator\Types\StringType;
|
||||||
|
use Dedoc\Scramble\Support\Type\Generic;
|
||||||
|
use Dedoc\Scramble\Support\Type\ObjectType;
|
||||||
|
use Dedoc\Scramble\Support\Type\Type;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class PaginatedResourceCollectionTypeToSchema extends TypeToSchemaExtension
|
||||||
|
{
|
||||||
|
public function shouldHandle(Type $type): bool
|
||||||
|
{
|
||||||
|
return $type instanceof ObjectType
|
||||||
|
&& $type->isInstanceOf(PaginatedResourceCollection::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toSchema(Type $type): ?OpenApiObjectType
|
||||||
|
{
|
||||||
|
/** @var Type|null $collectingClassType */
|
||||||
|
$collectingClassType = $type->templateTypes[0] ?? null;
|
||||||
|
|
||||||
|
if (! $collectingClassType instanceof ObjectType) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $collectingClassType->isInstanceOf(JsonResource::class) && ! $collectingClassType->isInstanceOf(Model::class)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$collectingType = $this->openApiTransformer->transform($collectingClassType);
|
||||||
|
|
||||||
|
$newType = new OpenApiObjectType;
|
||||||
|
$newType->addProperty('data', (new ArrayType)->setItems($collectingType));
|
||||||
|
if ($type instanceof ObjectType && $type->isInstanceOf(TimeEntryCollection::class)) {
|
||||||
|
$newType->addProperty(
|
||||||
|
'meta',
|
||||||
|
(new OpenApiObjectType)
|
||||||
|
->addProperty('total', (new IntegerType)->setDescription('Total number of items being paginated.'))
|
||||||
|
->setRequired(['total'])
|
||||||
|
);
|
||||||
|
$newType->setRequired(['data', 'meta']);
|
||||||
|
} else {
|
||||||
|
$newType->addProperty(
|
||||||
|
'links',
|
||||||
|
(new OpenApiObjectType)
|
||||||
|
->addProperty('first', (new StringType)->nullable(true))
|
||||||
|
->addProperty('last', (new StringType)->nullable(true))
|
||||||
|
->addProperty('prev', (new StringType)->nullable(true))
|
||||||
|
->addProperty('next', (new StringType)->nullable(true))
|
||||||
|
->setRequired(['first', 'last', 'prev', 'next'])
|
||||||
|
);
|
||||||
|
$newType->addProperty(
|
||||||
|
'meta',
|
||||||
|
(new OpenApiObjectType)
|
||||||
|
->addProperty('current_page', new IntegerType)
|
||||||
|
->addProperty('from', (new IntegerType)->nullable(true))
|
||||||
|
->addProperty('last_page', new IntegerType)
|
||||||
|
->addProperty('links', (new ArrayType)->setItems(
|
||||||
|
(new OpenApiObjectType)
|
||||||
|
->addProperty('url', (new StringType)->nullable(true))
|
||||||
|
->addProperty('label', new StringType)
|
||||||
|
->addProperty('active', new BooleanType)
|
||||||
|
->setRequired(['url', 'label', 'active'])
|
||||||
|
)->setDescription('Generated paginator links.'))
|
||||||
|
->addProperty('path', (new StringType)->nullable(true)->setDescription('Base path for paginator generated URLs.'))
|
||||||
|
->addProperty('per_page', (new IntegerType)->setDescription('Number of items shown per page.'))
|
||||||
|
->addProperty('to', (new IntegerType)->nullable(true)->setDescription('Number of the last item in the slice.'))
|
||||||
|
->addProperty('total', (new IntegerType)->setDescription('Total number of items being paginated.'))
|
||||||
|
->setRequired(['current_page', 'from', 'last_page', 'links', 'path', 'per_page', 'to', 'total'])
|
||||||
|
);
|
||||||
|
$newType->setRequired(['data', 'links', 'meta']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $newType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Generic $type
|
||||||
|
*/
|
||||||
|
public function toResponse(Type $type): ?Response
|
||||||
|
{
|
||||||
|
/** @var ObjectType|null $collectingClassType */
|
||||||
|
$collectingClassType = $type->templateTypes[0] ?? null;
|
||||||
|
if (! $collectingClassType instanceof ObjectType) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$type = $this->toSchema($type);
|
||||||
|
|
||||||
|
return Response::make(200)
|
||||||
|
->description('Paginated set of `'.$this->components->uniqueSchemaName($collectingClassType->name).'`')
|
||||||
|
->setContent('application/json', Schema::fromType($type));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Http\Controllers\Api;
|
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
use Dedoc\Scramble\Attributes\ExcludeAllRoutesFromDocs;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fallback for unknown /api/* routes, to prevent a rendered HTML page
|
|
||||||
*/
|
|
||||||
#[ExcludeAllRoutesFromDocs]
|
|
||||||
class FallbackController extends Controller
|
|
||||||
{
|
|
||||||
public function __invoke(): never
|
|
||||||
{
|
|
||||||
throw new NotFoundHttpException('API resource not found');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -29,6 +29,8 @@ class ClientController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Get clients
|
* Get clients
|
||||||
*
|
*
|
||||||
|
* @return ClientCollection<ClientResource>
|
||||||
|
*
|
||||||
* @throws AuthorizationException
|
* @throws AuthorizationException
|
||||||
*
|
*
|
||||||
* @operationId getClients
|
* @operationId getClients
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use App\Exceptions\Api\UserIsAlreadyMemberOfOrganizationApiException;
|
|||||||
use App\Http\Requests\V1\Invitation\InvitationIndexRequest;
|
use App\Http\Requests\V1\Invitation\InvitationIndexRequest;
|
||||||
use App\Http\Requests\V1\Invitation\InvitationStoreRequest;
|
use App\Http\Requests\V1\Invitation\InvitationStoreRequest;
|
||||||
use App\Http\Resources\V1\Invitation\InvitationCollection;
|
use App\Http\Resources\V1\Invitation\InvitationCollection;
|
||||||
|
use App\Http\Resources\V1\Invitation\InvitationResource;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use App\Models\OrganizationInvitation;
|
use App\Models\OrganizationInvitation;
|
||||||
use App\Service\InvitationService;
|
use App\Service\InvitationService;
|
||||||
@@ -29,6 +30,8 @@ class InvitationController extends Controller
|
|||||||
/**
|
/**
|
||||||
* List all invitations of an organization
|
* List all invitations of an organization
|
||||||
*
|
*
|
||||||
|
* @return InvitationCollection<InvitationResource>
|
||||||
|
*
|
||||||
* @throws AuthorizationException
|
* @throws AuthorizationException
|
||||||
*
|
*
|
||||||
* @operationId getInvitations
|
* @operationId getInvitations
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ class MemberController extends Controller
|
|||||||
/**
|
/**
|
||||||
* List all members of an organization
|
* List all members of an organization
|
||||||
*
|
*
|
||||||
|
* @return MemberCollection<MemberResource>
|
||||||
|
*
|
||||||
* @throws AuthorizationException
|
* @throws AuthorizationException
|
||||||
*
|
*
|
||||||
* @operationId getMembers
|
* @operationId getMembers
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ class ProjectController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Get projects visible to the current user
|
* Get projects visible to the current user
|
||||||
*
|
*
|
||||||
|
* @return ProjectCollection<ProjectResource>
|
||||||
|
*
|
||||||
* @throws AuthorizationException
|
* @throws AuthorizationException
|
||||||
*
|
*
|
||||||
* @operationId getProjects
|
* @operationId getProjects
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ class ProjectMemberController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Get project members for project
|
* Get project members for project
|
||||||
*
|
*
|
||||||
|
* @return ProjectMemberCollection<ProjectMemberResource>
|
||||||
|
*
|
||||||
* @throws AuthorizationException
|
* @throws AuthorizationException
|
||||||
*
|
*
|
||||||
* @operationId getProjectMembers
|
* @operationId getProjectMembers
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use App\Http\Requests\V1\Report\ReportStoreRequest;
|
|||||||
use App\Http\Requests\V1\Report\ReportUpdateRequest;
|
use App\Http\Requests\V1\Report\ReportUpdateRequest;
|
||||||
use App\Http\Resources\V1\Report\DetailedReportResource;
|
use App\Http\Resources\V1\Report\DetailedReportResource;
|
||||||
use App\Http\Resources\V1\Report\ReportCollection;
|
use App\Http\Resources\V1\Report\ReportCollection;
|
||||||
|
use App\Http\Resources\V1\Report\ReportResource;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use App\Models\Report;
|
use App\Models\Report;
|
||||||
use App\Service\Dto\ReportPropertiesDto;
|
use App\Service\Dto\ReportPropertiesDto;
|
||||||
@@ -34,6 +35,8 @@ class ReportController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Get reports
|
* Get reports
|
||||||
*
|
*
|
||||||
|
* @return ReportCollection<ReportResource>
|
||||||
|
*
|
||||||
* @throws AuthorizationException
|
* @throws AuthorizationException
|
||||||
*
|
*
|
||||||
* @operationId getReports
|
* @operationId getReports
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ class TagController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Get tags
|
* Get tags
|
||||||
*
|
*
|
||||||
|
* @return TagCollection<TagResource>
|
||||||
|
*
|
||||||
* @operationId getTags
|
* @operationId getTags
|
||||||
*
|
*
|
||||||
* @throws AuthorizationException
|
* @throws AuthorizationException
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ class TaskController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Get tasks
|
* Get tasks
|
||||||
*
|
*
|
||||||
|
* @return TaskCollection<TaskResource>
|
||||||
|
*
|
||||||
* @throws AuthorizationException
|
* @throws AuthorizationException
|
||||||
*
|
*
|
||||||
* @operationId getTasks
|
* @operationId getTasks
|
||||||
|
|||||||
@@ -117,6 +117,8 @@ class TimeEntryController extends Controller
|
|||||||
* Results are paginated with `limit` (default 100, max 500) and `offset`; check `meta.total` and fetch further pages when needed.
|
* Results are paginated with `limit` (default 100, max 500) and `offset`; check `meta.total` and fetch further pages when needed.
|
||||||
* To find the running timer, use `active=true` (or `GET /v1/users/me/time-entries/active`).
|
* To find the running timer, use `active=true` (or `GET /v1/users/me/time-entries/active`).
|
||||||
*
|
*
|
||||||
|
* @return TimeEntryCollection<TimeEntryResource>
|
||||||
|
*
|
||||||
* @throws AuthorizationException
|
* @throws AuthorizationException
|
||||||
*
|
*
|
||||||
* @operationId getTimeEntries
|
* @operationId getTimeEntries
|
||||||
|
|||||||
@@ -43,10 +43,9 @@ class UserUpdateRequest extends BaseFormRequest
|
|||||||
'email' => [
|
'email' => [
|
||||||
'email:rfc,strict',
|
'email:rfc,strict',
|
||||||
'max:255',
|
'max:255',
|
||||||
UniqueEloquent::make(User::class, 'email')->query(function (Builder $query) {
|
UniqueEloquent::make(User::class, 'email')->ignore($this->user->id)->query(function (Builder $query) {
|
||||||
/** @var Builder<User> $query */
|
/** @var Builder<User> $query */
|
||||||
return $query->where('is_placeholder', '=', false)
|
return $query->where('is_placeholder', '=', false);
|
||||||
->whereKeyNot($this->user->getKey());
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
'photo' => [
|
'photo' => [
|
||||||
|
|||||||
7
app/Http/Resources/PaginatedResourceCollection.php
Normal file
7
app/Http/Resources/PaginatedResourceCollection.php
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
|
interface PaginatedResourceCollection {}
|
||||||
@@ -21,15 +21,15 @@ class ApiTokenResource extends BaseResource
|
|||||||
public function toArray(Request $request): array
|
public function toArray(Request $request): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
/** ID of the API token, this ID is NOT a UUID */
|
/** @var string $id ID of the API token, this ID is NOT a UUID */
|
||||||
'id' => $this->resource->id,
|
'id' => $this->resource->id,
|
||||||
/** @var string $name Name of the API token */
|
/** @var string $name Name of the API token */
|
||||||
'name' => $this->resource->name,
|
'name' => $this->resource->name,
|
||||||
/** Whether the API token is revoked */
|
/** @var bool $revoked Whether the API token is revoked */
|
||||||
'revoked' => $this->resource->revoked,
|
'revoked' => $this->resource->revoked,
|
||||||
/** List of scopes that the API token has */
|
/** @var array<string> $scopes List of scopes that the API token has */
|
||||||
'scopes' => $this->resource->scopes,
|
'scopes' => $this->resource->scopes,
|
||||||
/** When the API token was created (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z) */
|
/** @var string $created_at When the API token was created (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z) */
|
||||||
'created_at' => $this->formatDateTime($this->resource->created_at),
|
'created_at' => $this->formatDateTime($this->resource->created_at),
|
||||||
/** @var string|null $expires_at At what time the API token expires (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z) */
|
/** @var string|null $expires_at At what time the API token expires (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z) */
|
||||||
'expires_at' => $this->formatDateTime($this->resource->expires_at),
|
'expires_at' => $this->formatDateTime($this->resource->expires_at),
|
||||||
|
|||||||
@@ -29,15 +29,15 @@ class ApiTokenWithAccessTokenResource extends BaseResource
|
|||||||
public function toArray(Request $request): array
|
public function toArray(Request $request): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
/** ID of the API token, this ID is NOT a UUID */
|
/** @var string $id ID of the API token, this ID is NOT a UUID */
|
||||||
'id' => $this->resource->id,
|
'id' => $this->resource->id,
|
||||||
/** @var string $name Name of the API token */
|
/** @var string $name Name of the API token */
|
||||||
'name' => $this->resource->name,
|
'name' => $this->resource->name,
|
||||||
/** Whether the API token is revoked */
|
/** @var bool $revoked Whether the API token is revoked */
|
||||||
'revoked' => $this->resource->revoked,
|
'revoked' => $this->resource->revoked,
|
||||||
/** List of scopes that the API token has */
|
/** @var array<string> $scopes List of scopes that the API token has */
|
||||||
'scopes' => $this->resource->scopes,
|
'scopes' => $this->resource->scopes,
|
||||||
/** When the API token was created (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z) */
|
/** @var string $created_at When the API token was created (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z) */
|
||||||
'created_at' => $this->formatDateTime($this->resource->created_at),
|
'created_at' => $this->formatDateTime($this->resource->created_at),
|
||||||
/** @var string|null $expires_at At what time the API token expires (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z) */
|
/** @var string|null $expires_at At what time the API token expires (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z) */
|
||||||
'expires_at' => $this->formatDateTime($this->resource->expires_at),
|
'expires_at' => $this->formatDateTime($this->resource->expires_at),
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Resources\V1\Client;
|
namespace App\Http\Resources\V1\Client;
|
||||||
|
|
||||||
|
use App\Http\Resources\PaginatedResourceCollection;
|
||||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||||
|
|
||||||
class ClientCollection extends ResourceCollection
|
class ClientCollection extends ResourceCollection implements PaginatedResourceCollection
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The resource that this resource collects.
|
* The resource that this resource collects.
|
||||||
|
|||||||
@@ -21,15 +21,15 @@ class ClientResource extends BaseResource
|
|||||||
public function toArray(Request $request): array
|
public function toArray(Request $request): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
/** ID */
|
/** @var string $id ID */
|
||||||
'id' => $this->resource->id,
|
'id' => $this->resource->id,
|
||||||
/** Name */
|
/** @var string $name Name */
|
||||||
'name' => $this->resource->name,
|
'name' => $this->resource->name,
|
||||||
/** Whether the client is archived */
|
/** @var bool $is_archived Whether the client is archived */
|
||||||
'is_archived' => $this->resource->is_archived,
|
'is_archived' => $this->resource->is_archived,
|
||||||
/** When the tag was created */
|
/** @var string $created_at When the tag was created */
|
||||||
'created_at' => $this->formatDateTime($this->resource->created_at),
|
'created_at' => $this->formatDateTime($this->resource->created_at),
|
||||||
/** When the tag was last updated */
|
/** @var string $updated_at When the tag was last updated */
|
||||||
'updated_at' => $this->formatDateTime($this->resource->updated_at),
|
'updated_at' => $this->formatDateTime($this->resource->updated_at),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Resources\V1\Invitation;
|
namespace App\Http\Resources\V1\Invitation;
|
||||||
|
|
||||||
|
use App\Http\Resources\PaginatedResourceCollection;
|
||||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||||
|
|
||||||
class InvitationCollection extends ResourceCollection
|
class InvitationCollection extends ResourceCollection implements PaginatedResourceCollection
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The resource that this resource collects.
|
* The resource that this resource collects.
|
||||||
|
|||||||
@@ -21,11 +21,11 @@ class InvitationResource extends BaseResource
|
|||||||
public function toArray(Request $request): array
|
public function toArray(Request $request): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
/** ID of the invitation */
|
/** @var string $id ID of the invitation */
|
||||||
'id' => $this->resource->id,
|
'id' => $this->resource->id,
|
||||||
/** Email */
|
/** @var string $email Email */
|
||||||
'email' => $this->resource->email,
|
'email' => $this->resource->email,
|
||||||
/** Role */
|
/** @var string $role Role */
|
||||||
'role' => $this->resource->role,
|
'role' => $this->resource->role,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Resources\V1\Member;
|
namespace App\Http\Resources\V1\Member;
|
||||||
|
|
||||||
|
use App\Http\Resources\PaginatedResourceCollection;
|
||||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||||
|
|
||||||
class MemberCollection extends ResourceCollection
|
class MemberCollection extends ResourceCollection implements PaginatedResourceCollection
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The resource that this resource collects.
|
* The resource that this resource collects.
|
||||||
|
|||||||
@@ -22,19 +22,19 @@ class MemberResource extends BaseResource
|
|||||||
public function toArray(Request $request): array
|
public function toArray(Request $request): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
/** ID of membership */
|
/** @var string $id ID of membership */
|
||||||
'id' => $this->resource->id,
|
'id' => $this->resource->id,
|
||||||
/** ID of user */
|
/** @var string $id ID of user */
|
||||||
'user_id' => $this->resource->user->id,
|
'user_id' => $this->resource->user->id,
|
||||||
/** Name */
|
/** @var string $name Name */
|
||||||
'name' => $this->resource->user->name,
|
'name' => $this->resource->user->name,
|
||||||
/** Email */
|
/** @var string $email Email */
|
||||||
'email' => $this->resource->user->email,
|
'email' => $this->resource->user->email,
|
||||||
/** Role */
|
/** @var string $role Role */
|
||||||
'role' => $this->resource->role,
|
'role' => $this->resource->role,
|
||||||
/** Placeholder user for imports, user might not really exist and does not know about this placeholder membership */
|
/** @var bool $is_placeholder Placeholder user for imports, user might not really exist and does not know about this placeholder membership */
|
||||||
'is_placeholder' => $this->resource->user->is_placeholder,
|
'is_placeholder' => $this->resource->user->is_placeholder,
|
||||||
/** Billable rate in cents per hour */
|
/** @var int|null $billable_rate Billable rate in cents per hour */
|
||||||
'billable_rate' => $this->resource->billable_rate,
|
'billable_rate' => $this->resource->billable_rate,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Resources\V1\Member;
|
namespace App\Http\Resources\V1\Member;
|
||||||
|
|
||||||
|
use App\Http\Resources\PaginatedResourceCollection;
|
||||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||||
|
|
||||||
class PersonalMembershipCollection extends ResourceCollection
|
class PersonalMembershipCollection extends ResourceCollection implements PaginatedResourceCollection
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The resource that this resource collects.
|
* The resource that this resource collects.
|
||||||
|
|||||||
@@ -21,17 +21,17 @@ class PersonalMembershipResource extends BaseResource
|
|||||||
public function toArray(Request $request): array
|
public function toArray(Request $request): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
/** ID of membership */
|
/** @var string $id ID of membership */
|
||||||
'id' => $this->resource->id,
|
'id' => $this->resource->id,
|
||||||
'organization' => [
|
'organization' => [
|
||||||
/** ID of organization */
|
/** @var string $id ID of organization */
|
||||||
'id' => $this->resource->organization->id,
|
'id' => $this->resource->organization->id,
|
||||||
/** Name of organization */
|
/** @var string $name Name of organization */
|
||||||
'name' => $this->resource->organization->name,
|
'name' => $this->resource->organization->name,
|
||||||
/** Currency code (ISO 4217) of organization */
|
/** @var string $currency Currency code (ISO 4217) of organization */
|
||||||
'currency' => $this->resource->organization->currency,
|
'currency' => $this->resource->organization->currency,
|
||||||
],
|
],
|
||||||
/** Role */
|
/** @var string $role Role */
|
||||||
'role' => $this->resource->role,
|
'role' => $this->resource->role,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,23 +43,23 @@ class OrganizationResource extends BaseResource
|
|||||||
$currencyService = app(CurrencyService::class);
|
$currencyService = app(CurrencyService::class);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
/** ID */
|
/** @var string $id ID */
|
||||||
'id' => $this->resource->id,
|
'id' => $this->resource->id,
|
||||||
/** Name */
|
/** @var string $name Name */
|
||||||
'name' => $this->resource->name,
|
'name' => $this->resource->name,
|
||||||
/** Personal organizations automatically created after registration */
|
/** @var bool $color Personal organizations automatically created after registration */
|
||||||
'is_personal' => $this->resource->personal_team,
|
'is_personal' => $this->resource->personal_team,
|
||||||
/** Billable rate in cents per hour */
|
/** @var int|null $billable_rate Billable rate in cents per hour */
|
||||||
'billable_rate' => $this->showBillableRate ? $this->resource->billable_rate : null,
|
'billable_rate' => $this->showBillableRate ? $this->resource->billable_rate : null,
|
||||||
/** Can members of the organization with role "employee" see the billable rates */
|
/** @var bool $employees_can_see_billable_rates Can members of the organization with role "employee" see the billable rates */
|
||||||
'employees_can_see_billable_rates' => $this->resource->employees_can_see_billable_rates,
|
'employees_can_see_billable_rates' => $this->resource->employees_can_see_billable_rates,
|
||||||
/** Can members of the organization with role "employee" manage tasks in public projects and projects they are assigned to */
|
/** @var bool $employees_can_manage_tasks Can members of the organization with role "employee" manage tasks in public projects and projects they are assigned to */
|
||||||
'employees_can_manage_tasks' => $this->resource->employees_can_manage_tasks,
|
'employees_can_manage_tasks' => $this->resource->employees_can_manage_tasks,
|
||||||
/** Prevent creating overlapping time entries (only new entries) */
|
/** @var bool $prevent_overlapping_time_entries Prevent creating overlapping time entries (only new entries) */
|
||||||
'prevent_overlapping_time_entries' => $this->resource->prevent_overlapping_time_entries,
|
'prevent_overlapping_time_entries' => $this->resource->prevent_overlapping_time_entries,
|
||||||
/** Whether members of the organization can track breaks */
|
/** @var bool $breaks_enabled Whether members of the organization can track breaks */
|
||||||
'breaks_enabled' => $this->resource->breaks_enabled,
|
'breaks_enabled' => $this->resource->breaks_enabled,
|
||||||
/** Currency code (ISO 4217) */
|
/** @var string $currency Currency code (ISO 4217) */
|
||||||
'currency' => $this->resource->currency,
|
'currency' => $this->resource->currency,
|
||||||
/** @var string $currency_symbol Currency symbol */
|
/** @var string $currency_symbol Currency symbol */
|
||||||
'currency_symbol' => $currencyService->getCurrencySymbol($this->resource->currency),
|
'currency_symbol' => $currencyService->getCurrencySymbol($this->resource->currency),
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Resources\V1\Project;
|
namespace App\Http\Resources\V1\Project;
|
||||||
|
|
||||||
|
use App\Http\Resources\PaginatedResourceCollection;
|
||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||||
|
|
||||||
class ProjectCollection extends ResourceCollection
|
class ProjectCollection extends ResourceCollection implements PaginatedResourceCollection
|
||||||
{
|
{
|
||||||
private bool $showBillableRates;
|
private bool $showBillableRates;
|
||||||
|
|
||||||
|
|||||||
@@ -30,25 +30,25 @@ class ProjectResource extends BaseResource
|
|||||||
public function toArray(Request $request): array
|
public function toArray(Request $request): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
/** ID of project */
|
/** @var string $id ID of project */
|
||||||
'id' => $this->resource->id,
|
'id' => $this->resource->id,
|
||||||
/** Name of project */
|
/** @var string $name Name of project */
|
||||||
'name' => $this->resource->name,
|
'name' => $this->resource->name,
|
||||||
/** Color of project */
|
/** @var string $color Color of project */
|
||||||
'color' => $this->resource->color,
|
'color' => $this->resource->color,
|
||||||
/** @var string|null $client_id ID of client */
|
/** @var string|null $client_id ID of client */
|
||||||
'client_id' => $this->resource->client_id,
|
'client_id' => $this->resource->client_id,
|
||||||
/** Whether the client is archived */
|
/** @var bool $is_archived Whether the client is archived */
|
||||||
'is_archived' => $this->resource->is_archived,
|
'is_archived' => $this->resource->is_archived,
|
||||||
/** Billable rate in cents per hour */
|
/** @var int|null $billable_rate Billable rate in cents per hour */
|
||||||
'billable_rate' => $this->showBillableRate ? $this->resource->billable_rate : null,
|
'billable_rate' => $this->showBillableRate ? $this->resource->billable_rate : null,
|
||||||
/** Project time entries billable default */
|
/** @var bool $is_billable Project time entries billable default */
|
||||||
'is_billable' => $this->resource->is_billable,
|
'is_billable' => $this->resource->is_billable,
|
||||||
/** Estimated time in seconds */
|
/** @var int|null $estimated_time Estimated time in seconds */
|
||||||
'estimated_time' => $this->resource->estimated_time,
|
'estimated_time' => $this->resource->estimated_time,
|
||||||
/** Spent time on this project in seconds (sum of the duration of all associated time entries, excl. still running time entries) */
|
/** @var int $spent_time Spent time on this project in seconds (sum of the duration of all associated time entries, excl. still running time entries) */
|
||||||
'spent_time' => $this->resource->spent_time,
|
'spent_time' => $this->resource->spent_time,
|
||||||
/** Whether the project is public */
|
/** @var bool $is_public Whether the project is public */
|
||||||
'is_public' => $this->resource->is_public,
|
'is_public' => $this->resource->is_public,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Resources\V1\ProjectMember;
|
namespace App\Http\Resources\V1\ProjectMember;
|
||||||
|
|
||||||
|
use App\Http\Resources\PaginatedResourceCollection;
|
||||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||||
|
|
||||||
class ProjectMemberCollection extends ResourceCollection
|
class ProjectMemberCollection extends ResourceCollection implements PaginatedResourceCollection
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The resource that this resource collects.
|
* The resource that this resource collects.
|
||||||
|
|||||||
@@ -21,13 +21,13 @@ class ProjectMemberResource extends BaseResource
|
|||||||
public function toArray(Request $request): array
|
public function toArray(Request $request): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
/** ID of project member */
|
/** @var string $id ID of project member */
|
||||||
'id' => $this->resource->id,
|
'id' => $this->resource->id,
|
||||||
/** Billable rate in cents per hour */
|
/** @var int|null $billable_rate Billable rate in cents per hour */
|
||||||
'billable_rate' => $this->resource->billable_rate,
|
'billable_rate' => $this->resource->billable_rate,
|
||||||
/** ID of the organization member */
|
/** @var string $member_id ID of the organization member */
|
||||||
'member_id' => $this->resource->member_id,
|
'member_id' => $this->resource->member_id,
|
||||||
/** ID of the project */
|
/** @var string $project_id ID of the project */
|
||||||
'project_id' => $this->resource->project_id,
|
'project_id' => $this->resource->project_id,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,36 +21,36 @@ class DetailedReportResource extends BaseResource
|
|||||||
public function toArray(Request $request): array
|
public function toArray(Request $request): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
/** ID of the report */
|
/** @var string $id ID of the report */
|
||||||
'id' => $this->resource->id,
|
'id' => $this->resource->id,
|
||||||
/** Name */
|
/** @var string $name Name */
|
||||||
'name' => $this->resource->name,
|
'name' => $this->resource->name,
|
||||||
/** Description */
|
/** @var string|null $email Description */
|
||||||
'description' => $this->resource->description,
|
'description' => $this->resource->description,
|
||||||
/** Whether the report can be accessed via an external link */
|
/** @var bool $is_public Whether the report can be accessed via an external link */
|
||||||
'is_public' => $this->resource->is_public,
|
'is_public' => $this->resource->is_public,
|
||||||
/** @var string|null $public_until Date until the report is public */
|
/** @var string|null $public_until Date until the report is public */
|
||||||
'public_until' => $this->formatDateTime($this->resource->public_until),
|
'public_until' => $this->formatDateTime($this->resource->public_until),
|
||||||
/** @var string|null $shareable_link Get link to access the report externally, not set if the report is private */
|
/** @var string|null $shareable_link Get link to access the report externally, not set if the report is private */
|
||||||
'shareable_link' => $this->resource->getShareableLink(),
|
'shareable_link' => $this->resource->getShareableLink(),
|
||||||
'properties' => [
|
'properties' => [
|
||||||
/** Type of first grouping */
|
/** @var string $group Type of first grouping */
|
||||||
'group' => $this->resource->properties->group->value,
|
'group' => $this->resource->properties->group->value,
|
||||||
/** Type of second grouping */
|
/** @var string $sub_group Type of second grouping */
|
||||||
'sub_group' => $this->resource->properties->subGroup->value,
|
'sub_group' => $this->resource->properties->subGroup->value,
|
||||||
/** Type of grouping of the historic aggregation (time chart) */
|
/** @var string $history_group Type of grouping of the historic aggregation (time chart) */
|
||||||
'history_group' => $this->resource->properties->historyGroup->value,
|
'history_group' => $this->resource->properties->historyGroup->value,
|
||||||
/** Start date of the report */
|
/** @var string $start Start date of the report */
|
||||||
'start' => $this->formatDateTime($this->resource->properties->start),
|
'start' => $this->formatDateTime($this->resource->properties->start),
|
||||||
/** End date of the report */
|
/** @var string $end End date of the report */
|
||||||
'end' => $this->formatDateTime($this->resource->properties->end),
|
'end' => $this->formatDateTime($this->resource->properties->end),
|
||||||
/** Whether the report is active */
|
/** @var bool|null $active Whether the report is active */
|
||||||
'active' => $this->resource->properties->active,
|
'active' => $this->resource->properties->active,
|
||||||
/** @var array<string>|null $member_ids Filter by multiple member IDs, member IDs are OR combined */
|
/** @var array<string>|null $member_ids Filter by multiple member IDs, member IDs are OR combined */
|
||||||
'member_ids' => $this->resource->properties->memberIds?->toArray(),
|
'member_ids' => $this->resource->properties->memberIds?->toArray(),
|
||||||
/** Filter by billable status */
|
/** @var bool|null $billable Filter by billable status */
|
||||||
'billable' => $this->resource->properties->billable,
|
'billable' => $this->resource->properties->billable,
|
||||||
/** Filter by time entry type */
|
/** @var string|null $time_entry_type Filter by time entry type */
|
||||||
'time_entry_type' => $this->resource->properties->timeEntryType?->value,
|
'time_entry_type' => $this->resource->properties->timeEntryType?->value,
|
||||||
/** @var array<string>|null $client_ids Filter by client IDs, client IDs are OR combined */
|
/** @var array<string>|null $client_ids Filter by client IDs, client IDs are OR combined */
|
||||||
'client_ids' => $this->resource->properties->clientIds?->toArray(),
|
'client_ids' => $this->resource->properties->clientIds?->toArray(),
|
||||||
@@ -58,18 +58,18 @@ class DetailedReportResource extends BaseResource
|
|||||||
'project_ids' => $this->resource->properties->projectIds?->toArray(),
|
'project_ids' => $this->resource->properties->projectIds?->toArray(),
|
||||||
/** @var array<string>|null $tags_ids Filter by tag IDs, tag IDs are OR combined */
|
/** @var array<string>|null $tags_ids Filter by tag IDs, tag IDs are OR combined */
|
||||||
'tag_ids' => $this->resource->properties->tagIds?->toArray(),
|
'tag_ids' => $this->resource->properties->tagIds?->toArray(),
|
||||||
/** Tag match type */
|
/** @var string|null $tag_match_type Tag match type */
|
||||||
'tag_match_type' => $this->resource->properties->tagMatchType?->value,
|
'tag_match_type' => $this->resource->properties->tagMatchType?->value,
|
||||||
/** @var array<string>|null $task_ids Filter by task IDs, task IDs are OR combined */
|
/** @var array<string>|null $task_ids Filter by task IDs, task IDs are OR combined */
|
||||||
'task_ids' => $this->resource->properties->taskIds?->toArray(),
|
'task_ids' => $this->resource->properties->taskIds?->toArray(),
|
||||||
/** Rounding type for time entries */
|
/** @var string|null $rounding_type Rounding type for time entries */
|
||||||
'rounding_type' => $this->resource->properties->roundingType?->value,
|
'rounding_type' => $this->resource->properties->roundingType?->value,
|
||||||
/** Rounding minutes for time entries */
|
/** @var int|null $rounding_minutes Rounding minutes for time entries */
|
||||||
'rounding_minutes' => $this->resource->properties->roundingMinutes,
|
'rounding_minutes' => $this->resource->properties->roundingMinutes,
|
||||||
],
|
],
|
||||||
/** Date when the report was created */
|
/** @var string $created_at Date when the report was created */
|
||||||
'created_at' => $this->formatDateTime($this->resource->created_at),
|
'created_at' => $this->formatDateTime($this->resource->created_at),
|
||||||
/** Date when the report was last updated */
|
/** @var string $updated_at Date when the report was last updated */
|
||||||
'updated_at' => $this->formatDateTime($this->resource->updated_at),
|
'updated_at' => $this->formatDateTime($this->resource->updated_at),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,13 +73,13 @@ class DetailedWithDataReportResource extends BaseResource
|
|||||||
$currencyService = app(CurrencyService::class);
|
$currencyService = app(CurrencyService::class);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
/** Name */
|
/** @var string $name Name */
|
||||||
'name' => $this->resource->name,
|
'name' => $this->resource->name,
|
||||||
/** Description */
|
/** @var string|null $email Description */
|
||||||
'description' => $this->resource->description,
|
'description' => $this->resource->description,
|
||||||
/** @var string|null $public_until Date until the report is public */
|
/** @var string|null $public_until Date until the report is public */
|
||||||
'public_until' => $this->formatDateTime($this->resource->public_until),
|
'public_until' => $this->formatDateTime($this->resource->public_until),
|
||||||
/** Currency code (ISO 4217) */
|
/** @var string $currency Currency code (ISO 4217) */
|
||||||
'currency' => $this->resource->organization->currency,
|
'currency' => $this->resource->organization->currency,
|
||||||
/** @var NumberFormat $number_format Number format */
|
/** @var NumberFormat $number_format Number format */
|
||||||
'number_format' => $this->resource->organization->number_format->value,
|
'number_format' => $this->resource->organization->number_format->value,
|
||||||
@@ -94,15 +94,15 @@ class DetailedWithDataReportResource extends BaseResource
|
|||||||
/** @var TimeFormat $time_format Time format */
|
/** @var TimeFormat $time_format Time format */
|
||||||
'time_format' => $this->resource->organization->time_format->value,
|
'time_format' => $this->resource->organization->time_format->value,
|
||||||
'properties' => [
|
'properties' => [
|
||||||
/** Type of first grouping */
|
/** @var string $group Type of first grouping */
|
||||||
'group' => $this->resource->properties->group->value,
|
'group' => $this->resource->properties->group->value,
|
||||||
/** Type of second grouping */
|
/** @var string $sub_group Type of second grouping */
|
||||||
'sub_group' => $this->resource->properties->subGroup->value,
|
'sub_group' => $this->resource->properties->subGroup->value,
|
||||||
/** Type of grouping of the historic aggregation (time chart) */
|
/** @var string $history_group Type of grouping of the historic aggregation (time chart) */
|
||||||
'history_group' => $this->resource->properties->historyGroup->value,
|
'history_group' => $this->resource->properties->historyGroup->value,
|
||||||
/** Start date of the report */
|
/** @var string $start Start date of the report */
|
||||||
'start' => $this->formatDateTime($this->resource->properties->start),
|
'start' => $this->formatDateTime($this->resource->properties->start),
|
||||||
/** End date of the report */
|
/** @var string $end End date of the report */
|
||||||
'end' => $this->formatDateTime($this->resource->properties->end),
|
'end' => $this->formatDateTime($this->resource->properties->end),
|
||||||
],
|
],
|
||||||
/** @var array{
|
/** @var array{
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Resources\V1\Report;
|
namespace App\Http\Resources\V1\Report;
|
||||||
|
|
||||||
|
use App\Http\Resources\PaginatedResourceCollection;
|
||||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||||
|
|
||||||
class ReportCollection extends ResourceCollection
|
class ReportCollection extends ResourceCollection implements PaginatedResourceCollection
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The resource that this resource collects.
|
* The resource that this resource collects.
|
||||||
|
|||||||
@@ -21,21 +21,21 @@ class ReportResource extends BaseResource
|
|||||||
public function toArray(Request $request): array
|
public function toArray(Request $request): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
/** ID of the report */
|
/** @var string $id ID of the report */
|
||||||
'id' => $this->resource->id,
|
'id' => $this->resource->id,
|
||||||
/** Name */
|
/** @var string $name Name */
|
||||||
'name' => $this->resource->name,
|
'name' => $this->resource->name,
|
||||||
/** Description */
|
/** @var string|null $email Description */
|
||||||
'description' => $this->resource->description,
|
'description' => $this->resource->description,
|
||||||
/** Whether the report can be accessed via an external link */
|
/** @var bool $is_public Whether the report can be accessed via an external link */
|
||||||
'is_public' => $this->resource->is_public,
|
'is_public' => $this->resource->is_public,
|
||||||
/** @var string|null $public_until Date until the report is public */
|
/** @var string|null $public_until Date until the report is public */
|
||||||
'public_until' => $this->formatDateTime($this->resource->public_until),
|
'public_until' => $this->formatDateTime($this->resource->public_until),
|
||||||
/** @var string|null $shareable_link Get link to access the report externally, not set if the report is private */
|
/** @var string|null $shareable_link Get link to access the report externally, not set if the report is private */
|
||||||
'shareable_link' => $this->resource->getShareableLink(),
|
'shareable_link' => $this->resource->getShareableLink(),
|
||||||
/** Date when the report was created */
|
/** @var string $created_at Date when the report was created */
|
||||||
'created_at' => $this->formatDateTime($this->resource->created_at),
|
'created_at' => $this->formatDateTime($this->resource->created_at),
|
||||||
/** Date when the report was last updated */
|
/** @var string $updated_at Date when the report was last updated */
|
||||||
'updated_at' => $this->formatDateTime($this->resource->updated_at),
|
'updated_at' => $this->formatDateTime($this->resource->updated_at),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Resources\V1\Tag;
|
namespace App\Http\Resources\V1\Tag;
|
||||||
|
|
||||||
|
use App\Http\Resources\PaginatedResourceCollection;
|
||||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||||
|
|
||||||
class TagCollection extends ResourceCollection
|
class TagCollection extends ResourceCollection implements PaginatedResourceCollection
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The resource that this resource collects.
|
* The resource that this resource collects.
|
||||||
|
|||||||
@@ -21,13 +21,13 @@ class TagResource extends BaseResource
|
|||||||
public function toArray(Request $request): array
|
public function toArray(Request $request): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
/** ID */
|
/** @var string $id ID */
|
||||||
'id' => $this->resource->id,
|
'id' => $this->resource->id,
|
||||||
/** Name */
|
/** @var string $name Name */
|
||||||
'name' => $this->resource->name,
|
'name' => $this->resource->name,
|
||||||
/** When the tag was created */
|
/** @var string $created_at When the tag was created */
|
||||||
'created_at' => $this->formatDateTime($this->resource->created_at),
|
'created_at' => $this->formatDateTime($this->resource->created_at),
|
||||||
/** When the tag was last updated */
|
/** @var string $updated_at When the tag was last updated */
|
||||||
'updated_at' => $this->formatDateTime($this->resource->updated_at),
|
'updated_at' => $this->formatDateTime($this->resource->updated_at),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Resources\V1\Task;
|
namespace App\Http\Resources\V1\Task;
|
||||||
|
|
||||||
|
use App\Http\Resources\PaginatedResourceCollection;
|
||||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||||
|
|
||||||
class TaskCollection extends ResourceCollection
|
class TaskCollection extends ResourceCollection implements PaginatedResourceCollection
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The resource that this resource collects.
|
* The resource that this resource collects.
|
||||||
|
|||||||
@@ -22,21 +22,21 @@ class TaskResource extends BaseResource
|
|||||||
public function toArray(Request $request): array
|
public function toArray(Request $request): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
/** ID */
|
/** @var string $id ID */
|
||||||
'id' => $this->resource->id,
|
'id' => $this->resource->id,
|
||||||
/** Name */
|
/** @var string $name Name */
|
||||||
'name' => $this->resource->name,
|
'name' => $this->resource->name,
|
||||||
/** Whether the task is done */
|
/** @var bool $is_done Whether the task is done */
|
||||||
'is_done' => $this->resource->is_done,
|
'is_done' => $this->resource->is_done,
|
||||||
/** ID of the project */
|
/** @var string $project_id ID of the project */
|
||||||
'project_id' => $this->resource->project_id,
|
'project_id' => $this->resource->project_id,
|
||||||
/** Estimated time in seconds */
|
/** @var int|null $estimated_time Estimated time in seconds */
|
||||||
'estimated_time' => $this->resource->estimated_time,
|
'estimated_time' => $this->resource->estimated_time,
|
||||||
/** Spent time on this task in seconds (sum of the duration of all associated time entries, excl. still running time entries) */
|
/** @var int $spent_time Spent time on this task in seconds (sum of the duration of all associated time entries, excl. still running time entries) */
|
||||||
'spent_time' => $this->resource->spent_time,
|
'spent_time' => $this->resource->spent_time,
|
||||||
/** When the tag was created */
|
/** @var string $created_at When the tag was created */
|
||||||
'created_at' => $this->formatDateTime($this->resource->created_at),
|
'created_at' => $this->formatDateTime($this->resource->created_at),
|
||||||
/** When the tag was last updated */
|
/** @var string $updated_at When the tag was last updated */
|
||||||
'updated_at' => $this->formatDateTime($this->resource->updated_at),
|
'updated_at' => $this->formatDateTime($this->resource->updated_at),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Resources\V1\TimeEntry;
|
namespace App\Http\Resources\V1\TimeEntry;
|
||||||
|
|
||||||
|
use App\Http\Resources\PaginatedResourceCollection;
|
||||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||||
|
|
||||||
class TimeEntryCollection extends ResourceCollection
|
class TimeEntryCollection extends ResourceCollection implements PaginatedResourceCollection
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The resource that this resource collects.
|
* The resource that this resource collects.
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ class TimeEntryResource extends BaseResource
|
|||||||
public function toArray(Request $request): array
|
public function toArray(Request $request): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
/** ID of time entry */
|
/** @var string $id ID of time entry */
|
||||||
'id' => $this->resource->id,
|
'id' => $this->resource->id,
|
||||||
/**
|
/**
|
||||||
* Start of time entry (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z)
|
* @var string $start Start of time entry (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z)
|
||||||
*/
|
*/
|
||||||
'start' => $this->formatDateTime($this->resource->start),
|
'start' => $this->formatDateTime($this->resource->start),
|
||||||
/**
|
/**
|
||||||
@@ -35,19 +35,19 @@ class TimeEntryResource extends BaseResource
|
|||||||
'duration' => (int) $this->resource->getDuration()?->totalSeconds,
|
'duration' => (int) $this->resource->getDuration()?->totalSeconds,
|
||||||
/** @var string|null $description Description of time entry */
|
/** @var string|null $description Description of time entry */
|
||||||
'description' => $this->resource->description,
|
'description' => $this->resource->description,
|
||||||
/** ID of task */
|
/** @var string|null $task_id ID of task */
|
||||||
'task_id' => $this->resource->task_id,
|
'task_id' => $this->resource->task_id,
|
||||||
/** ID of project */
|
/** @var string|null $project_id ID of project */
|
||||||
'project_id' => $this->resource->project_id,
|
'project_id' => $this->resource->project_id,
|
||||||
/** ID of organization */
|
/** @var string $organization_id ID of organization */
|
||||||
'organization_id' => $this->resource->organization_id,
|
'organization_id' => $this->resource->organization_id,
|
||||||
/** ID of user */
|
/** @var string $user_id ID of user */
|
||||||
'user_id' => $this->resource->user_id,
|
'user_id' => $this->resource->user_id,
|
||||||
/** List of tag IDs */
|
/** @var array<string> $tags List of tag IDs */
|
||||||
'tags' => $this->resource->tags ?? [],
|
'tags' => $this->resource->tags ?? [],
|
||||||
/** Whether time entry is billable */
|
/** @var bool $billable Whether time entry is billable */
|
||||||
'billable' => $this->resource->billable,
|
'billable' => $this->resource->billable,
|
||||||
/** Type of the time entry (`work` time or a `break`) */
|
/** @var string $type Type of the time entry (`work` time or a `break`) */
|
||||||
'type' => $this->resource->type->value,
|
'type' => $this->resource->type->value,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,21 +22,21 @@ class UserResource extends BaseResource
|
|||||||
public function toArray(Request $request): array
|
public function toArray(Request $request): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
/** ID of user */
|
/** @var string $id ID of user */
|
||||||
'id' => $this->resource->id,
|
'id' => $this->resource->id,
|
||||||
/** Name of user */
|
/** @var string $name Name of user */
|
||||||
'name' => $this->resource->name,
|
'name' => $this->resource->name,
|
||||||
/** Email of user */
|
/** @var string $email Email of user */
|
||||||
'email' => $this->resource->email,
|
'email' => $this->resource->email,
|
||||||
/** Email address awaiting verification (set when the user has requested an email change but not yet verified the new address) */
|
/** @var string|null $pending_email Email address awaiting verification (set when the user has requested an email change but not yet verified the new address) */
|
||||||
'pending_email' => $this->resource->pending_email,
|
'pending_email' => $this->resource->pending_email,
|
||||||
/** Profile photo URL */
|
/** @var string $profile_photo_url Profile photo URL */
|
||||||
'profile_photo_url' => $this->resource->profile_photo_url,
|
'profile_photo_url' => $this->resource->profile_photo_url,
|
||||||
/** Timezone (f.e. Europe/Berlin or America/New_York) */
|
/** @var string $timezone Timezone (f.e. Europe/Berlin or America/New_York) */
|
||||||
'timezone' => $this->resource->timezone,
|
'timezone' => $this->resource->timezone,
|
||||||
/** @var Weekday $week_start Starting day of the week */
|
/** @var Weekday $week_start Starting day of the week */
|
||||||
'week_start' => $this->resource->week_start->value,
|
'week_start' => $this->resource->week_start->value,
|
||||||
/** Whether to email the user when a time entry has been running for more than 8 hours */
|
/** @var bool $send_time_entry_still_running_email Whether to email the user when a time entry has been running for more than 8 hours */
|
||||||
'send_time_entry_still_running_email' => $this->resource->send_time_entry_still_running_email,
|
'send_time_entry_still_running_email' => $this->resource->send_time_entry_still_running_email,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use App\Extensions\Scramble\ApiExceptionTypeToSchema;
|
use App\Extensions\Scramble\ApiExceptionTypeToSchema;
|
||||||
|
use App\Extensions\Scramble\PaginatedResourceCollectionTypeToSchema;
|
||||||
use Dedoc\Scramble\Http\Middleware\RestrictedDocsAccess;
|
use Dedoc\Scramble\Http\Middleware\RestrictedDocsAccess;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -100,5 +101,6 @@ MD,
|
|||||||
|
|
||||||
'extensions' => [
|
'extensions' => [
|
||||||
ApiExceptionTypeToSchema::class,
|
ApiExceptionTypeToSchema::class,
|
||||||
|
PaginatedResourceCollectionTypeToSchema::class,
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
},
|
},
|
||||||
"Invoicing": {
|
"Invoicing": {
|
||||||
"repository": "solidtime-io/extension-invoicing",
|
"repository": "solidtime-io/extension-invoicing",
|
||||||
"ref": "v0.0.10"
|
"ref": "v0.0.8"
|
||||||
},
|
},
|
||||||
"Auditing": {
|
"Auditing": {
|
||||||
"repository": "solidtime-io/extension-auditing",
|
"repository": "solidtime-io/extension-auditing",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { defineConfig, devices } from '@playwright/test';
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
import { existsSync, readFileSync } from 'node:fs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read environment variables from file.
|
* Read environment variables from file.
|
||||||
@@ -6,11 +7,57 @@ import { defineConfig, devices } from '@playwright/test';
|
|||||||
*/
|
*/
|
||||||
// require('dotenv').config();
|
// require('dotenv').config();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extensions ship their e2e specs in extensions/<Name>/tests/e2e. Only the specs of extensions
|
||||||
|
* that are enabled in modules_statuses.json (php artisan module:enable <Name>) are collected,
|
||||||
|
* a checked out but disabled extension would fail its specs otherwise.
|
||||||
|
*/
|
||||||
|
function enabledExtensions(): string[] {
|
||||||
|
const statusesFile = './modules_statuses.json';
|
||||||
|
if (!existsSync(statusesFile)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const statuses: Record<string, boolean> = JSON.parse(readFileSync(statusesFile, 'utf-8'));
|
||||||
|
return Object.entries(statuses)
|
||||||
|
.filter(([, enabled]) => enabled)
|
||||||
|
.map(([name]) => name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Every test root gets its own testDir (instead of testDir: '.') so playwright does not walk the
|
||||||
|
* whole repository, including node_modules, vendor and nested worktrees, to find the specs.
|
||||||
|
*/
|
||||||
|
const testRoots = [
|
||||||
|
{ suffix: '', testDir: './e2e' },
|
||||||
|
...enabledExtensions().map((name) => ({
|
||||||
|
suffix: '-' + name,
|
||||||
|
testDir: `./extensions/${name}/tests/e2e`,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
const browsers = [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: { ...devices['Desktop Chrome'] },
|
||||||
|
},
|
||||||
|
|
||||||
|
// Firefox only in CI to keep local runs fast
|
||||||
|
...(process.env.CI
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
name: 'firefox',
|
||||||
|
use: { ...devices['Desktop Firefox'] },
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* See https://playwright.dev/docs/test-configuration.
|
* See https://playwright.dev/docs/test-configuration.
|
||||||
*/
|
*/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testDir: './e2e',
|
/* Resolves the @e2e/* and @e2e-support/* aliases that extension specs import core helpers with */
|
||||||
|
tsconfig: './tsconfig.json',
|
||||||
/* Run tests in files in parallel */
|
/* Run tests in files in parallel */
|
||||||
fullyParallel: true,
|
fullyParallel: true,
|
||||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
@@ -32,23 +79,14 @@ export default defineConfig({
|
|||||||
|
|
||||||
timeout: 20 * 1000,
|
timeout: 20 * 1000,
|
||||||
|
|
||||||
/* Configure projects for major browsers */
|
/* Configure projects for major browsers, core specs keep the plain browser project name */
|
||||||
projects: [
|
projects: browsers.flatMap((browser) =>
|
||||||
{
|
testRoots.map((root) => ({
|
||||||
name: 'chromium',
|
name: browser.name + root.suffix,
|
||||||
use: { ...devices['Desktop Chrome'] },
|
testDir: root.testDir,
|
||||||
},
|
use: browser.use,
|
||||||
|
}))
|
||||||
// Firefox only in CI to keep local runs fast
|
),
|
||||||
...(process.env.CI
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
name: 'firefox',
|
|
||||||
use: { ...devices['Desktop Firefox'] },
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
],
|
|
||||||
|
|
||||||
/* Run your local dev server before starting the tests */
|
/* Run your local dev server before starting the tests */
|
||||||
// webServer: {
|
// webServer: {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use App\Http\Controllers\Api\FallbackController;
|
|
||||||
use App\Http\Controllers\Api\V1\ApiTokenController;
|
use App\Http\Controllers\Api\V1\ApiTokenController;
|
||||||
use App\Http\Controllers\Api\V1\ChartController;
|
use App\Http\Controllers\Api\V1\ChartController;
|
||||||
use App\Http\Controllers\Api\V1\ClientController;
|
use App\Http\Controllers\Api\V1\ClientController;
|
||||||
@@ -24,6 +23,7 @@ use App\Http\Controllers\Api\V1\UserController;
|
|||||||
use App\Http\Controllers\Api\V1\UserMembershipController;
|
use App\Http\Controllers\Api\V1\UserMembershipController;
|
||||||
use App\Http\Controllers\Api\V1\UserTimeEntryController;
|
use App\Http\Controllers\Api\V1\UserTimeEntryController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
@@ -200,5 +200,9 @@ Route::prefix('v1')->name('v1.')->group(static function (): void {
|
|||||||
* Fallback routes, to prevent a rendered HTML page in /api/* routes
|
* Fallback routes, to prevent a rendered HTML page in /api/* routes
|
||||||
* The / route is also included since the fallback is not triggered on the root route
|
* The / route is also included since the fallback is not triggered on the root route
|
||||||
*/
|
*/
|
||||||
Route::get('/', FallbackController::class);
|
Route::get('/', function (): void {
|
||||||
Route::fallback(FallbackController::class);
|
throw new NotFoundHttpException('API resource not found');
|
||||||
|
});
|
||||||
|
Route::fallback(function (): void {
|
||||||
|
throw new NotFoundHttpException('API resource not found');
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Tests\Unit\ApiDocs;
|
|
||||||
|
|
||||||
use App\Http\Controllers\Api\FallbackController;
|
|
||||||
use Dedoc\Scramble\Contracts\Diagnostics\Diagnostic;
|
|
||||||
use Dedoc\Scramble\Generator;
|
|
||||||
use Dedoc\Scramble\Infer\Context;
|
|
||||||
use Dedoc\Scramble\Scramble;
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
use Illuminate\Support\Facades\File;
|
|
||||||
use PHPUnit\Framework\Attributes\CoversClass;
|
|
||||||
use Tests\TestCaseWithDatabase;
|
|
||||||
|
|
||||||
#[CoversClass(FallbackController::class)]
|
|
||||||
class ApiDocsExportTest extends TestCaseWithDatabase
|
|
||||||
{
|
|
||||||
private string $path;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
|
||||||
{
|
|
||||||
parent::setUp();
|
|
||||||
// Scramble keeps its inference context in a static property, which would otherwise outlive the refreshed application
|
|
||||||
Context::reset();
|
|
||||||
$this->path = storage_path('framework/testing/api-docs-'.uniqid().'.json');
|
|
||||||
File::ensureDirectoryExists(dirname($this->path));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function tearDown(): void
|
|
||||||
{
|
|
||||||
File::delete($this->path);
|
|
||||||
parent::tearDown();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
private function exportApiDocs(): array
|
|
||||||
{
|
|
||||||
$exitCode = $this->withoutMockingConsoleOutput()->artisan('scramble:export', [
|
|
||||||
'--path' => $this->path,
|
|
||||||
]);
|
|
||||||
$this->assertSame(Command::SUCCESS, $exitCode);
|
|
||||||
$this->assertFileExists($this->path);
|
|
||||||
|
|
||||||
return json_decode(File::get($this->path), true, flags: JSON_THROW_ON_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $docs
|
|
||||||
* @param array<string, mixed> $schema
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
private function resolveSchema(array $docs, array $schema): array
|
|
||||||
{
|
|
||||||
if (isset($schema['$ref'])) {
|
|
||||||
$name = str_replace('#/components/schemas/', '', $schema['$ref']);
|
|
||||||
|
|
||||||
return $this->resolveSchema($docs, $docs['components']['schemas'][$name]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $schema;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_api_docs_can_be_exported(): void
|
|
||||||
{
|
|
||||||
// Act
|
|
||||||
$docs = $this->exportApiDocs();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
$this->assertArrayHasKey('paths', $docs);
|
|
||||||
$this->assertNotEmpty($docs['paths']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_api_docs_are_generated_without_diagnostics(): void
|
|
||||||
{
|
|
||||||
// Act
|
|
||||||
$result = app(Generator::class)->generate(Scramble::getGeneratorConfig('default'));
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
$diagnostics = $result->diagnostics()->map(function (Diagnostic $diagnostic): string {
|
|
||||||
$location = $diagnostic->codeLocation();
|
|
||||||
|
|
||||||
return sprintf(
|
|
||||||
'[%s] %s%s',
|
|
||||||
$diagnostic->code(),
|
|
||||||
$diagnostic->message(),
|
|
||||||
$location !== null ? ' ('.$location->file.':'.$location->line.')' : ''
|
|
||||||
);
|
|
||||||
})->all();
|
|
||||||
$this->assertSame([], $diagnostics);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_paginated_endpoints_are_documented_with_pagination(): void
|
|
||||||
{
|
|
||||||
// Act
|
|
||||||
$docs = $this->exportApiDocs();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
$tagsSchema = $docs['paths']['/v1/organizations/{organization}/tags']['get']['responses']['200']['content']['application/json']['schema'];
|
|
||||||
$this->assertSame(['data', 'links', 'meta'], $tagsSchema['required']);
|
|
||||||
$tagsData = $this->resolveSchema($docs, $tagsSchema['properties']['data']);
|
|
||||||
$this->assertSame('array', $tagsData['type']);
|
|
||||||
$this->assertSame('#/components/schemas/TagResource', $tagsData['items']['$ref']);
|
|
||||||
|
|
||||||
$timeEntriesSchema = $docs['paths']['/v1/organizations/{organization}/time-entries']['get']['responses']['200']['content']['application/json']['schema'];
|
|
||||||
$this->assertSame(['data', 'meta'], $timeEntriesSchema['required']);
|
|
||||||
$this->assertSame(['total'], $timeEntriesSchema['properties']['meta']['required']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_fallback_routes_are_excluded_from_api_docs(): void
|
|
||||||
{
|
|
||||||
// Act
|
|
||||||
$docs = $this->exportApiDocs();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
$this->assertArrayNotHasKey('/', $docs['paths']);
|
|
||||||
$this->assertArrayNotHasKey('/{fallbackPlaceholder}', $docs['paths']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_fallback_routes_return_not_found(): void
|
|
||||||
{
|
|
||||||
// Act
|
|
||||||
$rootResponse = $this->getJson('/api');
|
|
||||||
$unknownResponse = $this->getJson('/api/does-not-exist');
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
$rootResponse->assertNotFound();
|
|
||||||
$rootResponse->assertJsonPath('message', 'API resource not found');
|
|
||||||
$unknownResponse->assertNotFound();
|
|
||||||
$unknownResponse->assertJsonPath('message', 'API resource not found');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -245,44 +245,6 @@ class UserEndpointTest extends ApiEndpointTestAbstract
|
|||||||
Mail::assertNothingSent();
|
Mail::assertNothingSent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_update_fails_if_email_is_already_used_by_another_user(): void
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
Mail::fake();
|
|
||||||
$data = $this->createUserWithPermission();
|
|
||||||
User::factory()->create(['email' => 'taken@example.com']);
|
|
||||||
Passport::actingAs($data->user);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
$response = $this->putJson(route('api.v1.users.update', $data->user->getKey()), [
|
|
||||||
'email' => 'taken@example.com',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
$response->assertUnprocessable();
|
|
||||||
$response->assertJsonValidationErrors(['email']);
|
|
||||||
$this->assertNull($data->user->fresh()->pending_email);
|
|
||||||
Mail::assertNothingSent();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_update_email_succeeds_if_email_is_only_used_by_a_placeholder_user(): void
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
Mail::fake();
|
|
||||||
$data = $this->createUserWithPermission();
|
|
||||||
User::factory()->placeholder()->create(['email' => 'placeholder@example.com']);
|
|
||||||
Passport::actingAs($data->user);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
$response = $this->putJson(route('api.v1.users.update', $data->user->getKey()), [
|
|
||||||
'email' => 'placeholder@example.com',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
$response->assertSuccessful();
|
|
||||||
$this->assertSame('placeholder@example.com', $data->user->fresh()->pending_email);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_update_fails_if_email_format_is_invalid(): void
|
public function test_update_fails_if_email_format_is_invalid(): void
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|||||||
@@ -6,7 +6,9 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./resources/js/*"],
|
"@/*": ["./resources/js/*"],
|
||||||
"@solidtime/ui": ["./resources/js/packages/ui/src/index.ts"]
|
"@solidtime/ui": ["./resources/js/packages/ui/src/index.ts"],
|
||||||
|
"@e2e/*": ["./e2e/*"],
|
||||||
|
"@e2e-support/*": ["./playwright/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user