mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-08 08:12:17 +01:00
Compare commits
9 Commits
v0.6.0
...
feature/di
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a28be83a1 | ||
|
|
4eb716d2cc | ||
|
|
55323fa4b4 | ||
|
|
6df20ed1e5 | ||
|
|
bc7c564eb2 | ||
|
|
5423b03201 | ||
|
|
0e910ba565 | ||
|
|
bad1cd1343 | ||
|
|
dd312b396b |
42
.env.ci
42
.env.ci
@@ -1,4 +1,3 @@
|
||||
# Application
|
||||
APP_NAME=solidtime
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
@@ -6,6 +5,7 @@ APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
APP_FORCE_HTTPS=false
|
||||
APP_ENABLE_REGISTRATION=true
|
||||
SESSION_SECURE_COOKIE=false
|
||||
|
||||
# Logging
|
||||
LOG_CHANNEL=stack
|
||||
@@ -20,39 +20,35 @@ DB_TEST_DATABASE=laravel
|
||||
DB_TEST_USERNAME=root
|
||||
DB_TEST_PASSWORD=root
|
||||
|
||||
# Broadcasting
|
||||
BROADCAST_DRIVER=null
|
||||
|
||||
# Cache
|
||||
BROADCAST_DRIVER=log
|
||||
CACHE_DRIVER=file
|
||||
|
||||
# Queue
|
||||
QUEUE_CONNECTION=sync
|
||||
|
||||
# Session
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
|
||||
# Mail
|
||||
MAIL_MAILER=log
|
||||
MAIL_FROM_ADDRESS="no-reply@solidtime.test"
|
||||
MAIL_FROM_NAME="solidtime"
|
||||
MAIL_REPLY_TO_ADDRESS="hello@solidtime.test"
|
||||
MAIL_REPLY_TO_NAME="solidtime"
|
||||
MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
# Filesystems
|
||||
FILESYSTEM_DISK=local
|
||||
PUBLIC_FILESYSTEM_DISK=public
|
||||
|
||||
# Passport
|
||||
PASSPORT_PERSONAL_ACCESS_CLIENT_ID="9e27f54d-5dfb-4dde-99d7-834518236c92"
|
||||
PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET="EL5mXp3aF8ITjcwoOXRpbSK7zGrWhW4zTDpQXTkf"
|
||||
|
||||
# Auditing
|
||||
AUDITING_ENABLED=true
|
||||
|
||||
# Telescope
|
||||
TELESCOPE_ENABLED=false
|
||||
|
||||
# Services
|
||||
GOTENBERG_URL=http://0.0.0.0:3000
|
||||
|
||||
PUSHER_APP_ID=
|
||||
PUSHER_APP_KEY=
|
||||
PUSHER_APP_SECRET=
|
||||
PUSHER_HOST=
|
||||
PUSHER_PORT=443
|
||||
PUSHER_SCHEME=https
|
||||
PUSHER_APP_CLUSTER=mt1
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
||||
VITE_PUSHER_HOST="${PUSHER_HOST}"
|
||||
VITE_PUSHER_PORT="${PUSHER_PORT}"
|
||||
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
|
||||
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
||||
|
||||
22
.env.example
22
.env.example
@@ -4,7 +4,7 @@ APP_ENV=local
|
||||
APP_KEY=base64:UNQNf1SXeASNkWux01Rj8EnHYx8FO0kAxWNDwktclkk=
|
||||
APP_DEBUG=true
|
||||
APP_URL=https://solidtime.test
|
||||
APP_FORCE_HTTPS=false
|
||||
AUDITING_ENABLED=true
|
||||
APP_ENABLE_REGISTRATION=true
|
||||
SUPER_ADMINS=admin@example.com
|
||||
PAGINATION_PER_PAGE_DEFAULT=500
|
||||
@@ -49,9 +49,7 @@ MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_ENCRYPTION=null
|
||||
MAIL_FROM_ADDRESS="no-reply@solidtime.test"
|
||||
MAIL_FROM_NAME="solidtime"
|
||||
MAIL_REPLY_TO_ADDRESS="hello@solidtime.test"
|
||||
MAIL_REPLY_TO_NAME="solidtime"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
# Filesystems
|
||||
FILESYSTEM_DISK=s3
|
||||
@@ -64,24 +62,14 @@ S3_URL=http://storage.solidtime.test/local
|
||||
S3_ENDPOINT=http://storage.solidtime.test
|
||||
S3_USE_PATH_STYLE_ENDPOINT=true
|
||||
|
||||
# Passport
|
||||
PASSPORT_PERSONAL_ACCESS_CLIENT_ID="9e27f54d-5dfb-4dde-99d7-834518236c92"
|
||||
PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET="EL5mXp3aF8ITjcwoOXRpbSK7zGrWhW4zTDpQXTkf"
|
||||
|
||||
# Auditing
|
||||
AUDITING_ENABLED=true
|
||||
|
||||
# Telescope
|
||||
TELESCOPE_ENABLED=false
|
||||
|
||||
# Services
|
||||
GOTENBERG_URL=http://gotenberg:3000
|
||||
|
||||
VITE_HOST_NAME=vite.solidtime.test
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
|
||||
# Local setup
|
||||
NGINX_HOST_NAME=solidtime.test
|
||||
NETWORK_NAME=reverse-proxy-docker-traefik_routing
|
||||
FORWARD_DB_PORT=5432
|
||||
FORWARD_WEB_PORT=8083
|
||||
VITE_HOST_NAME=vite.solidtime.test
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
#SAIL_XDEBUG_MODE=develop,debug,coverage
|
||||
|
||||
@@ -5,6 +5,7 @@ VITE_APP_NAME=solidtime
|
||||
APP_ENV=production
|
||||
APP_DEBUG=false
|
||||
APP_FORCE_HTTPS=true
|
||||
SESSION_SECURE_COOKIE=true
|
||||
OCTANE_SERVER=frankenphp
|
||||
PAGINATION_PER_PAGE_DEFAULT=500
|
||||
|
||||
|
||||
13
.eslintrc.cjs
Normal file
13
.eslintrc.cjs
Normal file
@@ -0,0 +1,13 @@
|
||||
/* eslint-env node */
|
||||
require("@rushstack/eslint-patch/modern-module-resolution")
|
||||
|
||||
module.exports = {
|
||||
extends: ['plugin:vue/vue3-essential', '@vue/eslint-config-typescript/recommended', '@vue/eslint-config-prettier'],
|
||||
rules: {
|
||||
'vue/multi-word-component-names': 'off',
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"unused-imports/no-unused-vars": "error",
|
||||
},
|
||||
plugins: ['unused-imports'],
|
||||
}
|
||||
47
.github/ISSUE_TEMPLATE/1_bug_report.yml
vendored
47
.github/ISSUE_TEMPLATE/1_bug_report.yml
vendored
@@ -1,47 +0,0 @@
|
||||
name: Bug Report
|
||||
description: "Report a bug"
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Before creating a new bug report, please check that there isn't already a similar issue.
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
description: A clear and concise description of what the bug is.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "Steps To Reproduce"
|
||||
description: How do you trigger this bug? Please walk us through it step by step.
|
||||
value: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
...
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: "Self-hosted or Cloud?"
|
||||
options:
|
||||
- Self-Hosted
|
||||
- solidtime Cloud
|
||||
- Both
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: "Version of solidtime: (for self-hosted)"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: "solidtime self-hosting guide: (for self-hosted)"
|
||||
description: "Did you use the official guide to self-host solidtime? If yes, which one?"
|
||||
validations:
|
||||
required: false
|
||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
8
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,8 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 🚀 Feature Request
|
||||
url: https://github.com/solidtime-io/solidtime/discussions/new?category=feature-requests
|
||||
about: Share ideas for new features
|
||||
- name: ❓ Ask a Question
|
||||
url: https://github.com/solidtime-io/solidtime/discussions/new?category=general
|
||||
about: Ask the community for help
|
||||
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,8 +0,0 @@
|
||||
<!--
|
||||
This project is early stage. The structure and APIs are still subject to change and not stable.
|
||||
Therefore, we do not currently accept any contributions, unless you are a member of the team.
|
||||
|
||||
As soon as we feel comfortable enough that the application structure is stable enough, we will open up the project for contributions.
|
||||
|
||||
We do accept contributions in the [documentation repository](https://github.com/solidtime-io/docs) f.e. to add new self-hosting guides.
|
||||
-->
|
||||
143
.github/workflows/build-public.yml
vendored
143
.github/workflows/build-public.yml
vendored
@@ -11,21 +11,10 @@ on:
|
||||
- 'docker/prod/**'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
DOCKERHUB_REPO: solidtime/solidtime
|
||||
GHCR_REPO: ghcr.io/solidtime-io/solidtime
|
||||
|
||||
name: Build - Public
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- runs-on: "ubuntu-24.04-arm"
|
||||
platform: "linux/arm64"
|
||||
- runs-on: "ubuntu-24.04"
|
||||
platform: "linux/amd64"
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
@@ -40,7 +29,7 @@ jobs:
|
||||
fetch-depth: 0 # Required for WyriHaximus/github-action-get-previous-tag
|
||||
|
||||
- name: "Get build"
|
||||
id: release-build
|
||||
id: build
|
||||
run: echo "build=$(git rev-parse --short=8 HEAD)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: "Get Previous tag (normal push)"
|
||||
@@ -51,7 +40,7 @@ jobs:
|
||||
prefix: "v"
|
||||
|
||||
- name: "Get version"
|
||||
id: release-version
|
||||
id: version
|
||||
run: |
|
||||
if ${{ !startsWith(github.ref, 'refs/tags/v') }}; then
|
||||
if ${{ startsWith(steps.previoustag.outputs.tag, 'v') }}; then
|
||||
@@ -72,23 +61,21 @@ jobs:
|
||||
rm .env.production .env.ci .env.example
|
||||
|
||||
- name: "Add version to .env"
|
||||
run: sed -i 's/APP_VERSION=0.0.0/APP_VERSION=${{ steps.release-version.outputs.app_version }}/g' .env
|
||||
run: sed -i 's/APP_VERSION=0.0.0/APP_VERSION=${{ steps.version.outputs.app_version }}/g' .env
|
||||
|
||||
- name: "Add build to .env"
|
||||
run: sed -i 's/APP_BUILD=0/APP_BUILD=${{ steps.release-build.outputs.build }}/g' .env
|
||||
run: sed -i 's/APP_BUILD=0/APP_BUILD=${{ steps.build.outputs.build }}/g' .env
|
||||
|
||||
- name: "Output .env"
|
||||
run: cat .env
|
||||
|
||||
- name: "Setup PHP with PECL extension"
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.3'
|
||||
extensions: mbstring, dom, fileinfo, pgsql
|
||||
|
||||
- name: "Install dependencies"
|
||||
run: composer install --no-dev --no-ansi --no-interaction --prefer-dist --ignore-platform-reqs --classmap-authoritative
|
||||
uses: php-actions/composer@v6
|
||||
if: steps.cache-vendor.outputs.cache-hit != 'true' # Skip if cache hit
|
||||
with:
|
||||
command: install
|
||||
only_args: --no-dev --no-ansi --no-interaction --prefer-dist --ignore-platform-reqs --classmap-authoritative
|
||||
php_version: 8.3
|
||||
|
||||
- name: "Use Node.js"
|
||||
uses: actions/setup-node@v4
|
||||
@@ -101,31 +88,29 @@ jobs:
|
||||
- name: "Build"
|
||||
run: npm run build
|
||||
|
||||
- name: "Prepare"
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: "Docker meta"
|
||||
id: "meta"
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.DOCKERHUB_REPO }}
|
||||
${{ env.GHCR_REPO }}
|
||||
|
||||
- name: "Login to Docker Hub Container Registry"
|
||||
- name: "Login to GitHub Container Registry"
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: "Login to GitHub Container Registry"
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: "Docker meta"
|
||||
id: "meta"
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
solidtime/solidtime
|
||||
ghcr.io/${{ github.repository }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
|
||||
- name: "Set up QEMU"
|
||||
uses: docker/setup-qemu-action@v3
|
||||
@@ -133,90 +118,16 @@ jobs:
|
||||
- name: "Set up Docker Buildx"
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: "Build and push by digest"
|
||||
id: build
|
||||
- name: "Build and push"
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/prod/Dockerfile
|
||||
build-args: |
|
||||
DOCKER_FILES_BASE_PATH=docker/prod/
|
||||
platforms: ${{ matrix.platform }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,"name=${{ env.DOCKERHUB_REPO }},${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: "Export digest"
|
||||
run: |
|
||||
mkdir -p ${{ runner.temp }}/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||
|
||||
- name: "Upload digest"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
attestations: write
|
||||
id-token: write
|
||||
timeout-minutes: 90
|
||||
needs:
|
||||
- build
|
||||
steps:
|
||||
- name: "Download digests"
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ${{ runner.temp }}/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: "Login to Docker Hub"
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: "Login to GHCR"
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: "Set up Docker Buildx"
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: "Docker meta"
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.DOCKERHUB_REPO }}
|
||||
${{ env.GHCR_REPO }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
|
||||
- name: "Create manifest list and push"
|
||||
working-directory: ${{ runner.temp }}/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.DOCKERHUB_REPO }}@sha256:%s ' *)
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.GHCR_REPO }}@sha256:%s ' *)
|
||||
|
||||
- name: "Inspect image"
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.DOCKERHUB_REPO }}:${{ steps.meta.outputs.version }}
|
||||
docker buildx imagetools inspect ${{ env.GHCR_REPO }}:${{ steps.meta.outputs.version }}
|
||||
|
||||
3
.github/workflows/npm-publish-api.yml
vendored
3
.github/workflows/npm-publish-api.yml
vendored
@@ -8,8 +8,7 @@ jobs:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v4
|
||||
# Setup .npmrc file to publish to npm
|
||||
- name: Install root project dependencies
|
||||
run: npm ci
|
||||
|
||||
3
.github/workflows/npm-publish-ui.yml
vendored
3
.github/workflows/npm-publish-ui.yml
vendored
@@ -8,8 +8,7 @@ jobs:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v4
|
||||
# Setup .npmrc file to publish to npm
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
|
||||
2
.github/workflows/phpunit.yml
vendored
2
.github/workflows/phpunit.yml
vendored
@@ -63,7 +63,7 @@ jobs:
|
||||
run: php artisan test --stop-on-failure --coverage-text --coverage-clover=coverage.xml
|
||||
|
||||
- name: "Upload coverage reports to Codecov"
|
||||
uses: codecov/codecov-action@v5.3.1
|
||||
uses: codecov/codecov-action@v4.5.0
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
slug: solidtime-io/solidtime
|
||||
|
||||
2
.github/workflows/pint.yml
vendored
2
.github/workflows/pint.yml
vendored
@@ -10,6 +10,6 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: "Check code style"
|
||||
uses: aglipanci/laravel-pint-action@2.5
|
||||
uses: aglipanci/laravel-pint-action@2.4
|
||||
with:
|
||||
configPath: "pint.json"
|
||||
|
||||
24
.github/workflows/playwright.yml
vendored
24
.github/workflows/playwright.yml
vendored
@@ -27,47 +27,45 @@ jobs:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: "Setup node"
|
||||
uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
|
||||
- name: "Setup PHP"
|
||||
- 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: "Run composer install"
|
||||
- name: Run composer install
|
||||
run: composer install -n --prefer-dist
|
||||
|
||||
- name: "Prepare Laravel Application"
|
||||
- name: Prepare Laravel Application
|
||||
run: |
|
||||
cp .env.ci .env
|
||||
php artisan key:generate
|
||||
php artisan passport:keys
|
||||
php artisan migrate --seed
|
||||
php artisan passport:keys
|
||||
|
||||
- name: "Install dependencies"
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: "Build Frontend"
|
||||
- name: Build Frontend
|
||||
run: npm run build
|
||||
|
||||
- name: "Run Laravel Server"
|
||||
- name: Run Laravel Server
|
||||
run: php artisan serve > /dev/null 2>&1 &
|
||||
|
||||
- name: "Install Playwright Browsers"
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps
|
||||
|
||||
- name: "Run Playwright tests"
|
||||
- name: Run Playwright tests
|
||||
run: npx playwright test
|
||||
env:
|
||||
PLAYWRIGHT_BASE_URL: 'http://127.0.0.1:8000'
|
||||
|
||||
- name: "Upload test results"
|
||||
uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: test-results
|
||||
|
||||
@@ -57,7 +57,7 @@ class UserCreateCommand extends Command
|
||||
}
|
||||
|
||||
$user = null;
|
||||
DB::transaction(function () use (&$user, $name, $email, $password, $verifyEmail): void {
|
||||
DB::transaction(function () use (&$user, $name, $email, $password): void {
|
||||
$user = app(UserService::class)->createUser(
|
||||
$name,
|
||||
$email,
|
||||
@@ -65,7 +65,6 @@ class UserCreateCommand extends Command
|
||||
'UTC',
|
||||
Weekday::Monday,
|
||||
'EUR',
|
||||
$verifyEmail
|
||||
);
|
||||
});
|
||||
/** @var Organization|null $organization */
|
||||
@@ -74,6 +73,10 @@ class UserCreateCommand extends Command
|
||||
throw new LogicException('User does not have an organization');
|
||||
}
|
||||
|
||||
if ($verifyEmail) {
|
||||
$user->markEmailAsVerified();
|
||||
}
|
||||
|
||||
$this->info('Created user "'.$name.'" ("'.$email.'")');
|
||||
$this->line('ID: '.$user->getKey());
|
||||
$this->line('Name: '.$name);
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Exceptions\Api;
|
||||
|
||||
class PersonalAccessClientIsNotConfiguredException extends ApiException
|
||||
{
|
||||
public const string KEY = 'personal_access_client_is_not_configured';
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\TokenResource\Pages;
|
||||
use App\Models\Passport\Client;
|
||||
use App\Models\Passport\Token;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Filters\TernaryFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class TokenResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Token::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-key';
|
||||
|
||||
protected static ?string $navigationGroup = 'Auth';
|
||||
|
||||
protected static ?int $navigationSort = 6;
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->columns(1)
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('id')
|
||||
->label('ID')
|
||||
->disabled()
|
||||
->visibleOn(['update', 'show'])
|
||||
->readOnly()
|
||||
->maxLength(255),
|
||||
Forms\Components\TextInput::make('name')
|
||||
->label('Name')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Forms\Components\Select::make('user_id')
|
||||
->label('User')
|
||||
->relationship(name: 'user', titleAttribute: 'name')
|
||||
->searchable(['name'])
|
||||
->disabled()
|
||||
->required(),
|
||||
Forms\Components\Select::make('client_id')
|
||||
->label('Client')
|
||||
->relationship(name: 'client', titleAttribute: 'name')
|
||||
->searchable(['name'])
|
||||
->required(),
|
||||
Forms\Components\Toggle::make('revoked')
|
||||
->label('Revoked')
|
||||
->required(),
|
||||
Forms\Components\DateTimePicker::make('expires_at')
|
||||
->label('Expires At')
|
||||
->disabled(),
|
||||
Forms\Components\DateTimePicker::make('created_at')
|
||||
->label('Created At')
|
||||
->disabled(),
|
||||
Forms\Components\DateTimePicker::make('updated_at')
|
||||
->label('Updated At')
|
||||
->disabled(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('user.name')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('client.name')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\IconColumn::make('client.personal_access_client')
|
||||
->boolean()
|
||||
->label('API token?')
|
||||
->sortable(),
|
||||
Tables\Columns\IconColumn::make('revoked')
|
||||
->boolean()
|
||||
->label('Revoked?')
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('expires_at')
|
||||
->dateTime()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('updated_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->defaultSort('created_at', 'desc')
|
||||
->filters([
|
||||
TernaryFilter::make('is_personal_access_client')
|
||||
->queries(
|
||||
true: function (Builder $query) {
|
||||
/** @var Builder<Token> $query */
|
||||
return $query->whereHas('client', function (Builder $query) {
|
||||
/** @var Builder<Client> $query */
|
||||
return $query->where('personal_access_client', true);
|
||||
});
|
||||
},
|
||||
false: function (Builder $query) {
|
||||
/** @var Builder<Token> $query */
|
||||
return $query->whereHas('client', function (Builder $query) {
|
||||
/** @var Builder<Client> $query */
|
||||
return $query->where('personal_access_client', false);
|
||||
});
|
||||
},
|
||||
blank: function (Builder $query) {
|
||||
/** @var Builder<Token> $query */
|
||||
return $query;
|
||||
},
|
||||
)
|
||||
->label('API token?'),
|
||||
TernaryFilter::make('revoked')
|
||||
->label('Revoked?'),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\ViewAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListTokens::route('/'),
|
||||
'view' => Pages\ViewToken::route('/{record}'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Resources\TokenResource\Pages;
|
||||
|
||||
use App\Filament\Resources\TokenResource;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListTokens extends ListRecords
|
||||
{
|
||||
protected static string $resource = TokenResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Resources\TokenResource\Pages;
|
||||
|
||||
use App\Filament\Resources\TokenResource;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewToken extends ViewRecord
|
||||
{
|
||||
protected static string $resource = TokenResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,6 @@ use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
||||
use STS\FilamentImpersonate\Tables\Actions\Impersonate;
|
||||
|
||||
class UserResource extends Resource
|
||||
@@ -40,9 +39,6 @@ class UserResource extends Resource
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
/** @var User|null $record */
|
||||
$record = $form->getRecord();
|
||||
|
||||
return $form
|
||||
->columns(1)
|
||||
->schema([
|
||||
@@ -59,13 +55,6 @@ class UserResource extends Resource
|
||||
Forms\Components\TextInput::make('email')
|
||||
->label('Email')
|
||||
->required()
|
||||
->rules($record?->is_placeholder ? [] : [
|
||||
UniqueEloquent::make(User::class, 'email')
|
||||
->ignore($record?->getKey()),
|
||||
])
|
||||
->rule([
|
||||
'email',
|
||||
])
|
||||
->maxLength(255),
|
||||
Forms\Components\Toggle::make('is_placeholder')
|
||||
->label('Is Placeholder?')
|
||||
@@ -73,11 +62,7 @@ class UserResource extends Resource
|
||||
->disabledOn(['edit']),
|
||||
Forms\Components\DateTimePicker::make('email_verified_at')
|
||||
->label('Email Verified At')
|
||||
->hiddenOn(['create'])
|
||||
->nullable(),
|
||||
Forms\Components\Toggle::make('is_email_verified')
|
||||
->label('Email Verified?')
|
||||
->visibleOn(['create']),
|
||||
Forms\Components\Select::make('timezone')
|
||||
->label('Timezone')
|
||||
->options(fn (): array => app(TimezoneService::class)->getSelectOptions())
|
||||
@@ -89,16 +74,8 @@ class UserResource extends Resource
|
||||
->required(),
|
||||
TextInput::make('password')
|
||||
->password()
|
||||
->label('Password')
|
||||
->dehydrateStateUsing(fn ($state) => Hash::make($state))
|
||||
->dehydrated(fn ($state) => filled($state))
|
||||
->hiddenOn(['create'])
|
||||
->required(fn (string $context): bool => $context === 'create')
|
||||
->maxLength(255),
|
||||
TextInput::make('password_create')
|
||||
->password()
|
||||
->label('Password')
|
||||
->visibleOn(['create'])
|
||||
->required(fn (string $context): bool => $context === 'create')
|
||||
->maxLength(255),
|
||||
Forms\Components\Select::make('currency')
|
||||
|
||||
@@ -20,11 +20,10 @@ class CreateUser extends CreateRecord
|
||||
$user = $userService->createUser(
|
||||
$data['name'],
|
||||
$data['email'],
|
||||
$data['password_create'],
|
||||
$data['password'],
|
||||
$data['timezone'],
|
||||
Weekday::from($data['week_start']),
|
||||
$data['currency'],
|
||||
(bool) $data['is_email_verified']
|
||||
);
|
||||
|
||||
return $user;
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Exceptions\Api\PersonalAccessClientIsNotConfiguredException;
|
||||
use App\Http\Requests\V1\ApiToken\ApiTokenStoreRequest;
|
||||
use App\Http\Resources\V1\ApiToken\ApiTokenCollection;
|
||||
use App\Http\Resources\V1\ApiToken\ApiTokenWithAccessTokenResource;
|
||||
use App\Models\Passport\Token;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class ApiTokenController extends Controller
|
||||
{
|
||||
/**
|
||||
* List all api token of the currently authenticated user
|
||||
*
|
||||
* This endpoint is independent of organization.
|
||||
*
|
||||
* @operationId getApiTokens
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function index(): ApiTokenCollection
|
||||
{
|
||||
$user = $this->user();
|
||||
|
||||
$tokens = $user->tokens()
|
||||
->where('client_id', '=', config('passport.personal_access_client.id'))
|
||||
->get();
|
||||
|
||||
return new ApiTokenCollection($tokens);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new api token for the currently authenticated user
|
||||
*
|
||||
* The response will contain the access token that can be used to send authenticated API requests.
|
||||
* Please note that the access token is only shown in this response and cannot be retrieved later.
|
||||
*
|
||||
* @operationId createApiToken
|
||||
*
|
||||
* @throws AuthorizationException|PersonalAccessClientIsNotConfiguredException
|
||||
*/
|
||||
public function store(ApiTokenStoreRequest $request): ApiTokenWithAccessTokenResource
|
||||
{
|
||||
$user = $this->user();
|
||||
|
||||
if (config('passport.personal_access_client.id') === null || config('passport.personal_access_client.secret') === null) {
|
||||
throw new PersonalAccessClientIsNotConfiguredException;
|
||||
}
|
||||
|
||||
$token = $user->createToken($request->getName(), ['*']);
|
||||
/** @var Token $tokenModel */
|
||||
$tokenModel = $token->token;
|
||||
|
||||
return new ApiTokenWithAccessTokenResource($tokenModel, $token->accessToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke an api token
|
||||
*
|
||||
* @operationId revokeApiToken
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
* @throws PersonalAccessClientIsNotConfiguredException
|
||||
*/
|
||||
public function revoke(Token $apiToken): JsonResponse
|
||||
{
|
||||
$user = $this->user();
|
||||
|
||||
if (config('passport.personal_access_client.id') === null || config('passport.personal_access_client.secret') === null) {
|
||||
throw new PersonalAccessClientIsNotConfiguredException;
|
||||
}
|
||||
if ($apiToken->user_id !== $user->getKey()) {
|
||||
throw new AuthorizationException('API token does not belong to user');
|
||||
}
|
||||
if ($apiToken->client_id !== config('passport.personal_access_client.id')) {
|
||||
throw new AuthorizationException('API token is not a personal access token');
|
||||
}
|
||||
|
||||
$apiToken->revoke();
|
||||
|
||||
return response()->json(null, 204);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an api token
|
||||
*
|
||||
* @operationId deleteApiToken
|
||||
*
|
||||
* @throws AuthorizationException|PersonalAccessClientIsNotConfiguredException
|
||||
*/
|
||||
public function destroy(Token $apiToken): JsonResponse
|
||||
{
|
||||
$user = $this->user();
|
||||
|
||||
if (config('passport.personal_access_client.id') === null || config('passport.personal_access_client.secret') === null) {
|
||||
throw new PersonalAccessClientIsNotConfiguredException;
|
||||
}
|
||||
if ($apiToken->user_id !== $user->getKey()) {
|
||||
throw new AuthorizationException('API token does not belong to user');
|
||||
}
|
||||
if ($apiToken->client_id !== config('passport.personal_access_client.id')) {
|
||||
throw new AuthorizationException('API token is not a personal access token');
|
||||
}
|
||||
|
||||
$apiToken->delete();
|
||||
|
||||
return response()->json(null, 204);
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,6 @@ class HealthCheckController extends Controller
|
||||
$response['app_env'] = app()->environment();
|
||||
$response['app_timezone'] = config('app.timezone');
|
||||
$response['app_force_https'] = config('app.force_https');
|
||||
$response['session_secure'] = config('session.secure');
|
||||
$response['trusted_proxies'] = config('trustedproxy.proxies');
|
||||
$headers = $request->headers->all();
|
||||
if (isset($headers['cookie'])) {
|
||||
|
||||
@@ -18,7 +18,7 @@ class Kernel extends HttpKernel
|
||||
* @var array<int, class-string|string>
|
||||
*/
|
||||
protected $middleware = [
|
||||
\App\Http\Middleware\ForceHttps::class,
|
||||
// \App\Http\Middleware\TrustHosts::class,
|
||||
\App\Http\Middleware\TrustProxies::class,
|
||||
\Illuminate\Http\Middleware\HandleCors::class,
|
||||
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ForceHttps
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next, string ...$guards): Response
|
||||
{
|
||||
if (config('app.force_https', false)) {
|
||||
URL::forceScheme('https');
|
||||
$request->server->set('HTTPS', 'on');
|
||||
$request->headers->set('X-Forwarded-Proto', 'https');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
22
app/Http/Middleware/TrustHosts.php
Normal file
22
app/Http/Middleware/TrustHosts.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Http\Middleware\TrustHosts as Middleware;
|
||||
|
||||
class TrustHosts extends Middleware
|
||||
{
|
||||
/**
|
||||
* Get the host patterns that should be trusted.
|
||||
*
|
||||
* @return array<int, string|null>
|
||||
*/
|
||||
public function hosts(): array
|
||||
{
|
||||
return [
|
||||
$this->allSubdomainsOfApplicationUrl(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\V1\ApiToken;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ApiTokenStoreRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, array<string>>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => [
|
||||
'required',
|
||||
'string',
|
||||
'min:1',
|
||||
'max:255',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->input('name');
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Resources\V1\ApiToken;
|
||||
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class ApiTokenCollection extends ResourceCollection
|
||||
{
|
||||
/**
|
||||
* The resource that this resource collects.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $collects = ApiTokenResource::class;
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Resources\V1\ApiToken;
|
||||
|
||||
use App\Http\Resources\V1\BaseResource;
|
||||
use App\Models\Passport\Token;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* @property-read Token $resource
|
||||
*/
|
||||
class ApiTokenResource extends BaseResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, string|bool|int|null|array<string>>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
/** @var string $id ID of the API token, this ID is NOT a UUID */
|
||||
'id' => $this->resource->id,
|
||||
/** @var string $name Name of the API token */
|
||||
'name' => $this->resource->name,
|
||||
/** @var bool $revoked Whether the API token is revoked */
|
||||
'revoked' => $this->resource->revoked,
|
||||
/** @var array<string> $scopes List of scopes that the API token has */
|
||||
'scopes' => $this->resource->scopes,
|
||||
/** @var string $created_at When the API token was created (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z) */
|
||||
'created_at' => $this->formatDateTime($this->resource->created_at),
|
||||
/** @var string|null $expires_at At what time the API token expires (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z) */
|
||||
'expires_at' => $this->formatDateTime($this->resource->expires_at),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Resources\V1\ApiToken;
|
||||
|
||||
use App\Http\Resources\V1\BaseResource;
|
||||
use App\Models\Passport\Token;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* @property-read Token $resource
|
||||
*/
|
||||
class ApiTokenWithAccessTokenResource extends BaseResource
|
||||
{
|
||||
private string $accessToken;
|
||||
|
||||
public function __construct(Token $resource, string $accessToken)
|
||||
{
|
||||
$this->accessToken = $accessToken;
|
||||
parent::__construct($resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, string|bool|int|null|array<string>>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
/** @var string $id ID of the API token, this ID is NOT a UUID */
|
||||
'id' => $this->resource->id,
|
||||
/** @var string $name Name of the API token */
|
||||
'name' => $this->resource->name,
|
||||
/** @var bool $revoked Whether the API token is revoked */
|
||||
'revoked' => $this->resource->revoked,
|
||||
/** @var array<string> $scopes List of scopes that the API token has */
|
||||
'scopes' => $this->resource->scopes,
|
||||
/** @var string $created_at When the API token was created (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z) */
|
||||
'created_at' => $this->formatDateTime($this->resource->created_at),
|
||||
/** @var string|null $expires_at At what time the API token expires (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z) */
|
||||
'expires_at' => $this->formatDateTime($this->resource->expires_at),
|
||||
// Additional fields
|
||||
/** @var string $access_token Access token that can be used to authenticate requests */
|
||||
'access_token' => $this->accessToken,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models\Passport;
|
||||
|
||||
use Laravel\Passport\AuthCode as PassportAuthCode;
|
||||
|
||||
class AuthCode extends PassportAuthCode {}
|
||||
@@ -1,26 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models\Passport;
|
||||
|
||||
use Database\Factories\Passport\ClientFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Laravel\Passport\Client as PassportClient;
|
||||
|
||||
/**
|
||||
* @property string $id
|
||||
* @property string|null $user_id
|
||||
* @property string $name
|
||||
* @property string|null $secret
|
||||
* @property string|null $provider
|
||||
* @property string $redirect
|
||||
* @property bool $personal_access_client
|
||||
* @property bool $password_client
|
||||
* @property bool $revoked
|
||||
*/
|
||||
class Client extends PassportClient
|
||||
{
|
||||
/** @use HasFactory<ClientFactory> */
|
||||
use HasFactory;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models\Passport;
|
||||
|
||||
use Laravel\Passport\PersonalAccessClient as PassportPersonalAccessClient;
|
||||
|
||||
class PersonalAccessClient extends PassportPersonalAccessClient {}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models\Passport;
|
||||
|
||||
use Laravel\Passport\RefreshToken as PassportRefreshToken;
|
||||
|
||||
class RefreshToken extends PassportRefreshToken {}
|
||||
@@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models\Passport;
|
||||
|
||||
use Database\Factories\Passport\TokenFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Laravel\Passport\Token as PassportToken;
|
||||
|
||||
/**
|
||||
* @property string $id
|
||||
* @property null|string $user_id
|
||||
* @property string $client_id
|
||||
* @property null|string $name
|
||||
* @property array<string> $scopes
|
||||
* @property bool $revoked
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property Carbon|null $expires_at
|
||||
*/
|
||||
class Token extends PassportToken
|
||||
{
|
||||
/** @use HasFactory<TokenFactory> */
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* Get the client that the token belongs to.
|
||||
*
|
||||
* @return BelongsTo<Client, Token>
|
||||
*/
|
||||
public function client(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Client::class, 'client_id', 'id');
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ namespace App\Models;
|
||||
use App\Enums\Weekday;
|
||||
use App\Models\Concerns\CustomAuditable;
|
||||
use App\Models\Concerns\HasUuids;
|
||||
use App\Models\Passport\Token;
|
||||
use Database\Factories\UserFactory;
|
||||
use Filament\Models\Contracts\FilamentUser;
|
||||
use Filament\Panel;
|
||||
@@ -28,13 +27,14 @@ use Laravel\Jetstream\HasProfilePhoto;
|
||||
use Laravel\Jetstream\HasTeams;
|
||||
use Laravel\Passport\AuthCode;
|
||||
use Laravel\Passport\HasApiTokens;
|
||||
use Laravel\Passport\Token;
|
||||
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
|
||||
|
||||
/**
|
||||
* @property string $id
|
||||
* @property string $name
|
||||
* @property string $email
|
||||
* @property Carbon|null $email_verified_at
|
||||
* @property string|null $email_verified_at
|
||||
* @property string|null $password
|
||||
* @property string|null $two_factor_secret
|
||||
* @property string $timezone
|
||||
@@ -44,7 +44,6 @@ use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
|
||||
* @property-read Organization|null $currentOrganization
|
||||
* @property-read Organization|null $currentTeam
|
||||
* @property-read string $profile_photo_url
|
||||
* @property-read Collection<int, Token> $tokens
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property string|null $current_team_id
|
||||
@@ -197,17 +196,6 @@ class User extends Authenticatable implements AuditableContract, FilamentUser, M
|
||||
return $this->hasMany(AuthCode::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the access tokens for the user.
|
||||
*
|
||||
* @return HasMany<Token>
|
||||
*/
|
||||
public function tokens(): HasMany
|
||||
{
|
||||
return $this->hasMany(Token::class, 'user_id')
|
||||
->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder<User> $builder
|
||||
*/
|
||||
|
||||
@@ -9,7 +9,6 @@ use App\Models\FailedJob;
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Models\OrganizationInvitation;
|
||||
use App\Models\Passport\Token;
|
||||
use App\Models\Project;
|
||||
use App\Models\ProjectMember;
|
||||
use App\Models\Tag;
|
||||
@@ -30,6 +29,7 @@ use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
@@ -90,6 +90,12 @@ class AppServiceProvider extends ServiceProvider
|
||||
);
|
||||
});
|
||||
|
||||
if (config('app.force_https', false)) {
|
||||
URL::forceScheme('https');
|
||||
request()->server->set('HTTPS', 'on');
|
||||
request()->headers->set('X-Forwarded-Proto', 'https');
|
||||
}
|
||||
|
||||
$this->app->scoped(PermissionStore::class, function (Application $app): PermissionStore {
|
||||
return new PermissionStore;
|
||||
});
|
||||
@@ -101,6 +107,5 @@ class AppServiceProvider extends ServiceProvider
|
||||
// Routing
|
||||
Route::model('member', Member::class);
|
||||
Route::model('invitation', OrganizationInvitation::class);
|
||||
Route::model('apiToken', Token::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,6 @@ declare(strict_types=1);
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Models\Organization;
|
||||
use App\Models\Passport\AuthCode;
|
||||
use App\Models\Passport\Client;
|
||||
use App\Models\Passport\PersonalAccessClient;
|
||||
use App\Models\Passport\RefreshToken;
|
||||
use App\Models\Passport\Token;
|
||||
use App\Policies\OrganizationPolicy;
|
||||
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
|
||||
use Laravel\Jetstream\Jetstream;
|
||||
@@ -47,16 +42,6 @@ class AuthServiceProvider extends ServiceProvider
|
||||
// 'delete',
|
||||
]);
|
||||
|
||||
Passport::useTokenModel(Token::class);
|
||||
Passport::useRefreshTokenModel(RefreshToken::class);
|
||||
Passport::useAuthCodeModel(AuthCode::class);
|
||||
Passport::useClientModel(Client::class);
|
||||
Passport::usePersonalAccessClientModel(PersonalAccessClient::class);
|
||||
|
||||
// Passport::tokensExpireIn(now()->addDays(15));
|
||||
// Passport::refreshTokensExpireIn(now()->addDays(30));
|
||||
Passport::personalAccessTokensExpireIn(now()->addMonths(12));
|
||||
|
||||
// same as passport default above
|
||||
Jetstream::defaultApiTokenPermissions(['read']);
|
||||
|
||||
|
||||
@@ -69,9 +69,6 @@ class AdminPanelProvider extends PanelProvider
|
||||
NavigationGroup::make()
|
||||
->label('System')
|
||||
->collapsed(),
|
||||
NavigationGroup::make()
|
||||
->label('Auth')
|
||||
->collapsed(),
|
||||
])
|
||||
->middleware([
|
||||
EncryptCookies::class,
|
||||
|
||||
@@ -12,12 +12,11 @@ use App\Models\Organization;
|
||||
use App\Models\ProjectMember;
|
||||
use App\Models\TimeEntry;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class UserService
|
||||
{
|
||||
public function createUser(string $name, string $email, string $password, string $timezone, Weekday $weekStart, string $currency, bool $verifyEmail = false): User
|
||||
public function createUser(string $name, string $email, string $password, string $timezone, Weekday $weekStart, string $currency): User
|
||||
{
|
||||
$user = new User;
|
||||
$user->name = $name;
|
||||
@@ -25,9 +24,6 @@ class UserService
|
||||
$user->password = Hash::make($password);
|
||||
$user->timezone = $timezone;
|
||||
$user->week_start = $weekStart;
|
||||
if ($verifyEmail) {
|
||||
$user->email_verified_at = Carbon::now();
|
||||
}
|
||||
$user->save();
|
||||
|
||||
$organization = new Organization;
|
||||
|
||||
116
composer.lock
generated
116
composer.lock
generated
@@ -128,16 +128,16 @@
|
||||
},
|
||||
{
|
||||
"name": "aws/aws-sdk-php",
|
||||
"version": "3.339.7",
|
||||
"version": "3.339.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/aws/aws-sdk-php.git",
|
||||
"reference": "7b7e48ce7970c0416c5fda045df7b93948fbf643"
|
||||
"reference": "cc0b21de3b1eaabb7d0a1ed4f3f067bad4be5f31"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/7b7e48ce7970c0416c5fda045df7b93948fbf643",
|
||||
"reference": "7b7e48ce7970c0416c5fda045df7b93948fbf643",
|
||||
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/cc0b21de3b1eaabb7d0a1ed4f3f067bad4be5f31",
|
||||
"reference": "cc0b21de3b1eaabb7d0a1ed4f3f067bad4be5f31",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -220,9 +220,9 @@
|
||||
"support": {
|
||||
"forum": "https://github.com/aws/aws-sdk-php/discussions",
|
||||
"issues": "https://github.com/aws/aws-sdk-php/issues",
|
||||
"source": "https://github.com/aws/aws-sdk-php/tree/3.339.7"
|
||||
"source": "https://github.com/aws/aws-sdk-php/tree/3.339.6"
|
||||
},
|
||||
"time": "2025-02-05T19:06:15+00:00"
|
||||
"time": "2025-02-04T19:03:40+00:00"
|
||||
},
|
||||
{
|
||||
"name": "bacon/bacon-qr-code",
|
||||
@@ -692,16 +692,16 @@
|
||||
},
|
||||
{
|
||||
"name": "composer/class-map-generator",
|
||||
"version": "1.6.0",
|
||||
"version": "1.5.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/composer/class-map-generator.git",
|
||||
"reference": "ffe442c5974c44a9343e37a0abcb1cc37319f5b9"
|
||||
"reference": "4b0a223cf5be7c9ee7e0ef1bc7db42b4a97c9915"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/composer/class-map-generator/zipball/ffe442c5974c44a9343e37a0abcb1cc37319f5b9",
|
||||
"reference": "ffe442c5974c44a9343e37a0abcb1cc37319f5b9",
|
||||
"url": "https://api.github.com/repos/composer/class-map-generator/zipball/4b0a223cf5be7c9ee7e0ef1bc7db42b4a97c9915",
|
||||
"reference": "4b0a223cf5be7c9ee7e0ef1bc7db42b4a97c9915",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -745,7 +745,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/composer/class-map-generator/issues",
|
||||
"source": "https://github.com/composer/class-map-generator/tree/1.6.0"
|
||||
"source": "https://github.com/composer/class-map-generator/tree/1.5.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -761,7 +761,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-02-05T10:05:34+00:00"
|
||||
"time": "2024-11-25T16:11:06+00:00"
|
||||
},
|
||||
{
|
||||
"name": "composer/composer",
|
||||
@@ -1519,16 +1519,16 @@
|
||||
},
|
||||
{
|
||||
"name": "dedoc/scramble",
|
||||
"version": "v0.12.5",
|
||||
"version": "v0.12.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dedoc/scramble.git",
|
||||
"reference": "de055584b61338dd6de6cc1cc20d79da6a442230"
|
||||
"reference": "9482d3c6285ebb16a067bd28b01e94899e1fddd8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/dedoc/scramble/zipball/de055584b61338dd6de6cc1cc20d79da6a442230",
|
||||
"reference": "de055584b61338dd6de6cc1cc20d79da6a442230",
|
||||
"url": "https://api.github.com/repos/dedoc/scramble/zipball/9482d3c6285ebb16a067bd28b01e94899e1fddd8",
|
||||
"reference": "9482d3c6285ebb16a067bd28b01e94899e1fddd8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1583,7 +1583,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/dedoc/scramble/issues",
|
||||
"source": "https://github.com/dedoc/scramble/tree/v0.12.5"
|
||||
"source": "https://github.com/dedoc/scramble/tree/v0.12.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1591,7 +1591,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-02-05T11:22:52+00:00"
|
||||
"time": "2025-02-03T12:23:33+00:00"
|
||||
},
|
||||
{
|
||||
"name": "defuse/php-encryption",
|
||||
@@ -2249,16 +2249,16 @@
|
||||
},
|
||||
{
|
||||
"name": "filament/actions",
|
||||
"version": "v3.2.137",
|
||||
"version": "v3.2.136",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/filamentphp/actions.git",
|
||||
"reference": "0eee8f8eeea4422c279c2622442b2a9a101d558c"
|
||||
"reference": "cdefacc18993050cdd37e8e980ec66ca4109ae9a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/filamentphp/actions/zipball/0eee8f8eeea4422c279c2622442b2a9a101d558c",
|
||||
"reference": "0eee8f8eeea4422c279c2622442b2a9a101d558c",
|
||||
"url": "https://api.github.com/repos/filamentphp/actions/zipball/cdefacc18993050cdd37e8e980ec66ca4109ae9a",
|
||||
"reference": "cdefacc18993050cdd37e8e980ec66ca4109ae9a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2298,20 +2298,20 @@
|
||||
"issues": "https://github.com/filamentphp/filament/issues",
|
||||
"source": "https://github.com/filamentphp/filament"
|
||||
},
|
||||
"time": "2025-02-06T11:47:20+00:00"
|
||||
"time": "2025-01-31T11:08:24+00:00"
|
||||
},
|
||||
{
|
||||
"name": "filament/filament",
|
||||
"version": "v3.2.137",
|
||||
"version": "v3.2.136",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/filamentphp/panels.git",
|
||||
"reference": "407b52ee57feed6f67706af2a18bc81d32aea3f1"
|
||||
"reference": "c92daf4b6e4b478be5d32d5e1b404ba92bb45414"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/filamentphp/panels/zipball/407b52ee57feed6f67706af2a18bc81d32aea3f1",
|
||||
"reference": "407b52ee57feed6f67706af2a18bc81d32aea3f1",
|
||||
"url": "https://api.github.com/repos/filamentphp/panels/zipball/c92daf4b6e4b478be5d32d5e1b404ba92bb45414",
|
||||
"reference": "c92daf4b6e4b478be5d32d5e1b404ba92bb45414",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2363,20 +2363,20 @@
|
||||
"issues": "https://github.com/filamentphp/filament/issues",
|
||||
"source": "https://github.com/filamentphp/filament"
|
||||
},
|
||||
"time": "2025-02-06T11:47:25+00:00"
|
||||
"time": "2025-01-31T11:08:29+00:00"
|
||||
},
|
||||
{
|
||||
"name": "filament/forms",
|
||||
"version": "v3.2.137",
|
||||
"version": "v3.2.136",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/filamentphp/forms.git",
|
||||
"reference": "6ab0419fd3599aacdd7b2a8f69641cab6f158674"
|
||||
"reference": "8856b3b3714a0efae65d9f817fcc1934b6f34fda"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/filamentphp/forms/zipball/6ab0419fd3599aacdd7b2a8f69641cab6f158674",
|
||||
"reference": "6ab0419fd3599aacdd7b2a8f69641cab6f158674",
|
||||
"url": "https://api.github.com/repos/filamentphp/forms/zipball/8856b3b3714a0efae65d9f817fcc1934b6f34fda",
|
||||
"reference": "8856b3b3714a0efae65d9f817fcc1934b6f34fda",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2419,11 +2419,11 @@
|
||||
"issues": "https://github.com/filamentphp/filament/issues",
|
||||
"source": "https://github.com/filamentphp/filament"
|
||||
},
|
||||
"time": "2025-02-06T11:47:21+00:00"
|
||||
"time": "2025-01-31T11:08:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "filament/infolists",
|
||||
"version": "v3.2.137",
|
||||
"version": "v3.2.136",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/filamentphp/infolists.git",
|
||||
@@ -2474,7 +2474,7 @@
|
||||
},
|
||||
{
|
||||
"name": "filament/notifications",
|
||||
"version": "v3.2.137",
|
||||
"version": "v3.2.136",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/filamentphp/notifications.git",
|
||||
@@ -2526,7 +2526,7 @@
|
||||
},
|
||||
{
|
||||
"name": "filament/support",
|
||||
"version": "v3.2.137",
|
||||
"version": "v3.2.136",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/filamentphp/support.git",
|
||||
@@ -2585,16 +2585,16 @@
|
||||
},
|
||||
{
|
||||
"name": "filament/tables",
|
||||
"version": "v3.2.137",
|
||||
"version": "v3.2.136",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/filamentphp/tables.git",
|
||||
"reference": "d420b0b0cbe605c712ab054a43d51baf5095dc87"
|
||||
"reference": "1b1d9c3b837c11408ad28240dc9e3e33340d870b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/filamentphp/tables/zipball/d420b0b0cbe605c712ab054a43d51baf5095dc87",
|
||||
"reference": "d420b0b0cbe605c712ab054a43d51baf5095dc87",
|
||||
"url": "https://api.github.com/repos/filamentphp/tables/zipball/1b1d9c3b837c11408ad28240dc9e3e33340d870b",
|
||||
"reference": "1b1d9c3b837c11408ad28240dc9e3e33340d870b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2633,11 +2633,11 @@
|
||||
"issues": "https://github.com/filamentphp/filament/issues",
|
||||
"source": "https://github.com/filamentphp/filament"
|
||||
},
|
||||
"time": "2025-02-06T11:47:34+00:00"
|
||||
"time": "2025-01-31T11:08:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "filament/widgets",
|
||||
"version": "v3.2.137",
|
||||
"version": "v3.2.136",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/filamentphp/widgets.git",
|
||||
@@ -9112,27 +9112,27 @@
|
||||
},
|
||||
{
|
||||
"name": "spatie/laravel-package-tools",
|
||||
"version": "1.19.0",
|
||||
"version": "1.18.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/spatie/laravel-package-tools.git",
|
||||
"reference": "1c9c30ac6a6576b8d15c6c37b6cf23d748df2faa"
|
||||
"reference": "ba67eee37d86ed775dab7dad58a7cbaf9a6cfe78"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/1c9c30ac6a6576b8d15c6c37b6cf23d748df2faa",
|
||||
"reference": "1c9c30ac6a6576b8d15c6c37b6cf23d748df2faa",
|
||||
"url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/ba67eee37d86ed775dab7dad58a7cbaf9a6cfe78",
|
||||
"reference": "ba67eee37d86ed775dab7dad58a7cbaf9a6cfe78",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/contracts": "^9.28|^10.0|^11.0|^12.0",
|
||||
"illuminate/contracts": "^9.28|^10.0|^11.0",
|
||||
"php": "^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "^1.5",
|
||||
"orchestra/testbench": "^7.7|^8.0|^9.0|^10.0",
|
||||
"pestphp/pest": "^1.23|^2.1|^3.1",
|
||||
"phpunit/phpunit": "^9.5.24|^10.5|^11.5",
|
||||
"orchestra/testbench": "^7.7|^8.0|^9.0",
|
||||
"pestphp/pest": "^1.22|^2",
|
||||
"phpunit/phpunit": "^9.5.24|^10.5",
|
||||
"spatie/pest-plugin-test-time": "^1.1|^2.2"
|
||||
},
|
||||
"type": "library",
|
||||
@@ -9160,7 +9160,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/spatie/laravel-package-tools/issues",
|
||||
"source": "https://github.com/spatie/laravel-package-tools/tree/1.19.0"
|
||||
"source": "https://github.com/spatie/laravel-package-tools/tree/1.18.3"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -9168,7 +9168,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-02-06T14:58:20+00:00"
|
||||
"time": "2025-01-22T08:51:18+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/regex",
|
||||
@@ -14188,16 +14188,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpunit/phpunit",
|
||||
"version": "11.5.7",
|
||||
"version": "11.5.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
||||
"reference": "e1cb706f019e2547039ca2c839898cd5f557ee5d"
|
||||
"reference": "3c3ae14c90f244cdda95028c3e469028e8d1c02c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e1cb706f019e2547039ca2c839898cd5f557ee5d",
|
||||
"reference": "e1cb706f019e2547039ca2c839898cd5f557ee5d",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3c3ae14c90f244cdda95028c3e469028e8d1c02c",
|
||||
"reference": "3c3ae14c90f244cdda95028c3e469028e8d1c02c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -14269,7 +14269,7 @@
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
|
||||
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.7"
|
||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -14285,7 +14285,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-02-06T16:10:05+00:00"
|
||||
"time": "2025-01-31T07:03:30+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/cli-parser",
|
||||
|
||||
@@ -168,7 +168,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'secure' => env('SESSION_SECURE_COOKIE', env('APP_FORCE_HTTPS')),
|
||||
'secure' => env('SESSION_SECURE_COOKIE'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Factories\Passport;
|
||||
|
||||
use App\Models\Passport\Client;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends Factory<Client>
|
||||
*/
|
||||
class ClientFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->faker->uuid,
|
||||
'user_id' => null,
|
||||
'name' => $this->faker->company(),
|
||||
'secret' => $this->faker->regexify('[A-Za-z]{40}'),
|
||||
'provider' => 'users',
|
||||
'redirect' => $this->faker->url(),
|
||||
'personal_access_client' => false,
|
||||
'password_client' => false,
|
||||
'revoked' => false,
|
||||
'created_at' => $this->faker->dateTime(),
|
||||
'updated_at' => $this->faker->dateTime(),
|
||||
];
|
||||
}
|
||||
|
||||
public function personalAccessClient(): self
|
||||
{
|
||||
return $this->state(function (array $attributes) {
|
||||
return [
|
||||
'personal_access_client' => true,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
public function forUser(User $user): self
|
||||
{
|
||||
return $this->state(function (array $attributes) use ($user): array {
|
||||
return [
|
||||
'user_id' => $user->getKey(),
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Factories\Passport;
|
||||
|
||||
use App\Models\Passport\Client;
|
||||
use App\Models\Passport\Token;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends Factory<Token>
|
||||
*/
|
||||
class TokenFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->faker->uuid,
|
||||
'user_id' => null,
|
||||
'client_id' => $this->faker->uuid,
|
||||
'name' => null,
|
||||
'scopes' => [],
|
||||
'revoked' => false,
|
||||
'created_at' => $this->faker->dateTime,
|
||||
'updated_at' => $this->faker->dateTime,
|
||||
'expires_at' => $this->faker->dateTime,
|
||||
];
|
||||
}
|
||||
|
||||
public function forUser(User $user): self
|
||||
{
|
||||
return $this->state(function (array $attributes) use ($user): array {
|
||||
return [
|
||||
'user_id' => $user->getKey(),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
public function forClient(Client $client): self
|
||||
{
|
||||
return $this->state(function (array $attributes) use ($client): array {
|
||||
return [
|
||||
'client_id' => $client->getKey(),
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -34,29 +34,6 @@ class DatabaseSeeder extends Seeder
|
||||
public function run(): void
|
||||
{
|
||||
$this->deleteAll();
|
||||
|
||||
app(ClientRepository::class)->create(
|
||||
null,
|
||||
'desktop',
|
||||
'solidtime://oauth/callback',
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
);
|
||||
|
||||
$personalAccessClient = new PassportClient;
|
||||
$personalAccessClient->id = config('passport.personal_access_client.id');
|
||||
$personalAccessClient->secret = config('passport.personal_access_client.secret');
|
||||
$personalAccessClient->name = 'API';
|
||||
$personalAccessClient->redirect = 'http://localhost';
|
||||
$personalAccessClient->user_id = null;
|
||||
$personalAccessClient->revoked = false;
|
||||
$personalAccessClient->provider = null;
|
||||
$personalAccessClient->personal_access_client = true;
|
||||
$personalAccessClient->password_client = false;
|
||||
$personalAccessClient->save();
|
||||
|
||||
$userWithMultipleOrganizations = User::factory()->withPersonalOrganization()->create([
|
||||
'name' => 'Mister Overemployed',
|
||||
'email' => 'overemployed@acme.test',
|
||||
@@ -78,8 +55,6 @@ class DatabaseSeeder extends Seeder
|
||||
'name' => 'Acme Manager',
|
||||
'email' => 'test@example.com',
|
||||
]);
|
||||
$userAcmeManager->createToken('Testing Token 1')->accessToken;
|
||||
$userAcmeManager->createToken('Testing Token 2')->accessToken;
|
||||
$userAcmeAdmin = User::factory()->withPersonalOrganization()->create([
|
||||
'name' => 'Acme Admin',
|
||||
'email' => 'admin@acme.test',
|
||||
@@ -184,6 +159,15 @@ class DatabaseSeeder extends Seeder
|
||||
'email' => 'admin@example.com',
|
||||
]);
|
||||
|
||||
app(ClientRepository::class)->create(
|
||||
null,
|
||||
'desktop',
|
||||
'solidtime://oauth/callback',
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
private function deleteAll(): void
|
||||
|
||||
@@ -109,7 +109,7 @@ services:
|
||||
- sail
|
||||
- reverse-proxy
|
||||
playwright:
|
||||
image: mcr.microsoft.com/playwright:v1.50.0-jammy
|
||||
image: mcr.microsoft.com/playwright:v1.46.1-jammy
|
||||
command: ['npx', 'playwright', 'test', '--ui-port=8080', '--ui-host=0.0.0.0']
|
||||
working_dir: /src
|
||||
extra_hosts:
|
||||
|
||||
@@ -182,7 +182,7 @@ COPY --chown=${USER}:${USER} ${DOCKER_FILES_BASE_PATH}deployment/php.ini /lib/ph
|
||||
# && composer clear-cache
|
||||
|
||||
RUN cat .env
|
||||
#RUN php artisan env
|
||||
RUN php artisan env
|
||||
RUN php artisan storage:link
|
||||
|
||||
RUN chmod +x /usr/local/bin/start-container
|
||||
|
||||
@@ -1,69 +1,25 @@
|
||||
import {test, expect} from '../playwright/fixtures';
|
||||
import {PLAYWRIGHT_BASE_URL} from '../playwright/config';
|
||||
import { test, expect } from '../playwright/fixtures';
|
||||
import { PLAYWRIGHT_BASE_URL } from '../playwright/config';
|
||||
|
||||
test('test that user name can be updated', async ({page}) => {
|
||||
test('test that user name can be updated', async ({ page }) => {
|
||||
await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile');
|
||||
await page.getByLabel('Name', {exact: true} ).fill('NEW NAME');
|
||||
await page.getByLabel('Name').fill('NEW NAME');
|
||||
await Promise.all([
|
||||
page.getByRole('button', {name: 'Save'}).first().click(),
|
||||
page.getByRole('button', { name: 'Save' }).first().click(),
|
||||
page.waitForResponse('**/user/profile-information'),
|
||||
]);
|
||||
await page.reload();
|
||||
await expect(page.getByLabel('Name', {exact: true})).toHaveValue('NEW NAME');
|
||||
await expect(page.getByLabel('Name')).toHaveValue('NEW NAME');
|
||||
});
|
||||
|
||||
test.skip('test that user email can be updated', async ({page}) => {
|
||||
test.skip('test that user email can be updated', async ({ page }) => {
|
||||
// this does not work because of email verification currently
|
||||
await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile');
|
||||
const emailId = Math.round(Math.random() * 10000);
|
||||
await page.getByLabel('Email').fill(`newemail+${emailId}@test.com`);
|
||||
await page.getByRole('button', {name: 'Save'}).first().click();
|
||||
await page.getByRole('button', { name: 'Save' }).first().click();
|
||||
await page.reload();
|
||||
await expect(page.getByLabel('Email')).toHaveValue(
|
||||
`newemail+${emailId}@test.com`
|
||||
);
|
||||
});
|
||||
|
||||
async function createNewApiToken(page) {
|
||||
await page.getByLabel('API Key Name').fill('NEW API KEY');
|
||||
await Promise.all([
|
||||
page.getByRole('button', {name: 'Create API Key'}).click(),
|
||||
page.waitForResponse('**/users/me/api-tokens')
|
||||
]);
|
||||
|
||||
await expect(page.locator('body')).toContainText('API Token created successfully');
|
||||
await page.getByRole('dialog').getByText('Close').click();
|
||||
await expect(page.locator('body')).toContainText('NEW API KEY');
|
||||
}
|
||||
|
||||
test('test that user can create an API key', async ({page}) => {
|
||||
await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile');
|
||||
await createNewApiToken(page);
|
||||
});
|
||||
|
||||
test('test that user can delete an API key', async ({page}) => {
|
||||
await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile');
|
||||
await createNewApiToken(page);
|
||||
page.getByLabel('Delete API Token NEW API KEY').click();
|
||||
await expect(page.getByRole('dialog')).toContainText('Are you sure you would like to delete this API token?');
|
||||
await Promise.all([
|
||||
page.getByRole('dialog').getByRole('button', {name: 'Delete'}).click(),
|
||||
page.waitForResponse('**/users/me/api-tokens')
|
||||
]);
|
||||
await expect(page.locator('body')).not.toContainText('NEW API KEY');
|
||||
});
|
||||
|
||||
|
||||
test('test that user can revoke an API key', async ({page}) => {
|
||||
await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile');
|
||||
await createNewApiToken(page);
|
||||
page.getByLabel('Revoke API Token NEW API KEY').click();
|
||||
await expect(page.getByRole('dialog')).toContainText('Are you sure you would like to revoke this API token?');
|
||||
await Promise.all([
|
||||
page.getByRole('dialog').getByRole('button', {name: 'Revoke'}).click(),
|
||||
page.waitForResponse('**/users/me/api-tokens')
|
||||
]);
|
||||
await expect(page.getByRole('button', {name: 'Revoke'})).toBeHidden();
|
||||
await expect(page.locator('body')).toContainText('NEW API KEY');
|
||||
await expect(page.locator('body')).toContainText('Revoked');
|
||||
});
|
||||
|
||||
@@ -191,7 +191,7 @@ test('test that updating a the start of an existing time entry in the overview w
|
||||
'time_entry_range_selector'
|
||||
);
|
||||
await timeEntryRangeElement.click();
|
||||
await page.getByTestId('time_entry_range_start').first().fill('1');
|
||||
await page.getByTestId('time_picker_input').first().fill('1');
|
||||
await Promise.all([
|
||||
page.waitForResponse(async (response) => {
|
||||
return (
|
||||
@@ -204,7 +204,10 @@ test('test that updating a the start of an existing time entry in the overview w
|
||||
(await response.json()).data.end !== null
|
||||
);
|
||||
}),
|
||||
page.getByTestId('time_entry_range_end').press('Enter'),
|
||||
page
|
||||
.getByTestId('time_entry_range_end')
|
||||
.getByTestId('time_picker_input')
|
||||
.press('Enter'),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ test('test that starting and updating the time while running works', async ({
|
||||
JSON.stringify([])
|
||||
);
|
||||
}),
|
||||
page.getByTestId('time_entry_time').press('Enter'),
|
||||
page.getByTestId('time_entry_time').press('Tab'),
|
||||
]);
|
||||
|
||||
await expect(page.getByTestId('time_entry_time')).toHaveValue(/00:20/);
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import eslint from '@eslint/js';
|
||||
import eslintConfigPrettier from 'eslint-config-prettier';
|
||||
import eslintPluginVue from 'eslint-plugin-vue';
|
||||
import globals from 'globals';
|
||||
import typescriptEslint from 'typescript-eslint';
|
||||
import unusedImports from "eslint-plugin-unused-imports";
|
||||
|
||||
export default typescriptEslint.config(
|
||||
{ ignores: ['*.d.ts', '**/coverage', '**/dist'] },
|
||||
{
|
||||
extends: [
|
||||
eslint.configs.recommended,
|
||||
...typescriptEslint.configs.recommended,
|
||||
...eslintPluginVue.configs['flat/recommended'],
|
||||
],
|
||||
files: ['**/*.{ts,vue,js}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
parser: typescriptEslint.parser,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
"unused-imports": unusedImports,
|
||||
},
|
||||
rules: {
|
||||
"vue/multi-word-component-names": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"unused-imports/no-unused-vars": "error",
|
||||
},
|
||||
},
|
||||
eslintConfigPrettier
|
||||
);
|
||||
@@ -12,7 +12,6 @@ use App\Exceptions\Api\OnlyOwnerCanChangeOwnership;
|
||||
use App\Exceptions\Api\OrganizationHasNoSubscriptionButMultipleMembersException;
|
||||
use App\Exceptions\Api\OrganizationNeedsAtLeastOneOwner;
|
||||
use App\Exceptions\Api\PdfRendererIsNotConfiguredException;
|
||||
use App\Exceptions\Api\PersonalAccessClientIsNotConfiguredException;
|
||||
use App\Exceptions\Api\TimeEntryCanNotBeRestartedApiException;
|
||||
use App\Exceptions\Api\TimeEntryStillRunningApiException;
|
||||
use App\Exceptions\Api\UserIsAlreadyMemberOfOrganizationApiException;
|
||||
@@ -38,7 +37,6 @@ return [
|
||||
OrganizationHasNoSubscriptionButMultipleMembersException::KEY => 'Organization has no subscription but multiple members',
|
||||
PdfRendererIsNotConfiguredException::KEY => 'PDF renderer is not configured',
|
||||
FeatureIsNotAvailableInFreePlanApiException::KEY => 'Feature is not available in free plan',
|
||||
PersonalAccessClientIsNotConfiguredException::KEY => 'Personal access client is not configured',
|
||||
],
|
||||
'unknown_error_in_admin_panel' => 'An unknown error occurred. Please check the logs.',
|
||||
];
|
||||
|
||||
@@ -5,20 +5,18 @@ declare(strict_types=1);
|
||||
return [
|
||||
'clockify_time_entries' => [
|
||||
'name' => 'Clockify Time Entries',
|
||||
'description' => '1. First make sure that you set the Date format to "MM/DD/YYYY" and the Time format to "12-hour" in the user settings.<br>'.
|
||||
'2. In the same preferences page change the language of Clockfiy to English.<br>'.
|
||||
'3. Go to REPORTS -> TIME -> Detailed in the navigation on the left. <br>'.
|
||||
'4. Now select the date range that you want to export in the right top. '.
|
||||
'description' => '1. First make sure that you set the Date format to "MM/DD/YYYY" and the Time format to "12-hour" in the user settings.<br> '.
|
||||
'2. Go to REPORTS -> TIME -> Detailed in the navigation on the left. <br>'.
|
||||
'3. Now select the date range that you want to export in the right top. '.
|
||||
'It is currently not possible to select more than one year. You can export each year separately and import them one after another .'.
|
||||
'<br> 4. Now click Export -> Save as CSV. The Export dropdown is in the header of the export table left of the printer symbol. '.
|
||||
'<br><br>Before you import make sure that the Timezone settings in Clockify are the same as in solidtime.',
|
||||
],
|
||||
'clockify_projects' => [
|
||||
'name' => 'Clockify Projects',
|
||||
'description' => '1. Make sure to set the language of Clockify to English in "Preferences -> General".<br>'.
|
||||
'2. Go to PROJECTS in the navigation on the left.<br> '.
|
||||
'3. Now click on the three dots on the right of the project that you want to export and select Export.<br> '.
|
||||
'4. Now click Export -> Save as CSV. The Export dropdown is in the header of the export table in the top right corner.',
|
||||
'description' => '1. Go to PROJECTS in the navigation on the left.<br> '.
|
||||
'2. Now click on the three dots on the right of the project that you want to export and select Export.<br> '.
|
||||
'3. Now click Export -> Save as CSV. The Export dropdown is in the header of the export table in the top right corner.',
|
||||
],
|
||||
'toggl_data_importer' => [
|
||||
'name' => 'Toggl Data Importer',
|
||||
|
||||
3414
package-lock.json
generated
3414
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
41
package.json
41
package.json
@@ -4,60 +4,53 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint resources/js",
|
||||
"lint:fix": "eslint --fix resources/js",
|
||||
"lint": "eslint --ext .js,.vue,.ts --ignore-path .gitignore resources/js",
|
||||
"lint:fix": "eslint --fix --ext .js,.vue,.ts --ignore-path .gitignore resources/js",
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"test:e2e": "rm -rf test-results/.auth && npx playwright test",
|
||||
"zod:generate": "npx openapi-zod-client http://localhost:80/docs/api.json --output resources/js/packages/api/src/openapi.json.client.ts --base-url /api"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "^9.19.0",
|
||||
"@inertiajs/vue3": "^1.0.0",
|
||||
"@playwright/test": "^1.41.1",
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@types/node": "^22.10.10",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@types/node": "^20.11.5",
|
||||
"@vitejs/plugin-vue": "^4.5.0",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"axios": "^1.6.4",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"eslint-plugin-unused-imports": "^3.1.0",
|
||||
"laravel-vite-plugin": "^1.0.0",
|
||||
"openapi-zod-client": "^1.16.2",
|
||||
"postcss": "^8.4.47",
|
||||
"postcss-nesting": "^12.1.5",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^6.0.11",
|
||||
"vite-plugin-checker": "^0.8.0",
|
||||
"vue": "^3.5.0",
|
||||
"vue-tsc": "^2.2.0"
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.0",
|
||||
"vite-plugin-checker": "^0.7.2",
|
||||
"vue": "^3.4.0",
|
||||
"vue-tsc": "^2.0.28"
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.6.0",
|
||||
"@floating-ui/vue": "^1.0.6",
|
||||
"@heroicons/vue": "^2.1.1",
|
||||
"@rushstack/eslint-patch": "^1.10.5",
|
||||
"@rushstack/eslint-patch": "^1.7.0",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tanstack/vue-query": "^5.56.2",
|
||||
"@tanstack/vue-query-devtools": "^5.58.0",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vue/eslint-config-typescript": "^14.3.0",
|
||||
"@vueuse/core": "^12.5.0",
|
||||
"@vueuse/integrations": "^12.5.0",
|
||||
"@vue/eslint-config-prettier": "^9.0.0",
|
||||
"@vue/eslint-config-typescript": "^13.0.0",
|
||||
"@vueuse/core": "^10.11.0",
|
||||
"@vueuse/integrations": "^11.1.0",
|
||||
"dayjs": "^1.11.11",
|
||||
"echarts": "^5.5.0",
|
||||
"focus-trap": "^7.6.0",
|
||||
"parse-duration": "^2.0.1",
|
||||
"parse-duration": "^1.1.0",
|
||||
"pinia": "^2.1.7",
|
||||
"radix-vue": "^1.9.6",
|
||||
"tailwind-merge": "^2.2.1",
|
||||
"vue-echarts": "^7.0.3"
|
||||
},
|
||||
"overrides": {
|
||||
"vite-plugin-checker": {
|
||||
"vue-tsc": "$vue-tsc"
|
||||
}
|
||||
"vue-echarts": "^6.7.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,8 +29,6 @@
|
||||
</source>
|
||||
<php>
|
||||
<env name="APP_ENV" value="testing"/>
|
||||
<env name="APP_FORCE_HTTPS" value="false"/>
|
||||
<env name="TRUSTED_PROXIES" value="0.0.0.0/0,2000:0:0:0:0:0:0:0/3"/>
|
||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||
<env name="CACHE_DRIVER" value="array"/>
|
||||
<env name="DB_CONNECTION" value="pgsql_test"/>
|
||||
@@ -41,7 +39,5 @@
|
||||
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||
<env name="AUDITING_ENABLED" value="true"/>
|
||||
<env name="NEWSLETTER_URL" value="null"/>
|
||||
<env name="PASSPORT_PERSONAL_ACCESS_CLIENT_ID" value="null"/>
|
||||
<env name="PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET" value="null"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
|
||||
@@ -112,7 +112,7 @@ const showBlackFridayBanner = computed(() => {
|
||||
<span>Upgrade now</span>
|
||||
</div>
|
||||
</Link>
|
||||
<button class="p-1" @click="hideBlackFridayBanner = true">
|
||||
<button @click="hideBlackFridayBanner = true" class="p-1">
|
||||
<XMarkIcon
|
||||
class="w-4 opacity-50 hover:opacity-100"></XMarkIcon>
|
||||
</button>
|
||||
@@ -142,7 +142,7 @@ const showBlackFridayBanner = computed(() => {
|
||||
<span>Upgrade now</span>
|
||||
</div>
|
||||
</Link>
|
||||
<button class="p-1" @click="hideTrialBanner = true">
|
||||
<button @click="hideTrialBanner = true" class="p-1">
|
||||
<XMarkIcon
|
||||
class="w-4 opacity-50 hover:opacity-100"></XMarkIcon>
|
||||
</button>
|
||||
@@ -174,7 +174,7 @@ const showBlackFridayBanner = computed(() => {
|
||||
<span>Upgrade now</span>
|
||||
</div>
|
||||
</Link>
|
||||
<button class="p-1" @click="hideBlockedBanner = true">
|
||||
<button @click="hideBlockedBanner = true" class="p-1">
|
||||
<XMarkIcon
|
||||
class="w-4 opacity-50 hover:opacity-100"></XMarkIcon>
|
||||
</button>
|
||||
@@ -206,7 +206,7 @@ const showBlackFridayBanner = computed(() => {
|
||||
<span>Upgrade now</span>
|
||||
</div>
|
||||
</Link>
|
||||
<button class="p-1" @click="hideFreeUpgradeBanner = true">
|
||||
<button @click="hideFreeUpgradeBanner = true" class="p-1">
|
||||
<XMarkIcon
|
||||
class="w-4 opacity-50 hover:opacity-100"></XMarkIcon>
|
||||
</button>
|
||||
|
||||
@@ -45,10 +45,10 @@ useFocus(clientNameInput, { initialValue: true });
|
||||
v-model="client.name"
|
||||
type="text"
|
||||
placeholder="Client Name"
|
||||
@keydown.enter="submit"
|
||||
class="mt-1 block w-full"
|
||||
required
|
||||
autocomplete="clientName"
|
||||
@keydown.enter="submit" />
|
||||
autocomplete="clientName" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -46,10 +46,10 @@ useFocus(clientNameInput, { initialValue: true });
|
||||
v-model="clientBody.name"
|
||||
type="text"
|
||||
placeholder="Client Name"
|
||||
@keydown.enter="submit"
|
||||
class="mt-1 block w-full"
|
||||
required
|
||||
autocomplete="clientName"
|
||||
@keydown.enter="submit" />
|
||||
autocomplete="clientName" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -23,28 +23,28 @@ const props = defineProps<{
|
||||
<div class="min-w-[150px]">
|
||||
<button
|
||||
v-if="canUpdateClients()"
|
||||
@click="emit('edit')"
|
||||
:aria-label="'Edit Client ' + props.client.name"
|
||||
data-testid="client_edit"
|
||||
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
|
||||
@click="emit('edit')">
|
||||
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
|
||||
<PencilSquareIcon
|
||||
class="w-5 text-icon-active"></PencilSquareIcon>
|
||||
<span>Edit</span>
|
||||
</button>
|
||||
<button
|
||||
@click.prevent="emit('archive')"
|
||||
v-if="canUpdateClients()"
|
||||
:aria-label="'Archive Client ' + props.client.name"
|
||||
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
|
||||
@click.prevent="emit('archive')">
|
||||
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
|
||||
<ArchiveBoxIcon class="w-5 text-icon-active"></ArchiveBoxIcon>
|
||||
<span>{{ client.is_archived ? 'Unarchive' : 'Archive' }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="canDeleteClients()"
|
||||
@click="emit('delete')"
|
||||
:aria-label="'Delete Client ' + props.client.name"
|
||||
data-testid="client_delete"
|
||||
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
|
||||
@click="emit('delete')">
|
||||
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
|
||||
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
|
||||
@@ -18,7 +18,7 @@ function getNameForItem(item: Client) {
|
||||
|
||||
<template>
|
||||
<MultiselectDropdown
|
||||
search-placeholder="Search for a Client..."
|
||||
searchPlaceholder="Search for a Client..."
|
||||
:items="clients"
|
||||
:get-key-from-item="getKeyFromItem"
|
||||
:get-name-for-item="getNameForItem">
|
||||
|
||||
@@ -25,18 +25,18 @@ const createClient = ref(false);
|
||||
style="grid-template-columns: 1fr 150px 200px 80px">
|
||||
<ClientTableHeading></ClientTableHeading>
|
||||
<div
|
||||
v-if="clients.length === 0"
|
||||
class="col-span-2 py-24 text-center">
|
||||
class="col-span-2 py-24 text-center"
|
||||
v-if="clients.length === 0">
|
||||
<UserCircleIcon
|
||||
class="w-8 text-icon-default inline pb-2"></UserCircleIcon>
|
||||
<h3 class="text-white font-semibold">No clients found</h3>
|
||||
<p v-if="canCreateClients()" class="pb-5">
|
||||
<p class="pb-5" v-if="canCreateClients()">
|
||||
Create your first client now!
|
||||
</p>
|
||||
<SecondaryButton
|
||||
v-if="canCreateClients()"
|
||||
:icon="PlusIcon as Component"
|
||||
@click="createClient = true"
|
||||
:icon="PlusIcon as Component"
|
||||
>Create your First Client
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
|
||||
@@ -38,8 +38,8 @@ const showEditModal = ref(false);
|
||||
<template>
|
||||
<TableRow>
|
||||
<ClientEditModal
|
||||
v-model:show="showEditModal"
|
||||
:client="client"></ClientEditModal>
|
||||
:client="client"
|
||||
v-model:show="showEditModal"></ClientEditModal>
|
||||
<div
|
||||
class="whitespace-nowrap flex items-center space-x-5 3xl:pl-12 py-4 pr-3 text-sm font-medium text-white pl-4 sm:pl-6 lg:pl-8 3xl:pl-12">
|
||||
<span>
|
||||
|
||||
@@ -10,16 +10,16 @@ const emit = defineEmits<{
|
||||
<template>
|
||||
<MoreOptionsDropdown label="Actions for the invitation">
|
||||
<button
|
||||
@click="emit('resend')"
|
||||
data-testid="invitation_delete"
|
||||
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
|
||||
@click="emit('resend')">
|
||||
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
|
||||
<ArrowPathIcon class="w-5 text-icon-active"></ArrowPathIcon>
|
||||
<span>Resend Invitation</span>
|
||||
</button>
|
||||
<button
|
||||
@click="emit('delete')"
|
||||
data-testid="invitation_delete"
|
||||
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
|
||||
@click="emit('delete')">
|
||||
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
|
||||
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
|
||||
@@ -39,7 +39,7 @@ async function resendInvitation() {
|
||||
await handleApiRequestNotifications(
|
||||
() =>
|
||||
api.resendInvitationEmail(
|
||||
undefined,
|
||||
{},
|
||||
{
|
||||
params: {
|
||||
invitation: props.invitation.id,
|
||||
|
||||
@@ -18,10 +18,10 @@ defineEmits<{
|
||||
|
||||
<template>
|
||||
<BillableRateModal
|
||||
@submit="$emit('submit')"
|
||||
v-model:show="show"
|
||||
v-model:saving="saving"
|
||||
title="Update Member Billable Rate"
|
||||
@submit="$emit('submit')">
|
||||
title="Update Member Billable Rate">
|
||||
<p class="py-1 text-center">
|
||||
The billable rate of {{ memberName }} will be updated to
|
||||
<strong>{{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useMembersStore } from '@/utils/useMembers';
|
||||
import { UserIcon, ChevronDownIcon } from '@heroicons/vue/24/solid';
|
||||
@@ -17,8 +17,8 @@ const model = defineModel<string>({
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
hiddenMembers?: ProjectMember[];
|
||||
disabled?: boolean;
|
||||
hiddenMembers: ProjectMember[];
|
||||
disabled: boolean;
|
||||
}>(),
|
||||
{
|
||||
hiddenMembers: () => [] as ProjectMember[],
|
||||
@@ -46,6 +46,22 @@ const filteredMembers = computed<Member[]>(() => {
|
||||
});
|
||||
});
|
||||
|
||||
watch(filteredMembers, () => {
|
||||
resetHighlightedItem();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
resetHighlightedItem();
|
||||
});
|
||||
|
||||
function resetHighlightedItem() {
|
||||
if (filteredMembers.value.length > 0) {
|
||||
highlightedItemId.value = filteredMembers.value[0].id;
|
||||
}
|
||||
}
|
||||
|
||||
const highlightedItemId = ref<string | null>(null);
|
||||
|
||||
const currentValue = computed(() => {
|
||||
if (model.value) {
|
||||
return members.value.find((member) => member.id === model.value)?.name;
|
||||
@@ -60,7 +76,7 @@ const currentValue = computed(() => {
|
||||
:items="filteredMembers"
|
||||
:get-key-from-item="(member) => member.id"
|
||||
:get-name-for-item="(member) => member.name">
|
||||
<template #trigger>
|
||||
<template v-slot:trigger>
|
||||
<Badge
|
||||
tag="button"
|
||||
class="flex w-full text-base text-left space-x-3 px-3 text-text-secondary font-normal cursor py-1.5">
|
||||
@@ -68,7 +84,7 @@ const currentValue = computed(() => {
|
||||
<div v-if="currentValue" class="flex-1 truncate">
|
||||
{{ currentValue }}
|
||||
</div>
|
||||
<div v-else class="flex-1">Select a member...</div>
|
||||
<div class="flex-1" v-else>Select a member...</div>
|
||||
<ChevronDownIcon class="w-4 text-muted"></ChevronDownIcon>
|
||||
</Badge>
|
||||
</template>
|
||||
|
||||
@@ -108,11 +108,11 @@ const roleDescription = computed(() => {
|
||||
v-model:saving="saving"
|
||||
v-model:show="showBillableRateModal"
|
||||
:member-name="member.name"
|
||||
:new-billable-rate="memberBody.billable_rate"
|
||||
:newBillableRate="memberBody.billable_rate"
|
||||
@submit="submitBillableRate"></MemberBillableRateModal>
|
||||
<MemberOwnershipTransferConfirmModal
|
||||
v-model:show="showOwnershipTransferConfirmModal"
|
||||
:member-name="member.name"
|
||||
v-model:show="showOwnershipTransferConfirmModal"
|
||||
@submit="submit"></MemberOwnershipTransferConfirmModal>
|
||||
<DialogModal closeable :show="show" @close="show = false">
|
||||
<template #title>
|
||||
@@ -127,9 +127,9 @@ const roleDescription = computed(() => {
|
||||
<div>
|
||||
<InputLabel for="role" value="Role" />
|
||||
<MemberRoleSelect
|
||||
v-model="memberBody.role"
|
||||
class="mt-2"
|
||||
name="role"></MemberRoleSelect>
|
||||
name="role"
|
||||
v-model="memberBody.role"></MemberRoleSelect>
|
||||
</div>
|
||||
<div class="flex-1 text-xs flex items-center pt-6">
|
||||
<p>{{ roleDescription }}</p>
|
||||
@@ -140,28 +140,28 @@ const roleDescription = computed(() => {
|
||||
<div>
|
||||
<InputLabel for="billableType" value="Billable" />
|
||||
<MemberBillableSelect
|
||||
class="mt-2"
|
||||
name="billableType"
|
||||
v-model="
|
||||
billableRateSelect
|
||||
"
|
||||
class="mt-2"
|
||||
name="billableType"></MemberBillableSelect>
|
||||
"></MemberBillableSelect>
|
||||
</div>
|
||||
<div
|
||||
v-if="billableRateSelect === 'custom-rate'"
|
||||
class="flex-1">
|
||||
class="flex-1"
|
||||
v-if="billableRateSelect === 'custom-rate'">
|
||||
<InputLabel
|
||||
for="memberBillableRate"
|
||||
class="mb-2"
|
||||
value="Billable Rate" />
|
||||
<BillableRateInput
|
||||
v-model="
|
||||
memberBody.billable_rate
|
||||
"
|
||||
focus
|
||||
class="w-full"
|
||||
:currency="getOrganizationCurrencyString()"
|
||||
@keydown.enter="saveWithChecks()"
|
||||
name="memberBillableRate"
|
||||
@keydown.enter="saveWithChecks()"></BillableRateInput>
|
||||
v-model="
|
||||
memberBody.billable_rate
|
||||
"></BillableRateInput>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -112,11 +112,11 @@ useFocus(clientNameInput, { initialValue: true });
|
||||
v-if="isBillingActivated() && canManageBilling()"
|
||||
href="/billing">
|
||||
<PrimaryButton
|
||||
type="button"
|
||||
class="mt-6"
|
||||
v-if="
|
||||
isBillingActivated() && canUpdateOrganization()
|
||||
"
|
||||
type="button"
|
||||
class="mt-6">
|
||||
">
|
||||
<CreditCardIcon class="w-5 h-5 me-2" />
|
||||
Go to Billing
|
||||
</PrimaryButton>
|
||||
@@ -128,15 +128,15 @@ useFocus(clientNameInput, { initialValue: true });
|
||||
<InputLabel for="email" value="Email" />
|
||||
<TextInput
|
||||
id="email"
|
||||
name="email"
|
||||
ref="memberEmailInput"
|
||||
v-model="addTeamMemberForm.email"
|
||||
name="email"
|
||||
type="text"
|
||||
placeholder="Member Email"
|
||||
@keydown.enter="submit"
|
||||
class="mt-1 block w-full"
|
||||
required
|
||||
autocomplete="memberName"
|
||||
@keydown.enter="submit" />
|
||||
autocomplete="memberName" />
|
||||
<InputError :message="errors.email" class="mt-2" />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -20,19 +20,19 @@ const props = defineProps<{
|
||||
<div class="min-w-[150px]">
|
||||
<button
|
||||
v-if="canUpdateMembers()"
|
||||
@click="emit('edit')"
|
||||
:aria-label="'Edit Member ' + props.member.name"
|
||||
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
|
||||
@click="emit('edit')">
|
||||
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
|
||||
<PencilSquareIcon
|
||||
class="w-5 text-icon-active"></PencilSquareIcon>
|
||||
<span>Edit</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="canDeleteMembers()"
|
||||
@click="emit('delete')"
|
||||
:aria-label="'Delete Member ' + props.member.name"
|
||||
data-testid="member_delete"
|
||||
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
|
||||
@click="emit('delete')">
|
||||
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
|
||||
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
|
||||
@@ -18,7 +18,7 @@ function getNameForItem(item: Member) {
|
||||
|
||||
<template>
|
||||
<MultiselectDropdown
|
||||
search-placeholder="Search for a Member..."
|
||||
searchPlaceholder="Search for a Member..."
|
||||
:items="members"
|
||||
:get-key-from-item="getKeyFromItem"
|
||||
:get-name-for-item="getNameForItem">
|
||||
|
||||
@@ -36,9 +36,9 @@ const emit = defineEmits<{
|
||||
<SecondaryButton @click="show = false"> Cancel</SecondaryButton>
|
||||
<PrimaryButton
|
||||
class="ms-3"
|
||||
@click="emit('submit')"
|
||||
:class="{ 'opacity-25': saving }"
|
||||
:disabled="saving"
|
||||
@click="emit('submit')">
|
||||
:disabled="saving">
|
||||
Confirm Transfer
|
||||
</PrimaryButton>
|
||||
</template>
|
||||
|
||||
@@ -32,7 +32,7 @@ async function invitePlaceholder(id: string) {
|
||||
await handleApiRequestNotifications(
|
||||
() =>
|
||||
api.invitePlaceholder(
|
||||
undefined,
|
||||
{},
|
||||
{
|
||||
params: {
|
||||
organization: organizationId,
|
||||
@@ -89,8 +89,8 @@ async function invitePlaceholder(id: string) {
|
||||
member.is_placeholder === true &&
|
||||
canInvitePlaceholderMembers()
|
||||
"
|
||||
size="small"
|
||||
@click="invitePlaceholder(member.id)"
|
||||
size="small"
|
||||
>Invite</SecondaryButton
|
||||
>
|
||||
<MemberMoreOptionsDropdown
|
||||
@@ -99,8 +99,8 @@ async function invitePlaceholder(id: string) {
|
||||
@delete="removeMember"></MemberMoreOptionsDropdown>
|
||||
</div>
|
||||
<MemberEditModal
|
||||
v-model:show="showEditMemberModal"
|
||||
:member="member"></MemberEditModal>
|
||||
:member="member"
|
||||
v-model:show="showEditMemberModal"></MemberEditModal>
|
||||
</TableRow>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -34,8 +34,8 @@
|
||||
<div class="ml-4 flex flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex rounded-md bg-card-background text-muted hover:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||
@click="show = false">
|
||||
@click="show = false"
|
||||
class="inline-flex rounded-md bg-card-background text-muted hover:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
|
||||
<span class="sr-only">Close</span>
|
||||
<XMarkIcon class="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
@@ -17,10 +17,10 @@ defineEmits<{
|
||||
|
||||
<template>
|
||||
<BillableRateModal
|
||||
@submit="$emit('submit')"
|
||||
v-model:show="show"
|
||||
v-model:saving="saving"
|
||||
title="Update Organization Billable Rate"
|
||||
@submit="$emit('submit')">
|
||||
title="Update Organization Billable Rate">
|
||||
<p class="py-0.5 text-center">
|
||||
The organization billable rate will be updated to
|
||||
<strong>{{
|
||||
|
||||
@@ -40,7 +40,7 @@ const shownProjects = computed(() => {
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
border?: boolean;
|
||||
border: boolean;
|
||||
}>(),
|
||||
{
|
||||
border: true,
|
||||
@@ -123,17 +123,17 @@ function updateValue(project: Project) {
|
||||
<template #content>
|
||||
<ComboboxRoot
|
||||
:open="open"
|
||||
:model-value="currentProject"
|
||||
:search-term="searchValue"
|
||||
class="relative"
|
||||
@update:model-value="updateValue"
|
||||
@update:search-term="(e) => console.log(e)">
|
||||
:modelValue="currentProject"
|
||||
@update:modelValue="updateValue"
|
||||
@update:searchTerm="(e) => console.log(e)"
|
||||
:searchTerm="searchValue"
|
||||
class="relative">
|
||||
<ComboboxAnchor>
|
||||
<ComboboxInput
|
||||
@keydown.enter="addProjectIfNoneExists"
|
||||
ref="searchInput"
|
||||
class="bg-card-background border-0 placeholder-muted text-sm text-white py-2.5 focus:ring-0 border-b border-card-background-separator focus:border-card-background-separator w-full"
|
||||
placeholder="Search for a project..."
|
||||
@keydown.enter="addProjectIfNoneExists" />
|
||||
placeholder="Search for a project..." />
|
||||
</ComboboxAnchor>
|
||||
<ComboboxContent>
|
||||
<ComboboxViewport
|
||||
|
||||
@@ -90,8 +90,8 @@ async function submitBillableRate() {
|
||||
<div class="text-center">
|
||||
<InputLabel for="color" value="Color" />
|
||||
<ProjectColorSelector
|
||||
v-model="project.color"
|
||||
class="mt-1"></ProjectColorSelector>
|
||||
class="mt-1"
|
||||
v-model="project.color"></ProjectColorSelector>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
@@ -102,18 +102,18 @@ async function submitBillableRate() {
|
||||
v-model="project.name"
|
||||
type="text"
|
||||
placeholder="Project Name"
|
||||
@keydown.enter="submit()"
|
||||
class="mt-1 block w-full"
|
||||
required
|
||||
autocomplete="projectName"
|
||||
@keydown.enter="submit()" />
|
||||
autocomplete="projectName" />
|
||||
</div>
|
||||
<div class="">
|
||||
<InputLabel for="client" value="Client" />
|
||||
<ClientDropdown
|
||||
v-model="project.client_id"
|
||||
:create-client
|
||||
:createClient
|
||||
:clients="clients"
|
||||
class="mt-1">
|
||||
class="mt-1"
|
||||
v-model="project.client_id">
|
||||
<template #trigger>
|
||||
<Badge
|
||||
class="bg-input-background cursor-pointer hover:bg-tertiary"
|
||||
@@ -133,18 +133,18 @@ async function submitBillableRate() {
|
||||
<div class="lg:grid grid-cols-2 gap-12">
|
||||
<div>
|
||||
<ProjectEditBillableSection
|
||||
v-model:is-billable="project.is_billable"
|
||||
v-model:billable-rate="
|
||||
project.billable_rate
|
||||
"
|
||||
@submit="submit"
|
||||
:currency="getOrganizationCurrencyString()"
|
||||
@submit="submit"></ProjectEditBillableSection>
|
||||
v-model:isBillable="project.is_billable"
|
||||
v-model:billableRate="
|
||||
project.billable_rate
|
||||
"></ProjectEditBillableSection>
|
||||
</div>
|
||||
<div>
|
||||
<EstimatedTimeSection
|
||||
v-if="isAllowedToPerformPremiumAction()"
|
||||
v-model="project.estimated_time"
|
||||
@submit="submit()"></EstimatedTimeSection>
|
||||
@submit="submit()"
|
||||
v-model="project.estimated_time"></EstimatedTimeSection>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -163,9 +163,9 @@ async function submitBillableRate() {
|
||||
<ProjectBillableRateModal
|
||||
v-model:show="showBillableRateModal"
|
||||
:currency="getOrganizationCurrencyString()"
|
||||
@submit="submitBillableRate"
|
||||
:new-billable-rate="project.billable_rate"
|
||||
:project-name="project.name"
|
||||
@submit="submitBillableRate"></ProjectBillableRateModal>
|
||||
:project-name="project.name"></ProjectBillableRateModal>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -21,29 +21,29 @@ const props = defineProps<{
|
||||
<MoreOptionsDropdown :label="'Actions for Project ' + props.project.name">
|
||||
<div class="min-w-[150px]">
|
||||
<button
|
||||
@click.prevent="emit('edit')"
|
||||
v-if="canUpdateProjects()"
|
||||
:aria-label="'Edit Project ' + props.project.name"
|
||||
data-testid="project_edit"
|
||||
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
|
||||
@click.prevent="emit('edit')">
|
||||
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
|
||||
<PencilSquareIcon
|
||||
class="w-5 text-icon-active"></PencilSquareIcon>
|
||||
<span>Edit</span>
|
||||
</button>
|
||||
<button
|
||||
@click.prevent="emit('archive')"
|
||||
v-if="canUpdateProjects()"
|
||||
:aria-label="'Archive Project ' + props.project.name"
|
||||
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
|
||||
@click.prevent="emit('archive')">
|
||||
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
|
||||
<ArchiveBoxIcon class="w-5 text-icon-active"></ArchiveBoxIcon>
|
||||
<span>{{ project.is_archived ? 'Unarchive' : 'Archive' }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="canDeleteProjects()"
|
||||
@click.prevent="emit('delete')"
|
||||
:aria-label="'Delete Project ' + props.project.name"
|
||||
data-testid="project_delete"
|
||||
class="border-b border-card-background-separator flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
|
||||
@click.prevent="emit('delete')">
|
||||
v-if="canDeleteProjects()"
|
||||
class="border-b border-card-background-separator flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
|
||||
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
|
||||
@@ -18,7 +18,7 @@ function getNameForItem(item: Project) {
|
||||
|
||||
<template>
|
||||
<MultiselectDropdown
|
||||
search-placeholder="Search for a Project..."
|
||||
searchPlaceholder="Search for a Project..."
|
||||
:items="projects"
|
||||
:get-key-from-item="getKeyFromItem"
|
||||
:get-name-for-item="getNameForItem">
|
||||
|
||||
@@ -44,12 +44,12 @@ import { isAllowedToPerformPremiumAction } from '@/utils/billing';
|
||||
|
||||
<template>
|
||||
<ProjectCreateModal
|
||||
v-model:show="showCreateProjectModal"
|
||||
:create-project
|
||||
:create-client
|
||||
:createProject
|
||||
:createClient
|
||||
:currency="getOrganizationCurrencyString()"
|
||||
:clients="clients"
|
||||
:enable-estimated-time="isAllowedToPerformPremiumAction"></ProjectCreateModal>
|
||||
:enableEstimatedTime="isAllowedToPerformPremiumAction"
|
||||
v-model:show="showCreateProjectModal"></ProjectCreateModal>
|
||||
<div class="flow-root max-w-[100vw] overflow-x-auto">
|
||||
<div class="inline-block min-w-full align-middle">
|
||||
<div
|
||||
@@ -57,12 +57,12 @@ import { isAllowedToPerformPremiumAction } from '@/utils/billing';
|
||||
class="grid min-w-full"
|
||||
:style="gridTemplate">
|
||||
<ProjectTableHeading
|
||||
:show-billable-rate="
|
||||
:showBillableRate="
|
||||
props.showBillableRate
|
||||
"></ProjectTableHeading>
|
||||
<div
|
||||
v-if="projects.length === 0"
|
||||
class="col-span-5 py-24 text-center">
|
||||
class="col-span-5 py-24 text-center"
|
||||
v-if="projects.length === 0">
|
||||
<FolderPlusIcon
|
||||
class="w-8 text-icon-default inline pb-2"></FolderPlusIcon>
|
||||
<h3 class="text-white font-semibold">
|
||||
@@ -81,14 +81,14 @@ import { isAllowedToPerformPremiumAction } from '@/utils/billing';
|
||||
</p>
|
||||
<SecondaryButton
|
||||
v-if="canCreateProjects()"
|
||||
:icon="PlusIcon"
|
||||
@click="showCreateProjectModal = true"
|
||||
:icon="PlusIcon"
|
||||
>Create your First Project
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
<template v-for="project in projects" :key="project.id">
|
||||
<ProjectTableRow
|
||||
:show-billable-rate="props.showBillableRate"
|
||||
:showBillableRate="props.showBillableRate"
|
||||
:project="project"></ProjectTableRow>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -19,8 +19,8 @@ defineProps<{
|
||||
Progress
|
||||
</div>
|
||||
<div
|
||||
v-if="showBillableRate"
|
||||
class="px-3 py-1.5 text-left font-semibold text-white">
|
||||
class="px-3 py-1.5 text-left font-semibold text-white"
|
||||
v-if="showBillableRate">
|
||||
Billable Rate
|
||||
</div>
|
||||
<div class="px-3 py-1.5 text-left font-semibold text-white">Status</div>
|
||||
|
||||
@@ -83,8 +83,8 @@ const showEditProjectModal = ref(false);
|
||||
</div>
|
||||
<div class="whitespace-nowrap min-w-0 px-3 py-4 text-sm text-muted">
|
||||
<div
|
||||
v-if="project.client_id"
|
||||
class="overflow-ellipsis overflow-hidden">
|
||||
class="overflow-ellipsis overflow-hidden"
|
||||
v-if="project.client_id">
|
||||
{{ client?.name }}
|
||||
</div>
|
||||
<div v-else>No client</div>
|
||||
@@ -106,8 +106,8 @@ const showEditProjectModal = ref(false);
|
||||
<span v-else> -- </span>
|
||||
</div>
|
||||
<div
|
||||
v-if="showBillableRate"
|
||||
class="whitespace-nowrap px-3 py-4 text-sm text-muted">
|
||||
class="whitespace-nowrap px-3 py-4 text-sm text-muted"
|
||||
v-if="showBillableRate">
|
||||
{{ billableRateInfo }}
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -18,10 +18,10 @@ defineEmits<{
|
||||
|
||||
<template>
|
||||
<BillableRateModal
|
||||
@submit="$emit('submit')"
|
||||
v-model:show="show"
|
||||
v-model:saving="saving"
|
||||
title="Update Project Member Billable Rate"
|
||||
@submit="$emit('submit')">
|
||||
title="Update Project Member Billable Rate">
|
||||
<p class="py-1 text-center">
|
||||
The billable rate of {{ memberName }} will be updated to
|
||||
<strong>{{
|
||||
|
||||
@@ -52,16 +52,16 @@ useFocus(projectNameInput, { initialValue: true });
|
||||
<div class="grid grid-cols-3 items-center space-x-4">
|
||||
<div class="col-span-3 sm:col-span-2">
|
||||
<MemberCombobox
|
||||
v-model="projectMember.member_id"
|
||||
:hidden-members="props.existingMembers"></MemberCombobox>
|
||||
:hidden-members="props.existingMembers"
|
||||
v-model="projectMember.member_id"></MemberCombobox>
|
||||
</div>
|
||||
<div class="col-span-3 sm:col-span-1 flex-1">
|
||||
<BillableRateInput
|
||||
name="billable_rate"
|
||||
:currency="getOrganizationCurrencyString()"
|
||||
v-model="
|
||||
projectMember.billable_rate
|
||||
"
|
||||
name="billable_rate"
|
||||
:currency="getOrganizationCurrencyString()"></BillableRateInput>
|
||||
"></BillableRateInput>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -75,8 +75,8 @@ useFocus(projectNameInput, { initialValue: true });
|
||||
|
||||
<template #content>
|
||||
<ProjectMemberBillableRateModal
|
||||
v-model:show="showBillableRateModal"
|
||||
:member-name="props.name"
|
||||
v-model:show="showBillableRateModal"
|
||||
:new-billable-rate="projectMemberBody.billable_rate"
|
||||
@close="showBillableRateModal = false"
|
||||
@submit="submitBillableRate"></ProjectMemberBillableRateModal>
|
||||
@@ -92,12 +92,12 @@ useFocus(projectNameInput, { initialValue: true });
|
||||
class="mb-2"
|
||||
value="Billable Rate"></InputLabel>
|
||||
<BillableRateInput
|
||||
v-model="
|
||||
projectMemberBody.billable_rate
|
||||
"
|
||||
@keydown.enter="submit"
|
||||
:currency="getOrganizationCurrencyString()"
|
||||
name="billable_rate"
|
||||
@keydown.enter="submit"></BillableRateInput>
|
||||
v-model="
|
||||
projectMemberBody.billable_rate
|
||||
"></BillableRateInput>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -27,17 +27,17 @@ const currentMember = computed(() => {
|
||||
<MoreOptionsDropdown
|
||||
:label="'Actions for Project Member ' + currentMember?.name">
|
||||
<button
|
||||
@click.prevent="emit('edit')"
|
||||
:aria-label="'Edit Project Member ' + currentMember?.name"
|
||||
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
|
||||
@click.prevent="emit('edit')">
|
||||
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
|
||||
<PencilSquareIcon class="w-5 text-icon-active"></PencilSquareIcon>
|
||||
<span>Edit</span>
|
||||
</button>
|
||||
<button
|
||||
@click.prevent="emit('delete')"
|
||||
:aria-label="'Delete Project Member ' + currentMember?.name"
|
||||
data-testid="project_delete"
|
||||
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
|
||||
@click.prevent="emit('delete')">
|
||||
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
|
||||
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
|
||||
<span>Remove from Team</span>
|
||||
</button>
|
||||
|
||||
@@ -18,9 +18,9 @@ const createProjectMember = ref(false);
|
||||
|
||||
<template>
|
||||
<ProjectMemberCreateModal
|
||||
v-model:show="createProjectMember"
|
||||
:existing-members="projectMembers"
|
||||
:project-id="projectId"></ProjectMemberCreateModal>
|
||||
:project-id="projectId"
|
||||
v-model:show="createProjectMember"></ProjectMemberCreateModal>
|
||||
<div class="flow-root">
|
||||
<div class="inline-block min-w-full align-middle">
|
||||
<div
|
||||
@@ -29,15 +29,15 @@ const createProjectMember = ref(false);
|
||||
style="grid-template-columns: 1fr 150px 150px 80px">
|
||||
<ProjectMemberTableHeading></ProjectMemberTableHeading>
|
||||
<div
|
||||
v-if="projectMembers.length === 0"
|
||||
class="col-span-5 py-24 text-center">
|
||||
class="col-span-5 py-24 text-center"
|
||||
v-if="projectMembers.length === 0">
|
||||
<UserGroupIcon
|
||||
class="w-8 text-icon-default inline pb-2"></UserGroupIcon>
|
||||
<h3 class="text-white font-semibold">No project members</h3>
|
||||
<p class="pb-5">Add the first project member!</p>
|
||||
<SecondaryButton
|
||||
:icon="PlusIcon"
|
||||
@click="createProjectMember = true"
|
||||
:icon="PlusIcon"
|
||||
>Add a new Project Member
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
|
||||
@@ -37,8 +37,8 @@ const showEditModal = ref(false);
|
||||
<template>
|
||||
<TableRow>
|
||||
<ProjectMemberEditModal
|
||||
v-model:show="showEditModal"
|
||||
:name="member?.name"
|
||||
v-model:show="showEditModal"
|
||||
:project-member="projectMember"></ProjectMemberEditModal>
|
||||
<div
|
||||
class="whitespace-nowrap flex items-center space-x-5 3xl:pl-12 py-4 pr-3 text-sm font-medium text-white pl-4 sm:pl-6 lg:pl-8 3xl:pl-12">
|
||||
|
||||
@@ -82,22 +82,22 @@ async function submit() {
|
||||
<InputLabel for="name" value="Name" />
|
||||
<TextInput
|
||||
id="name"
|
||||
v-model="report.name"
|
||||
class="mt-1.5 w-full"></TextInput>
|
||||
class="mt-1.5 w-full"
|
||||
v-model="report.name"></TextInput>
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel for="description" value="Description" />
|
||||
<TextInput
|
||||
id="description"
|
||||
v-model="report.description"
|
||||
class="mt-1.5 w-full"></TextInput>
|
||||
class="mt-1.5 w-full"
|
||||
v-model="report.description"></TextInput>
|
||||
</div>
|
||||
<InputLabel value="Visibility" />
|
||||
<div class="flex items-center space-x-12">
|
||||
<div class="flex items-center space-x-3 px-2 py-3">
|
||||
<Checkbox
|
||||
id="is_public"
|
||||
v-model:checked="report.is_public"></Checkbox>
|
||||
v-model:checked="report.is_public"
|
||||
id="is_public"></Checkbox>
|
||||
<InputLabel for="is_public" value="Public" />
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -96,22 +96,22 @@ async function submit() {
|
||||
<InputLabel for="name" value="Name" />
|
||||
<TextInput
|
||||
id="name"
|
||||
v-model="report.name"
|
||||
class="mt-1.5 w-full"></TextInput>
|
||||
class="mt-1.5 w-full"
|
||||
v-model="report.name"></TextInput>
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel for="description" value="Description" />
|
||||
<TextInput
|
||||
id="description"
|
||||
v-model="report.description"
|
||||
class="mt-1.5 w-full"></TextInput>
|
||||
class="mt-1.5 w-full"
|
||||
v-model="report.description"></TextInput>
|
||||
</div>
|
||||
<InputLabel value="Visibility" />
|
||||
<div class="flex items-center space-x-12">
|
||||
<div class="flex items-center space-x-2 px-2 py-3">
|
||||
<Checkbox
|
||||
id="is_public"
|
||||
v-model:checked="report.is_public"></Checkbox>
|
||||
v-model:checked="report.is_public"
|
||||
id="is_public"></Checkbox>
|
||||
<InputLabel for="is_public" value="Public" />
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -17,19 +17,19 @@ const props = defineProps<{
|
||||
<MoreOptionsDropdown :label="'Actions for Project ' + props.report.name">
|
||||
<div class="min-w-[150px]">
|
||||
<button
|
||||
@click.prevent="emit('edit')"
|
||||
v-if="canUpdateReport()"
|
||||
:aria-label="'Edit Report ' + props.report.name"
|
||||
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
|
||||
@click.prevent="emit('edit')">
|
||||
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
|
||||
<PencilSquareIcon
|
||||
class="w-5 text-icon-active"></PencilSquareIcon>
|
||||
<span>Edit</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="canDeleteReport()"
|
||||
@click.prevent="emit('delete')"
|
||||
:aria-label="'Delete Report ' + props.report.name"
|
||||
class="border-b border-card-background-separator flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
|
||||
@click.prevent="emit('delete')">
|
||||
v-if="canDeleteReport()"
|
||||
class="border-b border-card-background-separator flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
|
||||
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
|
||||
@@ -27,8 +27,8 @@ function onSaveReportClick() {
|
||||
|
||||
<template>
|
||||
<ReportCreateModal
|
||||
v-model:show="showCreateReportModal"
|
||||
:properties="reportProperties"></ReportCreateModal>
|
||||
:properties="reportProperties"
|
||||
v-model:show="showCreateReportModal"></ReportCreateModal>
|
||||
<UpgradeModal v-model:show="showPremiumModal">
|
||||
<strong>Sharable Reports</strong> is only available in solidtime
|
||||
Professional.
|
||||
|
||||
@@ -27,19 +27,19 @@ const gridTemplate = computed(() => {
|
||||
:style="gridTemplate">
|
||||
<ReportTableHeading></ReportTableHeading>
|
||||
<div
|
||||
v-if="reports.length === 0"
|
||||
class="col-span-5 py-24 text-center">
|
||||
class="col-span-5 py-24 text-center"
|
||||
v-if="reports.length === 0">
|
||||
<FolderPlusIcon
|
||||
class="w-8 text-icon-default inline pb-2"></FolderPlusIcon>
|
||||
<h3 class="text-white font-semibold">
|
||||
No shared reports found
|
||||
</h3>
|
||||
<p v-if="canCreateProjects()" class="pb-5">
|
||||
<p class="pb-5" v-if="canCreateProjects()">
|
||||
Create your first project now!
|
||||
</p>
|
||||
<SecondaryButton
|
||||
:icon="PlusIcon"
|
||||
@click="router.visit(route('reporting'))"
|
||||
:icon="PlusIcon"
|
||||
>Go to the overview to create a report
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
|
||||
@@ -87,7 +87,7 @@ async function deleteReport() {
|
||||
<span v-else>Copied!</span>
|
||||
</SecondaryButton>
|
||||
<button
|
||||
class="outline-0 focus-visible:ring-2 w-6 h-6 flex items-center justify-center rounded focus-visible:ring-ring"
|
||||
class="outline-0 focus-visible:ring-2 w-6 h-6 flex items-center justify-center rounded focus-visible:ring-white/80"
|
||||
@click="openSharableLink">
|
||||
<ArrowTopRightOnSquareIcon
|
||||
class="w-4 text-text-tertiary hover:text-text-secondary transition"></ArrowTopRightOnSquareIcon>
|
||||
|
||||
@@ -157,7 +157,7 @@ const option = ref({
|
||||
:autoresize="true"
|
||||
class="chart"
|
||||
:option="option" />
|
||||
<div v-else class="chart flex flex-col items-center justify-center">
|
||||
<div class="chart flex flex-col items-center justify-center" v-else>
|
||||
<p class="text-lg text-white font-semibold">
|
||||
No time entries found
|
||||
</p>
|
||||
|
||||
@@ -22,10 +22,10 @@ function downloadCurrentExport() {
|
||||
<Modal
|
||||
closeable
|
||||
max-width="lg"
|
||||
:show="showExportModal"
|
||||
@close="showExportModal = false">
|
||||
@close="showExportModal = false"
|
||||
:show="showExportModal">
|
||||
<button
|
||||
class="text-text-tertiary w-6 mx-auto absolute focus-visible:outline-none focus-visible:ring-2 rounded-full focus-visible:ring-ring transition focus-visible:text-text-primary hover:text-text-primary top-2 right-2">
|
||||
class="text-text-tertiary w-6 mx-auto absolute focus-visible:outline-none focus-visible:ring-2 rounded-full focus-visible:ring-white/80 transition focus-visible:text-text-primary hover:text-text-primary top-2 right-2">
|
||||
<XMarkIcon @click="showExportModal = false"></XMarkIcon>
|
||||
</button>
|
||||
<div class="text-center text-text-primary py-6">
|
||||
|
||||
@@ -21,7 +21,6 @@ const activeClass = computed(() => {
|
||||
<template>
|
||||
<Badge
|
||||
size="large"
|
||||
tag="button"
|
||||
:class="
|
||||
twMerge(
|
||||
'cursor-pointer hover:bg-card-background transition flex',
|
||||
|
||||
@@ -23,7 +23,7 @@ const title = computed(() => {
|
||||
:get-key-from-item="(item) => item.value"
|
||||
:get-name-for-item="(item) => item.label"
|
||||
:items="groupByOptions">
|
||||
<template #trigger>
|
||||
<template v-slot:trigger>
|
||||
<Badge
|
||||
size="large"
|
||||
class="cursor-pointer hover:bg-card-background transition space-x-5 flex">
|
||||
|
||||
@@ -35,9 +35,9 @@ const expanded = ref(false);
|
||||
)
|
||||
">
|
||||
<GroupedItemsCountButton
|
||||
v-if="entry.grouped_data && entry.grouped_data?.length > 0"
|
||||
:expanded="expanded"
|
||||
@click="expanded = !expanded">
|
||||
@click="expanded = !expanded"
|
||||
v-if="entry.grouped_data && entry.grouped_data?.length > 0">
|
||||
{{ entry.grouped_data?.length }}
|
||||
</GroupedItemsCountButton>
|
||||
<span>
|
||||
@@ -52,13 +52,13 @@ const expanded = ref(false);
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="expanded && entry.grouped_data"
|
||||
class="col-span-3 grid bg-quaternary"
|
||||
style="grid-template-columns: 1fr 150px 150px">
|
||||
style="grid-template-columns: 1fr 150px 150px"
|
||||
v-if="expanded && entry.grouped_data">
|
||||
<ReportingRow
|
||||
indent
|
||||
v-for="subEntry in entry.grouped_data"
|
||||
:key="subEntry.description ?? 'none'"
|
||||
indent
|
||||
:entry="subEntry"></ReportingRow>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -10,18 +10,18 @@ defineProps<{
|
||||
<template>
|
||||
<TabBar>
|
||||
<TabBarItem
|
||||
:active="active === 'reporting'"
|
||||
@click="router.visit(route('reporting'))"
|
||||
:active="active === 'reporting'"
|
||||
>Overview</TabBarItem
|
||||
>
|
||||
<TabBarItem
|
||||
:active="active === 'detailed'"
|
||||
@click="router.visit(route('reporting.detailed'))"
|
||||
:active="active === 'detailed'"
|
||||
>Detailed</TabBarItem
|
||||
>
|
||||
<TabBarItem
|
||||
:active="active === 'shared'"
|
||||
@click="router.visit(route('reporting.shared'))"
|
||||
:active="active === 'shared'"
|
||||
>Shared</TabBarItem
|
||||
>
|
||||
</TabBar>
|
||||
|
||||
@@ -14,10 +14,10 @@ const props = defineProps<{
|
||||
<template>
|
||||
<MoreOptionsDropdown :label="'Actions for Tag ' + props.tag.name">
|
||||
<button
|
||||
@click="emit('delete')"
|
||||
:aria-label="'Delete Tag ' + props.tag.name"
|
||||
data-testid="tag_delete"
|
||||
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
|
||||
@click="emit('delete')">
|
||||
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
|
||||
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user