Compare commits

..

2 Commits

Author SHA1 Message Date
Constantin Graf
3c61303d88 Fix for tests 2026-07-21 16:57:32 +02:00
Constantin Graf
a70c4fe3fb Add parallel testing to GitHub action phpunit 2026-07-21 16:56:01 +02:00
474 changed files with 5278 additions and 21049 deletions

View File

@@ -1,7 +1,5 @@
.git
**/.git
.gitmodules
**/.gitmodules
.github
.DS_Store
.fleet
@@ -10,13 +8,6 @@
*.log
npm-debug.log
yarn-error.log
k8s
docs
e2e
tests
docker-compose.yml
docker/local
.phpunit.cache
.phpunit.result.cache
@@ -25,18 +16,6 @@ test-results
playwright-report
blob-report
playwright/.cache
openapi.json
playwright
playwright.config.ts
vitest.config.ts
phpunit.xml
phpstan.neon
pint.json
eslint.config.mjs
tsconfig.json
jsconfig.json
postcss.config.js
tailwind.config.js
node_modules
extensions/*/node_modules
@@ -51,4 +30,3 @@ _ide_helper.php
.phpstorm.meta.php
storage/logs/*
storage/*.key

View File

@@ -6,7 +6,6 @@ APP_DEBUG=true
APP_URL=http://localhost
APP_FORCE_HTTPS=false
APP_ENABLE_REGISTRATION=true
SUPER_ADMINS=admin@example.com
# Logging
LOG_CHANNEL=stack
@@ -65,6 +64,3 @@ GOTENBERG_URL=http://localhost:3000
# Octane
OCTANE_SERVER=frankenphp
# Inertia
INERTIA_SSR_ENABLED=false

View File

@@ -5,8 +5,7 @@ APP_KEY=base64:UNQNf1SXeASNkWux01Rj8EnHYx8FO0kAxWNDwktclkk=
APP_DEBUG=true
APP_URL=https://solidtime.test
APP_FORCE_HTTPS=false
# Supported values: on, off, invite-only (true/false are supported for backwards compatibility)
APP_ENABLE_REGISTRATION=on
APP_ENABLE_REGISTRATION=true
SUPER_ADMINS=admin@example.com
PAGINATION_PER_PAGE_DEFAULT=500
@@ -88,6 +87,3 @@ FORWARD_DB_PORT=54329
VITE_HOST_NAME=vite.solidtime.test
VITE_APP_NAME="${APP_NAME}"
#SAIL_XDEBUG_MODE=develop,debug,coverage
# This app does not ship a Node SSR bundle, so Inertia SSR must stay disabled.
INERTIA_SSR_ENABLED=false

View File

@@ -13,5 +13,3 @@ LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=pgsql
INERTIA_SSR_ENABLED=false

27
.github/VOUCHED.td vendored
View File

@@ -1,27 +0,0 @@
# Vouched contributors for solidtime.
#
# One handle per line, without the leading @, sorted alphabetically.
# Prefix a handle with - to denounce them, optionally followed by a reason.
# Format reference: https://github.com/mitchellh/vouch
#
# Maintainers do not need to edit this file by hand. Comment "vouch @user",
# "unvouch @user" or "denounce @user <reason>" on any issue, pull request or
# discussion and the vouch workflows will update this file.
#
# Collaborators with write access and bots are always allowed and do not need
# an entry here.
#
# Seeded 2026-07-25 from the authors of every merged pull request.
agross
akolenda
bufferhead-code
candideu
kasparrosin
korridor
nikbucher tasks table sorting
onatcer
shrootbuck
smilebeda
thespyder
utlark

View File

@@ -8,7 +8,6 @@ on:
pull_request:
paths:
- '.github/workflows/build-onpremise.yml'
- '.dockerignore'
- 'extensions/manifest.json'
- 'docker/prod/**'
workflow_dispatch:
@@ -103,8 +102,6 @@ jobs:
{
echo "invoicing_repository=$(jq -r '.Invoicing.repository' extensions/manifest.json)"
echo "invoicing_ref=$(jq -r '.Invoicing.ref' extensions/manifest.json)"
echo "auditing_repository=$(jq -r '.Auditing.repository' extensions/manifest.json)"
echo "auditing_ref=$(jq -r '.Auditing.ref' extensions/manifest.json)"
} >> "$GITHUB_OUTPUT"
- name: "Checkout invoicing extension"
@@ -121,26 +118,9 @@ jobs:
- name: "Install npm dependencies in invoicing extension"
run: cd extensions/Invoicing && npm ci
- name: "Checkout auditing extension"
uses: actions/checkout@v7
with:
repository: ${{ steps.extension-manifest.outputs.auditing_repository }}
ref: ${{ steps.extension-manifest.outputs.auditing_ref }}
path: extensions/Auditing
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_AUDITING_EXTENSION }}
- name: "Install composer dependencies in auditing extension"
run: cd extensions/Auditing && composer install --no-dev --no-ansi --no-interaction --prefer-dist --ignore-platform-reqs --classmap-authoritative
- name: "Install npm dependencies in auditing extension"
run: cd extensions/Auditing && npm ci
- name: "Activate invoicing extension"
run: php artisan module:enable Invoicing
- name: "Activate auditing extension"
run: php artisan module:enable Auditing
- name: "Install npm dependencies"
run: npm ci
@@ -160,7 +140,7 @@ jobs:
${{ env.DOCKER_REPO }}
- name: "Login to solidtime OnPremise Registry"
uses: docker/login-action@v4.6.0
uses: docker/login-action@v4
with:
registry: registry.on-premise.solidtime.io
username: ${{ secrets.ONPREMISE_USERNAME }}
@@ -214,7 +194,7 @@ jobs:
merge-multiple: true
- name: "Login to solidtime OnPremise Registry"
uses: docker/login-action@v4.6.0
uses: docker/login-action@v4
with:
registry: registry.on-premise.solidtime.io
username: ${{ secrets.ONPREMISE_USERNAME }}

View File

@@ -8,7 +8,6 @@ on:
pull_request:
paths:
- '.github/workflows/build-private.yml'
- '.dockerignore'
- 'extensions/manifest.json'
- 'docker/prod/**'
workflow_dispatch:
@@ -84,8 +83,6 @@ jobs:
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)"
echo "auditing_repository=$(jq -r '.Auditing.repository' extensions/manifest.json)"
echo "auditing_ref=$(jq -r '.Auditing.ref' extensions/manifest.json)"
} >> "$GITHUB_OUTPUT"
- name: "Checkout billing extension"
@@ -99,7 +96,7 @@ jobs:
- name: "Install dependencies in billing extension"
uses: php-actions/composer@v6
env:
COMPOSER_AUTH: '{"http-basic": {"spark.laravel.com": {"username": "${{ secrets.LARAVEL_SPARK_USERNAME }}", "password": "${{ secrets.LARAVEL_SPARK_API_KEY }}"}}}'
COMPOSER_AUTH: '{"http-basic": {"spark.laravel.com": {"username": "gregor@vostrak.at", "password": "${{ secrets.LARAVEL_SPARK_API_KEY }}"}}}'
with:
working_dir: "extensions/Billing"
command: install
@@ -147,25 +144,6 @@ jobs:
- name: "Install npm dependencies in invoicing extension"
run: cd extensions/Invoicing && npm ci
- name: "Checkout auditing extension"
uses: actions/checkout@v7
with:
repository: ${{ steps.extension-manifest.outputs.auditing_repository }}
ref: ${{ steps.extension-manifest.outputs.auditing_ref }}
path: extensions/Auditing
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_AUDITING_EXTENSION }}
- name: "Install composer dependencies in auditing extension"
uses: php-actions/composer@v6
with:
working_dir: "extensions/Auditing"
command: install
only_args: --no-dev --no-ansi --no-interaction --prefer-dist --ignore-platform-reqs --classmap-authoritative
php_version: 8.3
- name: "Install npm dependencies in auditing extension"
run: cd extensions/Auditing && npm ci
- name: "Setup PHP with PECL extension"
uses: shivammathur/setup-php@v2
with:
@@ -189,9 +167,6 @@ jobs:
- name: "Activate invoicing extension"
run: php artisan module:enable Invoicing
- name: "Activate auditing extension"
run: php artisan module:enable Auditing
- name: "Install npm dependencies"
run: npm ci
@@ -201,7 +176,7 @@ jobs:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: "Login to GitHub Container Registry"
uses: docker/login-action@v4.6.0
uses: docker/login-action@v4
with:
registry: rg.fr-par.scw.cloud/solidtime
username: nologin

View File

@@ -8,7 +8,6 @@ on:
pull_request:
paths:
- '.github/workflows/build-public.yml'
- '.dockerignore'
- 'docker/prod/**'
workflow_dispatch:
@@ -117,13 +116,13 @@ jobs:
${{ env.GHCR_REPO }}
- name: "Login to Docker Hub Container Registry"
uses: docker/login-action@v4.6.0
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: "Login to GitHub Container Registry"
uses: docker/login-action@v4.6.0
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -177,13 +176,13 @@ jobs:
merge-multiple: true
- name: "Login to Docker Hub"
uses: docker/login-action@v4.6.0
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: "Login to GHCR"
uses: docker/login-action@v4.6.0
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}

View File

@@ -1,31 +0,0 @@
name: Extension Manifest Check
on:
pull_request:
paths:
- extensions/manifest.json
- .github/workflows/extension-manifest-check.yml
permissions:
contents: read
jobs:
check-refs:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: "Checkout code"
uses: actions/checkout@v7
# Extension refs must be pinned to a release tag (vX.Y.Z) or a full commit
# SHA. Branch names (main, feature/foo, ...) are only acceptable while
# developing and must be replaced before the pull request is merged.
- name: "Check extension refs are pinned"
run: |
failed=0
while IFS=$'\t' read -r name ref; do
if [[ "$ref" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.]+)?$ ]] || [[ "$ref" =~ ^[0-9a-f]{40}$ ]]; then
echo "✅ $name: $ref"
else
echo "::error file=extensions/manifest.json::Extension \"$name\" uses ref \"$ref\", which is not a release tag or commit SHA. Pin it before merging."
failed=1
fi
done < <(jq -r 'to_entries[] | [.key, .value.ref] | @tsv' extensions/manifest.json)
exit $failed

View File

@@ -1,8 +1,6 @@
name: NPM Format Check
on: [push]
permissions:
contents: read
jobs:
format-check:

View File

@@ -1,136 +0,0 @@
name: PHPUnit Tests - Extensions
on: push
permissions:
contents: read
jobs:
phpunit-extensions:
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
matrix:
postgres_version: [ 15, 16, 17 ]
services:
pgsql_test:
image: postgres:${{ matrix.postgres_version }}
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 PHP"
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv
coverage: pcov
- uses: actions/setup-node@v7
with:
node-version: '20.x'
- 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"
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"
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"
run: cd extensions/Billing && npm ci
- name: "Checkout services extension"
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"
working-directory: extensions/Services
run: composer install -n --prefer-dist --ignore-platform-reqs
- name: "Install npm dependencies in services extension"
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 --ignore-platform-reqs
- name: "Activate billing extension"
run: php artisan module:enable Billing
- name: "Activate services extension"
run: php artisan module:enable Services
- name: "Activate invoicing extension"
run: php artisan module:enable Invoicing
- name: "Install dependencies"
run: npm ci
- name: "Build Frontend"
run: npm run build
- name: "Prepare Laravel Application"
run: |
cp .env.ci .env
php artisan key:generate
php artisan passport:keys
- name: "Run PHPUnit"
run: php artisan test extensions/Billing/tests extensions/Services/tests extensions/Invoicing/tests --stop-on-failure

View File

@@ -65,10 +65,10 @@ jobs:
php artisan passport:keys
- name: "Run PHPUnit"
run: php artisan test --stop-on-failure --coverage-text --coverage-clover=coverage.xml
run: php artisan test --parallel --stop-on-failure --coverage-text --coverage-clover=coverage.xml
- name: "Upload coverage reports to Codecov"
uses: codecov/codecov-action@v7.1.1
uses: codecov/codecov-action@v7.0.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: solidtime-io/solidtime

View File

@@ -1,185 +0,0 @@
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.

View File

@@ -1,75 +0,0 @@
name: Vouch (check PR)
on:
pull_request_target:
types: [opened, reopened, synchronize]
issue_comment:
types: [created]
permissions:
contents: read
pull-requests: write
jobs:
check:
runs-on: ubuntu-latest
timeout-minutes: 5
if: >-
github.event_name == 'pull_request_target' ||
(github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
contains(github.event.comment.body, '/recheck'))
steps:
# Pull requests of 50 changed lines or fewer skip the vouch requirement.
- name: "Measure diff size"
id: size
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
PR: ${{ github.event.pull_request.number || github.event.issue.number }}
# Changes to these files do not count towards the 50-line limit.
# One extended regex per line, matched against the whole repo-relative
# path, so use a leading .* to match a file in any directory.
IGNORED: |
package-lock\.json
composer\.lock
tests/.*
e2e/.*
.*\.(test|spec)\.(ts|js|vue)
run: |
set -euo pipefail
# An empty list yields "^()$", which matches no filename. grep exits
# 1 on an empty list, so swallow that rather than fail the step.
join() { { grep -vE '^[[:space:]]*$' || true; } | paste -sd'|' -; }
ignored="^($(join <<<"$IGNORED"))$"
total=$(gh api --paginate "repos/$REPO/pulls/$PR/files" \
--jq '.[] | [.filename, .additions + .deletions] | @tsv' |
awk -F'\t' -v ignored="$ignored" '
$1 ~ ignored { next }
{ n += $2 }
END { print n+0 }')
echo "total=$total" >> "$GITHUB_OUTPUT"
echo "Countable diff size: $total line(s)"
- name: "Small patch (denounced users still blocked)"
if: fromJSON(steps.size.outputs.total) <= 50
uses: mitchellh/vouch/action/check-pr@v1.5.0
with:
pr-number: ${{ github.event.pull_request.number || github.event.issue.number }}
auto-close: true
require-vouch: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: "Full vouch required"
if: fromJSON(steps.size.outputs.total) > 50
uses: mitchellh/vouch/action/check-pr@v1.5.0
with:
pr-number: ${{ github.event.pull_request.number || github.event.issue.number }}
auto-close: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,33 +0,0 @@
name: Vouch (manage by discussion)
# Same commands as vouch-manage-by-issue.yml, but for discussion comments.
on:
discussion_comment:
types: [created]
concurrency:
group: vouch-manage
cancel-in-progress: false
permissions:
contents: write
discussions: write
jobs:
manage:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: "Checkout code"
uses: actions/checkout@v7
- name: "Apply vouch command"
uses: mitchellh/vouch/action/manage-by-discussion@v1.5.0
with:
discussion-number: ${{ github.event.discussion.number }}
comment-node-id: ${{ github.event.comment.node_id }}
roles: admin,maintain,write
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,35 +0,0 @@
name: Vouch (manage by issue)
# Maintainers comment "vouch @user", "unvouch @user" or "denounce @user <reason>"
# on any issue or pull request, and this workflow updates .github/VOUCHED.td.
on:
issue_comment:
types: [created]
concurrency:
group: vouch-manage
cancel-in-progress: false
permissions:
contents: write
issues: write
pull-requests: write
jobs:
manage:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: "Checkout code"
uses: actions/checkout@v7
- name: "Apply vouch command"
uses: mitchellh/vouch/action/manage-by-issue@v1.5.0
with:
issue-id: ${{ github.event.issue.number }}
comment-id: ${{ github.event.comment.id }}
roles: admin,maintain,write
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

2
.gitignore vendored
View File

@@ -6,7 +6,6 @@ dist
/public/storage
/public/css
/public/js
/public/fonts/filament
/public/vendor
/lang/vendor
/storage/*.key
@@ -44,5 +43,4 @@ yarn-error.log
/data
/config/caddy
/config/composer
/config/psysh
/AGENTS.md

View File

@@ -12,22 +12,6 @@ In order to keep the issues of the repository clean we decided to only use them
To respect your time and help us manage contributions effectively, please open an issue or start a discussion and wait for approval before submitting a pull request (PR). This does not apply to tiny fixes or changes however, please keep in mind that we might not merge PRs for various reasons.
### Vouched contributors
Pull requests from authors who are not vouched are closed automatically. This lets us keep up with the volume of AI slop pull requests without a maintainer having to triage every one of them by hand.
Your pull request is not affected if any of the following applies:
- You have write access to this repository.
- Someone with write access has vouched for you. The list lives in [.github/VOUCHED.td](.github/VOUCHED.td).
- Your pull request changes 50 lines or fewer. Test files and lockfiles do not count towards that number, so a small fix that comes with tests still qualifies.
To get vouched, open an issue or discussion before you start and explain how you intend to implement the change. We will discuss the approach with you, and only once we have agreed on the implementation does a maintainer comment `vouch @your-handle`, which puts you on the list from then on.
Being vouched only stops your pull requests from being closed automatically. [Only work on approved issues](#only-work-on-approved-issues) still applies to every pull request you send.
Contributors who abuse this are denounced, and their pull requests are closed regardless of size.
### Contributor License Agreement
You'll also notice that we’ve set up a [Contributor License Agreement (CLA)](https://cla-assistant.io/solidtime-io/solidtime), which must be signed before any PR can be merged. Don’t worry - the process is quick and only takes a few clicks.

View File

@@ -39,8 +39,6 @@ Please open an issue or start a discussion and wait for approval before submitti
**If you submit an AI slop pull request (especially without following the proper procedure), you will be banned from future contributions to solidtime.**
To keep that manageable, pull requests from authors who are not vouched are closed automatically, unless they change 50 lines or fewer. To get vouched, open an issue or discussion first and explain how you intend to implement the change. Once we have agreed on the approach, we vouch for you. See [Vouched contributors](./CONTRIBUTING.md#vouched-contributors).
Please read the [CONTRIBUTING.md](./CONTRIBUTING.md) before sumbitting a Pull Request.
We do accept contributions in the [documentation repository](https://github.com/solidtime-io/docs) f.e. to add new self-hosting guides.

View File

@@ -4,11 +4,9 @@ declare(strict_types=1);
namespace App\Actions\Fortify;
use App\Enums\RegistrationMode;
use App\Enums\Weekday;
use App\Events\NewsletterRegistered;
use App\Models\User;
use App\Service\InvitationService;
use App\Service\IpLookup\IpLookupServiceContract;
use App\Service\TimezoneService;
use App\Service\UserService;
@@ -33,14 +31,13 @@ class CreateNewUser implements CreatesNewUsers
*/
public function create(array $input): User
{
$registrationMode = RegistrationMode::fromConfig(config('app.enable_registration'));
if ($registrationMode === RegistrationMode::Off) {
if (! config('app.enable_registration')) {
throw ValidationException::withMessages([
'email' => [__('Registration is disabled.')],
]);
}
$validated = Validator::make($input, [
Validator::make($input, [
'name' => [
'required',
'string',
@@ -63,20 +60,6 @@ class CreateNewUser implements CreatesNewUsers
],
])->validate();
if ($registrationMode === RegistrationMode::InviteOnly) {
$invitationService = app(InvitationService::class);
$email = (string) $validated['email'];
if (! $invitationService->hasAcceptedInvitationForEmail($email)) {
$message = $invitationService->hasPendingInvitationForEmail($email)
? __('Please accept the organization invitation sent to your email address before registering.')
: __('Registration is only available to invited users.');
throw ValidationException::withMessages([
'email' => [$message],
]);
}
}
$timezone = null;
if (array_key_exists('timezone', $input) && is_string($input['timezone'])) {
if (app(TimezoneService::class)->isValid($input['timezone'])) {

View File

@@ -1,38 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Auth;
use Illuminate\Auth\EloquentUserProvider;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Override;
/**
* User provider that only resolves non-placeholder users.
*
* Placeholder users are created by imports and when members are removed from an
* organization. They can share an email address with a real user, so resolving a user by
* email can return a placeholder instead of the real account. The login flow filters them
* out explicitly, but the password broker and the guard credential checks (for example the
* password confirmation) resolve users through the configured user provider.
*
* Registered as the "eloquent" provider driver in the AuthServiceProvider, so it replaces the
* built-in one for every provider in config/auth.php.
*/
class ActiveUserProvider extends EloquentUserProvider
{
/**
* @param Model|null $model
* @return Builder<Model>
*/
#[Override]
protected function newModelQuery($model = null): Builder
{
$query = parent::newModelQuery($model);
$query->getQuery()->where('is_placeholder', '=', false);
return $query;
}
}

View File

@@ -10,7 +10,6 @@ use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use stdClass;
class SelfHostDatabaseConsistency extends Command
{
@@ -102,7 +101,7 @@ class SelfHostDatabaseConsistency extends Command
}
/**
* @param Collection<int, stdClass> $problems
* @param Collection<int, \stdClass> $problems
*/
private function logProblems(Collection $problems, string $message, bool &$hadAProblem): void
{

View File

@@ -7,7 +7,7 @@ namespace App\Console\Commands\SelfHost;
use Illuminate\Console\Command;
use Illuminate\Encryption\Encrypter;
use Illuminate\Support\Str;
use phpseclib4\Crypt\RSA;
use phpseclib3\Crypt\RSA;
class SelfHostGenerateKeysCommand extends Command
{

View File

@@ -51,8 +51,7 @@ class TimeEntrySendStillRunningMailsCommand extends Command
])
->whereHas('user', function (Builder $query): void {
/** @var Builder<User> $query */
$query->where('is_placeholder', '=', false)
->where('send_time_entry_still_running_email', '=', true);
$query->where('is_placeholder', '=', false);
})
->orderBy('created_at', 'asc')
->chunk(500, function (Collection $timeEntries) use ($dryRun, &$sentMails): void {

View File

@@ -1,29 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Enums;
enum RegistrationMode: string
{
case On = 'on';
case InviteOnly = 'invite-only';
case Off = 'off';
public static function fromConfig(mixed $value): self
{
if ($value === true) {
return self::On;
}
if ($value === false || $value === null) {
return self::Off;
}
return match (strtolower(trim((string) $value))) {
'1', 'on', 'true' => self::On,
'invite-only' => self::InviteOnly,
default => self::Off,
};
}
}

View File

@@ -21,7 +21,6 @@ enum TimeEntryAggregationType: string
case Billable = 'billable';
case Description = 'description';
case Tag = 'tag';
case Type = 'type';
public static function fromInterval(TimeEntryAggregationTypeInterval $timeEntryAggregationTypeInterval): TimeEntryAggregationType
{

View File

@@ -1,15 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Enums;
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
enum TimeEntryType: string
{
use LaravelEnumHelper;
case Work = 'work';
case Break = 'break';
}

View File

@@ -60,6 +60,7 @@ abstract class ApiException extends Exception
*/
public function report(): bool
{
// TODO: temporary activated
return false;
}
}

View File

@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
class CanNotDeleteUserWhoIsOwnerOfOrganizationWithMultipleMembers extends ApiException
{
public const string KEY = 'can_not_delete_user_who_is_owner_of_organization_with_multiple_members';
public function report(): bool
{
return true;
}
}

View File

@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
class CanNotRemoveOwnerFromOrganization extends ApiException
{
public const string KEY = 'can_not_remove_owner_from_organization';
public function report(): bool
{
return true;
}
}

View File

@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
class ChangingRoleOfPlaceholderIsNotAllowed extends ApiException
{
public const string KEY = 'changing_role_of_placeholder_is_not_allowed';
public function report(): bool
{
return true;
}
}

View File

@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
class ChangingRoleToPlaceholderIsNotAllowed extends ApiException
{
public const string KEY = 'changing_role_to_placeholder_is_not_allowed';
public function report(): bool
{
return true;
}
}

View File

@@ -4,8 +4,6 @@ declare(strict_types=1);
namespace App\Exceptions\Api;
use Override;
class EntityStillInUseApiException extends ApiException
{
private string $modelToDelete;
@@ -21,20 +19,10 @@ class EntityStillInUseApiException extends ApiException
public const string KEY = 'entity_still_in_use';
/**
* Report the exception.
*
* @return bool true means the exception handler will not report it again
*/
public function report(): bool
{
return true;
}
/**
* Get the translated message for the exception.
*/
#[Override]
#[\Override]
public function getTranslatedMessage(): string
{
return __('exceptions.api.'.$this->getKey(), [

View File

@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
class FeatureIsNotAvailableInFreePlanApiException extends ApiException
{
public const string KEY = 'feature_is_not_available_in_free_plan';
public function report(): bool
{
return true;
}
}

View File

@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
class InactiveUserCanNotBeUsedApiException extends ApiException
{
public const string KEY = 'inactive_user_can_not_be_used';
public function report(): bool
{
return true;
}
}

View File

@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
class InvitationForTheEmailAlreadyExistsApiException extends ApiException
{
public const string KEY = 'invitation_for_the_email_already_exists';
public function report(): bool
{
return true;
}
}

View File

@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
class OnlyOwnerCanChangeOwnership extends ApiException
{
public const string KEY = 'only_owner_can_change_ownership';
public function report(): bool
{
return true;
}
}

View File

@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
class OnlyPlaceholdersCanBeMergedIntoAnotherMember extends ApiException
{
public const string KEY = 'only_placeholders_can_be_merged_into_another_member';
public function report(): bool
{
return true;
}
}

View File

@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
class OrganizationHasNoSubscriptionButMultipleMembersException extends ApiException
{
public const string KEY = 'organization_has_no_subscription_but_multiple_members';
public function report(): bool
{
return true;
}
}

View File

@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
class OrganizationNeedsAtLeastOneOwner extends ApiException
{
public const string KEY = 'organization_needs_at_least_one_owner';
public function report(): bool
{
return true;
}
}

View File

@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
class OverlappingTimeEntryApiException extends ApiException
{
public const string KEY = 'overlapping_time_entry';
public function report(): bool
{
return true;
}
}

View File

@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
class ThisPlaceholderCanNotBeInvitedUseTheMergeToolInsteadException extends ApiException
{
public const string KEY = 'this_placeholder_can_not_be_invited_use_the_merge_tool_instead_api_exception';
public function report(): bool
{
return true;
}
}

View File

@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
class TimeEntryCanNotBeRestartedApiException extends ApiException
{
public const string KEY = 'time_entry_can_not_be_restarted';
public function report(): bool
{
return true;
}
}

View File

@@ -7,14 +7,4 @@ namespace App\Exceptions\Api;
class TimeEntryStillRunningApiException extends ApiException
{
public const string KEY = 'time_entry_still_running';
/**
* Report the exception.
*
* @return bool true means the exception handler will not report it again
*/
public function report(): bool
{
return true;
}
}

View File

@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
class UserIsAlreadyMemberOfOrganizationApiException extends ApiException
{
public const string KEY = 'user_is_already_member_of_organization';
public function report(): bool
{
return true;
}
}

View File

@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
class UserIsAlreadyMemberOfProjectApiException extends ApiException
{
public const string KEY = 'user_is_already_member_of_project';
public function report(): bool
{
return true;
}
}

View File

@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
class UserNotPlaceholderApiException extends ApiException
{
public const string KEY = 'user_not_placeholder';
public function report(): bool
{
return true;
}
}

View File

@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
class UserResendEmailVerificationNoPendingEmailApiException extends ApiException
{
public const string KEY = 'user_resend_email_verification_no_pending_email';
public function report(): bool
{
return true;
}
}

View File

@@ -6,11 +6,7 @@ namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use League\OAuth2\Server\Exception\OAuthServerException;
use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Throwable;
class Handler extends ExceptionHandler
@@ -34,33 +30,6 @@ class Handler extends ExceptionHandler
$this->reportable(function (Throwable $e): void {
//
});
$this->dontReportWhen(fn (Throwable $e): bool => $e instanceof OAuthServerException
&& $e->getErrorType() === 'access_denied'
&& $e->getHttpStatusCode() === 401);
// A request on an untrusted host (see App\Http\Middleware\TrustHosts)
// otherwise renders as a bare "Bad request." 400. Show a message that
// says how to fix it instead. The framework has already converted the
// SuspiciousOperationException into a BadRequestHttpException by the time
// renderables run, so we match that and inspect the original.
$this->renderable(function (BadRequestHttpException $e, Request $request): ?Response {
$previous = $e->getPrevious();
if (! $previous instanceof SuspiciousOperationException
|| ! str_starts_with($previous->getMessage(), 'Untrusted Host')) {
return null; // any other bad request keeps the default response
}
$message = 'This hostname is not configured for this instance. '
.'Set APP_URL, or add the host to TRUSTED_HOSTS.';
if ($request->expectsJson()) {
return response()->json(['message' => $message], 400);
}
return response()->view('errors.untrusted-host', ['message' => $message], 400);
});
}
public function render($request, Throwable $e): Response|RedirectResponse

View File

@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources;
use App\Filament\Resources\AuditResource\Pages;
use App\Models\Audit;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Table;
use Illuminate\Support\Str;
use Novadaemon\FilamentPrettyJson\Form\PrettyJsonField;
class AuditResource extends Resource
{
protected static ?string $model = Audit::class;
protected static ?string $navigationIcon = 'heroicon-o-archive-box';
protected static ?string $navigationGroup = 'System';
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('user_type')
->maxLength(255),
Forms\Components\TextInput::make('user_id'),
Forms\Components\TextInput::make('event')
->required()
->maxLength(255),
Forms\Components\TextInput::make('auditable_type')
->required()
->maxLength(255),
Forms\Components\TextInput::make('auditable_id')
->required(),
PrettyJsonField::make('old_values'),
PrettyJsonField::make('new_values'),
Forms\Components\Textarea::make('url'),
Forms\Components\TextInput::make('ip_address'),
Forms\Components\TextInput::make('user_agent')
->maxLength(1023),
Forms\Components\TextInput::make('tags')
->maxLength(255),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('user.name'),
Tables\Columns\TextColumn::make('event'),
Tables\Columns\TextColumn::make('auditable_type'),
Tables\Columns\TextColumn::make('auditable_id'),
IconColumn::make('was_command')
->getStateUsing(fn (Audit $record) => Str::startsWith($record->url, 'artisan '))
->boolean(),
Tables\Columns\TextColumn::make('created_at')
->sortable()
->dateTime(),
Tables\Columns\TextColumn::make('updated_at')
->sortable()
->dateTime(),
])
->filters([
//
])
->actions([
Tables\Actions\ViewAction::make(),
])
->bulkActions([
])
->defaultSort('created_at', 'desc');
}
public static function getRelations(): array
{
return [
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListAudits::route('/'),
'create' => Pages\CreateAudit::route('/create'),
'view' => Pages\ViewAudit::route('/{record}'),
];
}
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\AuditResource\Pages;
use App\Filament\Resources\AuditResource;
use Filament\Resources\Pages\CreateRecord;
class CreateAudit extends CreateRecord
{
protected static string $resource = AuditResource::class;
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\AuditResource\Pages;
use App\Filament\Resources\AuditResource;
use Filament\Resources\Pages\ListRecords;
class ListAudits extends ListRecords
{
protected static string $resource = AuditResource::class;
protected function getHeaderActions(): array
{
return [];
}
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\AuditResource\Pages;
use App\Filament\Resources\AuditResource;
use Filament\Resources\Pages\ViewRecord;
class ViewAudit extends ViewRecord
{
protected static string $resource = AuditResource::class;
}

View File

@@ -4,18 +4,13 @@ declare(strict_types=1);
namespace App\Filament\Resources;
use App\Filament\Resources\ClientResource\Pages\CreateClient;
use App\Filament\Resources\ClientResource\Pages\EditClient;
use App\Filament\Resources\ClientResource\Pages\ListClients;
use App\Filament\Resources\ClientResource\Pages;
use App\Models\Client;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
@@ -23,16 +18,16 @@ class ClientResource extends Resource
{
protected static ?string $model = Client::class;
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-briefcase';
protected static ?string $navigationIcon = 'heroicon-o-briefcase';
protected static string|\UnitEnum|null $navigationGroup = 'Timetracking';
protected static ?string $navigationGroup = 'Timetracking';
protected static ?int $navigationSort = 4;
public static function form(Schema $schema): Schema
public static function form(Form $form): Form
{
return $schema
->components([
return $form
->schema([
TextInput::make('name')
->label('Name')
->required(),
@@ -48,17 +43,17 @@ class ClientResource extends Resource
{
return $table
->columns([
TextColumn::make('name')
Tables\Columns\TextColumn::make('name')
->label('Name')
->searchable()
->sortable(),
TextColumn::make('organization.name')
Tables\Columns\TextColumn::make('organization.name')
->sortable()
->label('Organization'),
TextColumn::make('created_at')
Tables\Columns\TextColumn::make('created_at')
->label('Created at')
->sortable(),
TextColumn::make('updated_at')
Tables\Columns\TextColumn::make('updated_at')
->label('Updated at')
->sortable(),
])
@@ -73,12 +68,12 @@ class ClientResource extends Resource
->relationship('organization', 'id')
->searchable(),
])
->recordActions([
EditAction::make(),
->actions([
Tables\Actions\EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
@@ -93,9 +88,9 @@ class ClientResource extends Resource
public static function getPages(): array
{
return [
'index' => ListClients::route('/'),
'create' => CreateClient::route('/create'),
'edit' => EditClient::route('/{record}/edit'),
'index' => Pages\ListClients::route('/'),
'create' => Pages\CreateClient::route('/create'),
'edit' => Pages\EditClient::route('/{record}/edit'),
];
}
}

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Filament\Resources\ClientResource\Pages;
use App\Filament\Resources\ClientResource;
use Filament\Actions\DeleteAction;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditClient extends EditRecord
@@ -15,7 +15,7 @@ class EditClient extends EditRecord
protected function getHeaderActions(): array
{
return [
DeleteAction::make()
Actions\DeleteAction::make()
->icon('heroicon-m-trash'),
];
}

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Filament\Resources\ClientResource\Pages;
use App\Filament\Resources\ClientResource;
use Filament\Actions\CreateAction;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListClients extends ListRecords
@@ -15,7 +15,7 @@ class ListClients extends ListRecords
protected function getHeaderActions(): array
{
return [
CreateAction::make()
Actions\CreateAction::make()
->icon('heroicon-s-plus'),
];
}

View File

@@ -7,16 +7,16 @@ namespace App\Filament\Resources;
use App\Filament\Resources\FailedJobResource\Pages\ListFailedJobs;
use App\Filament\Resources\FailedJobResource\Pages\ViewFailedJobs;
use App\Models\FailedJob;
use Filament\Actions\Action;
use Filament\Actions\BulkAction;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\ViewAction;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables\Actions\Action;
use Filament\Tables\Actions\BulkAction;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Actions\DeleteBulkAction;
use Filament\Tables\Actions\ViewAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Support\Collection;
@@ -30,19 +30,19 @@ class FailedJobResource extends Resource
{
protected static ?string $model = FailedJob::class;
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-exclamation-circle';
protected static ?string $navigationIcon = 'heroicon-o-exclamation-circle';
protected static string|\UnitEnum|null $navigationGroup = 'System';
protected static ?string $navigationGroup = 'System';
public static function getNavigationBadge(): ?string
{
return (string) FailedJob::query()->count();
}
public static function form(Schema $schema): Schema
public static function form(Form $form): Form
{
return $schema
->components([
return $form
->schema([
TextInput::make('uuid')->disabled()->columnSpan(4),
TextInput::make('failed_at')->disabled(),
TextInput::make('id')->disabled(),
@@ -74,7 +74,7 @@ class FailedJobResource extends Resource
TextColumn::make('queue')->sortable()->searchable()->toggleable(isToggledHiddenByDefault: true),
])
->filters([])
->toolbarActions([
->bulkActions([
BulkAction::make('retry')
->icon('heroicon-o-arrow-path')
->label('Retry selected')
@@ -91,7 +91,7 @@ class FailedJobResource extends Resource
}),
DeleteBulkAction::make(),
])
->recordActions([
->actions([
DeleteAction::make(),
ViewAction::make(),
Action::make('retry')

View File

@@ -5,21 +5,14 @@ declare(strict_types=1);
namespace App\Filament\Resources;
use App\Enums\Role;
use App\Filament\Resources\OrganizationInvitationResource\Pages\EditOrganizationInvitation;
use App\Filament\Resources\OrganizationInvitationResource\Pages\ListOrganizationInvitations;
use App\Filament\Resources\OrganizationInvitationResource\Pages\ViewOrganizationInvitation;
use App\Filament\Resources\OrganizationInvitationResource\Pages;
use App\Models\OrganizationInvitation;
use App\Service\OrganizationInvitationService;
use Filament\Actions\BulkAction;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Support\Collection;
@@ -29,18 +22,18 @@ class OrganizationInvitationResource extends Resource
protected static ?string $label = 'Invitations';
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-user-plus';
protected static ?string $navigationIcon = 'heroicon-o-user-plus';
protected static string|\UnitEnum|null $navigationGroup = 'Users';
protected static ?string $navigationGroup = 'Users';
protected static ?int $navigationSort = 9;
public static function form(Schema $schema): Schema
public static function form(Form $form): Form
{
return $schema
return $form
->columns(1)
->components([
TextInput::make('email')
->schema([
Forms\Components\TextInput::make('email')
->label('Email')
->disabledOn(['edit'])
->required(),
@@ -52,11 +45,11 @@ class OrganizationInvitationResource extends Resource
->searchable(['name'])
->disabledOn(['edit'])
->required(),
DateTimePicker::make('created_at')
Forms\Components\DateTimePicker::make('created_at')
->label('Created At')
->hiddenOn(['create'])
->disabled(),
DateTimePicker::make('updated_at')
Forms\Components\DateTimePicker::make('updated_at')
->label('Updated At')
->hiddenOn(['create'])
->disabled(),
@@ -67,17 +60,17 @@ class OrganizationInvitationResource extends Resource
{
return $table
->columns([
TextColumn::make('organization.name')
Tables\Columns\TextColumn::make('organization.name')
->searchable()
->sortable(),
TextColumn::make('email')
Tables\Columns\TextColumn::make('email')
->sortable(),
TextColumn::make('role'),
TextColumn::make('created_at')
Tables\Columns\TextColumn::make('role'),
Tables\Columns\TextColumn::make('created_at')
->label('Created At')
->dateTime()
->sortable(),
TextColumn::make('updated_at')
Tables\Columns\TextColumn::make('updated_at')
->label('Updated At')
->dateTime()
->sortable()
@@ -87,13 +80,13 @@ class OrganizationInvitationResource extends Resource
->filters([
//
])
->recordActions([
EditAction::make(),
DeleteAction::make(),
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
BulkAction::make('resend')
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\BulkAction::make('resend')
->label('Resend')
->action(function (Collection $records): void {
foreach ($records as $organizationInvite) {
@@ -113,9 +106,9 @@ class OrganizationInvitationResource extends Resource
public static function getPages(): array
{
return [
'index' => ListOrganizationInvitations::route('/'),
'edit' => EditOrganizationInvitation::route('/{record}/edit'),
'view' => ViewOrganizationInvitation::route('/{record}'),
'index' => Pages\ListOrganizationInvitations::route('/'),
'edit' => Pages\EditOrganizationInvitation::route('/{record}/edit'),
'view' => Pages\ViewOrganizationInvitation::route('/{record}'),
];
}
}

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Filament\Resources\OrganizationInvitationResource\Pages;
use App\Filament\Resources\OrganizationInvitationResource;
use Filament\Actions\DeleteAction;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditOrganizationInvitation extends EditRecord
@@ -15,7 +15,7 @@ class EditOrganizationInvitation extends EditRecord
protected function getHeaderActions(): array
{
return [
DeleteAction::make()
Actions\DeleteAction::make()
->icon('heroicon-m-trash'),
];
}

View File

@@ -9,10 +9,7 @@ use App\Enums\DateFormat;
use App\Enums\IntervalFormat;
use App\Enums\NumberFormat;
use App\Enums\TimeFormat;
use App\Filament\Resources\OrganizationResource\Pages\CreateOrganization;
use App\Filament\Resources\OrganizationResource\Pages\EditOrganization;
use App\Filament\Resources\OrganizationResource\Pages\ListOrganizations;
use App\Filament\Resources\OrganizationResource\Pages\ViewOrganization;
use App\Filament\Resources\OrganizationResource\Pages;
use App\Filament\Resources\OrganizationResource\RelationManagers\InvitationsRelationManager;
use App\Filament\Resources\OrganizationResource\RelationManagers\UsersRelationManager;
use App\Models\Organization;
@@ -23,45 +20,38 @@ use App\Service\Import\Importers\ImportException;
use App\Service\Import\Importers\ReportDto;
use App\Service\Import\ImportService;
use App\Service\TimezoneService;
use Brick\Money\IsoCurrencyProvider;
use Exception;
use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\FileUpload;
use Brick\Money\ISOCurrencyProvider;
use Filament\Forms;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables;
use Filament\Tables\Actions\Action;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Support\Facades\Storage;
use League\Flysystem\UnableToReadFile;
class OrganizationResource extends Resource
{
protected static ?string $model = Organization::class;
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-building-office-2';
protected static ?string $navigationIcon = 'heroicon-o-building-office-2';
protected static string|\UnitEnum|null $navigationGroup = 'Users';
protected static ?string $navigationGroup = 'Users';
protected static ?int $navigationSort = 7;
public static function form(Schema $schema): Schema
public static function form(Form $form): Form
{
return $schema
return $form
->columns(1)
->components([
TextInput::make('name')
->schema([
Forms\Components\TextInput::make('name')
->label('Name')
->required()
->maxLength(255),
Toggle::make('personal_team')
Forms\Components\Toggle::make('personal_team')
->label('Is personal?')
->hiddenOn(['create'])
->required(),
@@ -89,7 +79,7 @@ class OrganizationResource extends Resource
Select::make('currency')
->label('Currency')
->options(function (): array {
$currencies = IsoCurrencyProvider::getInstance()->getAvailableCurrencies();
$currencies = ISOCurrencyProvider::getInstance()->getAvailableCurrencies();
$select = [];
foreach ($currencies as $currency) {
$select[$currency->getCurrencyCode()] = $currency->getName().' ('.$currency->getCurrencyCode().')';
@@ -99,7 +89,7 @@ class OrganizationResource extends Resource
})
->required()
->searchable(),
TextInput::make('billable_rate')
Forms\Components\TextInput::make('billable_rate')
->label('Billable rate (in Cents)')
->nullable()
->rules([
@@ -109,11 +99,11 @@ class OrganizationResource extends Resource
'max:2147483647',
])
->numeric(),
DateTimePicker::make('created_at')
Forms\Components\DateTimePicker::make('created_at')
->label('Created At')
->hiddenOn(['create'])
->disabled(),
DateTimePicker::make('updated_at')
Forms\Components\DateTimePicker::make('updated_at')
->label('Updated At')
->hiddenOn(['create'])
->disabled(),
@@ -127,7 +117,7 @@ class OrganizationResource extends Resource
TextColumn::make('name')
->searchable()
->sortable(),
IconColumn::make('personal_team')
Tables\Columns\IconColumn::make('personal_team')
->boolean()
->label('Is personal?')
->sortable(),
@@ -148,9 +138,9 @@ class OrganizationResource extends Resource
->filters([
//
])
->recordActions([
EditAction::make(),
DeleteAction::make()
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make()
->using(function (Organization $record): void {
app(DeletionService::class)->deleteOrganization($record);
}),
@@ -168,7 +158,7 @@ class OrganizationResource extends Resource
return response()->streamDownload(function () use ($file): void {
echo Storage::disk(config('filesystems.private'))->get($file);
}, 'export.zip');
} catch (Exception $exception) {
} catch (\Exception $exception) {
report($exception);
Notification::make()
->title('Export failed')
@@ -182,13 +172,9 @@ class OrganizationResource extends Resource
->icon('heroicon-o-inbox-arrow-down')
->action(function (Organization $record, array $data): void {
try {
try {
$file = Storage::disk(config('filament.default_filesystem_disk'))->get($data['file']);
} catch (UnableToReadFile) {
$file = null;
}
$file = Storage::disk(config('filament.default_filesystem_disk'))->get($data['file']);
if ($file === null) {
throw new ImportException('File not found');
throw new \Exception('File not found');
}
/** @var string $timezone */
$timezone = $data['timezone'];
@@ -223,8 +209,8 @@ class OrganizationResource extends Resource
}
})
->tooltip(fn (Organization $record): string => 'Import into '.$record->name)
->schema([
FileUpload::make('file')
->form([
Forms\Components\FileUpload::make('file')
->label('File')
->required(),
Select::make('type')
@@ -244,7 +230,7 @@ class OrganizationResource extends Resource
->required(),
]),
])
->toolbarActions([
->bulkActions([
]);
}
@@ -259,10 +245,10 @@ class OrganizationResource extends Resource
public static function getPages(): array
{
return [
'index' => ListOrganizations::route('/'),
'create' => CreateOrganization::route('/create'),
'edit' => EditOrganization::route('/{record}/edit'),
'view' => ViewOrganization::route('/{record}'),
'index' => Pages\ListOrganizations::route('/'),
'create' => Pages\CreateOrganization::route('/create'),
'edit' => Pages\EditOrganization::route('/{record}/edit'),
'view' => Pages\ViewOrganization::route('/{record}'),
];
}
}

View File

@@ -5,7 +5,6 @@ declare(strict_types=1);
namespace App\Filament\Resources\OrganizationResource\Pages;
use App\Filament\Resources\OrganizationResource;
use App\Filament\Resources\OrganizationResource\Actions\DeleteOrganization;
use Filament\Resources\Pages\EditRecord;
class EditOrganization extends EditRecord
@@ -15,7 +14,7 @@ class EditOrganization extends EditRecord
protected function getHeaderActions(): array
{
return [
DeleteOrganization::make(),
OrganizationResource\Actions\DeleteOrganization::make(),
];
}
}

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Filament\Resources\OrganizationResource\Pages;
use App\Filament\Resources\OrganizationResource;
use Filament\Actions\CreateAction;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListOrganizations extends ListRecords
@@ -15,7 +15,7 @@ class ListOrganizations extends ListRecords
protected function getHeaderActions(): array
{
return [
CreateAction::make()
Actions\CreateAction::make()
->icon('heroicon-s-plus'),
];
}

View File

@@ -9,17 +9,12 @@ use App\Filament\Resources\OrganizationInvitationResource;
use App\Models\Organization;
use App\Models\OrganizationInvitation;
use App\Service\InvitationService;
use Filament\Actions\Action;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteAction;
use Filament\Actions\DetachBulkAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables;
use Filament\Tables\Actions\Action;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Validation\Rule;
@@ -30,10 +25,10 @@ class InvitationsRelationManager extends RelationManager
protected static ?string $title = 'Invitations';
public function form(Schema $schema): Schema
public function form(Form $form): Form
{
return $schema
->components([
return $form
->schema([
TextInput::make('email')
->label('Email')
->disabledOn(['edit'])
@@ -58,33 +53,33 @@ class InvitationsRelationManager extends RelationManager
->modelLabel('Invitation')
->pluralModelLabel('Invitations')
->columns([
TextColumn::make('email'),
TextColumn::make('role'),
Tables\Columns\TextColumn::make('email'),
Tables\Columns\TextColumn::make('role'),
])
->headerActions([
CreateAction::make()
Tables\Actions\CreateAction::make()
->icon('heroicon-s-plus')
->using(function (array $data, string $model): Model {
/** @var Organization $ownerRecord */
$ownerRecord = $this->getOwnerRecord();
return app(InvitationService::class)
->inviteUser($ownerRecord, $data['email'], ($data['role'] instanceof Role ? $data['role'] : Role::from($data['role'])), auth()->user());
->inviteUser($ownerRecord, $data['email'], Role::from($data['role']), auth()->user());
}),
])
->recordActions([
->actions([
Action::make('view')
->icon('heroicon-o-eye')
->color('gray')
->url(fn (OrganizationInvitation $record): string => OrganizationInvitationResource::getUrl('view', [
'record' => $record->getKey(),
])),
EditAction::make(),
DeleteAction::make(),
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DetachBulkAction::make(),
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DetachBulkAction::make(),
]),
]);
}

View File

@@ -12,15 +12,14 @@ use App\Models\Organization;
use App\Models\User;
use App\Service\BillableRateService;
use App\Service\MemberService;
use Filament\Actions\Action;
use Filament\Actions\AttachAction;
use Filament\Actions\DetachAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Schemas\Schema;
use Filament\Tables;
use Filament\Tables\Actions\Action;
use Filament\Tables\Actions\AttachAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Validation\Rule;
@@ -29,10 +28,10 @@ class UsersRelationManager extends RelationManager
{
protected static string $relationship = 'users';
public function form(Schema $schema): Schema
public function form(Form $form): Form
{
return $schema
->components([
return $form
->schema([
Select::make('role')
->options(Role::class),
TextInput::make('billable_rate')
@@ -76,17 +75,17 @@ class UsersRelationManager extends RelationManager
->using(function (User $record, array $data): void {
/** @var Organization $organization */
$organization = $this->getOwnerRecord();
app(MemberService::class)->addMember($record, $organization, ($data['role'] instanceof Role ? $data['role'] : Role::from($data['role'])), true);
app(MemberService::class)->addMember($record, $organization, Role::from($data['role']), true);
}),
])
->recordActions([
->actions([
Action::make('view')
->icon('heroicon-o-eye')
->color('gray')
->url(fn (User $record): string => UserResource::getUrl('view', [
'record' => $record->getKey(),
])),
EditAction::make()
Tables\Actions\EditAction::make()
->using(function (User $record, array $data): User {
/** @var Organization $organization */
$organization = $this->getOwnerRecord();
@@ -98,10 +97,9 @@ class UsersRelationManager extends RelationManager
app(BillableRateService::class)->updateTimeEntriesBillableRateForMember($member);
}
$newRole = $data['role'] instanceof Role ? $data['role'] : Role::from($data['role']);
if ($newRole->value !== $member->role) {
if ($data['role'] !== $member->role) {
try {
app(MemberService::class)->changeRole($member, $organization, $newRole, true);
app(MemberService::class)->changeRole($member, $organization, Role::from($data['role']), true);
} catch (ApiException $exception) {
Notification::make()
->danger()
@@ -115,7 +113,7 @@ class UsersRelationManager extends RelationManager
return $record;
}),
DetachAction::make()
Tables\Actions\DetachAction::make()
->using(function (User $record): void {
/** @var Organization $organization */
$organization = $this->getOwnerRecord();
@@ -135,7 +133,7 @@ class UsersRelationManager extends RelationManager
}
}),
])
->toolbarActions([
->bulkActions([
]);
}
}

View File

@@ -4,19 +4,12 @@ declare(strict_types=1);
namespace App\Filament\Resources;
use App\Filament\Resources\ProjectMemberResource\Pages\CreateProjectMember;
use App\Filament\Resources\ProjectMemberResource\Pages\EditProjectMember;
use App\Filament\Resources\ProjectMemberResource\Pages\ListProjectMembers;
use App\Filament\Resources\ProjectMemberResource\Pages\ViewProjectMembers;
use App\Filament\Resources\ProjectMemberResource\Pages;
use App\Models\ProjectMember;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables;
use Filament\Tables\Table;
class ProjectMemberResource extends Resource
@@ -25,11 +18,11 @@ class ProjectMemberResource extends Resource
protected static bool $shouldRegisterNavigation = false;
public static function form(Schema $schema): Schema
public static function form(Form $form): Form
{
return $schema
->components([
TextInput::make('billable_rate')
return $form
->schema([
Forms\Components\TextInput::make('billable_rate')
->label('Billable rate (in Cents)')
->nullable()
->rules([
@@ -39,10 +32,10 @@ class ProjectMemberResource extends Resource
'max:2147483647',
])
->numeric(),
Select::make('user_id')
Forms\Components\Select::make('user_id')
->relationship('user', 'name')
->required(),
Select::make('member_id')
Forms\Components\Select::make('member_id')
->relationship('member', 'id')
->required(),
]);
@@ -52,17 +45,17 @@ class ProjectMemberResource extends Resource
{
return $table
->columns([
TextColumn::make('id')
Tables\Columns\TextColumn::make('id')
->label('ID'),
TextColumn::make('billable_rate')
Tables\Columns\TextColumn::make('billable_rate')
->numeric()
->sortable(),
TextColumn::make('project.name'),
TextColumn::make('user.name'),
TextColumn::make('created_at')
Tables\Columns\TextColumn::make('project.name'),
Tables\Columns\TextColumn::make('user.name'),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable(),
TextColumn::make('updated_at')
Tables\Columns\TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
@@ -70,12 +63,12 @@ class ProjectMemberResource extends Resource
->filters([
//
])
->recordActions([
EditAction::make(),
->actions([
Tables\Actions\EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
@@ -90,10 +83,10 @@ class ProjectMemberResource extends Resource
public static function getPages(): array
{
return [
'index' => ListProjectMembers::route('/'),
'create' => CreateProjectMember::route('/create'),
'edit' => EditProjectMember::route('/{record}/edit'),
'view' => ViewProjectMembers::route('/{record}'),
'index' => Pages\ListProjectMembers::route('/'),
'create' => Pages\CreateProjectMember::route('/create'),
'edit' => Pages\EditProjectMember::route('/{record}/edit'),
'view' => Pages\ViewProjectMembers::route('/{record}'),
];
}
}

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Filament\Resources\ProjectMemberResource\Pages;
use App\Filament\Resources\ProjectMemberResource;
use Filament\Actions\DeleteAction;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditProjectMember extends EditRecord
@@ -15,7 +15,7 @@ class EditProjectMember extends EditRecord
protected function getHeaderActions(): array
{
return [
DeleteAction::make()
Actions\DeleteAction::make()
->icon('heroicon-m-trash'),
];
}

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Filament\Resources\ProjectMemberResource\Pages;
use App\Filament\Resources\ProjectMemberResource;
use Filament\Actions\CreateAction;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListProjectMembers extends ListRecords
@@ -15,7 +15,7 @@ class ListProjectMembers extends ListRecords
protected function getHeaderActions(): array
{
return [
CreateAction::make()
Actions\CreateAction::make()
->icon('heroicon-s-plus'),
];
}

View File

@@ -4,19 +4,14 @@ declare(strict_types=1);
namespace App\Filament\Resources;
use App\Filament\Resources\ProjectResource\Pages\CreateProject;
use App\Filament\Resources\ProjectResource\Pages\EditProject;
use App\Filament\Resources\ProjectResource\Pages\ListProjects;
use App\Filament\Resources\ProjectResource\Pages;
use App\Filament\Resources\ProjectResource\RelationManagers\ProjectMembersRelationManager;
use App\Models\Project;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Forms;
use Filament\Forms\Components\ColorPicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables;
use Filament\Tables\Columns\ColorColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
@@ -26,24 +21,24 @@ class ProjectResource extends Resource
{
protected static ?string $model = Project::class;
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-folder';
protected static ?string $navigationIcon = 'heroicon-o-folder';
protected static string|\UnitEnum|null $navigationGroup = 'Timetracking';
protected static ?string $navigationGroup = 'Timetracking';
protected static ?int $navigationSort = 2;
public static function form(Schema $schema): Schema
public static function form(Form $form): Form
{
return $schema
->components([
TextInput::make('name')
return $form
->schema([
Forms\Components\TextInput::make('name')
->label('Name')
->required()
->maxLength(255),
ColorPicker::make('color')
->label('Color')
->required(),
TextInput::make('billable_rate')
Forms\Components\TextInput::make('billable_rate')
->label('Billable rate (in Cents)')
->nullable()
->rules([
@@ -53,7 +48,7 @@ class ProjectResource extends Resource
'max:2147483647',
])
->numeric(),
Select::make('organization_id')
Forms\Components\Select::make('organization_id')
->relationship(name: 'organization', titleAttribute: 'name')
->searchable(['name'])
->required(),
@@ -86,12 +81,12 @@ class ProjectResource extends Resource
->searchable(),
])
->defaultSort('created_at', 'desc')
->recordActions([
EditAction::make(),
->actions([
Tables\Actions\EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
@@ -106,9 +101,9 @@ class ProjectResource extends Resource
public static function getPages(): array
{
return [
'index' => ListProjects::route('/'),
'create' => CreateProject::route('/create'),
'edit' => EditProject::route('/{record}/edit'),
'index' => Pages\ListProjects::route('/'),
'create' => Pages\CreateProject::route('/create'),
'edit' => Pages\EditProject::route('/{record}/edit'),
];
}
}

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Filament\Resources\ProjectResource\Pages;
use App\Filament\Resources\ProjectResource;
use Filament\Actions\DeleteAction;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditProject extends EditRecord
@@ -15,7 +15,7 @@ class EditProject extends EditRecord
protected function getHeaderActions(): array
{
return [
DeleteAction::make()
Actions\DeleteAction::make()
->icon('heroicon-m-trash'),
];
}

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Filament\Resources\ProjectResource\Pages;
use App\Filament\Resources\ProjectResource;
use Filament\Actions\CreateAction;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListProjects extends ListRecords
@@ -15,7 +15,7 @@ class ListProjects extends ListRecords
protected function getHeaderActions(): array
{
return [
CreateAction::make()
Actions\CreateAction::make()
->icon('heroicon-s-plus'),
];
}

View File

@@ -6,10 +6,10 @@ namespace App\Filament\Resources\ProjectResource\RelationManagers;
use App\Filament\Resources\ProjectMemberResource;
use App\Models\ProjectMember;
use Filament\Actions\Action;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables;
use Filament\Tables\Actions\Action;
use Filament\Tables\Table;
class ProjectMembersRelationManager extends RelationManager
@@ -18,10 +18,10 @@ class ProjectMembersRelationManager extends RelationManager
protected static string $relationship = 'members';
public function form(Schema $schema): Schema
public function form(Form $form): Form
{
return $schema
->components([
return $form
->schema([
]);
}
@@ -30,8 +30,8 @@ class ProjectMembersRelationManager extends RelationManager
return $table
->recordTitleAttribute('name')
->columns([
TextColumn::make('user.name'),
TextColumn::make('billable_rate')
Tables\Columns\TextColumn::make('user.name'),
Tables\Columns\TextColumn::make('billable_rate')
->numeric()
->sortable(),
])
@@ -40,7 +40,7 @@ class ProjectMembersRelationManager extends RelationManager
])
->headerActions([
])
->recordActions([
->actions([
Action::make('view')
->icon('heroicon-o-eye')
->color('gray')
@@ -54,7 +54,7 @@ class ProjectMembersRelationManager extends RelationManager
]))
->openUrlInNewTab(),
])
->toolbarActions([
->bulkActions([
]);
}
}

View File

@@ -4,21 +4,16 @@ declare(strict_types=1);
namespace App\Filament\Resources;
use App\Filament\Resources\ReportResource\Pages\EditReport;
use App\Filament\Resources\ReportResource\Pages\ListReports;
use App\Filament\Resources\ReportResource\Pages\ViewReport;
use App\Filament\Resources\ReportResource\Pages;
use App\Models\Report;
use App\Service\Dto\ReportPropertiesDto;
use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Actions\ViewAction;
use Filament\Forms;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables;
use Filament\Tables\Actions\Action;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\ToggleColumn;
use Filament\Tables\Filters\SelectFilter;
@@ -29,22 +24,22 @@ class ReportResource extends Resource
{
protected static ?string $model = Report::class;
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-document-chart-bar';
protected static ?string $navigationIcon = 'heroicon-o-document-chart-bar';
protected static string|\UnitEnum|null $navigationGroup = 'Timetracking';
protected static ?string $navigationGroup = 'Timetracking';
protected static ?int $navigationSort = 7;
public static function form(Schema $schema): Schema
public static function form(Form $form): Form
{
return $schema
return $form
->columns(1)
->components([
TextInput::make('name')
->schema([
Forms\Components\TextInput::make('name')
->label('Name')
->required()
->maxLength(255),
TextInput::make('description')
Forms\Components\TextInput::make('description')
->label('Description')
->nullable()
->maxLength(255),
@@ -54,13 +49,13 @@ class ReportResource extends Resource
DateTimePicker::make('public_until')
->label('Public until')
->nullable(),
Select::make('organization_id')
Forms\Components\Select::make('organization_id')
->label('Organization')
->relationship(name: 'organization', titleAttribute: 'name')
->searchable(['name'])
->disabled()
->required(),
TextInput::make('share_secret')
Forms\Components\TextInput::make('share_secret')
->label('Share Secret')
->nullable(),
PrettyJsonField::make('properties')
@@ -114,18 +109,18 @@ class ReportResource extends Resource
->relationship('organization', 'id')
->searchable(),
])
->recordActions([
->actions([
Action::make('public-view')
->label('Public')
->icon('heroicon-o-eye')
->color('gray')
->hidden(fn (Report $record): bool => $record->getShareableLink() === null)
->url(fn (Report $record): string => $record->getShareableLink(), true),
ViewAction::make(),
EditAction::make(),
DeleteAction::make(),
Tables\Actions\ViewAction::make(),
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->toolbarActions([
->bulkActions([
]);
}
@@ -138,9 +133,9 @@ class ReportResource extends Resource
public static function getPages(): array
{
return [
'index' => ListReports::route('/'),
'edit' => EditReport::route('/{record}/edit'),
'view' => ViewReport::route('/{record}'),
'index' => Pages\ListReports::route('/'),
'edit' => Pages\EditReport::route('/{record}/edit'),
'view' => Pages\ViewReport::route('/{record}'),
];
}
}

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Filament\Resources\ReportResource\Pages;
use App\Filament\Resources\ReportResource;
use Filament\Actions\DeleteAction;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditReport extends EditRecord
@@ -15,7 +15,7 @@ class EditReport extends EditRecord
protected function getHeaderActions(): array
{
return [
DeleteAction::make()
Actions\DeleteAction::make()
->icon('heroicon-m-trash'),
];
}

View File

@@ -4,18 +4,13 @@ declare(strict_types=1);
namespace App\Filament\Resources;
use App\Filament\Resources\TagResource\Pages\CreateTag;
use App\Filament\Resources\TagResource\Pages\EditTag;
use App\Filament\Resources\TagResource\Pages\ListTags;
use App\Filament\Resources\TagResource\Pages;
use App\Models\Tag;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\Select;
use Filament\Forms;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
@@ -23,20 +18,20 @@ class TagResource extends Resource
{
protected static ?string $model = Tag::class;
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-tag';
protected static ?string $navigationIcon = 'heroicon-o-tag';
protected static string|\UnitEnum|null $navigationGroup = 'Timetracking';
protected static ?string $navigationGroup = 'Timetracking';
protected static ?int $navigationSort = 5;
public static function form(Schema $schema): Schema
public static function form(Form $form): Form
{
return $schema
->components([
return $form
->schema([
TextInput::make('name')
->label('Name')
->required(),
Select::make('organization_id')
Forms\Components\Select::make('organization_id')
->relationship(name: 'organization', titleAttribute: 'name')
->label('Organization')
->searchable(['name'])
@@ -48,17 +43,17 @@ class TagResource extends Resource
{
return $table
->columns([
TextColumn::make('name')
Tables\Columns\TextColumn::make('name')
->label('Name')
->searchable()
->sortable(),
TextColumn::make('organization.name')
Tables\Columns\TextColumn::make('organization.name')
->sortable()
->label('Organization'),
TextColumn::make('created_at')
Tables\Columns\TextColumn::make('created_at')
->label('Created at')
->sortable(),
TextColumn::make('updated_at')
Tables\Columns\TextColumn::make('updated_at')
->label('Updated at')
->sortable(),
])
@@ -73,12 +68,12 @@ class TagResource extends Resource
->relationship('organization', 'id')
->searchable(),
])
->recordActions([
EditAction::make(),
->actions([
Tables\Actions\EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
@@ -93,9 +88,9 @@ class TagResource extends Resource
public static function getPages(): array
{
return [
'index' => ListTags::route('/'),
'create' => CreateTag::route('/create'),
'edit' => EditTag::route('/{record}/edit'),
'index' => Pages\ListTags::route('/'),
'create' => Pages\CreateTag::route('/create'),
'edit' => Pages\EditTag::route('/{record}/edit'),
];
}
}

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Filament\Resources\TagResource\Pages;
use App\Filament\Resources\TagResource;
use Filament\Actions\DeleteAction;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditTag extends EditRecord
@@ -15,7 +15,7 @@ class EditTag extends EditRecord
protected function getHeaderActions(): array
{
return [
DeleteAction::make()
Actions\DeleteAction::make()
->icon('heroicon-m-trash'),
];
}

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Filament\Resources\TagResource\Pages;
use App\Filament\Resources\TagResource;
use Filament\Actions\CreateAction;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListTags extends ListRecords
@@ -15,7 +15,7 @@ class ListTags extends ListRecords
protected function getHeaderActions(): array
{
return [
CreateAction::make()
Actions\CreateAction::make()
->icon('heroicon-s-plus'),
];
}

View File

@@ -4,18 +4,13 @@ declare(strict_types=1);
namespace App\Filament\Resources;
use App\Filament\Resources\TaskResource\Pages\CreateTask;
use App\Filament\Resources\TaskResource\Pages\EditTask;
use App\Filament\Resources\TaskResource\Pages\ListTasks;
use App\Filament\Resources\TaskResource\Pages;
use App\Models\Task;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Forms;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
@@ -23,17 +18,17 @@ class TaskResource extends Resource
{
protected static ?string $model = Task::class;
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-list-bullet';
protected static ?string $navigationIcon = 'heroicon-o-list-bullet';
protected static string|\UnitEnum|null $navigationGroup = 'Timetracking';
protected static ?string $navigationGroup = 'Timetracking';
protected static ?int $navigationSort = 3;
public static function form(Schema $schema): Schema
public static function form(Form $form): Form
{
return $schema
->components([
TextInput::make('name')
return $form
->schema([
Forms\Components\TextInput::make('name')
->label('Name')
->required()
->maxLength(255),
@@ -52,16 +47,16 @@ class TaskResource extends Resource
{
return $table
->columns([
TextColumn::make('name')
Tables\Columns\TextColumn::make('name')
->searchable()
->sortable(),
TextColumn::make('project.name')
Tables\Columns\TextColumn::make('project.name')
->sortable(),
TextColumn::make('organization.name')
Tables\Columns\TextColumn::make('organization.name')
->sortable(),
TextColumn::make('created_at')
Tables\Columns\TextColumn::make('created_at')
->sortable(),
TextColumn::make('updated_at')
Tables\Columns\TextColumn::make('updated_at')
->sortable(),
])
->filters([
@@ -75,12 +70,12 @@ class TaskResource extends Resource
->searchable(),
])
->defaultSort('created_at', 'desc')
->recordActions([
EditAction::make(),
->actions([
Tables\Actions\EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
@@ -95,9 +90,9 @@ class TaskResource extends Resource
public static function getPages(): array
{
return [
'index' => ListTasks::route('/'),
'create' => CreateTask::route('/create'),
'edit' => EditTask::route('/{record}/edit'),
'index' => Pages\ListTasks::route('/'),
'create' => Pages\CreateTask::route('/create'),
'edit' => Pages\EditTask::route('/{record}/edit'),
];
}
}

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Filament\Resources\TaskResource\Pages;
use App\Filament\Resources\TaskResource;
use Filament\Actions\DeleteAction;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditTask extends EditRecord
@@ -15,7 +15,7 @@ class EditTask extends EditRecord
protected function getHeaderActions(): array
{
return [
DeleteAction::make()
Actions\DeleteAction::make()
->icon('heroicon-m-trash'),
];
}

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Filament\Resources\TaskResource\Pages;
use App\Filament\Resources\TaskResource;
use Filament\Actions\CreateAction;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListTasks extends ListRecords
@@ -15,7 +15,7 @@ class ListTasks extends ListRecords
protected function getHeaderActions(): array
{
return [
CreateAction::make()
Actions\CreateAction::make()
->icon('heroicon-s-plus'),
];
}

View File

@@ -4,20 +4,16 @@ declare(strict_types=1);
namespace App\Filament\Resources;
use App\Filament\Resources\TimeEntryResource\Pages\CreateTimeEntry;
use App\Filament\Resources\TimeEntryResource\Pages\EditTimeEntry;
use App\Filament\Resources\TimeEntryResource\Pages\ListTimeEntries;
use App\Filament\Resources\TimeEntryResource\Pages;
use App\Models\Member;
use App\Models\TimeEntry;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
@@ -27,16 +23,16 @@ class TimeEntryResource extends Resource
{
protected static ?string $model = TimeEntry::class;
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-clock';
protected static ?string $navigationIcon = 'heroicon-o-clock';
protected static string|\UnitEnum|null $navigationGroup = 'Timetracking';
protected static ?string $navigationGroup = 'Timetracking';
protected static ?int $navigationSort = 1;
public static function form(Schema $schema): Schema
public static function form(Form $form): Form
{
return $schema
->components([
return $form
->schema([
TextInput::make('id')
->label('ID')
->readOnly()
@@ -115,12 +111,12 @@ class TimeEntryResource extends Resource
->searchable(),
])
->defaultSort('created_at', 'desc')
->recordActions([
EditAction::make(),
->actions([
Tables\Actions\EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
@@ -135,9 +131,9 @@ class TimeEntryResource extends Resource
public static function getPages(): array
{
return [
'index' => ListTimeEntries::route('/'),
'create' => CreateTimeEntry::route('/create'),
'edit' => EditTimeEntry::route('/{record}/edit'),
'index' => Pages\ListTimeEntries::route('/'),
'create' => Pages\CreateTimeEntry::route('/create'),
'edit' => Pages\EditTimeEntry::route('/{record}/edit'),
];
}
}

View File

@@ -6,7 +6,7 @@ namespace App\Filament\Resources\TimeEntryResource\Pages;
use App\Filament\Resources\TimeEntryResource;
use App\Models\Member;
use Filament\Actions\DeleteAction;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditTimeEntry extends EditRecord
@@ -16,7 +16,7 @@ class EditTimeEntry extends EditRecord
protected function getHeaderActions(): array
{
return [
DeleteAction::make()
Actions\DeleteAction::make()
->icon('heroicon-m-trash'),
];
}

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Filament\Resources\TimeEntryResource\Pages;
use App\Filament\Resources\TimeEntryResource;
use Filament\Actions\CreateAction;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListTimeEntries extends ListRecords
@@ -15,7 +15,7 @@ class ListTimeEntries extends ListRecords
protected function getHeaderActions(): array
{
return [
CreateAction::make()
Actions\CreateAction::make()
->icon('heroicon-s-plus'),
];
}

View File

@@ -4,18 +4,12 @@ declare(strict_types=1);
namespace App\Filament\Resources;
use App\Filament\Resources\TokenResource\Pages\ListTokens;
use App\Filament\Resources\TokenResource\Pages\ViewToken;
use App\Filament\Resources\TokenResource\Pages;
use App\Models\Passport\Token;
use Filament\Actions\ViewAction;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables;
use Filament\Tables\Filters\TernaryFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
@@ -24,48 +18,48 @@ class TokenResource extends Resource
{
protected static ?string $model = Token::class;
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-key';
protected static ?string $navigationIcon = 'heroicon-o-key';
protected static string|\UnitEnum|null $navigationGroup = 'Auth';
protected static ?string $navigationGroup = 'Auth';
protected static ?int $navigationSort = 6;
public static function form(Schema $schema): Schema
public static function form(Form $form): Form
{
return $schema
return $form
->columns(1)
->components([
TextInput::make('id')
->schema([
Forms\Components\TextInput::make('id')
->label('ID')
->disabled()
->visibleOn(['update', 'show'])
->readOnly()
->maxLength(255),
TextInput::make('name')
Forms\Components\TextInput::make('name')
->label('Name')
->required()
->maxLength(255),
Select::make('owner_id')
Forms\Components\Select::make('owner_id')
->label('User')
->relationship(name: 'user', titleAttribute: 'name')
->searchable(['name'])
->disabled()
->required(),
Select::make('client_id')
Forms\Components\Select::make('client_id')
->label('Client')
->relationship(name: 'client', titleAttribute: 'name')
->searchable(['name'])
->required(),
Toggle::make('revoked')
Forms\Components\Toggle::make('revoked')
->label('Revoked')
->required(),
DateTimePicker::make('expires_at')
Forms\Components\DateTimePicker::make('expires_at')
->label('Expires At')
->disabled(),
DateTimePicker::make('created_at')
Forms\Components\DateTimePicker::make('created_at')
->label('Created At')
->disabled(),
DateTimePicker::make('updated_at')
Forms\Components\DateTimePicker::make('updated_at')
->label('Updated At')
->disabled(),
]);
@@ -75,32 +69,32 @@ class TokenResource extends Resource
{
return $table
->columns([
TextColumn::make('name')
Tables\Columns\TextColumn::make('name')
->searchable()
->sortable(),
TextColumn::make('user.name')
Tables\Columns\TextColumn::make('user.name')
->searchable()
->sortable(),
TextColumn::make('client.name')
Tables\Columns\TextColumn::make('client.name')
->searchable()
->sortable(),
IconColumn::make('personal_access_client')
Tables\Columns\IconColumn::make('personal_access_client')
->state(function (Token $token): bool {
return in_array('personal_access', $token->client->grant_types ?? [], true);
})
->boolean()
->label('API token?'),
IconColumn::make('revoked')
Tables\Columns\IconColumn::make('revoked')
->boolean()
->label('Revoked?')
->sortable(),
TextColumn::make('expires_at')
Tables\Columns\TextColumn::make('expires_at')
->dateTime()
->sortable(),
TextColumn::make('created_at')
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable(),
TextColumn::make('updated_at')
Tables\Columns\TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
@@ -126,10 +120,10 @@ class TokenResource extends Resource
TernaryFilter::make('revoked')
->label('Revoked?'),
])
->recordActions([
ViewAction::make(),
->actions([
Tables\Actions\ViewAction::make(),
])
->toolbarActions([
->bulkActions([
]);
}
@@ -142,8 +136,8 @@ class TokenResource extends Resource
public static function getPages(): array
{
return [
'index' => ListTokens::route('/'),
'view' => ViewToken::route('/{record}'),
'index' => Pages\ListTokens::route('/'),
'view' => Pages\ViewToken::route('/{record}'),
];
}
}

View File

@@ -6,30 +6,21 @@ namespace App\Filament\Resources;
use App\Enums\Weekday;
use App\Exceptions\Api\ApiException;
use App\Filament\Resources\UserResource\Pages\CreateUser;
use App\Filament\Resources\UserResource\Pages\EditUser;
use App\Filament\Resources\UserResource\Pages\ListUsers;
use App\Filament\Resources\UserResource\Pages\ViewUser;
use App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource\RelationManagers\OrganizationsRelationManager;
use App\Filament\Resources\UserResource\RelationManagers\OwnedOrganizationsRelationManager;
use App\Models\User;
use App\Service\DeletionService;
use App\Service\TimezoneService;
use App\Service\UserService;
use Brick\Money\IsoCurrencyProvider;
use Brick\Money\ISOCurrencyProvider;
use Exception;
use Filament\Actions\BulkAction;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\Select;
use Filament\Forms;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables;
use Filament\Tables\Filters\TernaryFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
@@ -37,26 +28,26 @@ use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
use STS\FilamentImpersonate\Actions\Impersonate;
use STS\FilamentImpersonate\Tables\Actions\Impersonate;
class UserResource extends Resource
{
protected static ?string $model = User::class;
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-user';
protected static ?string $navigationIcon = 'heroicon-o-user';
protected static string|\UnitEnum|null $navigationGroup = 'Users';
protected static ?string $navigationGroup = 'Users';
protected static ?int $navigationSort = 6;
public static function form(Schema $schema): Schema
public static function form(Form $form): Form
{
/** @var User|null $record */
$record = $schema->getRecord();
$record = $form->getRecord();
return $schema
return $form
->columns(1)
->components([
->schema([
TextInput::make('id')
->label('ID')
->disabled()
@@ -78,23 +69,23 @@ class UserResource extends Resource
'email:rfc,strict',
])
->maxLength(255),
Toggle::make('is_placeholder')
Forms\Components\Toggle::make('is_placeholder')
->label('Is Placeholder?')
->hiddenOn(['create'])
->disabledOn(['edit']),
DateTimePicker::make('email_verified_at')
Forms\Components\DateTimePicker::make('email_verified_at')
->label('Email Verified At')
->hiddenOn(['create'])
->nullable(),
Toggle::make('is_email_verified')
Forms\Components\Toggle::make('is_email_verified')
->label('Email Verified?')
->visibleOn(['create']),
Select::make('timezone')
Forms\Components\Select::make('timezone')
->label('Timezone')
->options(fn (): array => app(TimezoneService::class)->getSelectOptions())
->searchable()
->required(),
Select::make('week_start')
Forms\Components\Select::make('week_start')
->label('Week Start')
->options(Weekday::class)
->required(),
@@ -112,10 +103,10 @@ class UserResource extends Resource
->visibleOn(['create'])
->required(fn (string $context): bool => $context === 'create')
->maxLength(255),
Select::make('currency')
Forms\Components\Select::make('currency')
->label('Currency (Personal Organization)')
->options(function (): array {
$currencies = IsoCurrencyProvider::getInstance()->getAvailableCurrencies();
$currencies = ISOCurrencyProvider::getInstance()->getAvailableCurrencies();
$select = [];
foreach ($currencies as $currency) {
$select[$currency->getCurrencyCode()] = $currency->getName().' ('.$currency->getCurrencyCode().')';
@@ -126,11 +117,11 @@ class UserResource extends Resource
->required()
->visibleOn(['create'])
->searchable(),
DateTimePicker::make('created_at')
Forms\Components\DateTimePicker::make('created_at')
->label('Created At')
->hiddenOn(['create'])
->disabled(),
DateTimePicker::make('updated_at')
Forms\Components\DateTimePicker::make('updated_at')
->label('Updated At')
->hiddenOn(['create'])
->disabled(),
@@ -141,25 +132,25 @@ class UserResource extends Resource
{
return $table
->columns([
TextColumn::make('name')
Tables\Columns\TextColumn::make('name')
->searchable()
->sortable(),
TextColumn::make('email')
Tables\Columns\TextColumn::make('email')
->icon('heroicon-m-envelope')
->searchable()
->sortable(),
IconColumn::make('is_real_user')
Tables\Columns\IconColumn::make('is_real_user')
->getStateUsing(fn (User $record): bool => ! $record->is_placeholder)
->label('Real user?')
->boolean(),
IconColumn::make('email_verified')
Tables\Columns\IconColumn::make('email_verified')
->getStateUsing(fn (User $record): bool => $record->email_verified_at !== null)
->label('Email verified?')
->boolean(),
TextColumn::make('created_at')
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable(),
TextColumn::make('updated_at')
Tables\Columns\TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
@@ -187,7 +178,7 @@ class UserResource extends Resource
->attribute('email_verified_at')
->nullable(),
])
->recordActions([
->actions([
Impersonate::make()->before(function (User $record): void {
if ($record->currentOrganization === null) {
$organization = $record->organizations()->where('personal_team', '=', true)->first();
@@ -200,8 +191,8 @@ class UserResource extends Resource
app(UserService::class)->switchCurrentOrganization($record, $organization);
}
}),
EditAction::make(),
DeleteAction::make()
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make()
->hidden(fn (User $record) => $record->is(Auth::user()))
->using(function (User $record): void {
try {
@@ -216,8 +207,8 @@ class UserResource extends Resource
}
}),
])
->toolbarActions([
BulkAction::make('Resend verification email')
->bulkActions([
Tables\Actions\BulkAction::make('Resend verification email')
->icon('heroicon-o-paper-airplane')
->action(function (Collection $records): void {
foreach ($records as $user) {
@@ -239,10 +230,10 @@ class UserResource extends Resource
public static function getPages(): array
{
return [
'index' => ListUsers::route('/'),
'create' => CreateUser::route('/create'),
'edit' => EditUser::route('/{record}/edit'),
'view' => ViewUser::route('/{record}'),
'index' => Pages\ListUsers::route('/'),
'create' => Pages\CreateUser::route('/create'),
'edit' => Pages\EditUser::route('/{record}/edit'),
'view' => Pages\ViewUser::route('/{record}'),
];
}
}

View File

@@ -22,7 +22,7 @@ class CreateUser extends CreateRecord
$data['email'],
$data['password_create'],
$data['timezone'],
$data['week_start'] instanceof Weekday ? $data['week_start'] : Weekday::from($data['week_start']),
Weekday::from($data['week_start']),
$data['currency'],
verifyEmail: (bool) $data['is_email_verified']
);

View File

@@ -5,9 +5,8 @@ declare(strict_types=1);
namespace App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource;
use App\Filament\Resources\UserResource\Actions\DeleteUser;
use Filament\Resources\Pages\EditRecord;
use STS\FilamentImpersonate\Actions\Impersonate;
use STS\FilamentImpersonate\Pages\Actions\Impersonate;
class EditUser extends EditRecord
{
@@ -17,7 +16,7 @@ class EditUser extends EditRecord
{
return [
Impersonate::make()->record($this->getRecord()),
DeleteUser::make(),
UserResource\Actions\DeleteUser::make(),
];
}
}

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource;
use Filament\Actions\CreateAction;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListUsers extends ListRecords
@@ -15,7 +15,7 @@ class ListUsers extends ListRecords
protected function getHeaderActions(): array
{
return [
CreateAction::make()
Actions\CreateAction::make()
->icon('heroicon-s-plus'),
];
}

View File

@@ -7,7 +7,7 @@ namespace App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource;
use Filament\Actions\EditAction;
use Filament\Resources\Pages\ViewRecord;
use STS\FilamentImpersonate\Actions\Impersonate;
use STS\FilamentImpersonate\Pages\Actions\Impersonate;
class ViewUser extends ViewRecord
{

View File

@@ -11,13 +11,12 @@ use App\Models\Member;
use App\Models\Organization;
use App\Models\User;
use App\Service\MemberService;
use Filament\Actions\Action;
use Filament\Actions\DetachAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\Select;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Schemas\Schema;
use Filament\Tables;
use Filament\Tables\Actions\Action;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
@@ -25,10 +24,10 @@ class OrganizationsRelationManager extends RelationManager
{
protected static string $relationship = 'organizations';
public function form(Schema $schema): Schema
public function form(Form $form): Form
{
return $schema
->components([
return $form
->schema([
Select::make('role')
->options(Role::class),
]);
@@ -47,22 +46,21 @@ class OrganizationsRelationManager extends RelationManager
])
->headerActions([
])
->recordActions([
->actions([
Action::make('view')
->icon('heroicon-o-eye')
->color('gray')
->url(fn (Organization $record): string => OrganizationResource::getUrl('view', [
'record' => $record->getKey(),
])),
EditAction::make()
Tables\Actions\EditAction::make()
->using(function (Organization $record, array $data): Organization {
/** @var Member $member */
$member = $record->getRelation('membership');
$newRole = $data['role'] instanceof Role ? $data['role'] : Role::from($data['role']);
if ($newRole->value !== $member->role) {
if ($data['role'] !== $member->role) {
try {
app(MemberService::class)->changeRole($member, $record, $newRole, true);
app(MemberService::class)->changeRole($member, $record, Role::from($data['role']), true);
} catch (ApiException $exception) {
Notification::make()
->danger()
@@ -76,7 +74,7 @@ class OrganizationsRelationManager extends RelationManager
return $record;
}),
DetachAction::make()
Tables\Actions\DetachAction::make()
->using(function (Organization $record): void {
/** @var User $user */
$user = $this->getOwnerRecord();
@@ -96,7 +94,7 @@ class OrganizationsRelationManager extends RelationManager
}
}),
])
->toolbarActions([
->bulkActions([
]);
}
}

View File

@@ -6,10 +6,10 @@ namespace App\Filament\Resources\UserResource\RelationManagers;
use App\Filament\Resources\OrganizationResource;
use App\Models\Organization;
use Filament\Actions\Action;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables;
use Filament\Tables\Actions\Action;
use Filament\Tables\Table;
class OwnedOrganizationsRelationManager extends RelationManager
@@ -18,10 +18,10 @@ class OwnedOrganizationsRelationManager extends RelationManager
protected static string $relationship = 'ownedOrganizations';
public function form(Schema $schema): Schema
public function form(Form $form): Form
{
return $schema
->components([
return $form
->schema([
]);
}
@@ -30,14 +30,14 @@ class OwnedOrganizationsRelationManager extends RelationManager
return $table
->recordTitleAttribute('name')
->columns([
TextColumn::make('name'),
Tables\Columns\TextColumn::make('name'),
])
->filters([
//
])
->headerActions([
])
->recordActions([
->actions([
Action::make('view')
->icon('heroicon-o-eye')
->color('gray')
@@ -51,7 +51,7 @@ class OwnedOrganizationsRelationManager extends RelationManager
]))
->openUrlInNewTab(),
])
->toolbarActions([
->bulkActions([
]);
}
}

View File

@@ -9,7 +9,7 @@ use Illuminate\Support\Facades\Cache;
class ServerOverview extends Widget
{
protected string $view = 'filament.widgets.server-overview';
protected static string $view = 'filament.widgets.server-overview';
/**
* @return array<string, mixed>

View File

@@ -11,7 +11,7 @@ use Flowframe\Trend\TrendValue;
class TimeEntriesCreated extends ChartWidget
{
protected ?string $heading = 'Time Entries Created';
protected static ?string $heading = 'Time Entries Created';
public ?string $filter = 'week';
@@ -53,7 +53,7 @@ class TimeEntriesCreated extends ChartWidget
return [
'datasets' => [
[
'label' => $this->heading,
'label' => self::$heading,
'data' => $data->map(fn (TrendValue $value) => $value->aggregate),
],
],

View File

@@ -11,7 +11,7 @@ use Flowframe\Trend\TrendValue;
class TimeEntriesImported extends ChartWidget
{
protected ?string $heading = 'Time Entries Imported';
protected static ?string $heading = 'Time Entries Imported';
public ?string $filter = 'week';
@@ -53,7 +53,7 @@ class TimeEntriesImported extends ChartWidget
return [
'datasets' => [
[
'label' => $this->heading,
'label' => self::$heading,
'data' => $data->map(fn (TrendValue $value) => $value->aggregate),
],
],

View File

@@ -11,7 +11,7 @@ use Flowframe\Trend\TrendValue;
class UserRegistrations extends ChartWidget
{
protected ?string $heading = 'User Registrations';
protected static ?string $heading = 'User Registrations';
public ?string $filter = 'week';
@@ -54,7 +54,7 @@ class UserRegistrations extends ChartWidget
return [
'datasets' => [
[
'label' => $this->heading,
'label' => self::$heading,
'data' => $data->map(fn (TrendValue $value) => $value->aggregate),
],
],

View File

@@ -14,7 +14,6 @@ use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Str;
use RuntimeException;
class ApiTokenController extends Controller
{
@@ -63,7 +62,7 @@ class ApiTokenController extends Controller
$tokenModel = $token->getToken();
return new ApiTokenWithAccessTokenResource($tokenModel, $token->accessToken);
} catch (RuntimeException $exception) {
} catch (\RuntimeException $exception) {
report($exception);
if (Str::contains($exception->getMessage(), ['Personal access client not found'])) {
throw new PersonalAccessClientIsNotConfiguredException;

View File

@@ -7,7 +7,7 @@ namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Service\CurrencyService;
use Brick\Money\Currency;
use Brick\Money\IsoCurrencyProvider;
use Brick\Money\ISOCurrencyProvider;
use Illuminate\Http\JsonResponse;
class CurrencyController extends Controller
@@ -29,7 +29,7 @@ class CurrencyController extends Controller
'name' => $currency->getName(),
'symbol' => $currencyService->getCurrencySymbol($currency->getCurrencyCode()),
],
IsoCurrencyProvider::getInstance()->getAvailableCurrencies()
ISOCurrencyProvider::getInstance()->getAvailableCurrencies()
));
return response()->json($currencies);

View File

@@ -28,9 +28,7 @@ class ExportController extends Controller
$filepath = $exportService->export($organization);
$downloadUrl = Storage::disk(config('filesystems.private'))
->temporaryUrl($filepath, Carbon::now()->addMinutes(10), [
'ResponseContentDisposition' => 'attachment; filename="'.basename($filepath).'"',
]);
->temporaryUrl($filepath, Carbon::now()->addMinutes(10));
return new JsonResponse([
'success' => true,

View File

@@ -88,8 +88,6 @@ class InvitationController extends Controller
/**
* Remove a pending invitation
*
* This revokes the invitation: the link in the invitation email stops working. Find the invitation ID with `GET /organizations/{organization}/invitations`.
*
* @throws AuthorizationException
*
* @operationId removeInvitation

View File

@@ -33,7 +33,6 @@ use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Throwable;
class MemberController extends Controller
{
@@ -145,12 +144,9 @@ class MemberController extends Controller
/**
* Merge one member into another
*
* Only placeholder members (for example people created by an import) can be merged. All time entries and other data of the placeholder
* are reassigned to the member given in `member_id`, and the placeholder is removed. Find both member IDs with `GET /organizations/{organization}/members`.
*
* @throws AuthorizationException
* @throws OnlyPlaceholdersCanBeMergedIntoAnotherMember
* @throws Throwable
* @throws \Throwable
*
* @operationId mergeMember
*/

View File

@@ -78,9 +78,6 @@ class OrganizationController extends Controller
if ($request->getPreventOverlappingTimeEntries() !== null) {
$organization->prevent_overlapping_time_entries = $request->getPreventOverlappingTimeEntries();
}
if ($request->getBreaksEnabled() !== null) {
$organization->breaks_enabled = $request->getBreaksEnabled();
}
$hasBillableRate = $request->has('billable_rate');
if ($hasBillableRate) {
$oldBillableRate = $organization->billable_rate;

Some files were not shown because too many files have changed in this diff Show More