Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
f308cf6295 Bump the minor-updates group across 1 directory with 25 updates
Bumps the minor-updates group with 25 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@floating-ui/core](https://github.com/floating-ui/floating-ui/tree/HEAD/packages/core) | `1.7.5` | `1.8.0` |
| [@lucide/vue](https://github.com/lucide-icons/lucide/tree/HEAD/packages/vue) | `1.14.0` | `1.24.0` |
| [@tanstack/vue-form](https://github.com/TanStack/form/tree/HEAD/packages/vue-form) | `1.32.0` | `1.33.2` |
| [@tanstack/vue-query](https://github.com/TanStack/query/tree/HEAD/packages/vue-query) | `5.100.10` | `5.101.2` |
| [@tanstack/vue-virtual](https://github.com/TanStack/virtual/tree/HEAD/packages/vue-virtual) | `3.13.24` | `3.13.32` |
| [@vue/eslint-config-typescript](https://github.com/vuejs/eslint-config-typescript) | `14.7.0` | `14.9.0` |
| [dayjs](https://github.com/iamkun/dayjs) | `1.11.20` | `1.11.21` |
| [echarts](https://github.com/apache/echarts) | `6.0.0` | `6.1.0` |
| [focus-trap](https://github.com/focus-trap/focus-trap) | `8.2.0` | `8.2.2` |
| [parse-duration](https://github.com/jkroso/parse-duration) | `2.1.6` | `2.1.8` |
| [reka-ui](https://github.com/unovue/reka-ui) | `2.9.7` | `2.10.1` |
| [@eslint/eslintrc](https://github.com/eslint/eslintrc) | `3.3.5` | `3.3.6` |
| [@playwright/test](https://github.com/microsoft/playwright) | `1.60.0` | `1.61.1` |
| [@tailwindcss/typography](https://github.com/tailwindlabs/tailwindcss-typography) | `0.5.19` | `0.5.20` |
| [@vitejs/plugin-vue](https://github.com/vitejs/vite-plugin-vue/tree/HEAD/packages/plugin-vue) | `6.0.6` | `6.0.7` |
| [@vue/test-utils](https://github.com/vuejs/test-utils) | `2.4.10` | `2.4.11` |
| [@vue/tsconfig](https://github.com/vuejs/tsconfig) | `0.8.1` | `0.9.1` |
| [autoprefixer](https://github.com/postcss/autoprefixer) | `10.5.0` | `10.5.2` |
| [axios](https://github.com/axios/axios) | `1.16.0` | `1.18.1` |
| [happy-dom](https://github.com/capricorn86/happy-dom) | `20.9.0` | `20.10.6` |
| [postcss](https://github.com/postcss/postcss) | `8.5.14` | `8.5.18` |
| [vite-plugin-checker](https://github.com/fi3ework/vite-plugin-checker) | `0.12.0` | `0.14.4` |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.7` | `4.1.10` |
| [vue](https://github.com/vuejs/core) | `3.5.34` | `3.5.39` |
| [vue-tsc](https://github.com/vuejs/language-tools/tree/HEAD/packages/tsc) | `3.2.8` | `3.3.7` |



Updates `@floating-ui/core` from 1.7.5 to 1.8.0
- [Release notes](https://github.com/floating-ui/floating-ui/releases)
- [Changelog](https://github.com/floating-ui/floating-ui/blob/master/packages/core/CHANGELOG.md)
- [Commits](https://github.com/floating-ui/floating-ui/commits/@floating-ui/core@1.8.0/packages/core)

Updates `@lucide/vue` from 1.14.0 to 1.24.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.24.0/packages/vue)

Updates `@tanstack/vue-form` from 1.32.0 to 1.33.2
- [Release notes](https://github.com/TanStack/form/releases)
- [Changelog](https://github.com/TanStack/form/blob/main/packages/vue-form/CHANGELOG.md)
- [Commits](https://github.com/TanStack/form/commits/@tanstack/vue-form@1.33.2/packages/vue-form)

Updates `@tanstack/vue-query` from 5.100.10 to 5.101.2
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/vue-query/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/vue-query@5.101.2/packages/vue-query)

Updates `@tanstack/vue-virtual` from 3.13.24 to 3.13.32
- [Release notes](https://github.com/TanStack/virtual/releases)
- [Changelog](https://github.com/TanStack/virtual/blob/main/packages/vue-virtual/CHANGELOG.md)
- [Commits](https://github.com/TanStack/virtual/commits/@tanstack/vue-virtual@3.13.32/packages/vue-virtual)

Updates `@vue/eslint-config-typescript` from 14.7.0 to 14.9.0
- [Release notes](https://github.com/vuejs/eslint-config-typescript/releases)
- [Commits](https://github.com/vuejs/eslint-config-typescript/compare/v14.7.0...v14.9.0)

Updates `dayjs` from 1.11.20 to 1.11.21
- [Release notes](https://github.com/iamkun/dayjs/releases)
- [Changelog](https://github.com/iamkun/dayjs/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/iamkun/dayjs/compare/v1.11.20...v1.11.21)

Updates `echarts` from 6.0.0 to 6.1.0
- [Release notes](https://github.com/apache/echarts/releases)
- [Commits](https://github.com/apache/echarts/compare/6.0.0...6.1.0)

Updates `focus-trap` from 8.2.0 to 8.2.2
- [Release notes](https://github.com/focus-trap/focus-trap/releases)
- [Changelog](https://github.com/focus-trap/focus-trap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/focus-trap/focus-trap/compare/v8.2.0...v8.2.2)

Updates `parse-duration` from 2.1.6 to 2.1.8
- [Release notes](https://github.com/jkroso/parse-duration/releases)
- [Commits](https://github.com/jkroso/parse-duration/compare/v2.1.6...v2.1.8)

Updates `reka-ui` from 2.9.7 to 2.10.1
- [Release notes](https://github.com/unovue/reka-ui/releases)
- [Commits](https://github.com/unovue/reka-ui/compare/v2.9.7...v2.10.1)

Updates `@eslint/eslintrc` from 3.3.5 to 3.3.6
- [Release notes](https://github.com/eslint/eslintrc/releases)
- [Changelog](https://github.com/eslint/eslintrc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslintrc/compare/eslintrc-v3.3.5...eslintrc-v3.3.6)

Updates `@playwright/test` from 1.60.0 to 1.61.1
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.60.0...v1.61.1)

Updates `@tailwindcss/typography` from 0.5.19 to 0.5.20
- [Release notes](https://github.com/tailwindlabs/tailwindcss-typography/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss-typography/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss-typography/compare/v0.5.19...v0.5.20)

Updates `@vitejs/plugin-vue` from 6.0.6 to 6.0.7
- [Release notes](https://github.com/vitejs/vite-plugin-vue/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-vue/blob/main/packages/plugin-vue/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-vue/commits/plugin-vue@6.0.7/packages/plugin-vue)

Updates `@vue/test-utils` from 2.4.10 to 2.4.11
- [Release notes](https://github.com/vuejs/test-utils/releases)
- [Commits](https://github.com/vuejs/test-utils/compare/v2.4.10...v2.4.11)

Updates `@vue/tsconfig` from 0.8.1 to 0.9.1
- [Release notes](https://github.com/vuejs/tsconfig/releases)
- [Commits](https://github.com/vuejs/tsconfig/compare/v0.8.1...v0.9.1)

Updates `autoprefixer` from 10.5.0 to 10.5.2
- [Release notes](https://github.com/postcss/autoprefixer/releases)
- [Changelog](https://github.com/postcss/autoprefixer/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/autoprefixer/compare/10.5.0...10.5.2)

Updates `axios` from 1.16.0 to 1.18.1
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.16.0...v1.18.1)

Updates `happy-dom` from 20.9.0 to 20.10.6
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v20.9.0...v20.10.6)

Updates `postcss` from 8.5.14 to 8.5.18
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.14...8.5.18)

Updates `vite-plugin-checker` from 0.12.0 to 0.14.4
- [Release notes](https://github.com/fi3ework/vite-plugin-checker/releases)
- [Commits](https://github.com/fi3ework/vite-plugin-checker/compare/vite-plugin-checker@0.12.0...v0.14.4)

Updates `vitest` from 4.1.7 to 4.1.10
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.10/packages/vitest)

Updates `vue` from 3.5.34 to 3.5.39
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/compare/v3.5.34...v3.5.39)

Updates `vue-tsc` from 3.2.8 to 3.3.7
- [Release notes](https://github.com/vuejs/language-tools/releases)
- [Changelog](https://github.com/vuejs/language-tools/blob/master/CHANGELOG.md)
- [Commits](https://github.com/vuejs/language-tools/commits/v3.3.7/packages/tsc)

---
updated-dependencies:
- dependency-name: "@floating-ui/core"
  dependency-version: 1.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-updates
- dependency-name: "@lucide/vue"
  dependency-version: 1.24.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-updates
- dependency-name: "@tanstack/vue-form"
  dependency-version: 1.33.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-updates
- dependency-name: "@tanstack/vue-query"
  dependency-version: 5.101.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-updates
- dependency-name: "@tanstack/vue-virtual"
  dependency-version: 3.13.32
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-updates
- dependency-name: "@vue/eslint-config-typescript"
  dependency-version: 14.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-updates
- dependency-name: dayjs
  dependency-version: 1.11.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-updates
- dependency-name: echarts
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-updates
- dependency-name: focus-trap
  dependency-version: 8.2.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-updates
- dependency-name: parse-duration
  dependency-version: 2.1.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-updates
- dependency-name: reka-ui
  dependency-version: 2.10.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-updates
- dependency-name: "@eslint/eslintrc"
  dependency-version: 3.3.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-updates
- dependency-name: "@playwright/test"
  dependency-version: 1.61.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-updates
- dependency-name: "@tailwindcss/typography"
  dependency-version: 0.5.20
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-updates
- dependency-name: "@vitejs/plugin-vue"
  dependency-version: 6.0.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-updates
- dependency-name: "@vue/test-utils"
  dependency-version: 2.4.11
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-updates
- dependency-name: "@vue/tsconfig"
  dependency-version: 0.9.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-updates
- dependency-name: autoprefixer
  dependency-version: 10.5.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-updates
- dependency-name: axios
  dependency-version: 1.18.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-updates
- dependency-name: happy-dom
  dependency-version: 20.10.6
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-updates
- dependency-name: postcss
  dependency-version: 8.5.18
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-updates
- dependency-name: vite-plugin-checker
  dependency-version: 0.14.4
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-updates
- dependency-name: vitest
  dependency-version: 4.1.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-updates
- dependency-name: vue
  dependency-version: 3.5.39
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-updates
- dependency-name: vue-tsc
  dependency-version: 3.3.7
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-20 08:07:44 +00:00
38 changed files with 1365 additions and 1979 deletions

View File

@@ -1,54 +0,0 @@
.git
**/.git
.gitmodules
**/.gitmodules
.github
.DS_Store
.fleet
.idea
.vscode
*.log
npm-debug.log
yarn-error.log
k8s
docs
e2e
tests
docker-compose.yml
docker/local
.phpunit.cache
.phpunit.result.cache
coverage
test-results
playwright-report
blob-report
playwright/.cache
openapi.json
playwright
playwright.config.ts
vitest.config.ts
phpunit.xml
phpstan.neon
pint.json
eslint.config.mjs
tsconfig.json
jsconfig.json
postcss.config.js
tailwind.config.js
node_modules
extensions/*/node_modules
Homestead.json
Homestead.yaml
auth.json
.env.backup
.rnd
_ide_helper.php
.phpstorm.meta.php
storage/logs/*
storage/*.key

View File

@@ -8,8 +8,6 @@ on:
pull_request: pull_request:
paths: paths:
- '.github/workflows/build-onpremise.yml' - '.github/workflows/build-onpremise.yml'
- '.dockerignore'
- 'extensions/manifest.json'
- 'docker/prod/**' - 'docker/prod/**'
workflow_dispatch: workflow_dispatch:
@@ -37,7 +35,7 @@ jobs:
steps: steps:
- name: "Check out code" - name: "Check out code"
uses: actions/checkout@v7 uses: actions/checkout@v6
with: with:
fetch-depth: 0 # Required for WyriHaximus/github-action-get-previous-tag fetch-depth: 0 # Required for WyriHaximus/github-action-get-previous-tag
@@ -93,23 +91,14 @@ jobs:
if: steps.cache-vendor.outputs.cache-hit != 'true' # Skip if cache hit if: steps.cache-vendor.outputs.cache-hit != 'true' # Skip if cache hit
- name: "Use Node.js" - name: "Use Node.js"
uses: actions/setup-node@v7 uses: actions/setup-node@v6
with: with:
node-version: '20.x' node-version: '20.x'
- name: "Read extension manifest"
id: extension-manifest
run: |
{
echo "invoicing_repository=$(jq -r '.Invoicing.repository' extensions/manifest.json)"
echo "invoicing_ref=$(jq -r '.Invoicing.ref' extensions/manifest.json)"
} >> "$GITHUB_OUTPUT"
- name: "Checkout invoicing extension" - name: "Checkout invoicing extension"
uses: actions/checkout@v7 uses: actions/checkout@v6
with: with:
repository: ${{ steps.extension-manifest.outputs.invoicing_repository }} repository: solidtime-io/extension-invoicing
ref: ${{ steps.extension-manifest.outputs.invoicing_ref }}
path: extensions/Invoicing path: extensions/Invoicing
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_INVOICING_EXTENSION }} ssh-key: ${{ secrets.SSH_PRIVATE_KEY_INVOICING_EXTENSION }}

View File

@@ -8,8 +8,6 @@ on:
pull_request: pull_request:
paths: paths:
- '.github/workflows/build-private.yml' - '.github/workflows/build-private.yml'
- '.dockerignore'
- 'extensions/manifest.json'
- 'docker/prod/**' - 'docker/prod/**'
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@@ -24,7 +22,7 @@ jobs:
steps: steps:
- name: "Check out code" - name: "Check out code"
uses: actions/checkout@v7 uses: actions/checkout@v6
with: with:
fetch-depth: 0 # Required for WyriHaximus/github-action-get-previous-tag fetch-depth: 0 # Required for WyriHaximus/github-action-get-previous-tag
@@ -70,27 +68,14 @@ jobs:
run: cat .env run: cat .env
- name: "Use Node.js" - name: "Use Node.js"
uses: actions/setup-node@v7 uses: actions/setup-node@v6
with: with:
node-version: '20.x' node-version: '20.x'
- name: "Read extension manifest"
id: extension-manifest
run: |
{
echo "billing_repository=$(jq -r '.Billing.repository' extensions/manifest.json)"
echo "billing_ref=$(jq -r '.Billing.ref' extensions/manifest.json)"
echo "services_repository=$(jq -r '.Services.repository' extensions/manifest.json)"
echo "services_ref=$(jq -r '.Services.ref' extensions/manifest.json)"
echo "invoicing_repository=$(jq -r '.Invoicing.repository' extensions/manifest.json)"
echo "invoicing_ref=$(jq -r '.Invoicing.ref' extensions/manifest.json)"
} >> "$GITHUB_OUTPUT"
- name: "Checkout billing extension" - name: "Checkout billing extension"
uses: actions/checkout@v7 uses: actions/checkout@v6
with: with:
repository: ${{ steps.extension-manifest.outputs.billing_repository }} repository: solidtime-io/extension-billing
ref: ${{ steps.extension-manifest.outputs.billing_ref }}
path: extensions/Billing path: extensions/Billing
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_BILLING_EXTENSION }} ssh-key: ${{ secrets.SSH_PRIVATE_KEY_BILLING_EXTENSION }}
@@ -108,10 +93,9 @@ jobs:
run: cd extensions/Billing && npm ci run: cd extensions/Billing && npm ci
- name: "Checkout services extension" - name: "Checkout services extension"
uses: actions/checkout@v7 uses: actions/checkout@v6
with: with:
repository: ${{ steps.extension-manifest.outputs.services_repository }} repository: solidtime-io/extension-services
ref: ${{ steps.extension-manifest.outputs.services_ref }}
path: extensions/Services path: extensions/Services
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_SERVICES_EXTENSION }} ssh-key: ${{ secrets.SSH_PRIVATE_KEY_SERVICES_EXTENSION }}
@@ -127,10 +111,9 @@ jobs:
run: cd extensions/Services && npm ci run: cd extensions/Services && npm ci
- name: "Checkout invoicing extension" - name: "Checkout invoicing extension"
uses: actions/checkout@v7 uses: actions/checkout@v6
with: with:
repository: ${{ steps.extension-manifest.outputs.invoicing_repository }} repository: solidtime-io/extension-invoicing
ref: ${{ steps.extension-manifest.outputs.invoicing_ref }}
path: extensions/Invoicing path: extensions/Invoicing
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_INVOICING_EXTENSION }} ssh-key: ${{ secrets.SSH_PRIVATE_KEY_INVOICING_EXTENSION }}

View File

@@ -8,7 +8,6 @@ on:
pull_request: pull_request:
paths: paths:
- '.github/workflows/build-public.yml' - '.github/workflows/build-public.yml'
- '.dockerignore'
- 'docker/prod/**' - 'docker/prod/**'
workflow_dispatch: workflow_dispatch:
@@ -37,7 +36,7 @@ jobs:
steps: steps:
- name: "Check out code" - name: "Check out code"
uses: actions/checkout@v7 uses: actions/checkout@v6
with: with:
fetch-depth: 0 # Required for WyriHaximus/github-action-get-previous-tag fetch-depth: 0 # Required for WyriHaximus/github-action-get-previous-tag
@@ -93,7 +92,7 @@ jobs:
if: steps.cache-vendor.outputs.cache-hit != 'true' # Skip if cache hit if: steps.cache-vendor.outputs.cache-hit != 'true' # Skip if cache hit
- name: "Use Node.js" - name: "Use Node.js"
uses: actions/setup-node@v7 uses: actions/setup-node@v6
with: with:
node-version: '20.x' node-version: '20.x'

View File

@@ -29,7 +29,7 @@ jobs:
steps: steps:
- name: "Checkout code" - name: "Checkout code"
uses: actions/checkout@v7 uses: actions/checkout@v6
- name: "Setup PHP" - name: "Setup PHP"
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2

View File

@@ -11,7 +11,7 @@ jobs:
steps: steps:
- name: "Checkout code" - name: "Checkout code"
uses: actions/checkout@v7 uses: actions/checkout@v6
- name: "Setup PHP (for Ziggy)" - name: "Setup PHP (for Ziggy)"
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2
@@ -24,7 +24,7 @@ jobs:
run: composer install -n --prefer-dist run: composer install -n --prefer-dist
- name: "Use Node.js" - name: "Use Node.js"
uses: actions/setup-node@v7 uses: actions/setup-node@v6
with: with:
node-version: '20.x' node-version: '20.x'

View File

@@ -9,10 +9,10 @@ jobs:
steps: steps:
- name: "Checkout code" - name: "Checkout code"
uses: actions/checkout@v7 uses: actions/checkout@v6
- name: "Use Node.js" - name: "Use Node.js"
uses: actions/setup-node@v7 uses: actions/setup-node@v6
with: with:
node-version: '20.x' node-version: '20.x'

View File

@@ -11,10 +11,10 @@ jobs:
steps: steps:
- name: "Checkout code" - name: "Checkout code"
uses: actions/checkout@v7 uses: actions/checkout@v6
- name: "Use Node.js" - name: "Use Node.js"
uses: actions/setup-node@v7 uses: actions/setup-node@v6
with: with:
node-version: '20.x' node-version: '20.x'

View File

@@ -11,11 +11,11 @@ jobs:
id-token: write id-token: write
steps: steps:
- name: "Checkout code" - name: "Checkout code"
uses: actions/checkout@v7 uses: actions/checkout@v6
# Setup .npmrc file to publish to npm # Setup .npmrc file to publish to npm
- name: Install root project dependencies - name: Install root project dependencies
run: npm ci run: npm ci
- uses: actions/setup-node@v7 - uses: actions/setup-node@v6
with: with:
node-version: '20.x' node-version: '20.x'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'

View File

@@ -11,9 +11,9 @@ jobs:
id-token: write id-token: write
steps: steps:
- name: "Checkout code" - name: "Checkout code"
uses: actions/checkout@v7 uses: actions/checkout@v6
# Setup .npmrc file to publish to npm # Setup .npmrc file to publish to npm
- uses: actions/setup-node@v7 - uses: actions/setup-node@v6
with: with:
node-version: '20.x' node-version: '20.x'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'

View File

@@ -13,10 +13,10 @@ jobs:
steps: steps:
- name: "Checkout code" - name: "Checkout code"
uses: actions/checkout@v7 uses: actions/checkout@v6
- name: "Use Node.js" - name: "Use Node.js"
uses: actions/setup-node@v7 uses: actions/setup-node@v6
with: with:
node-version: '20.x' node-version: '20.x'

View File

@@ -10,7 +10,7 @@ jobs:
steps: steps:
- name: "Checkout code" - name: "Checkout code"
uses: actions/checkout@v7 uses: actions/checkout@v6
- name: "Setup PHP (for Ziggy)" - name: "Setup PHP (for Ziggy)"
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2
@@ -23,7 +23,7 @@ jobs:
run: composer install -n --prefer-dist run: composer install -n --prefer-dist
- name: "Use Node.js" - name: "Use Node.js"
uses: actions/setup-node@v7 uses: actions/setup-node@v6
with: with:
node-version: '20.x' node-version: '20.x'

View File

@@ -9,7 +9,7 @@ jobs:
steps: steps:
- name: "Checkout code" - name: "Checkout code"
uses: actions/checkout@v7 uses: actions/checkout@v6
- name: "Setup PHP" - name: "Setup PHP"
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2

View File

@@ -36,7 +36,7 @@ jobs:
--health-retries 5 --health-retries 5
steps: steps:
- name: "Checkout code" - name: "Checkout code"
uses: actions/checkout@v7 uses: actions/checkout@v6
- name: "Setup PHP" - name: "Setup PHP"
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2
@@ -48,7 +48,7 @@ jobs:
- name: "Run composer install" - name: "Run composer install"
run: composer install -n --prefer-dist run: composer install -n --prefer-dist
- uses: actions/setup-node@v7 - uses: actions/setup-node@v6
with: with:
node-version: '20.x' node-version: '20.x'

View File

@@ -9,7 +9,7 @@ jobs:
steps: steps:
- name: "Checkout code" - name: "Checkout code"
uses: actions/checkout@v7 uses: actions/checkout@v6
- name: "Check code style" - name: "Check code style"
uses: aglipanci/laravel-pint-action@2.6 uses: aglipanci/laravel-pint-action@2.6

View File

@@ -35,10 +35,10 @@ jobs:
steps: steps:
- name: "Checkout code" - name: "Checkout code"
uses: actions/checkout@v7 uses: actions/checkout@v6
- name: "Setup node" - name: "Setup node"
uses: actions/setup-node@v7 uses: actions/setup-node@v6
with: with:
node-version: '20.x' node-version: '20.x'
@@ -99,10 +99,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: "Checkout code" - name: "Checkout code"
uses: actions/checkout@v7 uses: actions/checkout@v6
- name: "Setup node" - name: "Setup node"
uses: actions/setup-node@v7 uses: actions/setup-node@v6
with: with:
node-version: '20.x' node-version: '20.x'

3
.gitignore vendored
View File

@@ -26,10 +26,9 @@ yarn-error.log
/blob-report/ /blob-report/
/playwright/.cache/ /playwright/.cache/
/coverage /coverage
/extensions/* /extensions
!/extensions/.gitkeep !/extensions/.gitkeep
!/extensions/extensions_autoload.php !/extensions/extensions_autoload.php
!/extensions/manifest.json
/auth.json /auth.json
/modules_statuses.json /modules_statuses.json
/k8s /k8s

View File

@@ -67,7 +67,7 @@ class TimeEntryController extends Controller
$query = TimeEntry::query() $query = TimeEntry::query()
->where('organization_id', $organization->getKey()) ->where('organization_id', $organization->getKey())
->where('member_id', $member->getKey()) ->where('user_id', $member->user_id)
->when($exclude !== null, function (Builder $q) use ($exclude): void { ->when($exclude !== null, function (Builder $q) use ($exclude): void {
$q->where('id', '!=', $exclude->getKey()); $q->where('id', '!=', $exclude->getKey());
}) })
@@ -107,8 +107,8 @@ class TimeEntryController extends Controller
/** /**
* Get time entries in organization * Get time entries in organization
* *
* If you only need time entries for a specific user, you can filter by `member_id`. * If you only need time entries for a specific user, you can filter by `user_id`.
* Users with the permission `time-entries:view:own` can only use this endpoint with their own member ID in the member_id filter. * Users with the permission `time-entries:view:own` can only use this endpoint with their own user ID in the user_id filter.
* *
* @return TimeEntryCollection<TimeEntryResource> * @return TimeEntryCollection<TimeEntryResource>
* *
@@ -118,17 +118,16 @@ class TimeEntryController extends Controller
*/ */
public function index(Organization $organization, TimeEntryIndexRequest $request): JsonResource public function index(Organization $organization, TimeEntryIndexRequest $request): JsonResource
{ {
$member = $this->member($organization); /** @var Member|null $member */
/** @var Member|null $memberFilter */ $member = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
$memberFilter = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null; if ($member !== null && $member->user_id === Auth::id()) {
if ($memberFilter !== null && $memberFilter->getKey() === $member->getKey()) {
$this->checkPermission($organization, 'time-entries:view:own'); $this->checkPermission($organization, 'time-entries:view:own');
} else { } else {
$this->checkPermission($organization, 'time-entries:view:all'); $this->checkPermission($organization, 'time-entries:view:all');
} }
$canAccessPremiumFeatures = $this->canAccessPremiumFeatures($organization); $canAccessPremiumFeatures = $this->canAccessPremiumFeatures($organization);
$timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $memberFilter, $canAccessPremiumFeatures); $timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $member, $canAccessPremiumFeatures);
$totalCount = $timeEntriesQuery->count(); $totalCount = $timeEntriesQuery->count();
@@ -159,7 +158,7 @@ class TimeEntryController extends Controller
if ($timeEntries->count() === 0) { if ($timeEntries->count() === 0) {
Log::warning('User has has more than '.$limit.' time entries on one date', [ Log::warning('User has has more than '.$limit.' time entries on one date', [
'date' => $lastDate->toDateString(), 'date' => $lastDate->toDateString(),
'member_id' => $request->input('member_id'), 'user_id' => $request->input('user_id'),
'auth_user_id' => Auth::id(), 'auth_user_id' => Auth::id(),
'limit' => $limit, 'limit' => $limit,
]); ]);
@@ -222,10 +221,9 @@ class TimeEntryController extends Controller
*/ */
public function indexExport(Organization $organization, TimeEntryIndexExportRequest $request, TimeEntryAggregationService $timeEntryAggregationService): JsonResponse public function indexExport(Organization $organization, TimeEntryIndexExportRequest $request, TimeEntryAggregationService $timeEntryAggregationService): JsonResponse
{ {
$member = $this->member($organization); /** @var Member|null $member */
/** @var Member|null $memberFilter */ $member = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
$memberFilter = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null; if ($member !== null && $member->user_id === Auth::id()) {
if ($memberFilter !== null && $memberFilter->getKey() === $member->getKey()) {
$this->checkPermission($organization, 'time-entries:view:own'); $this->checkPermission($organization, 'time-entries:view:own');
} else { } else {
$this->checkPermission($organization, 'time-entries:view:all'); $this->checkPermission($organization, 'time-entries:view:all');
@@ -242,7 +240,7 @@ class TimeEntryController extends Controller
$roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null; $roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null;
$roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null; $roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null;
$timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $memberFilter, $canAccessPremiumFeatures); $timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $member, $canAccessPremiumFeatures);
$timeEntriesQuery->with([ $timeEntriesQuery->with([
'task', 'task',
'client', 'client',
@@ -265,7 +263,7 @@ class TimeEntryController extends Controller
if ($viewFile === false) { if ($viewFile === false) {
throw new \LogicException('View file not found'); throw new \LogicException('View file not found');
} }
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $memberFilter); $timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $member);
$aggregatedData = $timeEntryAggregationService->getAggregatedTimeEntries( $aggregatedData = $timeEntryAggregationService->getAggregatedTimeEntries(
$timeEntriesAggregateQuery, $timeEntriesAggregateQuery,
null, null,
@@ -372,10 +370,9 @@ class TimeEntryController extends Controller
*/ */
public function aggregate(Organization $organization, TimeEntryAggregateRequest $request, TimeEntryAggregationService $timeEntryAggregationService): array public function aggregate(Organization $organization, TimeEntryAggregateRequest $request, TimeEntryAggregationService $timeEntryAggregationService): array
{ {
$member = $this->member($organization); /** @var Member|null $member */
/** @var Member|null $memberFilter */ $member = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
$memberFilter = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null; if ($member !== null && $member->user_id === Auth::id()) {
if ($memberFilter !== null && $memberFilter->getKey() === $member->getKey()) {
$this->checkPermission($organization, 'time-entries:view:own'); $this->checkPermission($organization, 'time-entries:view:own');
} else { } else {
$this->checkPermission($organization, 'time-entries:view:all'); $this->checkPermission($organization, 'time-entries:view:all');
@@ -386,7 +383,7 @@ class TimeEntryController extends Controller
$group1Type = $request->getGroup(); $group1Type = $request->getGroup();
$group2Type = $request->getSubGroup(); $group2Type = $request->getSubGroup();
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $memberFilter); $timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $member);
$roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null; $roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null;
$roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null; $roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null;
@@ -422,10 +419,9 @@ class TimeEntryController extends Controller
*/ */
public function aggregateExport(Organization $organization, TimeEntryAggregateExportRequest $request, TimeEntryAggregationService $timeEntryAggregationService): JsonResponse public function aggregateExport(Organization $organization, TimeEntryAggregateExportRequest $request, TimeEntryAggregationService $timeEntryAggregationService): JsonResponse
{ {
$member = $this->member($organization); /** @var Member|null $member */
/** @var Member|null $memberFilter */ $member = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
$memberFilter = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null; if ($member !== null && $member->user_id === Auth::id()) {
if ($memberFilter !== null && $memberFilter->getKey() === $member->getKey()) {
$this->checkPermission($organization, 'time-entries:view:own'); $this->checkPermission($organization, 'time-entries:view:own');
} else { } else {
$this->checkPermission($organization, 'time-entries:view:all'); $this->checkPermission($organization, 'time-entries:view:all');
@@ -441,7 +437,7 @@ class TimeEntryController extends Controller
$group = $request->getGroup(); $group = $request->getGroup();
$subGroup = $request->getSubGroup(); $subGroup = $request->getSubGroup();
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $memberFilter); $timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $member);
$roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null; $roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null;
$roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null; $roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null;
@@ -584,7 +580,7 @@ class TimeEntryController extends Controller
{ {
/** @var Member $member */ /** @var Member $member */
$member = Member::query()->findOrFail($request->input('member_id')); $member = Member::query()->findOrFail($request->input('member_id'));
if ($member->getKey() === $this->member($organization)->getKey()) { if ($member->user_id === Auth::id()) {
$this->checkPermission($organization, 'time-entries:create:own'); $this->checkPermission($organization, 'time-entries:create:own');
} else { } else {
$this->checkPermission($organization, 'time-entries:create:all'); $this->checkPermission($organization, 'time-entries:create:all');
@@ -631,10 +627,9 @@ class TimeEntryController extends Controller
*/ */
public function update(Organization $organization, TimeEntry $timeEntry, TimeEntryUpdateRequest $request): JsonResource public function update(Organization $organization, TimeEntry $timeEntry, TimeEntryUpdateRequest $request): JsonResource
{ {
$member = $this->member($organization); /** @var Member|null $member */
/** @var Member|null $newMember */ $member = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
$newMember = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null; if ($timeEntry->member->user_id === Auth::id() && ($member === null || $member->user_id === Auth::id())) {
if ($timeEntry->member_id === $member->getKey() && ($newMember === null || $newMember->getKey() === $member->getKey())) {
$this->checkPermission($organization, 'time-entries:update:own', $timeEntry); $this->checkPermission($organization, 'time-entries:update:own', $timeEntry);
} else { } else {
$this->checkPermission($organization, 'time-entries:update:all', $timeEntry); $this->checkPermission($organization, 'time-entries:update:all', $timeEntry);
@@ -666,10 +661,6 @@ class TimeEntryController extends Controller
} }
$timeEntry->fill($request->validated()); $timeEntry->fill($request->validated());
if ($newMember !== null) {
$timeEntry->member()->associate($newMember);
$timeEntry->user()->associate($newMember->user);
}
$timeEntry->description = $request->input('description', $timeEntry->description) ?? ''; $timeEntry->description = $request->input('description', $timeEntry->description) ?? '';
$timeEntry->setComputedAttributeValue('billable_rate'); $timeEntry->setComputedAttributeValue('billable_rate');
$timeEntry->save(); $timeEntry->save();
@@ -699,7 +690,6 @@ class TimeEntryController extends Controller
*/ */
public function updateMultiple(Organization $organization, TimeEntryUpdateMultipleRequest $request): JsonResponse public function updateMultiple(Organization $organization, TimeEntryUpdateMultipleRequest $request): JsonResponse
{ {
$member = $this->member($organization);
$this->checkAnyPermission($organization, ['time-entries:update:all', 'time-entries:update:own']); $this->checkAnyPermission($organization, ['time-entries:update:all', 'time-entries:update:own']);
$canAccessAll = $this->hasPermission($organization, 'time-entries:update:all'); $canAccessAll = $this->hasPermission($organization, 'time-entries:update:all');
@@ -724,9 +714,6 @@ class TimeEntryController extends Controller
throw new AuthorizationException; throw new AuthorizationException;
} }
/** @var Member|null $newMember */
$newMember = isset($changes['member_id']) ? Member::query()->findOrFail($changes['member_id']) : null;
$project = null; $project = null;
$client = null; $client = null;
$overwriteClient = false; $overwriteClient = false;
@@ -753,7 +740,7 @@ class TimeEntryController extends Controller
continue; continue;
} }
if (! $canAccessAll && $timeEntry->member_id !== $member->getKey()) { if (! $canAccessAll && $timeEntry->user_id !== Auth::id()) {
$error->push($id); $error->push($id);
continue; continue;
@@ -763,10 +750,6 @@ class TimeEntryController extends Controller
$oldTask = $timeEntry->task; $oldTask = $timeEntry->task;
$timeEntry->fill($changes); $timeEntry->fill($changes);
if ($newMember !== null) {
$timeEntry->member()->associate($newMember);
$timeEntry->user_id = $newMember->user_id;
}
// If project is changed, but task is not, we remove the old task from the time entry // If project is changed, but task is not, we remove the old task from the time entry
if ($oldProject !== null && $project !== null && $oldProject->isNot($project) && $task === null) { if ($oldProject !== null && $project !== null && $oldProject->isNot($project) && $task === null) {
$timeEntry->task()->disassociate(); $timeEntry->task()->disassociate();
@@ -807,8 +790,7 @@ class TimeEntryController extends Controller
*/ */
public function destroy(Organization $organization, TimeEntry $timeEntry): JsonResponse public function destroy(Organization $organization, TimeEntry $timeEntry): JsonResponse
{ {
$member = $this->member($organization); if ($timeEntry->member->user_id === Auth::id()) {
if ($timeEntry->member_id === $member->getKey()) {
$this->checkPermission($organization, 'time-entries:delete:own', $timeEntry); $this->checkPermission($organization, 'time-entries:delete:own', $timeEntry);
} else { } else {
$this->checkPermission($organization, 'time-entries:delete:all', $timeEntry); $this->checkPermission($organization, 'time-entries:delete:all', $timeEntry);
@@ -865,7 +847,7 @@ class TimeEntryController extends Controller
continue; continue;
} }
if (! $canDeleteAll && $timeEntry->member_id !== $this->member($organization)->getKey()) { if (! $canDeleteAll && $timeEntry->user_id !== Auth::id()) {
$error->push($id); $error->push($id);
continue; continue;

View File

@@ -80,10 +80,6 @@ class PermissionStore
'invoices:update', 'invoices:update',
'invoices:download', 'invoices:download',
'invoices:delete', 'invoices:delete',
'invoice-recipients:view',
'invoice-recipients:create',
'invoice-recipients:update',
'invoice-recipients:delete',
'invoice-settings:view', 'invoice-settings:view',
'invoice-settings:update', 'invoice-settings:update',
], ],
@@ -151,10 +147,6 @@ class PermissionStore
'invoices:update', 'invoices:update',
'invoices:download', 'invoices:download',
'invoices:delete', 'invoices:delete',
'invoice-recipients:view',
'invoice-recipients:create',
'invoice-recipients:update',
'invoice-recipients:delete',
'invoice-settings:view', 'invoice-settings:view',
'invoice-settings:update', 'invoice-settings:update',
], ],
@@ -211,10 +203,6 @@ class PermissionStore
'invoices:update', 'invoices:update',
'invoices:download', 'invoices:download',
'invoices:delete', 'invoices:delete',
'invoice-recipients:view',
'invoice-recipients:create',
'invoice-recipients:update',
'invoice-recipients:delete',
'invoice-settings:view', 'invoice-settings:view',
'invoice-settings:update', 'invoice-settings:update',
], ],

View File

@@ -189,9 +189,7 @@ ENV WITH_HORIZON=false \
WITH_SCHEDULER=false \ WITH_SCHEDULER=false \
WITH_REVERB=false WITH_REVERB=false
COPY --link --chown=${WWWUSER}:${WWWUSER} . ./ COPY --link --chown=${WWWUSER}:${WWWUSER} . .
RUN test -z "$(find . -name .git -print -quit)"
#COPY --link --chown=${WWWUSER}:${WWWUSER} --from=build ${ROOT}/public public #COPY --link --chown=${WWWUSER}:${WWWUSER} --from=build ${ROOT}/public public
RUN mkdir -p \ RUN mkdir -p \

View File

@@ -1,14 +0,0 @@
{
"Billing": {
"repository": "solidtime-io/extension-billing",
"ref": "v0.0.4"
},
"Services": {
"repository": "solidtime-io/extension-services",
"ref": "v0.0.1"
},
"Invoicing": {
"repository": "solidtime-io/extension-invoicing",
"ref": "feature/recipients"
}
}

2338
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -25,58 +25,58 @@
"watch:api": "npm run watch --workspace=@solidtime/api" "watch:api": "npm run watch --workspace=@solidtime/api"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.3.5", "@eslint/eslintrc": "^3.3.6",
"@eslint/js": "^9.39.4", "@eslint/js": "^9.39.4",
"@inertiajs/vue3": "^2.3.23", "@inertiajs/vue3": "^2.3.23",
"@playwright/test": "^1.60.0", "@playwright/test": "^1.61.1",
"@tailwindcss/forms": "^0.5.11", "@tailwindcss/forms": "^0.5.11",
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.20",
"@types/chroma-js": "^3.1.2", "@types/chroma-js": "^3.1.2",
"@types/node": "^22.19.19", "@types/node": "^22.19.19",
"@vitejs/plugin-vue": "^6.0.6", "@vitejs/plugin-vue": "^6.0.7",
"@vue/test-utils": "^2.4.6", "@vue/test-utils": "^2.4.11",
"@vue/tsconfig": "^0.8.1", "@vue/tsconfig": "^0.9.1",
"autoprefixer": "^10.5.0", "autoprefixer": "^10.5.2",
"axios": "^1.16.0", "axios": "^1.18.1",
"eslint-plugin-unused-imports": "^4.4.1", "eslint-plugin-unused-imports": "^4.4.1",
"happy-dom": "^20.8.9", "happy-dom": "^20.10.6",
"laravel-vite-plugin": "^2.1.0", "laravel-vite-plugin": "^2.1.0",
"openapi-zod-client": "^1.18.3", "openapi-zod-client": "^1.18.3",
"postcss": "^8.5.14", "postcss": "^8.5.18",
"postcss-import": "^15.1.0", "postcss-import": "^15.1.0",
"postcss-nesting": "^12.1.5", "postcss-nesting": "^12.1.5",
"tailwindcss": "^3.4.19", "tailwindcss": "^3.4.19",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^7.3.3", "vite": "^7.3.3",
"vite-plugin-checker": "^0.12.0", "vite-plugin-checker": "^0.14.4",
"vitest": "^4.1.4", "vitest": "^4.1.10",
"vue": "^3.5.34", "vue": "^3.5.39",
"vue-tsc": "^3.2.8" "vue-tsc": "^3.3.7"
}, },
"dependencies": { "dependencies": {
"@floating-ui/core": "^1.7.5", "@floating-ui/core": "^1.8.0",
"@floating-ui/vue": "^1.1.11", "@floating-ui/vue": "^1.1.11",
"@heroicons/vue": "^2.2.0", "@heroicons/vue": "^2.2.0",
"@lucide/vue": "^1.28.0", "@lucide/vue": "^1.24.0",
"@rushstack/eslint-patch": "^1.16.1", "@rushstack/eslint-patch": "^1.16.1",
"@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/container-queries": "^0.1.1",
"@tanstack/vue-form": "^1.32.0", "@tanstack/vue-form": "^1.33.2",
"@tanstack/vue-query": "^5.100.10", "@tanstack/vue-query": "^5.101.2",
"@tanstack/vue-query-devtools": "^5.91.0", "@tanstack/vue-query-devtools": "^5.91.0",
"@tanstack/vue-table": "^8.21.3", "@tanstack/vue-table": "^8.21.3",
"@tanstack/vue-virtual": "^3.13.24", "@tanstack/vue-virtual": "^3.13.32",
"@vue/eslint-config-prettier": "^10.2.0", "@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.7.0", "@vue/eslint-config-typescript": "^14.9.0",
"@vueuse/core": "^14.4.0", "@vueuse/core": "^14.3.0",
"@vueuse/integrations": "^14.3.0", "@vueuse/integrations": "^14.3.0",
"@zodios/core": "^10.9.6", "@zodios/core": "^10.9.6",
"chroma-js": "^3.2.0", "chroma-js": "^3.2.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dayjs": "^1.11.20", "dayjs": "^1.11.21",
"echarts": "^6.0.0", "echarts": "^6.1.0",
"focus-trap": "^8.2.0", "focus-trap": "^8.2.2",
"parse-duration": "^2.1.6", "parse-duration": "^2.1.8",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"radix-vue": "^1.9.17", "radix-vue": "^1.9.17",
"reka-ui": "^2.10.1", "reka-ui": "^2.10.1",

View File

@@ -25,21 +25,16 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
<template> <template>
<AlertDialogPortal> <AlertDialogPortal>
<AlertDialogOverlay <AlertDialogOverlay
class="fixed inset-0 z-50 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"> class="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />
<div class="absolute inset-0 bg-default-background opacity-30" /> <AlertDialogContent
</AlertDialogOverlay> v-bind="forwarded"
<div :class="
class="fixed top-0 left-0 z-50 pointer-events-none w-screen h-screen flex items-start px-2 pt-3 md:pt-14 xl:pt-24 justify-center overflow-auto"> cn(
<AlertDialogContent 'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
v-bind="forwarded" props.class
:class=" )
cn( ">
'pointer-events-auto bg-default-background grid w-full max-w-lg gap-4 border border-border-tertiary p-6 shadow-lg duration-200 rounded-lg outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95', <slot />
props.class </AlertDialogContent>
)
">
<slot />
</AlertDialogContent>
</div>
</AlertDialogPortal> </AlertDialogPortal>
</template> </template>

View File

@@ -253,9 +253,7 @@ const page = usePage<{
v-if="isInvoicingActivated() && canViewInvoices()" v-if="isInvoicingActivated() && canViewInvoices()"
title="Invoices" title="Invoices"
:icon="DocumentTextIcon" :icon="DocumentTextIcon"
:current=" :current="route().current('invoices')"
route().current('invoices') || route().current('invoices.*')
"
href="/invoices"></NavigationSidebarItem> href="/invoices"></NavigationSidebarItem>
</ul> </ul>
</nav> </nav>

View File

@@ -117,8 +117,6 @@ export type DetailedInvoiceResponse = ZodiosResponseByAlias<SolidTimeApi, 'getIn
export type DetailedInvoice = DetailedInvoiceResponse['data']; export type DetailedInvoice = DetailedInvoiceResponse['data'];
export type InvoiceIndexEntry = ZodiosResponseByAlias<SolidTimeApi, 'getInvoices'>['data'][0]; export type InvoiceIndexEntry = ZodiosResponseByAlias<SolidTimeApi, 'getInvoices'>['data'][0];
export type InvoiceRecipient = ZodiosResponseByAlias<SolidTimeApi, 'getInvoiceRecipients'>['data'][0];
export type InvoiceRecipientBody = ZodiosBodyByAlias<SolidTimeApi, 'createInvoiceRecipient'>;
export type UpdateInvoiceSettings = ZodiosBodyByAlias<SolidTimeApi, 'updateInvoiceSettings'>; export type UpdateInvoiceSettings = ZodiosBodyByAlias<SolidTimeApi, 'updateInvoiceSettings'>;

View File

@@ -45,54 +45,14 @@ const InvitationResource = z
const InvitationStoreRequest = z const InvitationStoreRequest = z
.object({ email: z.string().email(), role: z.enum(['admin', 'manager', 'employee']) }) .object({ email: z.string().email(), role: z.enum(['admin', 'manager', 'employee']) })
.passthrough(); .passthrough();
const InvoiceRecipientResource = z
.object({
id: z.string(),
organization_id: z.string(),
name: z.string(),
vatin: z.union([z.string(), z.null()]),
address_line_1: z.union([z.string(), z.null()]),
address_line_2: z.union([z.string(), z.null()]),
address_line_3: z.union([z.string(), z.null()]),
address_post_code: z.union([z.string(), z.null()]),
address_city: z.union([z.string(), z.null()]),
address_country: z.union([z.string(), z.null()]),
phone: z.union([z.string(), z.null()]),
email: z.union([z.string(), z.null()]),
is_archived: z.boolean(),
archived_at: z.union([z.string(), z.null()]),
invoices_count: z.number().int(),
has_non_draft_invoices: z.boolean(),
created_at: z.union([z.string(), z.null()]),
updated_at: z.union([z.string(), z.null()]),
})
.passthrough();
const InvoiceRecipientCollection = z.array(InvoiceRecipientResource);
const InvoiceRecipientRequest = z
.object({
name: z.string(),
vatin: z.union([z.string(), z.null()]).optional(),
address_line_1: z.union([z.string(), z.null()]).optional(),
address_line_2: z.union([z.string(), z.null()]).optional(),
address_line_3: z.union([z.string(), z.null()]).optional(),
address_post_code: z.union([z.string(), z.null()]).optional(),
address_city: z.union([z.string(), z.null()]).optional(),
address_country: z.union([z.string(), z.null()]).optional(),
phone: z.union([z.string(), z.null()]).optional(),
email: z.union([z.string(), z.null()]).optional(),
is_archived: z.boolean().optional(),
})
.passthrough();
const InvoiceResource = z const InvoiceResource = z
.object({ .object({
id: z.string(), id: z.string(),
organization_id: z.string(), organization_id: z.string(),
invoice_recipient_id: z.string(),
reference: z.string(), reference: z.string(),
seller_name: z.string(), seller_name: z.string(),
recipient: z.string(), buyer_name: z.string(),
status: z.string(), status: z.string(),
status_label: z.string(),
date: z.string(), date: z.string(),
due_at: z.string(), due_at: z.string(),
paid_date: z.string(), paid_date: z.string(),
@@ -116,7 +76,16 @@ const InvoiceStoreRequest = z
seller_address_country: z.union([z.string(), z.null()]).optional(), seller_address_country: z.union([z.string(), z.null()]).optional(),
seller_phone: z.union([z.string(), z.null()]).optional(), seller_phone: z.union([z.string(), z.null()]).optional(),
seller_email: z.union([z.string(), z.null()]).optional(), seller_email: z.union([z.string(), z.null()]).optional(),
invoice_recipient_id: z.string(), buyer_name: z.string(),
buyer_vatin: z.union([z.string(), z.null()]).optional(),
buyer_address_line_1: z.union([z.string(), z.null()]).optional(),
buyer_address_line_2: z.union([z.string(), z.null()]).optional(),
buyer_address_line_3: z.union([z.string(), z.null()]).optional(),
buyer_address_post_code: z.union([z.string(), z.null()]).optional(),
buyer_address_city: z.union([z.string(), z.null()]).optional(),
buyer_address_country: z.union([z.string(), z.null()]).optional(),
buyer_phone: z.union([z.string(), z.null()]).optional(),
buyer_email: z.union([z.string(), z.null()]).optional(),
date: z.string(), date: z.string(),
billing_period_start: z.union([z.string(), z.null()]).optional(), billing_period_start: z.union([z.string(), z.null()]).optional(),
billing_period_end: z.union([z.string(), z.null()]).optional(), billing_period_end: z.union([z.string(), z.null()]).optional(),
@@ -161,7 +130,6 @@ const DetailedInvoiceResource = z
.object({ .object({
id: z.string(), id: z.string(),
organization_id: z.string(), organization_id: z.string(),
invoice_recipient_id: z.string(),
reference: z.string(), reference: z.string(),
seller_name: z.string(), seller_name: z.string(),
seller_vatin: z.string(), seller_vatin: z.string(),
@@ -173,7 +141,16 @@ const DetailedInvoiceResource = z
seller_address_country: z.string(), seller_address_country: z.string(),
seller_phone: z.string(), seller_phone: z.string(),
seller_email: z.string(), seller_email: z.string(),
recipient: InvoiceRecipientResource, buyer_name: z.string(),
buyer_vatin: z.string(),
buyer_address_line_1: z.string(),
buyer_address_line_2: z.string(),
buyer_address_line_3: z.string(),
buyer_address_post_code: z.string(),
buyer_address_city: z.string(),
buyer_address_country: z.string(),
buyer_phone: z.string(),
buyer_email: z.string(),
paid_date: z.string(), paid_date: z.string(),
due_at: z.string(), due_at: z.string(),
discount_type: z.string(), discount_type: z.string(),
@@ -194,7 +171,7 @@ const DetailedInvoiceResource = z
entries: z.array(InvoiceEntryResource), entries: z.array(InvoiceEntryResource),
}) })
.passthrough(); .passthrough();
const InvoiceStatus = z.enum(['draft', 'sent', 'paid', 'cancelled']); const InvoiceStatus = z.enum(['draft', 'sent', 'cancelled']);
const InvoiceUpdateRequest = z const InvoiceUpdateRequest = z
.object({ .object({
status: InvoiceStatus, status: InvoiceStatus,
@@ -210,7 +187,16 @@ const InvoiceUpdateRequest = z
seller_address_country: z.union([z.string(), z.null()]), seller_address_country: z.union([z.string(), z.null()]),
seller_phone: z.union([z.string(), z.null()]), seller_phone: z.union([z.string(), z.null()]),
seller_email: z.union([z.string(), z.null()]), seller_email: z.union([z.string(), z.null()]),
invoice_recipient_id: z.string(), buyer_name: z.string(),
buyer_vatin: z.union([z.string(), z.null()]),
buyer_address_line_1: z.union([z.string(), z.null()]),
buyer_address_line_2: z.union([z.string(), z.null()]),
buyer_address_line_3: z.union([z.string(), z.null()]),
buyer_address_post_code: z.union([z.string(), z.null()]),
buyer_address_city: z.union([z.string(), z.null()]),
buyer_address_country: z.union([z.string(), z.null()]),
buyer_phone: z.union([z.string(), z.null()]),
buyer_email: z.union([z.string(), z.null()]),
date: z.string(), date: z.string(),
billing_period_start: z.union([z.string(), z.null()]), billing_period_start: z.union([z.string(), z.null()]),
billing_period_end: z.union([z.string(), z.null()]), billing_period_end: z.union([z.string(), z.null()]),
@@ -1899,125 +1885,6 @@ const endpoints = makeApi([
}, },
], ],
}, },
{
method: 'get',
path: '/v1/organizations/:organization/invoice-recipients',
alias: 'getInvoiceRecipients',
requestFormat: 'json',
parameters: [
{
name: 'organization',
type: 'Path',
schema: z.string(),
},
],
response: z.object({ data: InvoiceRecipientCollection }).passthrough(),
},
{
method: 'post',
path: '/v1/organizations/:organization/invoice-recipients',
alias: 'createInvoiceRecipient',
requestFormat: 'json',
parameters: [
{
name: 'body',
type: 'Body',
schema: InvoiceRecipientRequest,
},
{
name: 'organization',
type: 'Path',
schema: z.string(),
},
],
response: z.object({ data: InvoiceRecipientResource }).passthrough(),
},
{
method: 'get',
path: '/v1/organizations/:organization/invoice-recipients/:invoiceRecipient',
alias: 'getInvoiceRecipient',
requestFormat: 'json',
parameters: [
{
name: 'organization',
type: 'Path',
schema: z.string(),
},
{
name: 'invoiceRecipient',
type: 'Path',
schema: z.string(),
},
],
response: z.object({ data: InvoiceRecipientResource }).passthrough(),
},
{
method: 'put',
path: '/v1/organizations/:organization/invoice-recipients/:invoiceRecipient',
alias: 'updateInvoiceRecipient',
requestFormat: 'json',
parameters: [
{
name: 'body',
type: 'Body',
schema: InvoiceRecipientRequest,
},
{
name: 'organization',
type: 'Path',
schema: z.string(),
},
{
name: 'invoiceRecipient',
type: 'Path',
schema: z.string(),
},
],
response: z.object({ data: InvoiceRecipientResource }).passthrough(),
},
{
method: 'post',
path: '/v1/organizations/:organization/invoice-recipients/:invoiceRecipient/duplicate',
alias: 'duplicateInvoiceRecipient',
requestFormat: 'json',
parameters: [
{
name: 'body',
type: 'Body',
schema: InvoiceRecipientRequest,
},
{
name: 'organization',
type: 'Path',
schema: z.string(),
},
{
name: 'invoiceRecipient',
type: 'Path',
schema: z.string(),
},
],
response: z.object({ data: InvoiceRecipientResource }).passthrough(),
},
{
method: 'delete',
path: '/v1/organizations/:organization/invoice-recipients/:invoiceRecipient',
alias: 'deleteInvoiceRecipient',
requestFormat: 'json',
parameters: [
{
name: 'organization',
type: 'Path',
schema: z.string(),
},
{
name: 'invoiceRecipient',
type: 'Path',
schema: z.string(),
},
],
response: z.void(),
},
{ {
method: 'get', method: 'get',
path: '/v1/organizations/:organization/invoices', path: '/v1/organizations/:organization/invoices',
@@ -2034,11 +1901,6 @@ const endpoints = makeApi([
type: 'Query', type: 'Query',
schema: z.number().int().gte(1).lte(2147483647).optional(), schema: z.number().int().gte(1).lte(2147483647).optional(),
}, },
{
name: 'status',
type: 'Query',
schema: InvoiceStatus.optional(),
},
], ],
response: z.object({ data: InvoiceCollection }).passthrough(), response: z.object({ data: InvoiceCollection }).passthrough(),
errors: [ errors: [

View File

@@ -1,25 +0,0 @@
<script setup lang="ts">
import type { ComboboxRootEmits, ComboboxRootProps } from 'reka-ui';
import type { HTMLAttributes } from 'vue';
import { reactiveOmit } from '@vueuse/core';
import { ComboboxRoot, useForwardPropsEmits } from 'reka-ui';
import { cn } from '../utils/cn';
const props = defineProps<ComboboxRootProps & { class?: HTMLAttributes['class'] }>();
const emits = defineEmits<ComboboxRootEmits>();
const delegatedProps = reactiveOmit(props, 'class');
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<!-- Keep the trigger inside this root. Reka treats anything within the root
element as "not outside", so it suppresses its own dismissal for trigger
clicks and ComboboxInput's blur-close ignores them. Putting the trigger
in a separate Popover instead gives two competing open states, and the
popover then closes on mousedown and reopens on the following click. -->
<ComboboxRoot v-slot="slotProps" v-bind="forwarded" :class="cn('min-w-0', props.class)">
<slot v-bind="slotProps" />
</ComboboxRoot>
</template>

View File

@@ -1,19 +0,0 @@
<script setup lang="ts">
import type { ComboboxAnchorProps } from 'reka-ui';
import type { HTMLAttributes } from 'vue';
import { reactiveOmit } from '@vueuse/core';
import { ComboboxAnchor, useForwardProps } from 'reka-ui';
import { cn } from '../utils/cn';
const props = defineProps<ComboboxAnchorProps & { class?: HTMLAttributes['class'] }>();
const delegatedProps = reactiveOmit(props, 'class');
const forwarded = useForwardProps(delegatedProps);
</script>
<template>
<ComboboxAnchor v-bind="forwarded" :class="cn('w-full', props.class)">
<slot />
</ComboboxAnchor>
</template>

View File

@@ -1,35 +0,0 @@
<script setup lang="ts">
import type { ComboboxInputEmits, ComboboxInputProps } from 'reka-ui';
import type { HTMLAttributes } from 'vue';
import { reactiveOmit } from '@vueuse/core';
import { Search } from '@lucide/vue';
import { ComboboxInput, useForwardPropsEmits } from 'reka-ui';
import { cn } from '../utils/cn';
defineOptions({
inheritAttrs: false,
});
const props = defineProps<ComboboxInputProps & { class?: HTMLAttributes['class'] }>();
const emits = defineEmits<ComboboxInputEmits>();
const delegatedProps = reactiveOmit(props, 'class');
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<div class="relative items-center border-b border-card-background-separator">
<ComboboxInput
v-bind="{ ...$attrs, ...forwarded }"
:class="
cn(
'h-10 w-full border-0 rounded-none bg-transparent pl-9 pr-3 text-sm text-text-primary placeholder:text-text-tertiary focus:outline-none focus:ring-0',
props.class
)
" />
<span class="absolute start-0 inset-y-0 flex items-center justify-center px-3">
<Search class="size-4 text-text-tertiary" />
</span>
</div>
</template>

View File

@@ -1,27 +0,0 @@
<script setup lang="ts">
import type { ComboboxItemEmits, ComboboxItemProps } from 'reka-ui';
import type { HTMLAttributes } from 'vue';
import { reactiveOmit } from '@vueuse/core';
import { ComboboxItem, useForwardPropsEmits } from 'reka-ui';
import { cn } from '../utils/cn';
const props = defineProps<ComboboxItemProps & { class?: HTMLAttributes['class'] }>();
const emits = defineEmits<ComboboxItemEmits>();
const delegatedProps = reactiveOmit(props, 'class');
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<ComboboxItem
v-bind="forwarded"
:class="
cn(
'flex w-full cursor-default items-center rounded-md px-2 py-1.5 text-sm text-text-primary data-[highlighted]:bg-card-background-active',
props.class
)
">
<slot />
</ComboboxItem>
</template>

View File

@@ -1,41 +0,0 @@
<script setup lang="ts">
import type { ComboboxContentEmits, ComboboxContentProps } from 'reka-ui';
import type { HTMLAttributes } from 'vue';
import { reactiveOmit } from '@vueuse/core';
import { ComboboxContent, ComboboxPortal, useForwardPropsEmits } from 'reka-ui';
import { cn } from '../utils/cn';
defineOptions({
inheritAttrs: false,
});
const props = withDefaults(
defineProps<ComboboxContentProps & { class?: HTMLAttributes['class'] }>(),
{
position: 'popper',
align: 'start',
sideOffset: 4,
class: undefined,
}
);
const emits = defineEmits<ComboboxContentEmits>();
const delegatedProps = reactiveOmit(props, 'class');
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<ComboboxPortal>
<ComboboxContent
v-bind="{ ...$attrs, ...forwarded }"
:class="
cn(
'z-50 w-[--reka-popper-anchor-width] min-w-60 overflow-hidden rounded-lg border border-popover-border bg-popover text-popover-foreground shadow-dropdown outline-none origin-[var(--reka-combobox-content-transform-origin)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
props.class
)
">
<slot />
</ComboboxContent>
</ComboboxPortal>
</template>

View File

@@ -1,19 +0,0 @@
<script setup lang="ts">
import type { ComboboxSeparatorProps } from 'reka-ui';
import type { HTMLAttributes } from 'vue';
import { reactiveOmit } from '@vueuse/core';
import { ComboboxSeparator, useForwardProps } from 'reka-ui';
import { cn } from '../utils/cn';
const props = defineProps<ComboboxSeparatorProps & { class?: HTMLAttributes['class'] }>();
const delegatedProps = reactiveOmit(props, 'class');
const forwarded = useForwardProps(delegatedProps);
</script>
<template>
<ComboboxSeparator
v-bind="forwarded"
:class="cn('my-1 h-px bg-card-background-separator', props.class)" />
</template>

View File

@@ -1,29 +0,0 @@
<script setup lang="ts">
import type { ComboboxTriggerProps } from 'reka-ui';
import type { HTMLAttributes } from 'vue';
import { reactiveOmit } from '@vueuse/core';
import { ComboboxTrigger, useForwardProps } from 'reka-ui';
import { cn } from '../utils/cn';
const props = defineProps<ComboboxTriggerProps & { class?: HTMLAttributes['class'] }>();
const delegatedProps = reactiveOmit(props, 'class');
const forwarded = useForwardProps(delegatedProps);
</script>
<template>
<!-- Reka hardcodes tabindex="-1" here because it expects a ComboboxInput
next to the trigger in the anchor to be the focusable control. Our input
lives inside ComboboxList, so this trigger is the control and has to be
tabbable. tabindex is a fallthrough attr, which Vue applies after Reka's
own props and therefore wins.
Reka also hardcodes aria-label="Show popup", which would otherwise be
the accessible name. Pass :aria-label on the child element (it wins
again, because Slot merges child props over attrs) to name the trigger
after its current value. -->
<ComboboxTrigger v-bind="forwarded" :class="cn(props.class)" tabindex="0">
<slot />
</ComboboxTrigger>
</template>

View File

@@ -1,38 +0,0 @@
<script setup lang="ts">
import type { ComboboxViewportProps } from 'reka-ui';
import type { HTMLAttributes } from 'vue';
import { reactiveOmit } from '@vueuse/core';
import { ComboboxViewport, useForwardProps } from 'reka-ui';
import { cn } from '../utils/cn';
const props = defineProps<ComboboxViewportProps & { class?: HTMLAttributes['class'] }>();
const delegatedProps = reactiveOmit(props, 'class');
const forwarded = useForwardProps(delegatedProps);
</script>
<template>
<ComboboxViewport
v-bind="forwarded"
:class="cn('ui-combobox-viewport max-h-60 overflow-y-auto p-2', props.class)">
<slot />
</ComboboxViewport>
</template>
<style>
/* Reka's ComboboxViewport injects a global style that hides scrollbars; the
attribute+class selector below is more specific and restores them. */
[data-reka-combobox-viewport].ui-combobox-viewport {
scrollbar-width: thin;
-ms-overflow-style: auto;
}
[data-reka-combobox-viewport].ui-combobox-viewport::-webkit-scrollbar {
display: block;
width: 8px;
}
[data-reka-combobox-viewport].ui-combobox-viewport::-webkit-scrollbar-thumb {
background-color: rgb(127 127 127 / 0.4);
border-radius: 4px;
}
</style>

View File

@@ -1,9 +0,0 @@
export { default as Combobox } from './Combobox.vue';
export { default as ComboboxAnchor } from './ComboboxAnchor.vue';
export { default as ComboboxInput } from './ComboboxInput.vue';
export { default as ComboboxItem } from './ComboboxItem.vue';
export { default as ComboboxList } from './ComboboxList.vue';
export { default as ComboboxSeparator } from './ComboboxSeparator.vue';
export { default as ComboboxTrigger } from './ComboboxTrigger.vue';
export { default as ComboboxViewport } from './ComboboxViewport.vue';
export { ComboboxVirtualizer } from 'reka-ui';

View File

@@ -57,16 +57,6 @@ import {
CalendarNextButton, CalendarNextButton,
CalendarPrevButton, CalendarPrevButton,
} from './calendar/index'; } from './calendar/index';
import {
Combobox,
ComboboxAnchor,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxSeparator,
ComboboxTrigger,
ComboboxViewport,
} from './combobox/index';
import { CommandPalette } from './CommandPalette/index'; import { CommandPalette } from './CommandPalette/index';
import { import {
ContextMenu, ContextMenu,
@@ -186,14 +176,6 @@ export {
CardTitle, CardTitle,
Checkbox, Checkbox,
color, color,
Combobox,
ComboboxAnchor,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxSeparator,
ComboboxTrigger,
ComboboxViewport,
CommandPalette, CommandPalette,
ContextMenu, ContextMenu,
ContextMenuCheckboxItem, ContextMenuCheckboxItem,

View File

@@ -92,30 +92,6 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
$response->assertJsonPath('data.0.id', $timeEntry->getKey()); $response->assertJsonPath('data.0.id', $timeEntry->getKey());
} }
public function test_index_endpoint_filters_by_member_id_instead_of_legacy_user_id(): void
{
// Arrange
$data = $this->createUserWithPermission([
'time-entries:view:own',
]);
$legacyUser = User::factory()->create();
$timeEntry = TimeEntry::factory()->forMember($data->member)->create([
'user_id' => $legacyUser->getKey(),
]);
Passport::actingAs($data->user);
// Act
$response = $this->getJson(route('api.v1.time-entries.index', [
$data->organization->getKey(),
'member_id' => $data->member->getKey(),
]));
// Assert
$this->assertResponseCode($response, 200);
$response->assertJsonCount(1, 'data');
$response->assertJsonPath('data.0.id', $timeEntry->getKey());
}
public function test_index_endpoint_fails_if_user_filter_is_from_different_organization(): void public function test_index_endpoint_fails_if_user_filter_is_from_different_organization(): void
{ {
// Arrange // Arrange
@@ -150,10 +126,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
Passport::actingAs($data->user); Passport::actingAs($data->user);
// Act // Act
$response = $this->getJson(route('api.v1.time-entries.index', [ $response = $this->getJson(route('api.v1.time-entries.index', [$data->organization->getKey(), 'user_id' => $user->getKey()]));
$data->organization->getKey(),
'member_id' => $member->getKey(),
]));
// Assert // Assert
$this->assertResponseCode($response, 200); $this->assertResponseCode($response, 200);
@@ -1799,29 +1772,6 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
]); ]);
} }
public function test_aggregate_endpoint_filters_by_member_id_instead_of_legacy_user_id(): void
{
// Arrange
$data = $this->createUserWithPermission([
'time-entries:view:own',
]);
$legacyUser = User::factory()->create();
TimeEntry::factory()->forMember($data->member)->startWithDuration(Carbon::now(), 100)->create([
'user_id' => $legacyUser->getKey(),
]);
Passport::actingAs($data->user);
// Act
$response = $this->getJson(route('api.v1.time-entries.aggregate', [
$data->organization->getKey(),
'member_id' => $data->member->getKey(),
]));
// Assert
$response->assertSuccessful();
$response->assertJsonPath('data.seconds', 100);
}
public function test_aggregate_endpoint_groups_by_two_groups(): void public function test_aggregate_endpoint_groups_by_two_groups(): void
{ {
// Arrange // Arrange
@@ -2869,32 +2819,6 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
]); ]);
} }
public function test_update_endpoint_updates_user_id_when_member_id_changes(): void
{
// Arrange
$data = $this->createUserWithPermission([
'time-entries:update:all',
]);
$otherUser = User::factory()->create();
$otherMember = Member::factory()->forOrganization($data->organization)->forUser($otherUser)->role(Role::Employee)->create();
$timeEntry = TimeEntry::factory()->forMember($data->member)->create();
Passport::actingAs($data->user);
// Act
$response = $this->putJson(route('api.v1.time-entries.update', [$data->organization->getKey(), $timeEntry->getKey()]), [
'member_id' => $otherMember->getKey(),
]);
// Assert
$response->assertValid();
$this->assertResponseCode($response, 200);
$this->assertDatabaseHas(TimeEntry::class, [
'id' => $timeEntry->getKey(),
'member_id' => $otherMember->getKey(),
'user_id' => $otherUser->getKey(),
]);
}
public function test_update_endpoint_can_update_project_and_automatically_set_client(): void public function test_update_endpoint_can_update_project_and_automatically_set_client(): void
{ {
// Arrange // Arrange
@@ -3231,40 +3155,6 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
]); ]);
} }
public function test_destroy_multiple_uses_member_id_for_own_permission_checks(): void
{
// Arrange
$data = $this->createUserWithPermission([
'time-entries:delete:own',
]);
$otherUser = User::factory()->create();
$otherMember = Member::factory()->forOrganization($data->organization)->forUser($otherUser)->role(Role::Employee)->create();
$timeEntry = TimeEntry::factory()->forMember($otherMember)->create([
'user_id' => $data->user->getKey(),
]);
Passport::actingAs($data->user);
// Act
$response = $this->deleteJson(route('api.v1.time-entries.destroy-multiple', [$data->organization->getKey()]), [
'ids' => [
$timeEntry->getKey(),
],
]);
// Assert
$response->assertValid();
$this->assertResponseCode($response, 200);
$response->assertExactJson([
'success' => [],
'error' => [
$timeEntry->getKey(),
],
]);
$this->assertDatabaseHas(TimeEntry::class, [
'id' => $timeEntry->getKey(),
]);
}
public function test_destroy_multiple_deletes_all_time_entries_and_fails_for_time_entries_of_other_users_and_and_other_organizations_with_all_time_entries_permission(): void public function test_destroy_multiple_deletes_all_time_entries_and_fails_for_time_entries_of_other_users_and_and_other_organizations_with_all_time_entries_permission(): void
{ {
// Arrange // Arrange
@@ -3676,46 +3566,6 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
]); ]);
} }
public function test_update_multiple_uses_member_id_for_own_permission_checks(): void
{
// Arrange
$data = $this->createUserWithPermission([
'time-entries:update:own',
'projects:view:all',
]);
$otherUser = User::factory()->create();
$otherMember = Member::factory()->forOrganization($data->organization)->forUser($otherUser)->role(Role::Employee)->create();
$timeEntry = TimeEntry::factory()->forMember($otherMember)->create([
'user_id' => $data->user->getKey(),
]);
$timeEntriesFake = TimeEntry::factory()->forOrganization($data->organization)->make();
Passport::actingAs($data->user);
// Act
$response = $this->patchJson(route('api.v1.time-entries.update-multiple', [$data->organization->getKey()]), [
'ids' => [
$timeEntry->getKey(),
],
'changes' => [
'description' => $timeEntriesFake->description,
],
]);
// Assert
$response->assertValid();
$this->assertResponseCode($response, 200);
$response->assertExactJson([
'success' => [],
'error' => [
$timeEntry->getKey(),
],
]);
$this->assertDatabaseHas(TimeEntry::class, [
'id' => $timeEntry->getKey(),
'description' => $timeEntry->description,
]);
}
public function test_update_multiple_updates_sets_description_to_empty_if_the_client_sends_null(): void public function test_update_multiple_updates_sets_description_to_empty_if_the_client_sends_null(): void
{ {
// Arrange // Arrange
@@ -3762,51 +3612,6 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
]); ]);
} }
public function test_update_multiple_updates_user_id_when_member_id_changes(): void
{
// Arrange
$data = $this->createUserWithPermission([
'time-entries:update:all',
]);
$otherUser = User::factory()->create();
$otherMember = Member::factory()->forOrganization($data->organization)->forUser($otherUser)->role(Role::Employee)->create();
$timeEntry1 = TimeEntry::factory()->forMember($data->member)->create();
$timeEntry2 = TimeEntry::factory()->forMember($data->member)->create();
Passport::actingAs($data->user);
// Act
$response = $this->patchJson(route('api.v1.time-entries.update-multiple', [$data->organization->getKey()]), [
'ids' => [
$timeEntry1->getKey(),
$timeEntry2->getKey(),
],
'changes' => [
'member_id' => $otherMember->getKey(),
],
]);
// Assert
$response->assertValid();
$response->assertStatus(200);
$response->assertExactJson([
'success' => [
$timeEntry1->getKey(),
$timeEntry2->getKey(),
],
'error' => [],
]);
$this->assertDatabaseHas(TimeEntry::class, [
'id' => $timeEntry1->getKey(),
'member_id' => $otherMember->getKey(),
'user_id' => $otherUser->getKey(),
]);
$this->assertDatabaseHas(TimeEntry::class, [
'id' => $timeEntry2->getKey(),
'member_id' => $otherMember->getKey(),
'user_id' => $otherUser->getKey(),
]);
}
public function test_update_multiple_updates_all_time_entries_and_fails_for_time_entries_of_other_users_and_and_other_organizations_with_all_time_entries_permission(): void public function test_update_multiple_updates_all_time_entries_and_fails_for_time_entries_of_other_users_and_and_other_organizations_with_all_time_entries_permission(): void
{ {
// Arrange // Arrange