Compare commits
19
Commits
v0.1.1
..
0f125f17a3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f125f17a3 | ||
|
|
5e7be28daf | ||
|
|
d250f0b380 | ||
|
|
c19cf34505 | ||
|
|
37b2e6034f | ||
|
|
84e427ef5d | ||
|
|
95b53ba74b | ||
|
|
a8952d7867 | ||
|
|
2390daf2de | ||
|
|
b1357cf854 | ||
|
|
2c229c9b39 | ||
|
|
9a7dd7fb59 | ||
|
|
39a039cd1e | ||
|
|
e38c77198d | ||
|
|
ee9b9a2765 | ||
|
|
57711b9659 | ||
|
|
5e6426067a | ||
|
|
58d2edcacd | ||
|
|
3b17c9ee14 |
@@ -1,96 +0,0 @@
|
||||
---
|
||||
name: mcp-development
|
||||
description: "Use this skill for Laravel MCP development only. Trigger when creating or editing MCP tools, resources, prompts, or servers in Laravel projects. Covers: artisan make:mcp-* generators, mcp:inspector, routes/ai.php, Tool/Resource/Prompt classes, schema validation, shouldRegister(), OAuth setup, URI templates, read-only attributes, and MCP debugging. Do not use for non-Laravel MCP projects or generic AI features without MCP."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# MCP Development
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Laravel MCP patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
Register MCP servers in `routes/ai.php`:
|
||||
|
||||
<!-- Register MCP Server -->
|
||||
```php
|
||||
use Laravel\Mcp\Facades\Mcp;
|
||||
|
||||
Mcp::web();
|
||||
```
|
||||
|
||||
### Creating MCP Primitives
|
||||
|
||||
Create MCP tools, resources, prompts, and servers using artisan commands:
|
||||
|
||||
```bash
|
||||
php artisan make:mcp-tool ToolName # Create a tool
|
||||
|
||||
php artisan make:mcp-resource ResourceName # Create a resource
|
||||
|
||||
php artisan make:mcp-prompt PromptName # Create a prompt
|
||||
|
||||
php artisan make:mcp-server ServerName # Create a server
|
||||
|
||||
```
|
||||
|
||||
After creating primitives, register them in your server's `$tools`, `$resources`, or `$prompts` properties.
|
||||
|
||||
### Tools
|
||||
|
||||
<!-- MCP Tool Example -->
|
||||
```php
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
use Laravel\Mcp\Server\Request;
|
||||
use Laravel\Mcp\Server\Response;
|
||||
|
||||
class MyTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
return new Response(['result' => 'success']);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Registering Primitives in a Server
|
||||
|
||||
Each MCP server must explicitly declare the tools, resources, and prompts it exposes.
|
||||
|
||||
<!-- Register Primitives in MCP Server -->
|
||||
```php
|
||||
use Laravel\Mcp\Server;
|
||||
|
||||
class AppServer extends Server
|
||||
{
|
||||
protected array $tools = [
|
||||
\App\Mcp\Tools\MyTool::class,
|
||||
];
|
||||
|
||||
protected array $resources = [
|
||||
\App\Mcp\Resources\MyResource::class,
|
||||
];
|
||||
|
||||
protected array $prompts = [
|
||||
\App\Mcp\Prompts\MyPrompt::class,
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
1. Check `routes/ai.php` for proper registration
|
||||
2. Test tool via MCP client
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Running `mcp:start` command (it hangs waiting for input)
|
||||
- Using HTTPS locally with Node-based MCP clients
|
||||
- Not using `search-docs` for the latest MCP documentation
|
||||
- Not registering MCP server routes in `routes/ai.php`
|
||||
- Do not register `ai.php` in `bootstrap.php`; it is registered automatically.
|
||||
- OAuth registration supports custom URI schemes (e.g., `cursor://`, `vscode://`) for native desktop clients via `mcp.custom_schemes` config
|
||||
+7
-6
@@ -25,6 +25,7 @@
|
||||
**/compose.*.y*ml
|
||||
!**/composer.json
|
||||
!**/composer.lock
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
@@ -40,9 +41,9 @@ vendor
|
||||
LICENSE
|
||||
README.md
|
||||
|
||||
# Runtime-generated data must not become part of an image layer.
|
||||
storage/framework/cache/data/*
|
||||
storage/framework/sessions/*
|
||||
storage/framework/testing/*
|
||||
storage/framework/views/*
|
||||
storage/logs/*
|
||||
# Ignore development-related files and directories
|
||||
# For development environment, we will use mount bind to mount the source code to the container
|
||||
# So we don't need to copy the source code to the container for development
|
||||
tests/
|
||||
phpunit.xml
|
||||
phpunit.xml.dist
|
||||
|
||||
+1
-17
@@ -10,7 +10,7 @@ APP_LOCALE=en
|
||||
APP_FALLBACK_LOCALE=en
|
||||
APP_FAKER_LOCALE=en_US
|
||||
|
||||
LOG_STACK=stderr
|
||||
LOG_STACK=daily
|
||||
|
||||
# Optional Discord error logging (append `,discord_errors` to LOG_STACK to enable it)
|
||||
LOG_DISCORD_WEBHOOK_URL=
|
||||
@@ -38,22 +38,6 @@ MAIL_ENCRYPTION=tls
|
||||
MAIL_FROM_ADDRESS=no-reply@example.com
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
CACHE_STORE=database
|
||||
QUEUE_CONNECTION=database
|
||||
SESSION_DRIVER=database
|
||||
|
||||
FILESYSTEM_DISK=public
|
||||
AVATAR_FILESYSTEM_DISK=public
|
||||
AVATAR_URLS_TEMPORARY=false
|
||||
AVATAR_URL_EXPIRATION_MINUTES=15
|
||||
|
||||
# Production: set AVATAR_FILESYSTEM_DISK=s3 and inject these as runtime secrets.
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=eu-central-1
|
||||
AWS_BUCKET=
|
||||
AWS_URL=
|
||||
AWS_ENDPOINT=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
name: test_and_check_app
|
||||
description: Build the test image and run all application checks
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Test and check app
|
||||
shell: bash
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
|
||||
docker buildx build \
|
||||
--file Dockerfile \
|
||||
--target test \
|
||||
--tag "local/kanbangit-test:${GITHUB_SHA}" \
|
||||
--progress plain \
|
||||
--load \
|
||||
.
|
||||
@@ -1,130 +0,0 @@
|
||||
name: CI/CD
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
REGISTRY: git.twyco.de
|
||||
|
||||
jobs:
|
||||
test_and_check_app:
|
||||
name: test_and_check_app
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
|
||||
- name: Run required application checks
|
||||
uses: ./.github/tests/test_and_check_app
|
||||
|
||||
publish:
|
||||
name: Publish immutable image
|
||||
if: >-
|
||||
github.event_name == 'push' &&
|
||||
(github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
|
||||
needs:
|
||||
- test_and_check_app
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Validate ref and calculate image name
|
||||
id: image
|
||||
shell: bash
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
|
||||
image_path="$(printf '%s' "${GITHUB_REPOSITORY}" | tr '[:upper:]' '[:lower:]')"
|
||||
image="${REGISTRY}/${image_path}"
|
||||
|
||||
case "${GITHUB_REF}" in
|
||||
refs/heads/main)
|
||||
printf -v run_attempt '%03d' "${GITHUB_RUN_ATTEMPT}"
|
||||
tag="main-${GITHUB_RUN_NUMBER}-${run_attempt}-${GITHUB_SHA:0:12}"
|
||||
environment="staging"
|
||||
;;
|
||||
refs/tags/*)
|
||||
tag="${GITHUB_REF#refs/tags/}"
|
||||
|
||||
if [[ ! "${tag}" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
|
||||
echo "Production tags must match vMAJOR.MINOR.PATCH." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git fetch --no-tags origin main:refs/remotes/origin/main
|
||||
|
||||
if ! git merge-base --is-ancestor "${GITHUB_SHA}" origin/main; then
|
||||
echo "Production tag must point to a commit contained in main." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
environment="production"
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported ref: ${GITHUB_REF}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
{
|
||||
echo "image=${image}"
|
||||
echo "tag=${tag}"
|
||||
echo "environment=${environment}"
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Log in to private registry
|
||||
shell: bash
|
||||
env:
|
||||
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
|
||||
if [[ -z "${REGISTRY_USERNAME}" || -z "${REGISTRY_TOKEN}" ]]; then
|
||||
echo "Registry credentials are missing." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '%s' "${REGISTRY_TOKEN}" |
|
||||
docker login \
|
||||
--username "${REGISTRY_USERNAME}" \
|
||||
--password-stdin \
|
||||
"${REGISTRY}"
|
||||
|
||||
- name: Build and publish runtime image
|
||||
shell: bash
|
||||
env:
|
||||
IMAGE: ${{ steps.image.outputs.image }}
|
||||
IMAGE_TAG: ${{ steps.image.outputs.tag }}
|
||||
DEPLOYMENT_ENVIRONMENT: ${{ steps.image.outputs.environment }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
|
||||
if [[ "${DEPLOYMENT_ENVIRONMENT}" == "production" ]] &&
|
||||
docker buildx imagetools inspect "${IMAGE}:${IMAGE_TAG}" >/dev/null 2>&1; then
|
||||
echo "Production image ${IMAGE}:${IMAGE_TAG} already exists and will not be overwritten." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker buildx build \
|
||||
--file Dockerfile \
|
||||
--target runtime \
|
||||
--label "org.opencontainers.image.created=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||
--label "org.opencontainers.image.revision=${GITHUB_SHA}" \
|
||||
--label "org.opencontainers.image.source=${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" \
|
||||
--label "de.twyco.environment=${DEPLOYMENT_ENVIRONMENT}" \
|
||||
--tag "${IMAGE}:${IMAGE_TAG}" \
|
||||
--push \
|
||||
.
|
||||
|
||||
- name: Remove registry credentials
|
||||
if: ${{ always() }}
|
||||
shell: bash
|
||||
run: docker logout "${REGISTRY}" >/dev/null 2>&1 || true
|
||||
@@ -0,0 +1,39 @@
|
||||
name: linter
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- staging
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
quality:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.4'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
|
||||
npm install
|
||||
|
||||
- name: Run Pint
|
||||
run: composer lint
|
||||
|
||||
- name: Lint Frontend
|
||||
run: npm run lint
|
||||
|
||||
# - name: Commit Changes
|
||||
# uses: stefanzweifel/git-auto-commit-action@v7
|
||||
# with:
|
||||
# commit_message: fix code style
|
||||
# commit_options: '--no-verify'
|
||||
@@ -0,0 +1,49 @@
|
||||
name: tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- staging
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.4']
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
tools: composer:v2
|
||||
coverage: xdebug
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Install Node Dependencies
|
||||
run: npm i
|
||||
|
||||
- name: Install Dependencies
|
||||
run: composer install --no-interaction --prefer-dist --optimize-autoloader
|
||||
|
||||
- name: Copy Environment File
|
||||
run: cp .env.example .env
|
||||
|
||||
- name: Generate Application Key
|
||||
run: php artisan key:generate
|
||||
|
||||
- name: Build Assets
|
||||
run: npm run build
|
||||
|
||||
- name: Tests
|
||||
run: ./vendor/bin/phpunit
|
||||
@@ -13,10 +13,10 @@ This application is a Laravel application and its main Laravel ecosystems packag
|
||||
- inertiajs/inertia-laravel (INERTIA_LARAVEL) - v2
|
||||
- laravel/fortify (FORTIFY) - v1
|
||||
- laravel/framework (LARAVEL) - v13
|
||||
- laravel/mcp (MCP) - v0
|
||||
- laravel/prompts (PROMPTS) - v0
|
||||
- laravel/wayfinder (WAYFINDER) - v0
|
||||
- laravel/boost (BOOST) - v2
|
||||
- laravel/mcp (MCP) - v0
|
||||
- laravel/pail (PAIL) - v1
|
||||
- laravel/pint (PINT) - v1
|
||||
- laravel/sail (SAIL) - v1
|
||||
@@ -34,7 +34,7 @@ This project has domain-specific skills available. You MUST activate the relevan
|
||||
|
||||
- `fortify-development` — ACTIVATE when the user works on authentication in Laravel. This includes login, registration, password reset, email verification, two-factor authentication (2FA/TOTP/QR codes/recovery codes), profile updates, password confirmation, or any auth-related routes and controllers. Activate when the user mentions Fortify, auth, authentication, login, register, signup, forgot password, verify email, 2FA, or references app/Actions/Fortify/, CreateNewUser, UpdateUserProfileInformation, FortifyServiceProvider, config/fortify.php, or auth guards. Fortify is the frontend-agnostic authentication backend for Laravel that registers all auth routes and controllers. Also activate when building SPA or headless authentication, customizing login redirects, overriding response contracts like LoginResponse, or configuring login throttling. Do NOT activate for Laravel Passport (OAuth2 API tokens), Socialite (OAuth social login), or non-auth Laravel features.
|
||||
- `laravel-best-practices` — Apply this skill whenever writing, reviewing, or refactoring Laravel PHP code. This includes creating or modifying controllers, models, migrations, form requests, policies, jobs, scheduled commands, service classes, and Eloquent queries. Triggers for N+1 and query performance issues, caching strategies, authorization and security patterns, validation, error handling, queue and job configuration, route definitions, and architectural decisions. Also use for Laravel code reviews and refactoring existing Laravel code to follow best practices. Covers any task involving Laravel backend PHP code patterns.
|
||||
- `mcp-development` — Use this skill for Laravel MCP development only. Trigger when creating or editing MCP tools, resources, prompts, or servers in Laravel projects. Covers: artisan make:mcp-* generators, mcp:inspector, routes/ai.php, Tool/Resource/Prompt classes, schema validation, shouldRegister(), OAuth setup, URI templates, read-only attributes, and MCP debugging. Do not use for non-Laravel MCP projects or generic AI features without MCP.
|
||||
- `project-route-structure` — Always apply in this project when creating, moving, or refactoring Laravel route definitions in routes/**/*.php; changing route prefixes, names, middleware, scoped bindings, or route files; or adding frontend-facing endpoints that Wayfinder generates. Documents the KanbanGit route-file layout and grouping conventions.
|
||||
- `wayfinder-development` — Use this skill for Laravel Wayfinder which auto-generates typed functions for Laravel controllers and routes. ALWAYS use this skill when frontend code needs to call backend routes or controller actions. Trigger when: connecting any React/Vue/Svelte/Inertia frontend to Laravel controllers, routes, building end-to-end features with both frontend and backend, wiring up forms or links to backend endpoints, fixing route-related TypeScript errors, importing from @/actions or @/routes, or running wayfinder:generate. Use Wayfinder route functions instead of hardcoded URLs. Covers: wayfinder() vite plugin, .url()/.get()/.post()/.form(), query params, route model binding, tree-shaking. Do not use for backend-only task
|
||||
- `inertia-vue-development` — Develops Inertia.js v2 Vue client-side applications. Activates when creating Vue pages, forms, or navigation; using <Link>, <Form>, useForm, or router; working with deferred props, prefetching, or polling; or when user mentions Vue with Inertia, Vue pages, Vue forms, or Vue navigation.
|
||||
- `tailwindcss-development` — Always invoke when the user's message includes 'tailwind' in any form. Also invoke for: building responsive grid layouts (multi-column card grids, product grids), flex/grid page structures (dashboards with sidebars, fixed topbars, mobile-toggle navs), styling UI components (cards, tables, navbars, pricing sections, forms, inputs, badges), adding dark mode variants, fixing spacing or typography, and Tailwind v3/v4 work. The core use case: writing or fixing Tailwind utility classes in HTML templates (Blade, JSX, Vue). Skip for backend PHP logic, database queries, API routes, JavaScript with no HTML/CSS component, CSS file audits, build tool configuration, and vanilla CSS.
|
||||
|
||||
-109
@@ -1,109 +0,0 @@
|
||||
ARG COMPOSER_IMAGE=composer:2.9.5@sha256:698d3801b2a622ace460c4743c781282fcbcb733a4cbf8b31c44731e846585e8
|
||||
ARG NODE_IMAGE=node:22-bookworm-slim@sha256:d649c27dae7ba0137b3cef5dd75baa422c08dc3d9e3fc0c23dfb172dc3cc6436
|
||||
ARG PHP_IMAGE=php:8.4-fpm-bookworm@sha256:c5fb7a0c02f4efe280691910c8b734995fa83598cdcf3115ef5dcb2e4617681c
|
||||
|
||||
FROM ${COMPOSER_IMAGE} AS composer_binary
|
||||
FROM ${NODE_IMAGE} AS node_binary
|
||||
|
||||
FROM ${PHP_IMAGE} AS php_base
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
curl \
|
||||
gettext-base \
|
||||
git \
|
||||
libfcgi-bin \
|
||||
libfreetype6-dev \
|
||||
libicu-dev \
|
||||
libjpeg62-turbo-dev \
|
||||
libmagickwand-dev \
|
||||
libonig-dev \
|
||||
libpng-dev \
|
||||
libpq-dev \
|
||||
libsqlite3-dev \
|
||||
libssl-dev \
|
||||
libxml2-dev \
|
||||
libzip-dev \
|
||||
nginx \
|
||||
procps \
|
||||
unzip \
|
||||
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
|
||||
&& docker-php-ext-install -j"$(nproc)" \
|
||||
bcmath \
|
||||
gd \
|
||||
intl \
|
||||
opcache \
|
||||
pcntl \
|
||||
pdo_mysql \
|
||||
pdo_pgsql \
|
||||
pdo_sqlite \
|
||||
pgsql \
|
||||
soap \
|
||||
zip \
|
||||
&& pecl install imagick \
|
||||
&& docker-php-ext-enable imagick \
|
||||
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
COPY --from=composer_binary /usr/bin/composer /usr/local/bin/composer
|
||||
|
||||
WORKDIR /var/www
|
||||
|
||||
FROM php_base AS build
|
||||
|
||||
COPY --from=node_binary /usr/local/ /usr/local/
|
||||
COPY . /var/www
|
||||
|
||||
RUN composer install --prefer-dist --no-interaction --no-progress \
|
||||
&& npm ci \
|
||||
&& npm run build
|
||||
|
||||
FROM build AS test
|
||||
|
||||
RUN touch .env \
|
||||
&& composer audit --locked --no-interaction \
|
||||
&& COMPOSER_ALLOW_SUPERUSER=1 composer ci:check
|
||||
|
||||
FROM build AS production_builder
|
||||
|
||||
RUN composer install \
|
||||
--classmap-authoritative \
|
||||
--no-dev \
|
||||
--no-interaction \
|
||||
--no-progress \
|
||||
--prefer-dist \
|
||||
&& rm -rf node_modules tests phpunit.xml
|
||||
|
||||
FROM php_base AS runtime
|
||||
|
||||
COPY docker/entrypoint.sh /usr/local/bin/kanbangit-entrypoint
|
||||
COPY docker/nginx/nginx.conf.template /etc/nginx/nginx.conf.template
|
||||
COPY docker/php-fpm-healthcheck.sh /usr/local/bin/php-fpm-healthcheck
|
||||
COPY --from=production_builder --chown=www-data:www-data /var/www /var/www
|
||||
|
||||
RUN chmod +x /usr/local/bin/kanbangit-entrypoint /usr/local/bin/php-fpm-healthcheck \
|
||||
&& mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" \
|
||||
&& printf '%s\n' \
|
||||
'[global]' \
|
||||
'daemonize = no' \
|
||||
'' \
|
||||
'[www]' \
|
||||
'pm.status_path = /status' \
|
||||
> /usr/local/etc/php-fpm.d/zz-kanbangit.conf \
|
||||
&& mkdir -p \
|
||||
/var/www/bootstrap/cache \
|
||||
/var/www/storage/app/public \
|
||||
/var/www/storage/framework/cache \
|
||||
/var/www/storage/framework/sessions \
|
||||
/var/www/storage/framework/testing \
|
||||
/var/www/storage/framework/views \
|
||||
/var/www/storage/logs \
|
||||
&& ln -s ../storage/app/public /var/www/public/storage \
|
||||
&& chown -R www-data:www-data /var/www/bootstrap/cache /var/www/storage
|
||||
|
||||
USER www-data
|
||||
|
||||
EXPOSE 8080 9000
|
||||
|
||||
ENTRYPOINT ["kanbangit-entrypoint"]
|
||||
CMD ["php-fpm"]
|
||||
@@ -2,19 +2,11 @@
|
||||
|
||||
namespace App\Actions\ProjectBoard;
|
||||
|
||||
use App\Actions\Projects\DuplicateProjectConfiguration;
|
||||
use App\Actions\Projects\ProjectTemplateOverrideManager;
|
||||
use App\Actions\Projects\RefreshProjectBaseTemplate;
|
||||
use App\Models\TicketStatus;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class UpdateTicketStatusTransitions
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ProjectTemplateOverrideManager $projectTemplateOverrideManager,
|
||||
private readonly RefreshProjectBaseTemplate $refreshProjectBaseTemplate,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array{allowed_ticket_status_ids?: array<int, int|string>|null} $validated
|
||||
*/
|
||||
@@ -27,14 +19,6 @@ class UpdateTicketStatusTransitions
|
||||
->all();
|
||||
|
||||
$ticketStatus->allowedTargetStatuses()->sync($allowedTicketStatusIds);
|
||||
$this->projectTemplateOverrideManager->mark(
|
||||
$ticketStatus->project,
|
||||
DuplicateProjectConfiguration::COPY_STATUS_TRANSITIONS,
|
||||
);
|
||||
$this->refreshProjectBaseTemplate->handle(
|
||||
$ticketStatus->project,
|
||||
DuplicateProjectConfiguration::COPY_STATUS_TRANSITIONS,
|
||||
);
|
||||
|
||||
return $ticketStatus->fresh();
|
||||
}
|
||||
|
||||
@@ -2,19 +2,11 @@
|
||||
|
||||
namespace App\Actions\ProjectTicketMetadata;
|
||||
|
||||
use App\Actions\Projects\DuplicateProjectConfiguration;
|
||||
use App\Actions\Projects\ProjectTemplateOverrideManager;
|
||||
use App\Actions\Projects\RefreshProjectBaseTemplate;
|
||||
use App\Models\Project;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class UpdateProjectTicketMetadataSettings
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ProjectTemplateOverrideManager $projectTemplateOverrideManager,
|
||||
private readonly RefreshProjectBaseTemplate $refreshProjectBaseTemplate,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array{show_author?: bool|null, show_story_points?: bool|null} $validated
|
||||
*/
|
||||
@@ -24,14 +16,6 @@ class UpdateProjectTicketMetadataSettings
|
||||
'show_author' => Arr::get($validated, 'show_author', false),
|
||||
'show_story_points' => Arr::get($validated, 'show_story_points', false),
|
||||
]);
|
||||
$this->projectTemplateOverrideManager->mark(
|
||||
$project,
|
||||
DuplicateProjectConfiguration::COPY_PROJECT_SETTINGS,
|
||||
);
|
||||
$this->refreshProjectBaseTemplate->handle(
|
||||
$project,
|
||||
DuplicateProjectConfiguration::COPY_PROJECT_SETTINGS,
|
||||
);
|
||||
|
||||
return $project->fresh();
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@ use App\Http\Resources\TicketPriorityResource;
|
||||
use App\Http\Resources\TicketReferenceResource;
|
||||
use App\Http\Resources\TicketStatusResource;
|
||||
use App\Http\Resources\TicketTypeResource;
|
||||
use App\Models\Ticket;
|
||||
use App\Http\Resources\UserResource;
|
||||
use App\Models\Project;
|
||||
use App\Models\Ticket;
|
||||
use App\Models\User;
|
||||
|
||||
class BuildProjectTicketOptionsPayload
|
||||
|
||||
@@ -18,7 +18,6 @@ class CreateProject
|
||||
private readonly DuplicateProjectConfiguration $duplicateProjectConfiguration,
|
||||
private readonly InitializeProjectTicketMetadata $initializeProjectTicketMetadata,
|
||||
private readonly PermissionService $permissionService,
|
||||
private readonly ProjectTemplateOverrideManager $projectTemplateOverrideManager,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -40,22 +39,18 @@ class CreateProject
|
||||
|
||||
$project->forceFill([
|
||||
'project_template_id' => $sourceTemplate->id,
|
||||
'template_overrides' => [],
|
||||
])->saveQuietly();
|
||||
])->save();
|
||||
} else {
|
||||
$baseTemplate = $this->createProjectBaseTemplate->handle($project);
|
||||
|
||||
$project->forceFill([
|
||||
'project_template_id' => $baseTemplate->id,
|
||||
'template_overrides' => [],
|
||||
])->saveQuietly();
|
||||
])->save();
|
||||
}
|
||||
|
||||
$project = $project->fresh() ?? $project;
|
||||
|
||||
$this->projectTemplateOverrideManager->withoutRecording(
|
||||
fn () => $this->initializeProjectTicketMetadata->handle($project),
|
||||
);
|
||||
$this->initializeProjectTicketMetadata->handle($project);
|
||||
$this->permissionService->grant($actor, Permission::ProjectAdmin, $project);
|
||||
|
||||
if ($creationMode === 'duplicate' && Arr::get($attributes, 'source_project_id') !== null) {
|
||||
|
||||
@@ -3,18 +3,12 @@
|
||||
namespace App\Actions\Projects;
|
||||
|
||||
use App\Models\Project;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class DuplicateProjectConfiguration
|
||||
{
|
||||
public const COPY_PROJECT_SETTINGS = 'project_settings';
|
||||
|
||||
public const COPY_BOARD_COLUMNS = 'board_columns';
|
||||
|
||||
public const COPY_TICKET_STATUSES = 'ticket_statuses';
|
||||
|
||||
public const COPY_STATUS_TRANSITIONS = 'status_transitions';
|
||||
public const COPY_BOARD_WORKFLOW = 'board_workflow';
|
||||
|
||||
public const COPY_TICKET_TYPES = 'ticket_types';
|
||||
|
||||
@@ -28,20 +22,14 @@ class DuplicateProjectConfiguration
|
||||
|
||||
public const COPY_NOTIFICATION_ACTIONS = 'notification_actions';
|
||||
|
||||
public const COPY_BOARD_WORKFLOW = 'board_workflow';
|
||||
|
||||
public function __construct(
|
||||
private readonly ProjectTemplateOverrideManager $projectTemplateOverrideManager,
|
||||
) {}
|
||||
|
||||
/** @return list<string> */
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function copyableAreas(): array
|
||||
{
|
||||
return [
|
||||
self::COPY_PROJECT_SETTINGS,
|
||||
self::COPY_BOARD_COLUMNS,
|
||||
self::COPY_TICKET_STATUSES,
|
||||
self::COPY_STATUS_TRANSITIONS,
|
||||
self::COPY_BOARD_WORKFLOW,
|
||||
self::COPY_TICKET_TYPES,
|
||||
self::COPY_TICKET_PRIORITIES,
|
||||
self::COPY_TICKET_FIELDS,
|
||||
@@ -51,67 +39,68 @@ class DuplicateProjectConfiguration
|
||||
];
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
public static function acceptedCopyAreas(): array
|
||||
{
|
||||
return [...self::copyableAreas(), self::COPY_BOARD_WORKFLOW];
|
||||
}
|
||||
|
||||
/** @return list<array{value: string, label: string, description: string}> */
|
||||
/**
|
||||
* @return list<array{value: string, label: string, description: string}>
|
||||
*/
|
||||
public static function copyAreaOptions(): array
|
||||
{
|
||||
return [
|
||||
['value' => self::COPY_PROJECT_SETTINGS, 'label' => 'Project settings', 'description' => 'General project preferences like story points and author visibility.'],
|
||||
['value' => self::COPY_BOARD_COLUMNS, 'label' => 'Board columns', 'description' => 'Columns and their order on the project board.'],
|
||||
['value' => self::COPY_TICKET_STATUSES, 'label' => 'Ticket statuses', 'description' => 'Statuses and their assignment to board columns.'],
|
||||
['value' => self::COPY_STATUS_TRANSITIONS, 'label' => 'Status transitions', 'description' => 'Allowed moves between ticket statuses.'],
|
||||
['value' => self::COPY_TICKET_TYPES, 'label' => 'Ticket types', 'description' => 'Available ticket types, including parent types.'],
|
||||
['value' => self::COPY_TICKET_PRIORITIES, 'label' => 'Ticket priorities', 'description' => 'Available ticket priorities.'],
|
||||
['value' => self::COPY_TICKET_FIELDS, 'label' => 'Custom ticket fields', 'description' => 'Custom field definitions without ticket values.'],
|
||||
['value' => self::COPY_TICKET_LINK_TYPES, 'label' => 'Ticket link types', 'description' => 'Relationship labels between tickets.'],
|
||||
['value' => self::COPY_REPOSITORY_CONNECTIONS, 'label' => 'Repository connections', 'description' => 'Repositories linked to the project.'],
|
||||
['value' => self::COPY_NOTIFICATION_ACTIONS, 'label' => 'Notification actions', 'description' => 'Notification rules and delivery settings.'],
|
||||
[
|
||||
'value' => self::COPY_PROJECT_SETTINGS,
|
||||
'label' => 'Project settings',
|
||||
'description' => 'Copy general project preferences like story points and author visibility.',
|
||||
],
|
||||
[
|
||||
'value' => self::COPY_BOARD_WORKFLOW,
|
||||
'label' => 'Board workflow',
|
||||
'description' => 'Copy board columns, ticket statuses, and allowed status transitions.',
|
||||
],
|
||||
[
|
||||
'value' => self::COPY_TICKET_TYPES,
|
||||
'label' => 'Ticket types',
|
||||
'description' => 'Copy available ticket types, including parent-ticket types.',
|
||||
],
|
||||
[
|
||||
'value' => self::COPY_TICKET_PRIORITIES,
|
||||
'label' => 'Ticket priorities',
|
||||
'description' => 'Copy the project-specific ticket priorities.',
|
||||
],
|
||||
[
|
||||
'value' => self::COPY_TICKET_FIELDS,
|
||||
'label' => 'Custom ticket fields',
|
||||
'description' => 'Copy custom ticket field definitions without any ticket values.',
|
||||
],
|
||||
[
|
||||
'value' => self::COPY_TICKET_LINK_TYPES,
|
||||
'label' => 'Ticket link types',
|
||||
'description' => 'Copy available relationship labels between tickets.',
|
||||
],
|
||||
[
|
||||
'value' => self::COPY_REPOSITORY_CONNECTIONS,
|
||||
'label' => 'Repository connections',
|
||||
'description' => 'Copy linked repositories without changing the new project identity.',
|
||||
],
|
||||
[
|
||||
'value' => self::COPY_NOTIFICATION_ACTIONS,
|
||||
'label' => 'Notification actions',
|
||||
'description' => 'Copy notification rules and delivery settings.',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/** @param list<string> $copyAreas @return list<string> */
|
||||
/**
|
||||
* @param list<string> $copyAreas
|
||||
* @return list<string>
|
||||
*/
|
||||
public function normalizeCopyAreas(array $copyAreas): array
|
||||
{
|
||||
if (in_array(self::COPY_BOARD_WORKFLOW, $copyAreas, true)) {
|
||||
$copyAreas = [
|
||||
...array_diff($copyAreas, [self::COPY_BOARD_WORKFLOW]),
|
||||
self::COPY_BOARD_COLUMNS,
|
||||
self::COPY_TICKET_STATUSES,
|
||||
self::COPY_STATUS_TRANSITIONS,
|
||||
];
|
||||
}
|
||||
|
||||
return array_values(array_intersect(self::copyableAreas(), array_unique($copyAreas)));
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $configuration @return array<string, mixed> */
|
||||
public function normalizeConfiguration(array $configuration): array
|
||||
{
|
||||
$workflow = $configuration[self::COPY_BOARD_WORKFLOW] ?? null;
|
||||
|
||||
if (is_array($workflow)) {
|
||||
$statuses = $workflow['ticket_statuses'] ?? [];
|
||||
$configuration[self::COPY_BOARD_COLUMNS] ??= $workflow['board_columns'] ?? [];
|
||||
$configuration[self::COPY_TICKET_STATUSES] ??= array_map(
|
||||
static fn (array $status): array => array_diff_key($status, ['allowed_target_indexes' => true]),
|
||||
$statuses,
|
||||
);
|
||||
$configuration[self::COPY_STATUS_TRANSITIONS] ??= array_map(
|
||||
static fn (array $status): array => $status['allowed_target_indexes'] ?? [],
|
||||
$statuses,
|
||||
);
|
||||
unset($configuration[self::COPY_BOARD_WORKFLOW]);
|
||||
}
|
||||
|
||||
return $configuration;
|
||||
}
|
||||
|
||||
/** @param list<string> $copyAreas @return array<string, mixed> */
|
||||
/**
|
||||
* @param list<string> $copyAreas
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function export(Project $sourceProject, array $copyAreas): array
|
||||
{
|
||||
$selectedAreas = $this->normalizeCopyAreas($copyAreas);
|
||||
@@ -120,10 +109,17 @@ class DuplicateProjectConfiguration
|
||||
return [];
|
||||
}
|
||||
|
||||
$sourceProject->load([
|
||||
'boardColumns', 'notificationActions', 'repositoryConnections', 'ticketFieldDefinitions',
|
||||
'ticketLinkTypes', 'ticketPriorities', 'ticketStatuses.allowedTargetStatuses', 'ticketTypes',
|
||||
$sourceProject->loadMissing([
|
||||
'boardColumns',
|
||||
'notificationActions',
|
||||
'repositoryConnections',
|
||||
'ticketFieldDefinitions',
|
||||
'ticketLinkTypes',
|
||||
'ticketPriorities',
|
||||
'ticketStatuses.allowedTargetStatuses',
|
||||
'ticketTypes',
|
||||
]);
|
||||
|
||||
$configuration = [];
|
||||
|
||||
if (in_array(self::COPY_PROJECT_SETTINGS, $selectedAreas, true)) {
|
||||
@@ -133,65 +129,120 @@ class DuplicateProjectConfiguration
|
||||
];
|
||||
}
|
||||
|
||||
$columnIndexes = $sourceProject->boardColumns->values()->mapWithKeys(
|
||||
static fn ($column, int $index): array => [$column->id => $index],
|
||||
);
|
||||
$statusIndexes = $sourceProject->ticketStatuses->values()->mapWithKeys(
|
||||
static fn ($status, int $index): array => [$status->id => $index],
|
||||
);
|
||||
if (in_array(self::COPY_BOARD_WORKFLOW, $selectedAreas, true)) {
|
||||
$boardColumnIndexes = [];
|
||||
$boardColumns = [];
|
||||
|
||||
if (in_array(self::COPY_BOARD_COLUMNS, $selectedAreas, true)) {
|
||||
$configuration[self::COPY_BOARD_COLUMNS] = $sourceProject->boardColumns->values()->map(
|
||||
static fn ($column): array => ['name' => $column->name, 'position' => $column->position],
|
||||
)->all();
|
||||
}
|
||||
|
||||
if (in_array(self::COPY_TICKET_STATUSES, $selectedAreas, true)) {
|
||||
$configuration[self::COPY_TICKET_STATUSES] = $sourceProject->ticketStatuses->values()->map(
|
||||
static fn ($status): array => [
|
||||
'board_column_index' => $status->board_column_id === null ? null : $columnIndexes->get($status->board_column_id),
|
||||
'name' => $status->name,
|
||||
'position' => $status->position,
|
||||
],
|
||||
)->all();
|
||||
}
|
||||
|
||||
if (in_array(self::COPY_STATUS_TRANSITIONS, $selectedAreas, true)) {
|
||||
$configuration[self::COPY_STATUS_TRANSITIONS] = $sourceProject->ticketStatuses->values()->map(
|
||||
static fn ($status): array => $status->allowedTargetStatuses->pluck('id')->map(
|
||||
static fn (int $id): ?int => $statusIndexes->get($id),
|
||||
)->filter(static fn (?int $index): bool => $index !== null)->values()->all(),
|
||||
)->all();
|
||||
}
|
||||
|
||||
$simpleAreas = [
|
||||
self::COPY_TICKET_TYPES => fn ($item): array => ['name' => $item->name, 'position' => $item->position, 'is_parent' => $item->is_parent],
|
||||
self::COPY_TICKET_PRIORITIES => fn ($item): array => ['name' => $item->name, 'position' => $item->position],
|
||||
self::COPY_TICKET_FIELDS => fn ($item): array => ['field_type' => $item->field_type, 'label' => $item->label, 'position' => $item->position],
|
||||
self::COPY_TICKET_LINK_TYPES => fn ($item): array => ['label' => $item->label, 'counterpart_label' => $item->counterpart_label, 'position' => $item->position],
|
||||
self::COPY_REPOSITORY_CONNECTIONS => fn ($item): array => [
|
||||
'provider' => $item->provider->value, 'base_url' => $item->base_url,
|
||||
'provider_repository_id' => $item->provider_repository_id, 'owner_name' => $item->owner_name,
|
||||
'repository_name' => $item->repository_name, 'repository_full_name' => $item->repository_full_name,
|
||||
'repository_url' => $item->repository_url,
|
||||
],
|
||||
self::COPY_NOTIFICATION_ACTIONS => fn ($item): array => [
|
||||
'name' => $item->name, 'enabled' => $item->enabled, 'event_type' => $item->event_type->value,
|
||||
'channel' => $item->channel->value, 'channel_configuration' => $item->channel_configuration ?? [],
|
||||
'message_template' => $item->message_template, 'delivery_url' => $item->delivery_url,
|
||||
'filters' => $item->filters ?? [],
|
||||
],
|
||||
];
|
||||
$relations = [
|
||||
self::COPY_TICKET_TYPES => 'ticketTypes', self::COPY_TICKET_PRIORITIES => 'ticketPriorities',
|
||||
self::COPY_TICKET_FIELDS => 'ticketFieldDefinitions', self::COPY_TICKET_LINK_TYPES => 'ticketLinkTypes',
|
||||
self::COPY_REPOSITORY_CONNECTIONS => 'repositoryConnections', self::COPY_NOTIFICATION_ACTIONS => 'notificationActions',
|
||||
];
|
||||
|
||||
foreach ($simpleAreas as $area => $mapper) {
|
||||
if (in_array($area, $selectedAreas, true)) {
|
||||
$configuration[$area] = $sourceProject->{$relations[$area]}->values()->map($mapper)->all();
|
||||
foreach ($sourceProject->boardColumns as $index => $boardColumn) {
|
||||
$boardColumnIndexes[$boardColumn->id] = $index;
|
||||
$boardColumns[] = [
|
||||
'name' => $boardColumn->name,
|
||||
'position' => $boardColumn->position,
|
||||
];
|
||||
}
|
||||
|
||||
$ticketStatusIndexes = [];
|
||||
$ticketStatuses = [];
|
||||
|
||||
foreach ($sourceProject->ticketStatuses as $index => $ticketStatus) {
|
||||
$ticketStatusIndexes[$ticketStatus->id] = $index;
|
||||
$ticketStatuses[] = [
|
||||
'board_column_index' => $ticketStatus->board_column_id === null
|
||||
? null
|
||||
: ($boardColumnIndexes[$ticketStatus->board_column_id] ?? null),
|
||||
'name' => $ticketStatus->name,
|
||||
'position' => $ticketStatus->position,
|
||||
'allowed_target_indexes' => [],
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($sourceProject->ticketStatuses as $index => $ticketStatus) {
|
||||
$ticketStatuses[$index]['allowed_target_indexes'] = $ticketStatus->allowedTargetStatuses
|
||||
->pluck('id')
|
||||
->map(fn (int $statusId): ?int => $ticketStatusIndexes[$statusId] ?? null)
|
||||
->filter(static fn (?int $statusIndex): bool => $statusIndex !== null)
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
$configuration[self::COPY_BOARD_WORKFLOW] = [
|
||||
'board_columns' => $boardColumns,
|
||||
'ticket_statuses' => $ticketStatuses,
|
||||
];
|
||||
}
|
||||
|
||||
if (in_array(self::COPY_TICKET_TYPES, $selectedAreas, true)) {
|
||||
$configuration[self::COPY_TICKET_TYPES] = $sourceProject->ticketTypes
|
||||
->map(fn ($ticketType): array => [
|
||||
'name' => $ticketType->name,
|
||||
'position' => $ticketType->position,
|
||||
'is_parent' => $ticketType->is_parent,
|
||||
])
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
if (in_array(self::COPY_TICKET_PRIORITIES, $selectedAreas, true)) {
|
||||
$configuration[self::COPY_TICKET_PRIORITIES] = $sourceProject->ticketPriorities
|
||||
->map(fn ($ticketPriority): array => [
|
||||
'name' => $ticketPriority->name,
|
||||
'position' => $ticketPriority->position,
|
||||
])
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
if (in_array(self::COPY_TICKET_FIELDS, $selectedAreas, true)) {
|
||||
$configuration[self::COPY_TICKET_FIELDS] = $sourceProject->ticketFieldDefinitions
|
||||
->map(fn ($ticketFieldDefinition): array => [
|
||||
'field_type' => $ticketFieldDefinition->field_type,
|
||||
'label' => $ticketFieldDefinition->label,
|
||||
'position' => $ticketFieldDefinition->position,
|
||||
])
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
if (in_array(self::COPY_TICKET_LINK_TYPES, $selectedAreas, true)) {
|
||||
$configuration[self::COPY_TICKET_LINK_TYPES] = $sourceProject->ticketLinkTypes
|
||||
->map(fn ($ticketLinkType): array => [
|
||||
'label' => $ticketLinkType->label,
|
||||
'counterpart_label' => $ticketLinkType->counterpart_label,
|
||||
'position' => $ticketLinkType->position,
|
||||
])
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
if (in_array(self::COPY_REPOSITORY_CONNECTIONS, $selectedAreas, true)) {
|
||||
$configuration[self::COPY_REPOSITORY_CONNECTIONS] = $sourceProject->repositoryConnections
|
||||
->map(fn ($repositoryConnection): array => [
|
||||
'provider' => $repositoryConnection->provider->value,
|
||||
'base_url' => $repositoryConnection->base_url,
|
||||
'provider_repository_id' => $repositoryConnection->provider_repository_id,
|
||||
'owner_name' => $repositoryConnection->owner_name,
|
||||
'repository_name' => $repositoryConnection->repository_name,
|
||||
'repository_full_name' => $repositoryConnection->repository_full_name,
|
||||
'repository_url' => $repositoryConnection->repository_url,
|
||||
])
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
if (in_array(self::COPY_NOTIFICATION_ACTIONS, $selectedAreas, true)) {
|
||||
$configuration[self::COPY_NOTIFICATION_ACTIONS] = $sourceProject->notificationActions
|
||||
->map(fn ($notificationAction): array => [
|
||||
'name' => $notificationAction->name,
|
||||
'enabled' => $notificationAction->enabled,
|
||||
'event_type' => $notificationAction->event_type->value,
|
||||
'channel' => $notificationAction->channel->value,
|
||||
'channel_configuration' => $notificationAction->channel_configuration ?? [],
|
||||
'message_template' => $notificationAction->message_template,
|
||||
'delivery_url' => $notificationAction->delivery_url,
|
||||
'filters' => $notificationAction->filters ?? [],
|
||||
])
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
return $configuration;
|
||||
@@ -199,141 +250,151 @@ class DuplicateProjectConfiguration
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $configuration
|
||||
* @param array<int|string, int|string> $statusReplacements
|
||||
* @return list<string> areas which retained referenced local values
|
||||
*/
|
||||
public function apply(Project $targetProject, array $configuration, array $statusReplacements = []): array
|
||||
public function apply(Project $targetProject, array $configuration): void
|
||||
{
|
||||
$configuration = $this->normalizeConfiguration($configuration);
|
||||
if (array_key_exists(self::COPY_PROJECT_SETTINGS, $configuration)) {
|
||||
$projectSettings = $configuration[self::COPY_PROJECT_SETTINGS];
|
||||
|
||||
$blockedAreas = $this->projectTemplateOverrideManager->withoutRecording(function () use ($targetProject, $configuration, $statusReplacements): array {
|
||||
$blockedAreas = [];
|
||||
|
||||
if (isset($configuration[self::COPY_PROJECT_SETTINGS])) {
|
||||
$targetProject->forceFill($configuration[self::COPY_PROJECT_SETTINGS])->saveQuietly();
|
||||
}
|
||||
|
||||
if (isset($configuration[self::COPY_BOARD_COLUMNS])) {
|
||||
$blocked = $this->syncModels(
|
||||
$targetProject->boardColumns()->get(),
|
||||
$configuration[self::COPY_BOARD_COLUMNS],
|
||||
fn (array $item) => $targetProject->boardColumns()->create($item),
|
||||
static fn (Model $model, array $item) => $model->forceFill($item)->saveQuietly(),
|
||||
static fn (Model $column): bool => ! $column->ticketStatuses()->exists(),
|
||||
);
|
||||
if ($blocked) {
|
||||
$blockedAreas[] = self::COPY_BOARD_COLUMNS;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($configuration[self::COPY_TICKET_STATUSES])) {
|
||||
$columns = $targetProject->boardColumns()->get()->values();
|
||||
$items = collect($configuration[self::COPY_TICKET_STATUSES])->map(static function (array $item) use ($columns): array {
|
||||
$columnIndex = $item['board_column_index'] ?? null;
|
||||
$item['board_column_id'] = $columnIndex === null ? null : $columns->get($columnIndex)?->id;
|
||||
unset($item['board_column_index']);
|
||||
|
||||
return $item;
|
||||
})->all();
|
||||
$blocked = $this->syncModels(
|
||||
$targetProject->ticketStatuses()->get(),
|
||||
$items,
|
||||
fn (array $item) => $targetProject->ticketStatuses()->create($item),
|
||||
static fn (Model $model, array $item) => $model->forceFill($item)->saveQuietly(),
|
||||
function (Model $status) use ($statusReplacements): bool {
|
||||
if (! $status->tickets()->exists()) {
|
||||
return true;
|
||||
}
|
||||
$replacementId = $statusReplacements[$status->id] ?? null;
|
||||
if ($replacementId === null) {
|
||||
return false;
|
||||
}
|
||||
$status->tickets()->update(['ticket_status_id' => $replacementId]);
|
||||
|
||||
return true;
|
||||
},
|
||||
);
|
||||
if ($blocked) {
|
||||
$blockedAreas[] = self::COPY_TICKET_STATUSES;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($configuration[self::COPY_STATUS_TRANSITIONS])) {
|
||||
$statuses = $targetProject->ticketStatuses()->get()->values();
|
||||
foreach ($configuration[self::COPY_STATUS_TRANSITIONS] as $index => $targets) {
|
||||
$status = $statuses->get($index);
|
||||
if ($status !== null) {
|
||||
$status->allowedTargetStatuses()->sync(collect($targets)->map(fn (int $target): ?int => $statuses->get($target)?->id)->filter()->all());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$relations = [
|
||||
self::COPY_TICKET_TYPES => ['ticketTypes', fn (Model $item): bool => ! $item->tickets()->exists()],
|
||||
self::COPY_TICKET_PRIORITIES => ['ticketPriorities', fn (Model $item): bool => ! $item->tickets()->exists()],
|
||||
self::COPY_TICKET_FIELDS => ['ticketFieldDefinitions', fn (Model $item): bool => ! $item->ticketFieldValues()->exists()],
|
||||
self::COPY_TICKET_LINK_TYPES => ['ticketLinkTypes', fn (Model $item): bool => ! $item->ticketLinks()->exists()],
|
||||
self::COPY_REPOSITORY_CONNECTIONS => ['repositoryConnections', null],
|
||||
self::COPY_NOTIFICATION_ACTIONS => ['notificationActions', null],
|
||||
];
|
||||
|
||||
foreach ($relations as $area => [$relation, $canDelete]) {
|
||||
if (! isset($configuration[$area])) {
|
||||
continue;
|
||||
}
|
||||
$blocked = $this->syncModels(
|
||||
$targetProject->{$relation}()->get(),
|
||||
$configuration[$area],
|
||||
fn (array $item) => $targetProject->{$relation}()->create($item),
|
||||
static fn (Model $model, array $item) => $model->forceFill($item)->saveQuietly(),
|
||||
$canDelete,
|
||||
);
|
||||
if ($blocked) {
|
||||
$blockedAreas[] = $area;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($targetProject->configurationRelations() as $relation) {
|
||||
$targetProject->{$relation}()->update(['project_template_id' => $targetProject->project_template_id]);
|
||||
}
|
||||
|
||||
return array_values(array_unique($blockedAreas));
|
||||
});
|
||||
|
||||
foreach ($blockedAreas as $area) {
|
||||
$this->projectTemplateOverrideManager->mark($targetProject->fresh(), $area);
|
||||
$targetProject->forceFill([
|
||||
'show_author' => (bool) ($projectSettings['show_author'] ?? $targetProject->show_author),
|
||||
'show_story_points' => (bool) ($projectSettings['show_story_points'] ?? $targetProject->show_story_points),
|
||||
])->save();
|
||||
}
|
||||
|
||||
return $blockedAreas;
|
||||
if (array_key_exists(self::COPY_BOARD_WORKFLOW, $configuration)) {
|
||||
$targetProject->ticketStatuses()->delete();
|
||||
$targetProject->boardColumns()->delete();
|
||||
|
||||
$boardColumnIds = [];
|
||||
|
||||
foreach (($configuration[self::COPY_BOARD_WORKFLOW]['board_columns'] ?? []) as $boardColumn) {
|
||||
$newBoardColumn = $targetProject->boardColumns()->create([
|
||||
'name' => $boardColumn['name'],
|
||||
'position' => $boardColumn['position'],
|
||||
]);
|
||||
|
||||
$boardColumnIds[] = $newBoardColumn->id;
|
||||
}
|
||||
|
||||
$ticketStatusIds = [];
|
||||
|
||||
foreach (($configuration[self::COPY_BOARD_WORKFLOW]['ticket_statuses'] ?? []) as $ticketStatus) {
|
||||
$boardColumnIndex = $ticketStatus['board_column_index'] ?? null;
|
||||
$newTicketStatus = $targetProject->ticketStatuses()->create([
|
||||
'board_column_id' => $boardColumnIndex === null ? null : ($boardColumnIds[$boardColumnIndex] ?? null),
|
||||
'name' => $ticketStatus['name'],
|
||||
'position' => $ticketStatus['position'],
|
||||
]);
|
||||
|
||||
$ticketStatusIds[] = $newTicketStatus->id;
|
||||
}
|
||||
|
||||
foreach (($configuration[self::COPY_BOARD_WORKFLOW]['ticket_statuses'] ?? []) as $index => $ticketStatus) {
|
||||
$newTicketStatusId = $ticketStatusIds[$index] ?? null;
|
||||
|
||||
if ($newTicketStatusId === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$targetProject->ticketStatuses()->findOrFail($newTicketStatusId)
|
||||
->allowedTargetStatuses()
|
||||
->sync(
|
||||
collect($ticketStatus['allowed_target_indexes'] ?? [])
|
||||
->map(fn (int $statusIndex): ?int => $ticketStatusIds[$statusIndex] ?? null)
|
||||
->filter(static fn (?int $statusId): bool => $statusId !== null)
|
||||
->values()
|
||||
->all(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists(self::COPY_TICKET_TYPES, $configuration)) {
|
||||
$targetProject->ticketTypes()->delete();
|
||||
|
||||
foreach ($configuration[self::COPY_TICKET_TYPES] as $ticketType) {
|
||||
$targetProject->ticketTypes()->create([
|
||||
'name' => $ticketType['name'],
|
||||
'position' => $ticketType['position'],
|
||||
'is_parent' => $ticketType['is_parent'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists(self::COPY_TICKET_PRIORITIES, $configuration)) {
|
||||
$targetProject->ticketPriorities()->delete();
|
||||
|
||||
foreach ($configuration[self::COPY_TICKET_PRIORITIES] as $ticketPriority) {
|
||||
$targetProject->ticketPriorities()->create([
|
||||
'name' => $ticketPriority['name'],
|
||||
'position' => $ticketPriority['position'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists(self::COPY_TICKET_FIELDS, $configuration)) {
|
||||
$targetProject->ticketFieldDefinitions()->delete();
|
||||
|
||||
foreach ($configuration[self::COPY_TICKET_FIELDS] as $ticketFieldDefinition) {
|
||||
$targetProject->ticketFieldDefinitions()->create([
|
||||
'field_type' => $ticketFieldDefinition['field_type'],
|
||||
'label' => $ticketFieldDefinition['label'],
|
||||
'position' => $ticketFieldDefinition['position'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists(self::COPY_TICKET_LINK_TYPES, $configuration)) {
|
||||
$targetProject->ticketLinkTypes()->delete();
|
||||
|
||||
foreach ($configuration[self::COPY_TICKET_LINK_TYPES] as $ticketLinkType) {
|
||||
$targetProject->ticketLinkTypes()->create([
|
||||
'label' => $ticketLinkType['label'],
|
||||
'counterpart_label' => $ticketLinkType['counterpart_label'],
|
||||
'position' => $ticketLinkType['position'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists(self::COPY_REPOSITORY_CONNECTIONS, $configuration)) {
|
||||
$targetProject->repositoryConnections()->delete();
|
||||
|
||||
foreach ($configuration[self::COPY_REPOSITORY_CONNECTIONS] as $repositoryConnection) {
|
||||
$targetProject->repositoryConnections()->create([
|
||||
'provider' => $repositoryConnection['provider'],
|
||||
'base_url' => $repositoryConnection['base_url'],
|
||||
'provider_repository_id' => $repositoryConnection['provider_repository_id'],
|
||||
'owner_name' => $repositoryConnection['owner_name'],
|
||||
'repository_name' => $repositoryConnection['repository_name'],
|
||||
'repository_full_name' => $repositoryConnection['repository_full_name'],
|
||||
'repository_url' => $repositoryConnection['repository_url'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists(self::COPY_NOTIFICATION_ACTIONS, $configuration)) {
|
||||
$targetProject->notificationActions()->delete();
|
||||
|
||||
foreach ($configuration[self::COPY_NOTIFICATION_ACTIONS] as $notificationAction) {
|
||||
$targetProject->notificationActions()->create([
|
||||
'name' => $notificationAction['name'],
|
||||
'enabled' => $notificationAction['enabled'],
|
||||
'event_type' => $notificationAction['event_type'],
|
||||
'channel' => $notificationAction['channel'],
|
||||
'channel_configuration' => $notificationAction['channel_configuration'] ?? [],
|
||||
'message_template' => $notificationAction['message_template'] ?? null,
|
||||
'delivery_url' => $notificationAction['delivery_url'],
|
||||
'filters' => $notificationAction['filters'] ?? [],
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @param list<string> $copyAreas */
|
||||
/**
|
||||
* @param list<string> $copyAreas
|
||||
*/
|
||||
public function handle(Project $sourceProject, Project $targetProject, array $copyAreas): void
|
||||
{
|
||||
$this->apply($targetProject, $this->export($sourceProject, $copyAreas));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param EloquentCollection<int, Model> $models
|
||||
* @param array<int, array<string, mixed>> $items
|
||||
*/
|
||||
private function syncModels(EloquentCollection $models, array $items, callable $create, callable $update, ?callable $canDelete = null): bool
|
||||
{
|
||||
foreach ($items as $index => $item) {
|
||||
$model = $models->get($index);
|
||||
$model === null ? $create($item) : $update($model, $item);
|
||||
}
|
||||
|
||||
$blocked = false;
|
||||
foreach ($models->slice(count($items)) as $model) {
|
||||
if ($canDelete !== null && ! $canDelete($model)) {
|
||||
$blocked = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
$model->deleteQuietly();
|
||||
}
|
||||
|
||||
return $blocked;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Projects;
|
||||
|
||||
use App\Models\Project;
|
||||
|
||||
class ProjectTemplateOverrideManager
|
||||
{
|
||||
private static int $suppressionDepth = 0;
|
||||
|
||||
public function mark(Project $project, string $area): void
|
||||
{
|
||||
if (self::$suppressionDepth > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$project->loadMissing('projectTemplate');
|
||||
|
||||
if ($project->projectTemplate === null || ! $project->projectTemplate->is_published) {
|
||||
return;
|
||||
}
|
||||
|
||||
$overrides = array_values(array_unique([
|
||||
...($project->template_overrides ?? []),
|
||||
$area,
|
||||
]));
|
||||
|
||||
$project->forceFill(['template_overrides' => $overrides])->saveQuietly();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $areas
|
||||
*/
|
||||
public function clear(Project $project, array $areas): void
|
||||
{
|
||||
$remaining = array_values(array_diff($project->template_overrides ?? [], $areas));
|
||||
$project->forceFill(['template_overrides' => $remaining])->saveQuietly();
|
||||
}
|
||||
|
||||
public function withoutRecording(callable $callback): mixed
|
||||
{
|
||||
self::$suppressionDepth++;
|
||||
|
||||
try {
|
||||
return $callback();
|
||||
} finally {
|
||||
self::$suppressionDepth--;
|
||||
}
|
||||
}
|
||||
|
||||
public function isRecording(): bool
|
||||
{
|
||||
return self::$suppressionDepth === 0;
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Projects;
|
||||
|
||||
use App\Models\Project;
|
||||
|
||||
class RefreshProjectBaseTemplate
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DuplicateProjectConfiguration $duplicateProjectConfiguration,
|
||||
) {}
|
||||
|
||||
public function handle(Project $project, string $area): void
|
||||
{
|
||||
$project->loadMissing('projectTemplate');
|
||||
|
||||
if ($project->projectTemplate === null || $project->projectTemplate->is_published) {
|
||||
return;
|
||||
}
|
||||
|
||||
$snapshot = $this->duplicateProjectConfiguration->normalizeConfiguration(
|
||||
$project->projectTemplate->configuration_snapshot ?? [],
|
||||
);
|
||||
|
||||
$project->projectTemplate->forceFill([
|
||||
'configuration_snapshot' => array_replace(
|
||||
$snapshot,
|
||||
$this->duplicateProjectConfiguration->export($project->fresh(), [$area]),
|
||||
),
|
||||
])->saveQuietly();
|
||||
}
|
||||
}
|
||||
@@ -30,16 +30,10 @@ class ResolveProjectTemplateState
|
||||
? DuplicateProjectConfiguration::copyableAreas()
|
||||
: $this->duplicateProjectConfiguration->normalizeCopyAreas($areas);
|
||||
|
||||
$normalizedTemplateConfiguration = $this->duplicateProjectConfiguration->normalizeConfiguration(
|
||||
$projectTemplate->configuration_snapshot ?? [],
|
||||
);
|
||||
$templateConfiguration = collect($normalizedTemplateConfiguration)
|
||||
$templateConfiguration = collect($projectTemplate->configuration_snapshot ?? [])
|
||||
->only($selectedAreas)
|
||||
->all();
|
||||
$currentConfiguration = $this->duplicateProjectConfiguration->export($project, $selectedAreas);
|
||||
$persistedOverrides = $projectTemplate->is_published
|
||||
? array_values(array_intersect($project->template_overrides ?? [], $selectedAreas))
|
||||
: [];
|
||||
|
||||
$overriddenAreas = [];
|
||||
$inheritedAreas = [];
|
||||
@@ -47,12 +41,13 @@ class ResolveProjectTemplateState
|
||||
foreach ($selectedAreas as $area) {
|
||||
$label = $this->labelForArea($area);
|
||||
$hasTemplateValue = array_key_exists($area, $templateConfiguration);
|
||||
$hasCurrentValue = array_key_exists($area, $currentConfiguration);
|
||||
|
||||
if (! $hasTemplateValue) {
|
||||
if (! $hasTemplateValue && ! $hasCurrentValue) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! in_array($area, $persistedOverrides, true)) {
|
||||
if ($hasTemplateValue && $hasCurrentValue && $currentConfiguration[$area] === $templateConfiguration[$area]) {
|
||||
$inheritedAreas[] = [
|
||||
'label' => $label,
|
||||
'value' => $area,
|
||||
@@ -77,7 +72,6 @@ class ResolveProjectTemplateState
|
||||
'inheritsFromTemplate' => $projectTemplate->is_published,
|
||||
'overriddenAreas' => $overriddenAreas,
|
||||
'overriddenConfiguration' => array_intersect_key($currentConfiguration, $overriddenAreaValues),
|
||||
'resetRequirements' => $this->resetRequirements($project, $templateConfiguration, $persistedOverrides),
|
||||
'templateConfiguration' => $templateConfiguration,
|
||||
'templateId' => $projectTemplate->id,
|
||||
'templateName' => $projectTemplate->name,
|
||||
@@ -85,44 +79,6 @@ class ResolveProjectTemplateState
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $templateConfiguration
|
||||
* @param list<string> $persistedOverrides
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function resetRequirements(Project $project, array $templateConfiguration, array $persistedOverrides): array
|
||||
{
|
||||
if (! in_array(DuplicateProjectConfiguration::COPY_TICKET_STATUSES, $persistedOverrides, true)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$templateStatuses = $templateConfiguration[DuplicateProjectConfiguration::COPY_TICKET_STATUSES] ?? [];
|
||||
if (! is_array($templateStatuses)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$statuses = $project->ticketStatuses()->withCount('tickets')->get()->values();
|
||||
$replacementOptions = $statuses->take(count($templateStatuses))->map(
|
||||
static fn ($status): array => ['id' => $status->id, 'name' => $status->name],
|
||||
)->all();
|
||||
$removedStatuses = $statuses->slice(count($templateStatuses))
|
||||
->filter(static fn ($status): bool => $status->tickets_count > 0)
|
||||
->map(static fn ($status): array => [
|
||||
'id' => $status->id,
|
||||
'name' => $status->name,
|
||||
'ticketCount' => $status->tickets_count,
|
||||
])
|
||||
->values()
|
||||
->all();
|
||||
|
||||
return $removedStatuses === [] ? [] : [
|
||||
DuplicateProjectConfiguration::COPY_TICKET_STATUSES => [
|
||||
'removedStatuses' => $removedStatuses,
|
||||
'replacementOptions' => $replacementOptions,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function labelForArea(string $area): string
|
||||
{
|
||||
foreach (DuplicateProjectConfiguration::copyAreaOptions() as $option) {
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Actions\Projects;
|
||||
|
||||
use App\Models\Project;
|
||||
use App\Models\ProjectTemplate;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -11,7 +10,6 @@ class UpdateProjectTemplate
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DuplicateProjectConfiguration $duplicateProjectConfiguration,
|
||||
private readonly ProjectTemplateOverrideManager $projectTemplateOverrideManager,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -19,68 +17,48 @@ class UpdateProjectTemplate
|
||||
*/
|
||||
public function handle(ProjectTemplate $projectTemplate, array $attributes): ProjectTemplate
|
||||
{
|
||||
$projectTemplate->loadMissing('sourceProject');
|
||||
$projectTemplate->loadMissing('sourceProject', 'linkedProjects.projectTemplate');
|
||||
|
||||
$sourceProject = $projectTemplate->sourceProject;
|
||||
abort_if($sourceProject === null, 422, 'This template cannot be edited because the source project is missing.');
|
||||
|
||||
$copyAreas = array_values(Arr::get($attributes, 'copy_areas', []));
|
||||
$normalizedCopyAreas = $this->duplicateProjectConfiguration->normalizeCopyAreas($copyAreas);
|
||||
$previousConfiguration = $this->duplicateProjectConfiguration->normalizeConfiguration(
|
||||
$projectTemplate->configuration_snapshot ?? [],
|
||||
);
|
||||
$previousConfiguration = $projectTemplate->configuration_snapshot ?? [];
|
||||
$nextConfiguration = $this->duplicateProjectConfiguration->export($sourceProject, $normalizedCopyAreas);
|
||||
$affectedAreas = collect(array_unique([...array_keys($previousConfiguration), ...array_keys($nextConfiguration)]))
|
||||
->filter(fn (string $area): bool => ($previousConfiguration[$area] ?? null) !== ($nextConfiguration[$area] ?? null))
|
||||
->values()
|
||||
->all();
|
||||
$affectedAreas = array_values(array_unique([
|
||||
...array_keys($previousConfiguration),
|
||||
...array_keys($nextConfiguration),
|
||||
]));
|
||||
|
||||
DB::transaction(function () use ($projectTemplate, $attributes, $normalizedCopyAreas, $nextConfiguration, $previousConfiguration, $affectedAreas): void {
|
||||
ProjectTemplate::query()->whereKey($projectTemplate->id)->lockForUpdate()->firstOrFail();
|
||||
|
||||
$projectTemplate->forceFill([
|
||||
'name' => $attributes['name'],
|
||||
'copy_areas' => $normalizedCopyAreas,
|
||||
'configuration_snapshot' => $nextConfiguration,
|
||||
])->save();
|
||||
|
||||
Project::query()
|
||||
->where('project_template_id', $projectTemplate->id)
|
||||
->orderBy('id')
|
||||
->chunkById(100, function ($linkedProjects) use ($affectedAreas, $nextConfiguration, $previousConfiguration): void {
|
||||
foreach ($linkedProjects as $linkedProject) {
|
||||
$linkedProject = Project::query()->whereKey($linkedProject->id)->lockForUpdate()->firstOrFail();
|
||||
$currentConfiguration = $this->duplicateProjectConfiguration->export($linkedProject, $affectedAreas);
|
||||
$overrides = $linkedProject->template_overrides ?? [];
|
||||
$replacementConfiguration = [];
|
||||
foreach ($projectTemplate->linkedProjects as $linkedProject) {
|
||||
$currentConfiguration = $this->duplicateProjectConfiguration->export($linkedProject->fresh(), $affectedAreas);
|
||||
$replacementConfiguration = [];
|
||||
|
||||
foreach ($affectedAreas as $area) {
|
||||
if (in_array($area, $overrides, true)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($nextConfiguration as $area => $configuration) {
|
||||
$isNewTemplateArea = ! array_key_exists($area, $previousConfiguration);
|
||||
$matchesPreviousTemplate = array_key_exists($area, $previousConfiguration)
|
||||
&& array_key_exists($area, $currentConfiguration)
|
||||
&& $currentConfiguration[$area] === $previousConfiguration[$area];
|
||||
|
||||
if (! array_key_exists($area, $nextConfiguration)) {
|
||||
$this->projectTemplateOverrideManager->mark($linkedProject, $area);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! array_key_exists($area, $previousConfiguration)
|
||||
&& array_key_exists($area, $currentConfiguration)
|
||||
&& $currentConfiguration[$area] !== []) {
|
||||
$this->projectTemplateOverrideManager->mark($linkedProject, $area);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$replacementConfiguration[$area] = $nextConfiguration[$area];
|
||||
}
|
||||
|
||||
if ($replacementConfiguration !== []) {
|
||||
$this->duplicateProjectConfiguration->apply($linkedProject, $replacementConfiguration);
|
||||
}
|
||||
if ($isNewTemplateArea || $matchesPreviousTemplate) {
|
||||
$replacementConfiguration[$area] = $configuration;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if ($replacementConfiguration === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->duplicateProjectConfiguration->apply($linkedProject->fresh(), $replacementConfiguration);
|
||||
}
|
||||
});
|
||||
|
||||
return $projectTemplate->fresh(['sourceProject'])->loadCount('linkedProjects');
|
||||
|
||||
@@ -10,39 +10,23 @@ class UpdateProjectTemplateLink
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DuplicateProjectConfiguration $duplicateProjectConfiguration,
|
||||
private readonly ProjectTemplateOverrideManager $projectTemplateOverrideManager,
|
||||
) {}
|
||||
|
||||
public function switch(Project $project, ProjectTemplate $template): void
|
||||
{
|
||||
DB::transaction(function () use ($project, $template): void {
|
||||
$project = Project::query()->whereKey($project->id)->lockForUpdate()->firstOrFail();
|
||||
$configuration = $this->duplicateProjectConfiguration->normalizeConfiguration(
|
||||
$template->configuration_snapshot ?? [],
|
||||
);
|
||||
$localAreas = array_values(array_diff(
|
||||
DuplicateProjectConfiguration::copyableAreas(),
|
||||
array_keys($configuration),
|
||||
));
|
||||
|
||||
$project->forceFill([
|
||||
'project_template_id' => $template->id,
|
||||
'template_overrides' => $localAreas,
|
||||
])->saveQuietly();
|
||||
])->save();
|
||||
|
||||
$blockedAreas = $this->duplicateProjectConfiguration->apply($project, $configuration);
|
||||
$this->projectTemplateOverrideManager->clear(
|
||||
$this->duplicateProjectConfiguration->apply(
|
||||
$project->fresh(),
|
||||
array_values(array_diff(array_keys($configuration), $blockedAreas)),
|
||||
$template->configuration_snapshot ?? [],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $areas
|
||||
* @param array<int|string, int|string> $statusReplacements
|
||||
*/
|
||||
public function reset(Project $project, array $areas, array $statusReplacements = []): void
|
||||
public function reset(Project $project): void
|
||||
{
|
||||
$template = $project->projectTemplate;
|
||||
|
||||
@@ -50,19 +34,10 @@ class UpdateProjectTemplateLink
|
||||
return;
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($project, $template, $areas, $statusReplacements): void {
|
||||
$project = Project::query()->whereKey($project->id)->lockForUpdate()->firstOrFail();
|
||||
$configuration = collect($this->duplicateProjectConfiguration->normalizeConfiguration(
|
||||
$template->configuration_snapshot ?? [],
|
||||
))->only($areas)->all();
|
||||
$blockedAreas = $this->duplicateProjectConfiguration->apply(
|
||||
$project,
|
||||
$configuration,
|
||||
$statusReplacements,
|
||||
);
|
||||
$this->projectTemplateOverrideManager->clear(
|
||||
DB::transaction(function () use ($project, $template): void {
|
||||
$this->duplicateProjectConfiguration->apply(
|
||||
$project->fresh(),
|
||||
array_values(array_diff(array_keys($configuration), $blockedAreas)),
|
||||
$template->configuration_snapshot ?? [],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ class CreateProjectTicket
|
||||
public function __construct(
|
||||
private readonly BuildTicketCreatedPayload $buildTicketCreatedPayload,
|
||||
private readonly CreateTicketWorklogEntry $createTicketWorklogEntry,
|
||||
private readonly SyncProjectTicketFieldValues $syncProjectTicketFieldValues,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -30,15 +29,7 @@ class CreateProjectTicket
|
||||
* text: string
|
||||
* }>,
|
||||
* assignee_user_id?: int|null,
|
||||
* author_user_id?: int|null,
|
||||
* blocked_comment?: string|null,
|
||||
* description: string|null,
|
||||
* field_values?: array<int, array{
|
||||
* ticket_field_definition_id: int,
|
||||
* value_number?: float|int|string|null,
|
||||
* value_user_id?: int|string|null
|
||||
* }>,
|
||||
* is_blocked?: bool,
|
||||
* parent_ticket_id?: int|null,
|
||||
* story_points?: int|null,
|
||||
* ticket_priority_id: int,
|
||||
@@ -56,17 +47,18 @@ class CreateProjectTicket
|
||||
$nextSequenceNumber = (int) Ticket::query()
|
||||
->where('project_id', $project->id)
|
||||
->max('sequence_number') + 1;
|
||||
$ticketStatusQuery = TicketStatus::query()->where('project_id', $project->id);
|
||||
$ticketStatusQuery = TicketStatus::query()
|
||||
->where(
|
||||
$project->project_template_id === null ? 'project_id' : 'project_template_id',
|
||||
$project->project_template_id ?? $project->id,
|
||||
);
|
||||
|
||||
$ticketStatus = $ticketStatusQuery->findOrFail(Arr::get($attributes, 'ticket_status_id'));
|
||||
$isBlocked = (bool) Arr::get($attributes, 'is_blocked', false);
|
||||
|
||||
$ticket = Ticket::query()->create([
|
||||
'assignee_user_id' => Arr::get($attributes, 'assignee_user_id'),
|
||||
'author_user_id' => Arr::get($attributes, 'author_user_id', $actor?->id),
|
||||
'blocked_comment' => $isBlocked ? Arr::get($attributes, 'blocked_comment') : null,
|
||||
'author_user_id' => $actor?->id,
|
||||
'description' => Arr::get($attributes, 'description'),
|
||||
'is_blocked' => $isBlocked,
|
||||
'parent_ticket_id' => Arr::get($attributes, 'parent_ticket_id'),
|
||||
'project_id' => $project->id,
|
||||
'sequence_number' => $nextSequenceNumber,
|
||||
@@ -80,17 +72,6 @@ class CreateProjectTicket
|
||||
|
||||
AcceptanceCriteriaChecklistMapper::syncTicket($ticket, $acceptanceCriteria);
|
||||
|
||||
$fieldValues = Arr::get($attributes, 'field_values', []);
|
||||
|
||||
if ($fieldValues !== []) {
|
||||
$this->syncProjectTicketFieldValues->handle(
|
||||
$ticket,
|
||||
$project->ticketFieldDefinitions()->get()->keyBy('id'),
|
||||
collect(),
|
||||
$fieldValues,
|
||||
);
|
||||
}
|
||||
|
||||
$occurredAt = CarbonImmutable::now();
|
||||
|
||||
if ($actor !== null) {
|
||||
|
||||
@@ -30,10 +30,9 @@ class UpdateUserAvatar
|
||||
$color = $this->colorFor($user);
|
||||
$path = sprintf('avatars/generated/%d-%s.svg', $user->id, (string) Str::uuid());
|
||||
|
||||
$stored = Storage::disk($this->avatarDisk())->put(
|
||||
$stored = Storage::disk('public')->put(
|
||||
$path,
|
||||
$this->svg($this->initials($user->name), $color),
|
||||
['ContentType' => 'image/svg+xml'],
|
||||
);
|
||||
|
||||
if (! $stored) {
|
||||
@@ -54,7 +53,7 @@ class UpdateUserAvatar
|
||||
public function upload(User $user, UploadedFile $avatar): User
|
||||
{
|
||||
$oldPath = $user->avatar_path;
|
||||
$path = $avatar->store('avatars/uploads', $this->avatarDisk());
|
||||
$path = $avatar->store('avatars/uploads', 'public');
|
||||
|
||||
if (! is_string($path)) {
|
||||
throw new RuntimeException('Uploaded avatar could not be stored.');
|
||||
@@ -91,14 +90,7 @@ class UpdateUserAvatar
|
||||
return;
|
||||
}
|
||||
|
||||
Storage::disk($this->avatarDisk())->delete($path);
|
||||
}
|
||||
|
||||
private function avatarDisk(): string
|
||||
{
|
||||
$disk = config('filesystems.avatar_disk', 'public');
|
||||
|
||||
return is_string($disk) && $disk !== '' ? $disk : 'public';
|
||||
Storage::disk('public')->delete($path);
|
||||
}
|
||||
|
||||
private function initials(string $name): string
|
||||
|
||||
@@ -8,7 +8,6 @@ use App\Models\User;
|
||||
use Illuminate\Console\Attributes\Description;
|
||||
use Illuminate\Console\Attributes\Signature;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
#[Signature('app:create-admin-user {name=Admin} {email=admin@kanbangit.test} {password=test}')]
|
||||
@@ -26,30 +25,20 @@ class CreateAdminUser extends Command
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
DB::beginTransaction();
|
||||
$user = User::create([
|
||||
'name' => $this->argument('name'),
|
||||
'email' => $this->argument('email'),
|
||||
'password' => Hash::make($this->argument('password')),
|
||||
]);
|
||||
|
||||
try {
|
||||
$user = User::create([
|
||||
'name' => $this->argument('name'),
|
||||
'email' => $this->argument('email'),
|
||||
'password' => Hash::make($this->argument('password')),
|
||||
]);
|
||||
$this->avatars->generate($user);
|
||||
|
||||
$this->avatars->generate($user);
|
||||
$user->permissionAssignments()->create([
|
||||
'permission' => Permission::GlobalAdmin,
|
||||
]);
|
||||
|
||||
$user->permissionAssignments()->create([
|
||||
'permission' => Permission::GlobalAdmin,
|
||||
]);
|
||||
$this->info(sprintf('Admin User created with emil=\'%s\' and password=\'%s\'', $this->argument('email'), $this->argument('password')));
|
||||
|
||||
$this->info(sprintf('Admin User created with emil=\'%s\' and password=\'%s\'', $this->argument('email'), $this->argument('password')));
|
||||
DB::commit();
|
||||
|
||||
return self::SUCCESS;
|
||||
} catch (\Exception $exception) {
|
||||
$this->error('Admin User could not be created: ', $exception->getMessage());
|
||||
DB::rollBack();
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ class ProjectController extends Controller
|
||||
private readonly BuildProjectBacklogPayload $buildProjectBacklogPayload,
|
||||
private readonly CreateProject $createProject,
|
||||
private readonly DeleteProject $deleteProject,
|
||||
private readonly DuplicateProjectConfiguration $duplicateProjectConfiguration,
|
||||
private readonly ListVisibleProjects $listVisibleProjects,
|
||||
private readonly PermissionService $permissionService,
|
||||
private readonly ResolveProjectTemplateState $resolveProjectTemplateState,
|
||||
@@ -136,10 +135,9 @@ class ProjectController extends Controller
|
||||
|
||||
public function switchTemplate(SwitchProjectTemplateRequest $request, Project $project): RedirectResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$template = ProjectTemplate::query()
|
||||
->published()
|
||||
->findOrFail((int) Arr::get($validated, 'source_template_id'));
|
||||
->findOrFail($request->integer('source_template_id'));
|
||||
|
||||
$this->updateProjectTemplateLink->switch($project, $template);
|
||||
|
||||
@@ -149,29 +147,16 @@ class ProjectController extends Controller
|
||||
|
||||
public function resetTemplate(ResetProjectTemplateRequest $request, Project $project): RedirectResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$request->validated();
|
||||
|
||||
$template = $project->projectTemplate;
|
||||
|
||||
abort_if($template === null, 422, 'This project has no linked template to reset.');
|
||||
|
||||
$configuration = $this->duplicateProjectConfiguration->normalizeConfiguration(
|
||||
$template->configuration_snapshot ?? [],
|
||||
);
|
||||
$areas = array_values(Arr::get($validated, 'areas', array_keys($configuration)));
|
||||
$statusReplacements = Arr::get($validated, 'status_replacements', []);
|
||||
$this->updateProjectTemplateLink->reset($project->load('projectTemplate'));
|
||||
|
||||
$this->updateProjectTemplateLink->reset(
|
||||
$project->load('projectTemplate'),
|
||||
$areas,
|
||||
$statusReplacements,
|
||||
);
|
||||
|
||||
$status = Arr::has($validated, 'areas')
|
||||
? "Selected settings were reset to the {$template->name} template."
|
||||
: "{$project->name} was reset to the {$template->name} template baseline.";
|
||||
|
||||
return back()->with('status', $status);
|
||||
return to_route('projects.settings.general', $project)
|
||||
->with('status', "{$project->name} was reset to the {$template->name} template baseline.");
|
||||
}
|
||||
|
||||
public function access(Request $request, Project $project): Response
|
||||
|
||||
@@ -24,13 +24,16 @@ class MoveProjectTicketRequest extends FormRequest
|
||||
{
|
||||
/** @var Project $project */
|
||||
$project = $this->route('project');
|
||||
$projectTemplateId = $project->project_template_id;
|
||||
$ownershipColumn = $projectTemplateId === null ? 'project_id' : 'project_template_id';
|
||||
$ownershipValue = $projectTemplateId ?? $project->id;
|
||||
|
||||
return [
|
||||
'ticket_status_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('ticket_statuses', 'id')->where(
|
||||
fn ($query) => $query->where('project_id', $project->id),
|
||||
fn ($query) => $query->where($ownershipColumn, $ownershipValue),
|
||||
),
|
||||
],
|
||||
];
|
||||
|
||||
@@ -29,13 +29,16 @@ class DestroyProjectTicketTypeRequest extends FormRequest
|
||||
{
|
||||
/** @var Project $project */
|
||||
$project = $this->route('project');
|
||||
$projectTemplateId = $project->project_template_id;
|
||||
$ownershipColumn = $projectTemplateId === null ? 'project_id' : 'project_template_id';
|
||||
$ownershipValue = $projectTemplateId ?? $project->id;
|
||||
|
||||
return [
|
||||
'replacement_ticket_type_id' => [
|
||||
'nullable',
|
||||
'integer',
|
||||
Rule::exists('ticket_types', 'id')->where(
|
||||
fn ($query) => $query->where('project_id', $project->id),
|
||||
fn ($query) => $query->where($ownershipColumn, $ownershipValue),
|
||||
),
|
||||
],
|
||||
];
|
||||
|
||||
@@ -29,13 +29,16 @@ class MoveProjectTicketRequest extends FormRequest
|
||||
{
|
||||
/** @var Project $project */
|
||||
$project = $this->route('project');
|
||||
$projectTemplateId = $project->project_template_id;
|
||||
$ownershipColumn = $projectTemplateId === null ? 'project_id' : 'project_template_id';
|
||||
$ownershipValue = $projectTemplateId ?? $project->id;
|
||||
|
||||
return [
|
||||
'ticket_status_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('ticket_statuses', 'id')->where(
|
||||
fn ($query) => $query->where('project_id', $project->id),
|
||||
fn ($query) => $query->where($ownershipColumn, $ownershipValue),
|
||||
),
|
||||
],
|
||||
'redirect_to' => ['required', 'string', Rule::in(['backlog', 'board'])],
|
||||
|
||||
@@ -2,13 +2,10 @@
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Actions\Projects\DuplicateProjectConfiguration;
|
||||
use App\Models\Project;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\Validator;
|
||||
|
||||
class ResetProjectTemplateRequest extends FormRequest
|
||||
{
|
||||
@@ -26,71 +23,11 @@ class ResetProjectTemplateRequest extends FormRequest
|
||||
$project = $this->route('project');
|
||||
|
||||
return [
|
||||
'areas' => ['sometimes', 'array', 'min:1'],
|
||||
'areas.*' => ['required', 'string', Rule::in(DuplicateProjectConfiguration::copyableAreas())],
|
||||
'confirmation_key' => [
|
||||
'required_without:areas',
|
||||
'required',
|
||||
'string',
|
||||
Rule::in([$project->key]),
|
||||
],
|
||||
'status_replacements' => ['sometimes', 'array'],
|
||||
'status_replacements.*' => [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('ticket_statuses', 'id')->where('project_id', $project->id),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<int, callable(Validator): void> */
|
||||
public function after(): array
|
||||
{
|
||||
return [
|
||||
function (Validator $validator): void {
|
||||
if ($validator->errors()->isNotEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var Project $project */
|
||||
$project = $this->route('project');
|
||||
$validated = $validator->validated();
|
||||
$areas = array_values(Arr::get(
|
||||
$validated,
|
||||
'areas',
|
||||
DuplicateProjectConfiguration::copyableAreas(),
|
||||
));
|
||||
|
||||
if (! in_array(DuplicateProjectConfiguration::COPY_TICKET_STATUSES, $areas, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$project->loadMissing('projectTemplate');
|
||||
$configuration = app(DuplicateProjectConfiguration::class)->normalizeConfiguration(
|
||||
$project->projectTemplate?->configuration_snapshot ?? [],
|
||||
);
|
||||
$templateStatuses = Arr::get(
|
||||
$configuration,
|
||||
DuplicateProjectConfiguration::COPY_TICKET_STATUSES,
|
||||
[],
|
||||
);
|
||||
$statuses = $project->ticketStatuses()->withCount('tickets')->get()->values();
|
||||
$allowedReplacementIds = $statuses->take(count($templateStatuses))->pluck('id');
|
||||
$replacements = Arr::get($validated, 'status_replacements', []);
|
||||
|
||||
foreach ($statuses->slice(count($templateStatuses)) as $status) {
|
||||
if ($status->tickets_count === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$replacementId = (int) Arr::get($replacements, (string) $status->id, 0);
|
||||
if (! $allowedReplacementIds->contains($replacementId)) {
|
||||
$validator->errors()->add(
|
||||
"status_replacements.{$status->id}",
|
||||
"Choose a replacement status for {$status->name}.",
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -101,7 +38,6 @@ class ResetProjectTemplateRequest extends FormRequest
|
||||
{
|
||||
return [
|
||||
'confirmation_key.in' => 'Enter the exact project key to reset this project to its template baseline.',
|
||||
'areas.*.in' => 'Choose a valid template area to reset.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ class StoreProjectRequest extends FormRequest
|
||||
'copy_areas' => ['nullable', 'array'],
|
||||
'copy_areas.*' => [
|
||||
'string',
|
||||
Rule::in(DuplicateProjectConfiguration::acceptedCopyAreas()),
|
||||
Rule::in(DuplicateProjectConfiguration::copyableAreas()),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class StoreProjectTemplateRequest extends FormRequest
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'slug' => ['required', 'string', 'max:255', 'alpha_dash', Rule::unique('project_templates', 'slug')],
|
||||
'copy_areas' => ['required', 'array', 'min:1'],
|
||||
'copy_areas.*' => ['string', Rule::in(DuplicateProjectConfiguration::acceptedCopyAreas())],
|
||||
'copy_areas.*' => ['string', Rule::in(DuplicateProjectConfiguration::copyableAreas())],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,9 @@ class StoreProjectTicketBranchRequest extends FormRequest
|
||||
{
|
||||
/** @var Project $project */
|
||||
$project = $this->route('project');
|
||||
$projectTemplateId = $project->project_template_id;
|
||||
$ownershipColumn = $projectTemplateId === null ? 'project_id' : 'project_template_id';
|
||||
$ownershipValue = $projectTemplateId ?? $project->id;
|
||||
|
||||
return [
|
||||
'branch_name' => ['required', 'string', 'max:255'],
|
||||
@@ -33,7 +36,7 @@ class StoreProjectTicketBranchRequest extends FormRequest
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('repository_connections', 'id')->where(
|
||||
fn ($query) => $query->where('project_id', $project->id),
|
||||
fn ($query) => $query->where($ownershipColumn, $ownershipValue),
|
||||
),
|
||||
],
|
||||
];
|
||||
|
||||
@@ -48,6 +48,9 @@ class StoreProjectTicketRequest extends FormRequest
|
||||
{
|
||||
/** @var Project $project */
|
||||
$project = $this->route('project');
|
||||
$projectTemplateId = $project->project_template_id;
|
||||
$ownershipColumn = $projectTemplateId === null ? 'project_id' : 'project_template_id';
|
||||
$ownershipValue = $projectTemplateId ?? $project->id;
|
||||
|
||||
return [
|
||||
'acceptance_criteria' => ['nullable', 'array'],
|
||||
@@ -67,21 +70,21 @@ class StoreProjectTicketRequest extends FormRequest
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('ticket_priorities', 'id')->where(
|
||||
fn ($query) => $query->where('project_id', $project->id),
|
||||
fn ($query) => $query->where($ownershipColumn, $ownershipValue),
|
||||
),
|
||||
],
|
||||
'ticket_status_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('ticket_statuses', 'id')->where(
|
||||
fn ($query) => $query->where('project_id', $project->id),
|
||||
fn ($query) => $query->where($ownershipColumn, $ownershipValue),
|
||||
),
|
||||
],
|
||||
'ticket_type_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('ticket_types', 'id')->where(
|
||||
fn ($query) => $query->where('project_id', $project->id),
|
||||
fn ($query) => $query->where($ownershipColumn, $ownershipValue),
|
||||
),
|
||||
],
|
||||
'story_points' => ['nullable', 'integer', 'min:0'],
|
||||
|
||||
@@ -28,6 +28,9 @@ class StoreTicketLinkRequest extends FormRequest
|
||||
{
|
||||
/** @var Project $project */
|
||||
$project = $this->route('project');
|
||||
$projectTemplateId = $project->project_template_id;
|
||||
$ownershipColumn = $projectTemplateId === null ? 'project_id' : 'project_template_id';
|
||||
$ownershipValue = $projectTemplateId ?? $project->id;
|
||||
|
||||
return [
|
||||
'linked_ticket_id' => [
|
||||
@@ -39,7 +42,7 @@ class StoreTicketLinkRequest extends FormRequest
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('ticket_link_types', 'id')->where(
|
||||
fn ($query) => $query->where('project_id', $project->id),
|
||||
fn ($query) => $query->where($ownershipColumn, $ownershipValue),
|
||||
),
|
||||
],
|
||||
];
|
||||
|
||||
@@ -26,13 +26,16 @@ class StoreTicketStatusRequest extends FormRequest
|
||||
{
|
||||
/** @var Project $project */
|
||||
$project = $this->route('project');
|
||||
$projectTemplateId = $project->project_template_id;
|
||||
$ownershipColumn = $projectTemplateId === null ? 'project_id' : 'project_template_id';
|
||||
$ownershipValue = $projectTemplateId ?? $project->id;
|
||||
|
||||
return [
|
||||
'board_column_id' => [
|
||||
'nullable',
|
||||
'integer',
|
||||
Rule::exists('board_columns', 'id')->where(
|
||||
fn ($query) => $query->where('project_id', $project->id),
|
||||
fn ($query) => $query->where($ownershipColumn, $ownershipValue),
|
||||
),
|
||||
],
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
|
||||
@@ -27,7 +27,7 @@ class UpdateProjectTemplateRequest extends FormRequest
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'copy_areas' => ['required', 'array', 'min:1'],
|
||||
'copy_areas.*' => ['string', Rule::in(DuplicateProjectConfiguration::acceptedCopyAreas())],
|
||||
'copy_areas.*' => ['string', Rule::in(DuplicateProjectConfiguration::copyableAreas())],
|
||||
'confirm_linked_projects_update' => [
|
||||
Rule::requiredIf($requiresImpactConfirmation),
|
||||
'accepted',
|
||||
|
||||
@@ -51,6 +51,9 @@ class UpdateProjectTicketRequest extends FormRequest
|
||||
{
|
||||
/** @var Project $project */
|
||||
$project = $this->route('project');
|
||||
$projectTemplateId = $project->project_template_id;
|
||||
$ownershipColumn = $projectTemplateId === null ? 'project_id' : 'project_template_id';
|
||||
$ownershipValue = $projectTemplateId ?? $project->id;
|
||||
|
||||
return [
|
||||
'acceptance_criteria' => ['nullable', 'array'],
|
||||
@@ -69,7 +72,7 @@ class UpdateProjectTicketRequest extends FormRequest
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('ticket_field_definitions', 'id')->where(
|
||||
fn ($query) => $query->where('project_id', $project->id),
|
||||
fn ($query) => $query->where($ownershipColumn, $ownershipValue),
|
||||
),
|
||||
],
|
||||
'field_values.*.value_number' => ['nullable', 'numeric'],
|
||||
@@ -84,14 +87,14 @@ class UpdateProjectTicketRequest extends FormRequest
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('ticket_priorities', 'id')->where(
|
||||
fn ($query) => $query->where('project_id', $project->id),
|
||||
fn ($query) => $query->where($ownershipColumn, $ownershipValue),
|
||||
),
|
||||
],
|
||||
'ticket_type_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('ticket_types', 'id')->where(
|
||||
fn ($query) => $query->where('project_id', $project->id),
|
||||
fn ($query) => $query->where($ownershipColumn, $ownershipValue),
|
||||
),
|
||||
],
|
||||
'title' => ['required', 'string', 'max:255'],
|
||||
@@ -147,7 +150,7 @@ class UpdateProjectTicketRequest extends FormRequest
|
||||
|
||||
$requestedTicketTypeId = Arr::get($validated, 'ticket_type_id');
|
||||
$requestedTicketType = TicketType::query()
|
||||
->where('project_id', $project->id)
|
||||
->where($project->project_template_id === null ? 'project_id' : 'project_template_id', $project->project_template_id ?? $project->id)
|
||||
->find($requestedTicketTypeId);
|
||||
|
||||
if (
|
||||
|
||||
@@ -29,13 +29,16 @@ class UpdateTicketStatusTransitionsRequest extends FormRequest
|
||||
{
|
||||
/** @var Project $project */
|
||||
$project = $this->route('project');
|
||||
$projectTemplateId = $project->project_template_id;
|
||||
$ownershipColumn = $projectTemplateId === null ? 'project_id' : 'project_template_id';
|
||||
$ownershipValue = $projectTemplateId ?? $project->id;
|
||||
|
||||
return [
|
||||
'allowed_ticket_status_ids' => ['nullable', 'array'],
|
||||
'allowed_ticket_status_ids.*' => [
|
||||
'integer',
|
||||
Rule::exists('ticket_statuses', 'id')->where(
|
||||
fn ($query) => $query->where('project_id', $project->id),
|
||||
fn ($query) => $query->where($ownershipColumn, $ownershipValue),
|
||||
),
|
||||
],
|
||||
];
|
||||
|
||||
@@ -26,6 +26,9 @@ class ValidateProjectTicketBranchRequest extends FormRequest
|
||||
{
|
||||
/** @var Project $project */
|
||||
$project = $this->route('project');
|
||||
$projectTemplateId = $project->project_template_id;
|
||||
$ownershipColumn = $projectTemplateId === null ? 'project_id' : 'project_template_id';
|
||||
$ownershipValue = $projectTemplateId ?? $project->id;
|
||||
|
||||
return [
|
||||
'branch_name' => ['required', 'string', 'max:255'],
|
||||
@@ -33,7 +36,7 @@ class ValidateProjectTicketBranchRequest extends FormRequest
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('repository_connections', 'id')->where(
|
||||
fn ($query) => $query->where('project_id', $project->id),
|
||||
fn ($query) => $query->where($ownershipColumn, $ownershipValue),
|
||||
),
|
||||
],
|
||||
];
|
||||
|
||||
@@ -37,7 +37,6 @@ class ProjectResource extends JsonResource
|
||||
'inherits_from_template' => Arr::get($templateState, 'inheritsFromTemplate', false),
|
||||
'overridden_areas' => Arr::get($templateState, 'overriddenAreas', []),
|
||||
'overridden_configuration' => Arr::get($templateState, 'overriddenConfiguration', []),
|
||||
'reset_requirements' => Arr::get($templateState, 'resetRequirements', []),
|
||||
'template_configuration' => Arr::get($templateState, 'templateConfiguration', []),
|
||||
'template_id' => Arr::get($templateState, 'templateId'),
|
||||
'template_name' => Arr::get($templateState, 'templateName'),
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use App\Actions\Projects\DuplicateProjectConfiguration;
|
||||
use App\Models\ProjectTemplate;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
@@ -18,8 +17,6 @@ class ProjectTemplateResource extends JsonResource
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
if ($this->resource instanceof ProjectTemplate) {
|
||||
$configuration = app(DuplicateProjectConfiguration::class);
|
||||
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'project_id' => $this->project_id,
|
||||
@@ -30,10 +27,8 @@ class ProjectTemplateResource extends JsonResource
|
||||
'name' => $this->name,
|
||||
'slug' => $this->slug,
|
||||
'is_published' => $this->is_published,
|
||||
'copy_areas' => $configuration->normalizeCopyAreas($this->copy_areas ?? []),
|
||||
'configuration_snapshot' => $configuration->normalizeConfiguration(
|
||||
$this->configuration_snapshot ?? [],
|
||||
),
|
||||
'copy_areas' => $this->copy_areas ?? [],
|
||||
'configuration_snapshot' => $this->configuration_snapshot ?? [],
|
||||
'linked_project_count' => $this->whenCounted('linkedProjects'),
|
||||
'created_at' => $this->created_at?->toISOString(),
|
||||
'updated_at' => $this->updated_at?->toISOString(),
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
|
||||
namespace App\Mcp\Servers;
|
||||
|
||||
use App\Mcp\Tools\CreateTicket;
|
||||
use App\Mcp\Tools\GetTicket;
|
||||
use App\Mcp\Tools\ListMyAssignedTickets;
|
||||
use App\Mcp\Tools\ListTicketsByStatus;
|
||||
use App\Mcp\Tools\ListTicketTypes;
|
||||
use App\Mcp\Tools\MoveTicketStatus;
|
||||
use App\Mcp\Tools\UpdateTicketAssignee;
|
||||
use App\Mcp\Tools\UpdateTicketDescription;
|
||||
@@ -17,7 +15,7 @@ use Laravel\Mcp\Server\Attributes\Version;
|
||||
|
||||
#[Name('KanbanGit MCP')]
|
||||
#[Version('0.1.0')]
|
||||
#[Instructions('Use these tools to read, create, and update KanbanGit tickets with the authenticated application key owner permissions. Prefer stable IDs when available, especially project_id, ticket_id, ticket_type_id, and ticket_status_id. Use list_ticket_types to discover the available ticket types in a project. Use create_ticket with parent_ticket_id to create a child ticket.')]
|
||||
#[Instructions('Use these tools to read and update KanbanGit tickets with the authenticated application key owner permissions. Prefer stable IDs when available, especially ticket_id and ticket_status_id.')]
|
||||
class KanbanGitServer extends Server
|
||||
{
|
||||
protected array $tools = [
|
||||
@@ -27,8 +25,6 @@ class KanbanGitServer extends Server
|
||||
UpdateTicketDescription::class,
|
||||
UpdateTicketAssignee::class,
|
||||
MoveTicketStatus::class,
|
||||
CreateTicket::class,
|
||||
ListTicketTypes::class,
|
||||
];
|
||||
|
||||
protected array $resources = [
|
||||
|
||||
@@ -120,24 +120,6 @@ class KanbanGitMcp
|
||||
return $project;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function projectForWrite(User $user, int $projectId): Project
|
||||
{
|
||||
$project = Project::query()->find($projectId);
|
||||
|
||||
if ($project === null) {
|
||||
throw ValidationException::withMessages([
|
||||
'project_id' => 'Project was not found.',
|
||||
]);
|
||||
}
|
||||
|
||||
$this->ensureCanWrite($user, $project);
|
||||
|
||||
return $project;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $validated
|
||||
* @return Collection<int, TicketStatus>|Response
|
||||
@@ -207,12 +189,10 @@ class KanbanGitMcp
|
||||
public function ticketPayload(Ticket $ticket): array
|
||||
{
|
||||
$ticket->load([
|
||||
'acceptanceCriteria',
|
||||
'assignee',
|
||||
'author',
|
||||
'fieldValues.ticketFieldDefinition',
|
||||
'fieldValues.userValue',
|
||||
'parentTicket',
|
||||
'project',
|
||||
'ticketPriority',
|
||||
'ticketStatus.boardColumn',
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Actions\Permissions\PermissionService;
|
||||
use App\Actions\Tickets\CreateProjectTicket;
|
||||
use App\Actions\Tickets\Support\AcceptanceCriteriaChecklistMapper;
|
||||
use App\Enums\Permission;
|
||||
use App\Mcp\Support\KanbanGitMcp;
|
||||
use App\Models\Project;
|
||||
use App\Models\Ticket;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\JsonSchema\Types\Type;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
use Laravel\Mcp\Server\Tools\Annotations\IsDestructive;
|
||||
|
||||
#[Name('create_ticket')]
|
||||
#[Description('Create a ticket with all editable ticket fields, including its type, initial status, priority, assignee, author, description, story points, acceptance criteria, blocked state, and custom field values. Set parent_ticket_id to create a child of an existing ticket in the same project whose type allows children. Requires project write permission; overriding the author requires project admin permission. Ticket IDs, keys, sequence numbers, timestamps, and worklog entries are generated automatically.')]
|
||||
#[IsDestructive(false)]
|
||||
class CreateTicket extends Tool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly KanbanGitMcp $kanbanGitMcp,
|
||||
private readonly CreateProjectTicket $createProjectTicket,
|
||||
private readonly PermissionService $permissionService,
|
||||
) {}
|
||||
|
||||
public function handle(Request $request): Response|ResponseFactory
|
||||
{
|
||||
$projectInput = $request->validate([
|
||||
'project_id' => ['required', 'integer'],
|
||||
]);
|
||||
|
||||
$user = $this->kanbanGitMcp->user($request);
|
||||
$project = $this->kanbanGitMcp->projectForWrite($user, (int) Arr::get($projectInput, 'project_id'));
|
||||
$validated = $request->validate([
|
||||
'title' => ['required', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'ticket_type_id' => ['required', 'integer', Rule::exists('ticket_types', 'id')->where('project_id', $project->id)],
|
||||
'ticket_status_id' => ['required', 'integer', Rule::exists('ticket_statuses', 'id')->where('project_id', $project->id)],
|
||||
'ticket_priority_id' => ['required', 'integer', Rule::exists('ticket_priorities', 'id')->where('project_id', $project->id)],
|
||||
'parent_ticket_id' => ['nullable', 'integer', Rule::exists('tickets', 'id')->where('project_id', $project->id)],
|
||||
'assignee_user_id' => ['nullable', 'integer', Rule::exists('users', 'id')],
|
||||
'author_user_id' => ['nullable', 'integer', Rule::exists('users', 'id')],
|
||||
'story_points' => ['nullable', 'integer', 'min:0'],
|
||||
'is_blocked' => ['sometimes', 'boolean'],
|
||||
'blocked_comment' => ['nullable', 'string'],
|
||||
'acceptance_criteria' => ['nullable', 'array', 'list'],
|
||||
'acceptance_criteria.*' => ['array:text,checked,indent'],
|
||||
'acceptance_criteria.*.text' => ['required', 'string'],
|
||||
'acceptance_criteria.*.checked' => ['sometimes', 'boolean'],
|
||||
'acceptance_criteria.*.indent' => ['sometimes', 'integer', 'min:0', 'max:'.AcceptanceCriteriaChecklistMapper::MAX_INDENT_LEVEL],
|
||||
'field_values' => ['sometimes', 'array', 'list'],
|
||||
'field_values.*' => ['array:ticket_field_definition_id,value_number,value_user_id'],
|
||||
'field_values.*.ticket_field_definition_id' => [
|
||||
'required', 'integer', 'distinct',
|
||||
Rule::exists('ticket_field_definitions', 'id')->where('project_id', $project->id),
|
||||
],
|
||||
'field_values.*.value_number' => ['nullable', 'numeric'],
|
||||
'field_values.*.value_user_id' => ['nullable', 'integer', Rule::exists('users', 'id')],
|
||||
]);
|
||||
|
||||
$this->validateTicketConstraints($project, $user, $validated);
|
||||
|
||||
$ticket = $this->createProjectTicket->handle($project, $validated, $user);
|
||||
|
||||
return Response::structured([
|
||||
'data' => $this->kanbanGitMcp->ticketPayload($ticket->fresh()),
|
||||
'meta' => [
|
||||
'message' => "{$ticket->ticket_key} was created.",
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $validated
|
||||
*/
|
||||
private function validateTicketConstraints(Project $project, User $user, array $validated): void
|
||||
{
|
||||
$errors = [];
|
||||
$parentTicketId = Arr::get($validated, 'parent_ticket_id');
|
||||
|
||||
if ($parentTicketId !== null) {
|
||||
$parentTicket = Ticket::query()
|
||||
->where('project_id', $project->id)
|
||||
->with('ticketType')
|
||||
->find($parentTicketId);
|
||||
|
||||
if (! $parentTicket?->ticketType?->is_parent) {
|
||||
$errors['parent_ticket_id'] = 'Only tickets with a parent-enabled type can be selected as a parent.';
|
||||
}
|
||||
}
|
||||
|
||||
if (Arr::get($validated, 'is_blocked', false) && blank(Arr::get($validated, 'blocked_comment'))) {
|
||||
$errors['blocked_comment'] = 'Blocked tickets require a blocker comment.';
|
||||
}
|
||||
|
||||
if (
|
||||
Arr::exists($validated, 'author_user_id')
|
||||
&& (int) Arr::get($validated, 'author_user_id') !== $user->id
|
||||
&& ! $this->permissionService->allows($user, Permission::ProjectAdmin, $project)
|
||||
) {
|
||||
$errors['author_user_id'] = 'Only project admins can change the author.';
|
||||
}
|
||||
|
||||
$fieldValues = Arr::get($validated, 'field_values', []);
|
||||
|
||||
if ($fieldValues !== []) {
|
||||
$fieldDefinitions = $project->ticketFieldDefinitions()->get()->keyBy('id');
|
||||
|
||||
foreach ($fieldValues as $index => $fieldValue) {
|
||||
$fieldDefinition = $fieldDefinitions->get((int) Arr::get($fieldValue, 'ticket_field_definition_id'));
|
||||
$incompatibleValue = $fieldDefinition->field_type === 'user' ? 'value_number' : 'value_user_id';
|
||||
|
||||
if (Arr::get($fieldValue, $incompatibleValue) !== null) {
|
||||
$errors["field_values.{$index}.{$incompatibleValue}"] = 'The value must match the custom field type.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($errors !== []) {
|
||||
throw ValidationException::withMessages($errors);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, Type>
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'project_id' => $schema->integer()->description('Project ID.')->required(),
|
||||
'title' => $schema->string()->max(255)->description('Ticket title.')->required(),
|
||||
'description' => $schema->string()->nullable()->description('Ticket description.'),
|
||||
'ticket_type_id' => $schema->integer()->description('Ticket type ID belonging to the project. Explicitly selects the type, including types that allow child tickets.')->required(),
|
||||
'ticket_status_id' => $schema->integer()->description('Initial ticket status ID belonging to the project.')->required(),
|
||||
'ticket_priority_id' => $schema->integer()->description('Ticket priority ID belonging to the project.')->required(),
|
||||
'parent_ticket_id' => $schema->integer()->nullable()->description('Existing parent ticket ID in the same project. Its type must allow children. Omit or pass null for a top-level ticket.'),
|
||||
'assignee_user_id' => $schema->integer()->nullable()->description('User ID to assign. Omit or pass null to leave unassigned.'),
|
||||
'author_user_id' => $schema->integer()->nullable()->description('Defaults to the application key owner when omitted. Only project admins may choose another author or pass null for no author.'),
|
||||
'story_points' => $schema->integer()->min(0)->nullable()->description('Non-negative story point estimate.'),
|
||||
'is_blocked' => $schema->boolean()->default(false)->description('Whether the ticket is blocked. Requires a nonblank blocked_comment when true.'),
|
||||
'blocked_comment' => $schema->string()->nullable()->description('Blocker reason. Stored only when is_blocked is true.'),
|
||||
'acceptance_criteria' => $schema->array()->nullable()
|
||||
->items($schema->object([
|
||||
'text' => $schema->string()->description('Checklist item text.')->required(),
|
||||
'checked' => $schema->boolean()->default(false),
|
||||
'indent' => $schema->integer()->min(0)->max(AcceptanceCriteriaChecklistMapper::MAX_INDENT_LEVEL)->default(0),
|
||||
])->withoutAdditionalProperties())
|
||||
->description('Ordered acceptance checklist. Indent 0 is a root item; larger values nest under preceding items.'),
|
||||
'field_values' => $schema->array()
|
||||
->items($schema->object([
|
||||
'ticket_field_definition_id' => $schema->integer()->description('Custom field definition ID belonging to the project. Each definition may appear once.')->required(),
|
||||
'value_number' => $schema->number()->nullable()->description('Value for a numeric field.'),
|
||||
'value_user_id' => $schema->integer()->nullable()->description('User ID for a user field.'),
|
||||
])->withoutAdditionalProperties())
|
||||
->description('Custom field values. Supply the value matching each definition type; omit or pass null for an empty value.'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Http\Resources\ProjectResource;
|
||||
use App\Http\Resources\TicketTypeResource;
|
||||
use App\Mcp\Support\KanbanGitMcp;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\JsonSchema\Types\Type;
|
||||
use Illuminate\Support\Arr;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
|
||||
|
||||
#[Name('list_ticket_types')]
|
||||
#[Description('List all ticket types in a visible project in their configured order, including IDs, names, and is_parent. Use a returned ID as ticket_type_id when creating a ticket. Types with is_parent=true allow tickets of that type to have child tickets; set parent_ticket_id on create_ticket to attach a child to an existing parent ticket.')]
|
||||
#[IsReadOnly]
|
||||
class ListTicketTypes extends Tool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly KanbanGitMcp $kanbanGitMcp,
|
||||
) {}
|
||||
|
||||
public function handle(Request $request): Response|ResponseFactory
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'project_id' => ['required', 'integer'],
|
||||
]);
|
||||
|
||||
$user = $this->kanbanGitMcp->user($request);
|
||||
$project = $this->kanbanGitMcp->projectForRead($user, (int) Arr::get($validated, 'project_id'));
|
||||
|
||||
return Response::structured([
|
||||
'data' => [
|
||||
'project' => ProjectResource::make($project)->resolve(),
|
||||
'ticket_types' => TicketTypeResource::collection($project->ticketTypes()->get())->resolve(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, Type>
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'project_id' => $schema->integer()
|
||||
->description('Project ID. Requires permission to view this project.')
|
||||
->required(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@ class BoardColumn extends Model
|
||||
{
|
||||
/** @use HasFactory<BoardColumnFactory> */
|
||||
use HasFactory;
|
||||
|
||||
use SyncsProjectTemplateOwnership;
|
||||
|
||||
protected static function booted(): void
|
||||
|
||||
@@ -31,7 +31,6 @@ class NotificationAction extends Model
|
||||
{
|
||||
/** @use HasFactory<NotificationActionFactory> */
|
||||
use HasFactory;
|
||||
|
||||
use SyncsProjectTemplateOwnership;
|
||||
|
||||
/**
|
||||
|
||||
+1
-17
@@ -13,7 +13,7 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
#[Fillable(['name', 'key', 'project_template_id', 'show_author', 'show_story_points', 'template_overrides'])]
|
||||
#[Fillable(['name', 'key', 'project_template_id', 'show_author', 'show_story_points'])]
|
||||
class Project extends Model
|
||||
{
|
||||
/** @use HasFactory<ProjectFactory> */
|
||||
@@ -24,7 +24,6 @@ class Project extends Model
|
||||
return [
|
||||
'show_author' => 'boolean',
|
||||
'show_story_points' => 'boolean',
|
||||
'template_overrides' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -109,21 +108,6 @@ class Project extends Model
|
||||
->orderBy('id');
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
public function configurationRelations(): array
|
||||
{
|
||||
return [
|
||||
'boardColumns',
|
||||
'notificationActions',
|
||||
'repositoryConnections',
|
||||
'ticketFieldDefinitions',
|
||||
'ticketLinkTypes',
|
||||
'ticketPriorities',
|
||||
'ticketStatuses',
|
||||
'ticketTypes',
|
||||
];
|
||||
}
|
||||
|
||||
public function tickets(): HasMany
|
||||
{
|
||||
return $this->hasMany(Ticket::class)
|
||||
|
||||
@@ -26,7 +26,6 @@ class RepositoryConnection extends Model
|
||||
{
|
||||
/** @use HasFactory<RepositoryConnectionFactory> */
|
||||
use HasFactory;
|
||||
|
||||
use SyncsProjectTemplateOwnership;
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,7 +23,6 @@ class TicketFieldDefinition extends Model
|
||||
|
||||
/** @use HasFactory<TicketFieldDefinitionFactory> */
|
||||
use HasFactory;
|
||||
|
||||
use SyncsProjectTemplateOwnership;
|
||||
|
||||
protected static function booted(): void
|
||||
|
||||
@@ -15,7 +15,6 @@ class TicketLinkType extends Model
|
||||
{
|
||||
/** @use HasFactory<TicketLinkTypeFactory> */
|
||||
use HasFactory;
|
||||
|
||||
use SyncsProjectTemplateOwnership;
|
||||
|
||||
protected static function booted(): void
|
||||
|
||||
@@ -8,14 +8,12 @@ use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
#[Fillable(['name', 'position', 'project_id', 'project_template_id'])]
|
||||
class TicketPriority extends Model
|
||||
{
|
||||
/** @use HasFactory<TicketPriorityFactory> */
|
||||
use HasFactory;
|
||||
|
||||
use SyncsProjectTemplateOwnership;
|
||||
|
||||
protected static function booted(): void
|
||||
@@ -29,9 +27,4 @@ class TicketPriority extends Model
|
||||
{
|
||||
return $this->belongsTo(Project::class);
|
||||
}
|
||||
|
||||
public function tickets(): HasMany
|
||||
{
|
||||
return $this->hasMany(Ticket::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ class TicketStatus extends Model
|
||||
{
|
||||
/** @use HasFactory<TicketStatusFactory> */
|
||||
use HasFactory;
|
||||
|
||||
use SyncsProjectTemplateOwnership;
|
||||
|
||||
protected static function booted(): void
|
||||
|
||||
@@ -15,7 +15,6 @@ class TicketType extends Model
|
||||
{
|
||||
/** @use HasFactory<TicketTypeFactory> */
|
||||
use HasFactory;
|
||||
|
||||
use SyncsProjectTemplateOwnership;
|
||||
|
||||
protected static function booted(): void
|
||||
|
||||
+3
-19
@@ -77,28 +77,12 @@ class User extends Authenticatable
|
||||
protected function avatar(): Attribute
|
||||
{
|
||||
return Attribute::get(
|
||||
fn (): ?string => $this->avatarUrl(),
|
||||
fn (): ?string => filled($this->avatar_path)
|
||||
? Storage::disk('public')->url($this->avatar_path)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
private function avatarUrl(): ?string
|
||||
{
|
||||
if (blank($this->avatar_path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$diskName = config('filesystems.avatar_disk', 'public');
|
||||
$disk = Storage::disk(is_string($diskName) && $diskName !== '' ? $diskName : 'public');
|
||||
|
||||
if ((bool) config('filesystems.avatar_urls_temporary', false)) {
|
||||
$expirationMinutes = max(1, (int) config('filesystems.avatar_url_expiration_minutes', 15));
|
||||
|
||||
return $disk->temporaryUrl($this->avatar_path, now()->addMinutes($expirationMinutes));
|
||||
}
|
||||
|
||||
return $disk->url($this->avatar_path);
|
||||
}
|
||||
|
||||
public function permissionAssignments(): MorphMany
|
||||
{
|
||||
return $this->morphMany(PermissionAssignment::class, 'assignee');
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Actions\Projects\DuplicateProjectConfiguration;
|
||||
use App\Actions\Projects\ProjectTemplateOverrideManager;
|
||||
use App\Actions\Projects\RefreshProjectBaseTemplate;
|
||||
use App\Models\BoardColumn;
|
||||
use App\Models\NotificationAction;
|
||||
use App\Models\RepositoryConnection;
|
||||
use App\Models\TicketFieldDefinition;
|
||||
use App\Models\TicketLinkType;
|
||||
use App\Models\TicketPriority;
|
||||
use App\Models\TicketStatus;
|
||||
use App\Models\TicketType;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ProjectConfigurationObserver
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ProjectTemplateOverrideManager $projectTemplateOverrideManager,
|
||||
private readonly RefreshProjectBaseTemplate $refreshProjectBaseTemplate,
|
||||
) {}
|
||||
|
||||
public function saved(Model $model): void
|
||||
{
|
||||
$this->markOverride($model);
|
||||
}
|
||||
|
||||
public function deleted(Model $model): void
|
||||
{
|
||||
$this->markOverride($model);
|
||||
}
|
||||
|
||||
private function markOverride(Model $model): void
|
||||
{
|
||||
if (! $this->projectTemplateOverrideManager->isRecording()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$project = $model->project()->first();
|
||||
|
||||
if ($project === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$area = match (true) {
|
||||
$model instanceof BoardColumn => DuplicateProjectConfiguration::COPY_BOARD_COLUMNS,
|
||||
$model instanceof TicketStatus => DuplicateProjectConfiguration::COPY_TICKET_STATUSES,
|
||||
$model instanceof TicketType => DuplicateProjectConfiguration::COPY_TICKET_TYPES,
|
||||
$model instanceof TicketPriority => DuplicateProjectConfiguration::COPY_TICKET_PRIORITIES,
|
||||
$model instanceof TicketFieldDefinition => DuplicateProjectConfiguration::COPY_TICKET_FIELDS,
|
||||
$model instanceof TicketLinkType => DuplicateProjectConfiguration::COPY_TICKET_LINK_TYPES,
|
||||
$model instanceof RepositoryConnection => DuplicateProjectConfiguration::COPY_REPOSITORY_CONNECTIONS,
|
||||
$model instanceof NotificationAction => DuplicateProjectConfiguration::COPY_NOTIFICATION_ACTIONS,
|
||||
default => null,
|
||||
};
|
||||
|
||||
if ($area === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->refreshProjectBaseTemplate->handle($project, $area);
|
||||
$this->projectTemplateOverrideManager->mark($project, $area);
|
||||
}
|
||||
}
|
||||
@@ -8,18 +8,9 @@ use App\Actions\Notifications\TicketCreated;
|
||||
use App\Actions\Notifications\TicketStatusChanged;
|
||||
use App\Actions\Permissions\PermissionService;
|
||||
use App\Enums\Permission;
|
||||
use App\Models\BoardColumn;
|
||||
use App\Models\NotificationAction;
|
||||
use App\Models\Project;
|
||||
use App\Models\RepositoryConnection;
|
||||
use App\Models\Team;
|
||||
use App\Models\TicketFieldDefinition;
|
||||
use App\Models\TicketLinkType;
|
||||
use App\Models\TicketPriority;
|
||||
use App\Models\TicketStatus;
|
||||
use App\Models\TicketType;
|
||||
use App\Models\User;
|
||||
use App\Observers\ProjectConfigurationObserver;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Support\Facades\Date;
|
||||
@@ -45,18 +36,6 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
foreach ([
|
||||
BoardColumn::class,
|
||||
NotificationAction::class,
|
||||
RepositoryConnection::class,
|
||||
TicketFieldDefinition::class,
|
||||
TicketLinkType::class,
|
||||
TicketPriority::class,
|
||||
TicketStatus::class,
|
||||
TicketType::class,
|
||||
] as $model) {
|
||||
$model::observe(ProjectConfigurationObserver::class);
|
||||
}
|
||||
|
||||
if ($this->app->environment() !== 'local') {
|
||||
URL::forceScheme('https');
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
"skills": [
|
||||
"fortify-development",
|
||||
"laravel-best-practices",
|
||||
"mcp-development",
|
||||
"wayfinder-development",
|
||||
"inertia-vue-development",
|
||||
"tailwindcss-development"
|
||||
|
||||
@@ -17,8 +17,6 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
$middleware->trustProxies(at: '*');
|
||||
|
||||
$middleware->encryptCookies(except: ['appearance', 'sidebar_state']);
|
||||
|
||||
$middleware->web(append: [
|
||||
|
||||
+3
-4
@@ -12,11 +12,10 @@
|
||||
"php": "^8.4",
|
||||
"inertiajs/inertia-laravel": "^2.0",
|
||||
"laravel/fortify": "^1.34",
|
||||
"laravel/framework": "^v13.26.1",
|
||||
"laravel/framework": "^13.0",
|
||||
"laravel/mcp": "^0.6",
|
||||
"laravel/tinker": "^3.0",
|
||||
"laravel/wayfinder": "^0.1.14",
|
||||
"league/flysystem-aws-s3-v3": "^3.0",
|
||||
"scalar/laravel": "^0.2.1"
|
||||
},
|
||||
"require-dev": {
|
||||
@@ -52,7 +51,7 @@
|
||||
],
|
||||
"dev": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"@php artisan dev"
|
||||
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
|
||||
],
|
||||
"dev:ssr": [
|
||||
"npm run build:ssr",
|
||||
@@ -67,8 +66,8 @@
|
||||
],
|
||||
"ci:check": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"npm audit --audit-level=high",
|
||||
"npm run lint:check",
|
||||
"npm run format:check",
|
||||
"npm run types:check",
|
||||
"@test"
|
||||
],
|
||||
|
||||
Generated
+332
-791
File diff suppressed because it is too large
Load Diff
@@ -15,12 +15,6 @@ return [
|
||||
|
||||
'default' => env('FILESYSTEM_DISK', 'local'),
|
||||
|
||||
'avatar_disk' => env('AVATAR_FILESYSTEM_DISK', env('FILESYSTEM_DISK', 'public')),
|
||||
|
||||
'avatar_urls_temporary' => env('AVATAR_URLS_TEMPORARY', false),
|
||||
|
||||
'avatar_url_expiration_minutes' => env('AVATAR_URL_EXPIRATION_MINUTES', 15),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Filesystem Disks
|
||||
|
||||
@@ -23,7 +23,6 @@ class ProjectFactory extends Factory
|
||||
'key' => Str::upper(fake()->unique()->lexify('?????')),
|
||||
'show_author' => true,
|
||||
'show_story_points' => true,
|
||||
'template_overrides' => [],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
+2
-6
@@ -44,17 +44,13 @@ return new class extends Migration
|
||||
'projects.name',
|
||||
'projects.project_template_id',
|
||||
'project_templates.project_id as template_project_id',
|
||||
'project_templates.is_published as template_is_published',
|
||||
])
|
||||
->get();
|
||||
|
||||
foreach ($projects as $project) {
|
||||
$projectTemplateId = $project->project_template_id;
|
||||
|
||||
if ($projectTemplateId === null || (
|
||||
! (bool) $project->template_is_published
|
||||
&& (int) ($project->template_project_id ?? 0) !== (int) $project->id
|
||||
)) {
|
||||
if ($projectTemplateId === null || (int) ($project->template_project_id ?? 0) !== (int) $project->id) {
|
||||
$projectTemplateId = DB::table('project_templates')->insertGetId([
|
||||
'project_id' => $project->id,
|
||||
'source_project_id' => $project->id,
|
||||
@@ -92,7 +88,7 @@ return new class extends Migration
|
||||
|
||||
$projectModel = Project::query()->find($project->id);
|
||||
|
||||
if ($projectModel !== null && ! (bool) $project->template_is_published) {
|
||||
if ($projectModel !== null) {
|
||||
DB::table('project_templates')
|
||||
->where('id', $projectTemplateId)
|
||||
->update([
|
||||
|
||||
-63
@@ -1,63 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Actions\Projects\DuplicateProjectConfiguration;
|
||||
use App\Models\Project;
|
||||
use App\Models\ProjectTemplate;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('projects', function (Blueprint $table): void {
|
||||
$table->json('template_overrides')->default('[]')->after('project_template_id');
|
||||
});
|
||||
|
||||
$configuration = app(DuplicateProjectConfiguration::class);
|
||||
|
||||
ProjectTemplate::query()->eachById(function (ProjectTemplate $template) use ($configuration): void {
|
||||
$template->forceFill([
|
||||
'copy_areas' => $configuration->normalizeCopyAreas($template->copy_areas ?? []),
|
||||
'configuration_snapshot' => $configuration->normalizeConfiguration(
|
||||
$template->configuration_snapshot ?? [],
|
||||
),
|
||||
])->saveQuietly();
|
||||
});
|
||||
|
||||
Project::query()
|
||||
->whereHas('projectTemplate', fn ($query) => $query->where('is_published', true))
|
||||
->with('projectTemplate')
|
||||
->eachById(function (Project $project) use ($configuration): void {
|
||||
$templateConfiguration = $configuration->normalizeConfiguration(
|
||||
$project->projectTemplate?->configuration_snapshot ?? [],
|
||||
);
|
||||
$areas = array_values(array_unique([
|
||||
...array_keys($templateConfiguration),
|
||||
...DuplicateProjectConfiguration::copyableAreas(),
|
||||
]));
|
||||
$currentConfiguration = $configuration->export($project, $areas);
|
||||
$overrides = collect($areas)
|
||||
->filter(fn (string $area): bool => ! array_key_exists($area, $templateConfiguration)
|
||||
|| ($currentConfiguration[$area] ?? null) !== $templateConfiguration[$area])
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$project->forceFill(['template_overrides' => $overrides])->saveQuietly();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('projects', function (Blueprint $table): void {
|
||||
$table->dropColumn('template_overrides');
|
||||
});
|
||||
}
|
||||
};
|
||||
+12
-32
@@ -1,38 +1,33 @@
|
||||
services:
|
||||
web:
|
||||
container_name: kanbangit-web
|
||||
image: kanbangit-app:local
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
target: runtime
|
||||
command: nginx
|
||||
dockerfile: ./docker/nginx/Dockerfile
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
- "8080:80"
|
||||
volumes:
|
||||
- kanbangit-storage:/var/www/storage:ro
|
||||
- kanbangit-public-assets:/var/www/public/build:ro
|
||||
env_file:
|
||||
- .env.production
|
||||
environment:
|
||||
PHP_FPM_UPSTREAM: php-fpm:9000
|
||||
networks:
|
||||
- kanbangit-web
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "--fail", "--silent", "http://127.0.0.1:8080/up"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
depends_on:
|
||||
php-fpm:
|
||||
condition: service_healthy
|
||||
|
||||
php-fpm:
|
||||
container_name: kanbangit-php-fpm
|
||||
image: kanbangit-app:local
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
target: runtime
|
||||
dockerfile: ./docker/php-fpm/Dockerfile
|
||||
target: production
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- kanbangit-storage:/var/www/storage
|
||||
- kanbangit-public-assets:/var/www/public/build
|
||||
env_file:
|
||||
- .env.production
|
||||
networks:
|
||||
@@ -47,23 +42,6 @@ services:
|
||||
mariadb:
|
||||
condition: service_healthy
|
||||
|
||||
worker:
|
||||
container_name: kanbangit-worker
|
||||
image: kanbangit-app:local
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
target: runtime
|
||||
command: php artisan queue:work --sleep=3 --tries=3 --timeout=60 --max-time=3600
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env.production
|
||||
networks:
|
||||
- kanbangit-db
|
||||
depends_on:
|
||||
mariadb:
|
||||
condition: service_healthy
|
||||
|
||||
mariadb:
|
||||
container_name: kanbangit-db
|
||||
image: mariadb:11.4
|
||||
@@ -87,3 +65,5 @@ networks:
|
||||
|
||||
volumes:
|
||||
mariadb-data-production:
|
||||
kanbangit-storage:
|
||||
kanbangit-public-assets:
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
if [ "${1:-}" = "nginx" ]; then
|
||||
: "${PHP_FPM_UPSTREAM:=127.0.0.1:9000}"
|
||||
export PHP_FPM_UPSTREAM
|
||||
|
||||
envsubst '${PHP_FPM_UPSTREAM}' \
|
||||
< /etc/nginx/nginx.conf.template \
|
||||
> /tmp/nginx.conf
|
||||
|
||||
exec nginx -c /tmp/nginx.conf -g 'daemon off;'
|
||||
fi
|
||||
|
||||
mkdir -p \
|
||||
bootstrap/cache \
|
||||
storage/framework/cache \
|
||||
storage/framework/sessions \
|
||||
storage/framework/testing \
|
||||
storage/framework/views \
|
||||
storage/logs
|
||||
|
||||
php artisan config:cache --no-ansi
|
||||
php artisan route:cache --no-ansi
|
||||
|
||||
exec "$@"
|
||||
@@ -0,0 +1,20 @@
|
||||
FROM php:8.4-cli-bookworm AS builder
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl ca-certificates git unzip \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
||||
&& apt-get update && apt-get install -y --no-install-recommends nodejs \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
RUN curl -sS https://getcomposer.org/installer | php -- \
|
||||
--install-dir=/usr/local/bin --filename=composer
|
||||
WORKDIR /var/www
|
||||
COPY . /var/www
|
||||
RUN composer install --no-dev --prefer-dist --no-interaction --no-progress --optimize-autoloader
|
||||
RUN npm ci && npm run build
|
||||
|
||||
FROM nginx:alpine
|
||||
COPY ./docker/nginx/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY --from=builder /var/www/public /var/www/public
|
||||
WORKDIR /var/www/public
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -0,0 +1,33 @@
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
client_max_body_size 100m;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /var/www/public;
|
||||
index index.php index.html;
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
location ^~ /storage/ {
|
||||
alias /var/www/storage/app/public/;
|
||||
try_files $uri =404;
|
||||
access_log off;
|
||||
expires 7d;
|
||||
add_header Cache-Control "public";
|
||||
}
|
||||
location ~ \.php$ {
|
||||
fastcgi_pass php-fpm:9000;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
include fastcgi_params;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
pid /tmp/nginx.pid;
|
||||
error_log /dev/stderr warn;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
access_log /dev/stdout;
|
||||
client_body_temp_path /tmp/client_body;
|
||||
proxy_temp_path /tmp/proxy;
|
||||
fastcgi_temp_path /tmp/fastcgi;
|
||||
uwsgi_temp_path /tmp/uwsgi;
|
||||
scgi_temp_path /tmp/scgi;
|
||||
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
client_max_body_size 3m;
|
||||
|
||||
server {
|
||||
listen 8080;
|
||||
server_name _;
|
||||
root /var/www/public;
|
||||
index index.php;
|
||||
|
||||
location = /nginx-health {
|
||||
access_log off;
|
||||
default_type text/plain;
|
||||
return 200 'ok';
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
location = /index.php {
|
||||
include /etc/nginx/fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param HTTP_PROXY '';
|
||||
fastcgi_param HTTP_X_FORWARDED_FOR $http_x_forwarded_for;
|
||||
fastcgi_param HTTP_X_FORWARDED_HOST $http_x_forwarded_host;
|
||||
fastcgi_param HTTP_X_FORWARDED_PORT $http_x_forwarded_port;
|
||||
fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto;
|
||||
fastcgi_pass ${PHP_FPM_UPSTREAM};
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
return 404;
|
||||
}
|
||||
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
SCRIPT_NAME=/status \
|
||||
SCRIPT_FILENAME=/status \
|
||||
REQUEST_METHOD=GET \
|
||||
cgi-fcgi -bind -connect 127.0.0.1:9000 2>/dev/null | grep -q '^pool:'
|
||||
@@ -0,0 +1,66 @@
|
||||
FROM php:8.4-fpm AS builder
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
unzip \
|
||||
libpq-dev \
|
||||
libonig-dev \
|
||||
libssl-dev \
|
||||
libxml2-dev \
|
||||
libcurl4-openssl-dev \
|
||||
libicu-dev \
|
||||
libzip-dev \
|
||||
libmagickwand-dev \
|
||||
libpng-dev \
|
||||
libjpeg-dev \
|
||||
libfreetype6-dev \
|
||||
&& docker-php-ext-install -j$(nproc) \
|
||||
pdo_mysql \
|
||||
pdo_pgsql \
|
||||
pgsql \
|
||||
opcache \
|
||||
intl \
|
||||
zip \
|
||||
bcmath \
|
||||
soap \
|
||||
&& docker-php-ext-configure gd --with-jpeg --with-freetype \
|
||||
&& docker-php-ext-install -j$(nproc) gd \
|
||||
&& pecl install imagick \
|
||||
&& docker-php-ext-enable imagick \
|
||||
&& apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
WORKDIR /var/www
|
||||
COPY . /var/www
|
||||
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \
|
||||
&& composer install --no-dev --optimize-autoloader --no-interaction --no-progress --prefer-dist
|
||||
FROM php:8.4-fpm AS production
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libpq-dev \
|
||||
libicu-dev \
|
||||
libzip-dev \
|
||||
libfcgi-bin \
|
||||
procps \
|
||||
libmagickwand-dev \
|
||||
&& apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
RUN curl -o /usr/local/bin/php-fpm-healthcheck \
|
||||
https://raw.githubusercontent.com/renatomefi/php-fpm-healthcheck/master/php-fpm-healthcheck \
|
||||
&& chmod +x /usr/local/bin/php-fpm-healthcheck
|
||||
COPY ./docker/php-fpm/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
COPY ./storage /var/www/storage-init
|
||||
COPY --from=builder /usr/local/lib/php/extensions/ /usr/local/lib/php/extensions/
|
||||
COPY --from=builder /usr/local/etc/php/conf.d/ /usr/local/etc/php/conf.d/
|
||||
COPY --from=builder /usr/local/bin/docker-php-ext-* /usr/local/bin/
|
||||
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
|
||||
RUN printf '%s\n' \
|
||||
'[global]' \
|
||||
'daemonize = no' \
|
||||
'' \
|
||||
'[www]' \
|
||||
'pm.status_path = /status' \
|
||||
> /usr/local/etc/php-fpm.d/zz-docker.conf
|
||||
COPY --from=builder /var/www /var/www
|
||||
WORKDIR /var/www
|
||||
RUN chown -R www-data:www-data /var/www
|
||||
USER www-data
|
||||
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||
EXPOSE 9000
|
||||
CMD ["php-fpm"]
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
if [ ! "$(ls -A /var/www/storage)" ]; then
|
||||
echo "Initializing storage directory..."
|
||||
cp -R /var/www/storage-init/. /var/www/storage
|
||||
chown -R www-data:www-data /var/www/storage
|
||||
fi
|
||||
rm -rf /var/www/storage-init
|
||||
php artisan config:cache
|
||||
php artisan route:cache
|
||||
exec "$@"
|
||||
@@ -1,35 +0,0 @@
|
||||
# UI Rework 1: Englische Lokalisierungsgrundlage
|
||||
|
||||
Epic: [Epic 1](epic-1.md)
|
||||
|
||||
Abhängigkeiten: Keine.
|
||||
|
||||
## Ziel und Grenzen
|
||||
|
||||
Englische Lokalisierungsgrundlage als abgegrenzten Teil des vollständigen Frontend-Neubaus umsetzen. Grundlage: Vue 3/Inertia 2 mit Laravel, Tauri 2 für Apps, gemeinsames Repository, servergeladene Oberfläche, HTTPS, Hell/Dunkel/System und englische Texte über Übersetzungsschlüssel. Bestehende Fachlogik bleibt maßgeblich; Resources und Routen nur soweit für diese Oberfläche erforderlich anpassen. Keine zusätzlichen Produktmodule.
|
||||
|
||||
## Einstieg
|
||||
|
||||
Gezielt prüfen: [resources/js/app.ts](../../resources/js/app.ts); [resources/js/ssr.ts](../../resources/js/ssr.ts); [resources/js/types](../../resources/js/types). Die Pfade sind Einstiegspunkte, keine Erlaubnis zu fachlichen Umbauten. [AGENTS.md](../../AGENTS.md) und die für den tatsächlich geänderten Bereich geltenden Skills beachten. Vor Codeänderungen die zur installierten Version passende Dokumentation prüfen.
|
||||
|
||||
Design: [Fluid Emerald Kanban](https://stitch.withgoogle.com/projects/9754281177402648015)
|
||||
|
||||
## Implementierungsweg
|
||||
|
||||
1. Eine zentrale Übersetzungsstruktur für Vue einführen.
|
||||
2. Englischen Katalog, Interpolation, Pluralformen und Formatierung festlegen.
|
||||
3. Serverseitige Validierungstexte einheitlich Englisch halten.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] Neue UI-Texte verwenden Schlüssel.
|
||||
- [ ] Fehlende Schlüssel fallen im Test auf.
|
||||
- [ ] Spätere Sprachen benötigen keine Änderungen an Komponenten.
|
||||
|
||||
## Prüfung
|
||||
|
||||
Gezielte Verhaltenstests für die Änderung; betroffene PHPUnit-Featuretests bei Controller-/Resource-/Route-Anpassungen. Oberfläche mit Tastatur und bei mobiler Breite in beiden Themes prüfen. Projektvorgaben für Lint/Build und bei Vue/TypeScript abschließend `npm run types:check` erfüllen. Keine ausschließlich optischen Momentaufnahmen als Ersatz für Verhaltenstests.
|
||||
|
||||
## Grenze der Agentensitzung
|
||||
|
||||
Nur dieses Ticket und bereits erfüllte Abhängigkeiten bearbeiten. Ein zusammenhängendes Ergebnis mit passenden Tests liefern; keine Nachbartickets mitziehen. Die Aufteilung zielt auf eine begrenzte Sitzung mit Reserve für Prüfung, garantiert aber kein festes Zeit- oder Tokenbudget. Wenn unerwartete Voraussetzungen den Umfang wesentlich vergrößern, rechtzeitig den Stand, betroffene Dateien, bereits ausgeführte Prüfungen und den konkret nötigen Folgeschritt festhalten.
|
||||
@@ -1,39 +0,0 @@
|
||||
# UI Rework 10: Persönliches Dashboard mit vorhandenen Daten
|
||||
|
||||
Epic: [Epic 1](epic-1.md)
|
||||
|
||||
Abhängigkeiten: [Issue 8](epic-1-issue-8.md), [Issue 11](epic-1-issue-11.md)
|
||||
|
||||
## Ziel und Grenzen
|
||||
|
||||
Persönliches Dashboard mit vorhandenen Daten als abgegrenzten Teil des vollständigen Frontend-Neubaus umsetzen. Grundlage: Vue 3/Inertia 2 mit Laravel, Tauri 2 für Apps, gemeinsames Repository, servergeladene Oberfläche, HTTPS, Hell/Dunkel/System und englische Texte über Übersetzungsschlüssel. Bestehende Fachlogik bleibt maßgeblich; Resources und Routen nur soweit für diese Oberfläche erforderlich anpassen. Keine zusätzlichen Produktmodule.
|
||||
|
||||
## Einstieg
|
||||
|
||||
Gezielt prüfen: [resources/js/pages/Dashboard.vue](../../resources/js/pages/Dashboard.vue); [app/Actions/Tickets/ListAssignedTicketsByVisibleProject.php](../../app/Actions/Tickets/ListAssignedTicketsByVisibleProject.php); [routes/web.php](../../routes/web.php). Die Pfade sind Einstiegspunkte, keine Erlaubnis zu fachlichen Umbauten. [AGENTS.md](../../AGENTS.md) und die für den tatsächlich geänderten Bereich geltenden Skills beachten. Vor Codeänderungen die zur installierten Version passende Dokumentation prüfen.
|
||||
|
||||
Design: [Fluid Emerald Kanban](https://stitch.withgoogle.com/projects/9754281177402648015)
|
||||
Relevanter Stitch-Screen: Persönliches Dashboard — 30b69ed84e5c4bdf8cd3fe26d7a9e803. Zugriff über Stitch-MCP, Projekt 9754281177402648015.
|
||||
|
||||
## Implementierungsweg
|
||||
|
||||
1. Dashboard aus zugewiesenen Tickets und sichtbaren Projekten aufbauen.
|
||||
2. Vorhandene Action über einen Inertia-Controller und Resources anbinden.
|
||||
3. Aus dem Mockup nicht belegbare Kennzahlen und Aktivitätsfeeds weglassen.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] Keine Platzhalterdaten.
|
||||
- [ ] Keine fremden Tickets.
|
||||
- [ ] Leere und gefüllte Dashboards funktionieren.
|
||||
- [ ] Neue Datenweitergabe ist durch Featuretests abgesichert.
|
||||
|
||||
## Prüfung
|
||||
|
||||
Vorhandene gezielte Abdeckung: [tests/Feature/DashboardTest.php](../../tests/Feature/DashboardTest.php), [tests/Feature/Api/AssignedTicketsApiTest.php](../../tests/Feature/Api/AssignedTicketsApiTest.php). Nur bei Berührung des jeweiligen Verhaltens aktualisieren bzw. ausführen.
|
||||
|
||||
Gezielte Verhaltenstests für die Änderung; betroffene PHPUnit-Featuretests bei Controller-/Resource-/Route-Anpassungen. Oberfläche mit Tastatur und bei mobiler Breite in beiden Themes prüfen. Projektvorgaben für Lint/Build und bei Vue/TypeScript abschließend `npm run types:check` erfüllen. Keine ausschließlich optischen Momentaufnahmen als Ersatz für Verhaltenstests.
|
||||
|
||||
## Grenze der Agentensitzung
|
||||
|
||||
Nur dieses Ticket und bereits erfüllte Abhängigkeiten bearbeiten. Ein zusammenhängendes Ergebnis mit passenden Tests liefern; keine Nachbartickets mitziehen. Die Aufteilung zielt auf eine begrenzte Sitzung mit Reserve für Prüfung, garantiert aber kein festes Zeit- oder Tokenbudget. Wenn unerwartete Voraussetzungen den Umfang wesentlich vergrößern, rechtzeitig den Stand, betroffene Dateien, bereits ausgeführte Prüfungen und den konkret nötigen Folgeschritt festhalten.
|
||||
@@ -1,36 +0,0 @@
|
||||
# UI Rework 11: Ticketkarten und wiederverwendbare Ticketlisten
|
||||
|
||||
Epic: [Epic 1](epic-1.md)
|
||||
|
||||
Abhängigkeiten: [Issue 3](epic-1-issue-3.md)
|
||||
|
||||
## Ziel und Grenzen
|
||||
|
||||
Ticketkarten und wiederverwendbare Ticketlisten als abgegrenzten Teil des vollständigen Frontend-Neubaus umsetzen. Grundlage: Vue 3/Inertia 2 mit Laravel, Tauri 2 für Apps, gemeinsames Repository, servergeladene Oberfläche, HTTPS, Hell/Dunkel/System und englische Texte über Übersetzungsschlüssel. Bestehende Fachlogik bleibt maßgeblich; Resources und Routen nur soweit für diese Oberfläche erforderlich anpassen. Keine zusätzlichen Produktmodule.
|
||||
|
||||
## Einstieg
|
||||
|
||||
Gezielt prüfen: [resources/js/components](../../resources/js/components); [resources/js/types/models/ticket-model.ts](../../resources/js/types/models/ticket-model.ts). Die Pfade sind Einstiegspunkte, keine Erlaubnis zu fachlichen Umbauten. [AGENTS.md](../../AGENTS.md) und die für den tatsächlich geänderten Bereich geltenden Skills beachten. Vor Codeänderungen die zur installierten Version passende Dokumentation prüfen.
|
||||
|
||||
Design: [Fluid Emerald Kanban](https://stitch.withgoogle.com/projects/9754281177402648015)
|
||||
Relevanter Stitch-Screen: Board View — 1a8363ad911a494f92cc4468955ce2f2. Zugriff über Stitch-MCP, Projekt 9754281177402648015.
|
||||
|
||||
## Implementierungsweg
|
||||
|
||||
1. Neue Karten und Listenzeilen mit Ticketkey, Titel, Typ, Priorität, Zuweisung und Blockerzustand entwickeln.
|
||||
2. Darstellung mit und ohne optionale Daten definieren.
|
||||
3. Gemeinsame Props und Ereignisse für Board, Backlog und Dashboard festlegen. Ticketlinks über Wayfinder aufbauen und Fokusführung sowie Touch-Ziele prüfen; die Komponenten dürfen keine eigenen Datenabfragen auslösen.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] Lange Titel bleiben bedienbar.
|
||||
- [ ] Prioritäten und Status stammen aus Projektdaten.
|
||||
- [ ] Links, Fokus und Touch-Ziele funktionieren.
|
||||
|
||||
## Prüfung
|
||||
|
||||
Gezielte Verhaltenstests für die Änderung; betroffene PHPUnit-Featuretests bei Controller-/Resource-/Route-Anpassungen. Oberfläche mit Tastatur und bei mobiler Breite in beiden Themes prüfen. Projektvorgaben für Lint/Build und bei Vue/TypeScript abschließend `npm run types:check` erfüllen. Keine ausschließlich optischen Momentaufnahmen als Ersatz für Verhaltenstests.
|
||||
|
||||
## Grenze der Agentensitzung
|
||||
|
||||
Nur dieses Ticket und bereits erfüllte Abhängigkeiten bearbeiten. Ein zusammenhängendes Ergebnis mit passenden Tests liefern; keine Nachbartickets mitziehen. Die Aufteilung zielt auf eine begrenzte Sitzung mit Reserve für Prüfung, garantiert aber kein festes Zeit- oder Tokenbudget. Wenn unerwartete Voraussetzungen den Umfang wesentlich vergrößern, rechtzeitig den Stand, betroffene Dateien, bereits ausgeführte Prüfungen und den konkret nötigen Folgeschritt festhalten.
|
||||
@@ -1,38 +0,0 @@
|
||||
# UI Rework 12: Boardansicht für Desktop und Mobilgeräte
|
||||
|
||||
Epic: [Epic 1](epic-1.md)
|
||||
|
||||
Abhängigkeiten: [Issue 4](epic-1-issue-4.md), [Issue 8](epic-1-issue-8.md), [Issue 11](epic-1-issue-11.md)
|
||||
|
||||
## Ziel und Grenzen
|
||||
|
||||
Boardansicht für Desktop und Mobilgeräte als abgegrenzten Teil des vollständigen Frontend-Neubaus umsetzen. Grundlage: Vue 3/Inertia 2 mit Laravel, Tauri 2 für Apps, gemeinsames Repository, servergeladene Oberfläche, HTTPS, Hell/Dunkel/System und englische Texte über Übersetzungsschlüssel. Bestehende Fachlogik bleibt maßgeblich; Resources und Routen nur soweit für diese Oberfläche erforderlich anpassen. Keine zusätzlichen Produktmodule.
|
||||
|
||||
## Einstieg
|
||||
|
||||
Gezielt prüfen: [resources/js/pages/projects/Board.vue](../../resources/js/pages/projects/Board.vue); [app/Actions/Projects/BuildProjectBoardPayload.php](../../app/Actions/Projects/BuildProjectBoardPayload.php); [app/Http/Resources/BoardColumnResource.php](../../app/Http/Resources/BoardColumnResource.php). Die Pfade sind Einstiegspunkte, keine Erlaubnis zu fachlichen Umbauten. [AGENTS.md](../../AGENTS.md) und die für den tatsächlich geänderten Bereich geltenden Skills beachten. Vor Codeänderungen die zur installierten Version passende Dokumentation prüfen.
|
||||
|
||||
Design: [Fluid Emerald Kanban](https://stitch.withgoogle.com/projects/9754281177402648015)
|
||||
Relevanter Stitch-Screen: Board View — 1a8363ad911a494f92cc4468955ce2f2. Zugriff über Stitch-MCP, Projekt 9754281177402648015.
|
||||
|
||||
## Implementierungsweg
|
||||
|
||||
1. Horizontales Board mit konfigurierten Spalten neu bauen.
|
||||
2. Mobil eine Spalte mit Auswahl anbieten.
|
||||
3. Statusunterteilung, Zähler, Leerzustände und vorhandene Statusauswahl darstellen.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] Beliebige Spaltenanzahl funktioniert.
|
||||
- [ ] Mehrere Status pro Spalte bleiben sichtbar.
|
||||
- [ ] Board und Backlog behalten ihre bestehende fachliche Trennung.
|
||||
|
||||
## Prüfung
|
||||
|
||||
Vorhandene gezielte Abdeckung: [tests/Feature/Projects/ProjectBoardControllerTest.php](../../tests/Feature/Projects/ProjectBoardControllerTest.php). Nur bei Berührung des jeweiligen Verhaltens aktualisieren bzw. ausführen.
|
||||
|
||||
Gezielte Verhaltenstests für die Änderung; betroffene PHPUnit-Featuretests bei Controller-/Resource-/Route-Anpassungen. Oberfläche mit Tastatur und bei mobiler Breite in beiden Themes prüfen. Projektvorgaben für Lint/Build und bei Vue/TypeScript abschließend `npm run types:check` erfüllen. Keine ausschließlich optischen Momentaufnahmen als Ersatz für Verhaltenstests.
|
||||
|
||||
## Grenze der Agentensitzung
|
||||
|
||||
Nur dieses Ticket und bereits erfüllte Abhängigkeiten bearbeiten. Ein zusammenhängendes Ergebnis mit passenden Tests liefern; keine Nachbartickets mitziehen. Die Aufteilung zielt auf eine begrenzte Sitzung mit Reserve für Prüfung, garantiert aber kein festes Zeit- oder Tokenbudget. Wenn unerwartete Voraussetzungen den Umfang wesentlich vergrößern, rechtzeitig den Stand, betroffene Dateien, bereits ausgeführte Prüfungen und den konkret nötigen Folgeschritt festhalten.
|
||||
@@ -1,40 +0,0 @@
|
||||
# UI Rework 13: Drag-and-drop für bestehende Statuswechsel
|
||||
|
||||
Epic: [Epic 1](epic-1.md)
|
||||
|
||||
Abhängigkeiten: [Issue 5](epic-1-issue-5.md), [Issue 12](epic-1-issue-12.md)
|
||||
|
||||
## Ziel und Grenzen
|
||||
|
||||
Drag-and-drop für bestehende Statuswechsel als abgegrenzten Teil des vollständigen Frontend-Neubaus umsetzen. Grundlage: Vue 3/Inertia 2 mit Laravel, Tauri 2 für Apps, gemeinsames Repository, servergeladene Oberfläche, HTTPS, Hell/Dunkel/System und englische Texte über Übersetzungsschlüssel. Bestehende Fachlogik bleibt maßgeblich; Resources und Routen nur soweit für diese Oberfläche erforderlich anpassen. Keine zusätzlichen Produktmodule.
|
||||
|
||||
## Einstieg
|
||||
|
||||
Gezielt prüfen: [resources/js/pages/projects/Board.vue](../../resources/js/pages/projects/Board.vue); [app/Http/Requests/MoveProjectTicketRequest.php](../../app/Http/Requests/MoveProjectTicketRequest.php); [app/Actions/Tickets/MoveProjectTicket.php](../../app/Actions/Tickets/MoveProjectTicket.php). Die Pfade sind Einstiegspunkte, keine Erlaubnis zu fachlichen Umbauten. [AGENTS.md](../../AGENTS.md) und die für den tatsächlich geänderten Bereich geltenden Skills beachten. Vor Codeänderungen die zur installierten Version passende Dokumentation prüfen.
|
||||
|
||||
Design: [Fluid Emerald Kanban](https://stitch.withgoogle.com/projects/9754281177402648015)
|
||||
Relevanter Stitch-Screen: Board View — 1a8363ad911a494f92cc4468955ce2f2. Zugriff über Stitch-MCP, Projekt 9754281177402648015.
|
||||
|
||||
## Implementierungsweg
|
||||
|
||||
1. Ziehen per Maus und Touch ergänzen.
|
||||
2. Zulässige Ziele aus Workflowdaten ableiten.
|
||||
3. Bei mehreren Zielstatus Auswahl zeigen.
|
||||
4. Vorhandenen Statusendpunkt über Wayfinder verwenden und Fehler zurückrollen.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] Unzulässige Ziele sind gesperrt.
|
||||
- [ ] Alternative Tastatur-/Menübedienung bleibt möglich.
|
||||
- [ ] Kein doppeltes Worklog.
|
||||
- [ ] Keine persistierte freie Kartenreihenfolge.
|
||||
|
||||
## Prüfung
|
||||
|
||||
Vorhandene gezielte Abdeckung: [tests/Feature/Projects/ProjectBoardControllerTest.php](../../tests/Feature/Projects/ProjectBoardControllerTest.php). Nur bei Berührung des jeweiligen Verhaltens aktualisieren bzw. ausführen.
|
||||
|
||||
Gezielte Verhaltenstests für die Änderung; betroffene PHPUnit-Featuretests bei Controller-/Resource-/Route-Anpassungen. Oberfläche mit Tastatur und bei mobiler Breite in beiden Themes prüfen. Projektvorgaben für Lint/Build und bei Vue/TypeScript abschließend `npm run types:check` erfüllen. Keine ausschließlich optischen Momentaufnahmen als Ersatz für Verhaltenstests.
|
||||
|
||||
## Grenze der Agentensitzung
|
||||
|
||||
Nur dieses Ticket und bereits erfüllte Abhängigkeiten bearbeiten. Ein zusammenhängendes Ergebnis mit passenden Tests liefern; keine Nachbartickets mitziehen. Die Aufteilung zielt auf eine begrenzte Sitzung mit Reserve für Prüfung, garantiert aber kein festes Zeit- oder Tokenbudget. Wenn unerwartete Voraussetzungen den Umfang wesentlich vergrößern, rechtzeitig den Stand, betroffene Dateien, bereits ausgeführte Prüfungen und den konkret nötigen Folgeschritt festhalten.
|
||||
@@ -1,37 +0,0 @@
|
||||
# UI Rework 14: Backlog und Ticketbaum
|
||||
|
||||
Epic: [Epic 1](epic-1.md)
|
||||
|
||||
Abhängigkeiten: [Issue 4](epic-1-issue-4.md), [Issue 11](epic-1-issue-11.md)
|
||||
|
||||
## Ziel und Grenzen
|
||||
|
||||
Backlog und Ticketbaum als abgegrenzten Teil des vollständigen Frontend-Neubaus umsetzen. Grundlage: Vue 3/Inertia 2 mit Laravel, Tauri 2 für Apps, gemeinsames Repository, servergeladene Oberfläche, HTTPS, Hell/Dunkel/System und englische Texte über Übersetzungsschlüssel. Bestehende Fachlogik bleibt maßgeblich; Resources und Routen nur soweit für diese Oberfläche erforderlich anpassen. Keine zusätzlichen Produktmodule.
|
||||
|
||||
## Einstieg
|
||||
|
||||
Gezielt prüfen: [resources/js/pages/projects/Backlog.vue](../../resources/js/pages/projects/Backlog.vue); [resources/js/components/TicketTreeList.vue](../../resources/js/components/TicketTreeList.vue); [app/Actions/Projects/BuildProjectBacklogPayload.php](../../app/Actions/Projects/BuildProjectBacklogPayload.php). Die Pfade sind Einstiegspunkte, keine Erlaubnis zu fachlichen Umbauten. [AGENTS.md](../../AGENTS.md) und die für den tatsächlich geänderten Bereich geltenden Skills beachten. Vor Codeänderungen die zur installierten Version passende Dokumentation prüfen.
|
||||
|
||||
Design: [Fluid Emerald Kanban](https://stitch.withgoogle.com/projects/9754281177402648015)
|
||||
|
||||
## Implementierungsweg
|
||||
|
||||
1. Backlog mit vorhandenen Filtern und Hierarchie neu darstellen.
|
||||
2. Eltern-Kind-Beziehungen, einklappbare Ebenen und vorhandene Aktionen integrieren.
|
||||
3. Mobile Listenansicht erstellen.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] Verschachtelte Tickets und leere Treffer funktionieren.
|
||||
- [ ] Berechtigungen und Statusübergänge bleiben erhalten.
|
||||
- [ ] Filter verändern keine fachlichen Daten.
|
||||
|
||||
## Prüfung
|
||||
|
||||
Vorhandene gezielte Abdeckung: [tests/Feature/Projects/ProjectBacklogTest.php](../../tests/Feature/Projects/ProjectBacklogTest.php). Nur bei Berührung des jeweiligen Verhaltens aktualisieren bzw. ausführen.
|
||||
|
||||
Gezielte Verhaltenstests für die Änderung; betroffene PHPUnit-Featuretests bei Controller-/Resource-/Route-Anpassungen. Oberfläche mit Tastatur und bei mobiler Breite in beiden Themes prüfen. Projektvorgaben für Lint/Build und bei Vue/TypeScript abschließend `npm run types:check` erfüllen. Keine ausschließlich optischen Momentaufnahmen als Ersatz für Verhaltenstests.
|
||||
|
||||
## Grenze der Agentensitzung
|
||||
|
||||
Nur dieses Ticket und bereits erfüllte Abhängigkeiten bearbeiten. Ein zusammenhängendes Ergebnis mit passenden Tests liefern; keine Nachbartickets mitziehen. Die Aufteilung zielt auf eine begrenzte Sitzung mit Reserve für Prüfung, garantiert aber kein festes Zeit- oder Tokenbudget. Wenn unerwartete Voraussetzungen den Umfang wesentlich vergrößern, rechtzeitig den Stand, betroffene Dateien, bereits ausgeführte Prüfungen und den konkret nötigen Folgeschritt festhalten.
|
||||
@@ -1,38 +0,0 @@
|
||||
# UI Rework 15: Neue Ticketdetailansicht und Seitenstruktur
|
||||
|
||||
Epic: [Epic 1](epic-1.md)
|
||||
|
||||
Abhängigkeiten: [Issue 4](epic-1-issue-4.md), [Issue 5](epic-1-issue-5.md), [Issue 11](epic-1-issue-11.md)
|
||||
|
||||
## Ziel und Grenzen
|
||||
|
||||
Neue Ticketdetailansicht und Seitenstruktur als abgegrenzten Teil des vollständigen Frontend-Neubaus umsetzen. Grundlage: Vue 3/Inertia 2 mit Laravel, Tauri 2 für Apps, gemeinsames Repository, servergeladene Oberfläche, HTTPS, Hell/Dunkel/System und englische Texte über Übersetzungsschlüssel. Bestehende Fachlogik bleibt maßgeblich; Resources und Routen nur soweit für diese Oberfläche erforderlich anpassen. Keine zusätzlichen Produktmodule.
|
||||
|
||||
## Einstieg
|
||||
|
||||
Gezielt prüfen: [resources/js/pages/projects/Ticket.vue](../../resources/js/pages/projects/Ticket.vue); [app/Actions/Projects/BuildProjectTicketDetailPayload.php](../../app/Actions/Projects/BuildProjectTicketDetailPayload.php). Die Pfade sind Einstiegspunkte, keine Erlaubnis zu fachlichen Umbauten. [AGENTS.md](../../AGENTS.md) und die für den tatsächlich geänderten Bereich geltenden Skills beachten. Vor Codeänderungen die zur installierten Version passende Dokumentation prüfen.
|
||||
|
||||
Design: [Fluid Emerald Kanban](https://stitch.withgoogle.com/projects/9754281177402648015)
|
||||
Relevanter Stitch-Screen: Ticket Detail View — 934e3eb46764456195f0eb627e67a363. Zugriff über Stitch-MCP, Projekt 9754281177402648015.
|
||||
|
||||
## Implementierungsweg
|
||||
|
||||
1. Detailansicht in klar abgegrenzte Komponenten aufteilen.
|
||||
2. Desktop-Inhalt und Metadatenbereich sowie mobile Stapelansicht neu aufbauen.
|
||||
3. Stabile Einhängepunkte für Bearbeitung, Checkliste, Beziehungen, Historie und Git definieren.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] Alle bestehenden Informationen bleiben zugänglich.
|
||||
- [ ] Keine bestehende Funktion wird beim Umbau entfernt.
|
||||
- [ ] Direkte Ticket-URLs funktionieren.
|
||||
|
||||
## Prüfung
|
||||
|
||||
Vorhandene gezielte Abdeckung: [tests/Feature/Projects/ProjectTicketControllerTest.php](../../tests/Feature/Projects/ProjectTicketControllerTest.php). Nur bei Berührung des jeweiligen Verhaltens aktualisieren bzw. ausführen.
|
||||
|
||||
Gezielte Verhaltenstests für die Änderung; betroffene PHPUnit-Featuretests bei Controller-/Resource-/Route-Anpassungen. Oberfläche mit Tastatur und bei mobiler Breite in beiden Themes prüfen. Projektvorgaben für Lint/Build und bei Vue/TypeScript abschließend `npm run types:check` erfüllen. Keine ausschließlich optischen Momentaufnahmen als Ersatz für Verhaltenstests.
|
||||
|
||||
## Grenze der Agentensitzung
|
||||
|
||||
Nur dieses Ticket und bereits erfüllte Abhängigkeiten bearbeiten. Ein zusammenhängendes Ergebnis mit passenden Tests liefern; keine Nachbartickets mitziehen. Die Aufteilung zielt auf eine begrenzte Sitzung mit Reserve für Prüfung, garantiert aber kein festes Zeit- oder Tokenbudget. Wenn unerwartete Voraussetzungen den Umfang wesentlich vergrößern, rechtzeitig den Stand, betroffene Dateien, bereits ausgeführte Prüfungen und den konkret nötigen Folgeschritt festhalten.
|
||||
@@ -1,38 +0,0 @@
|
||||
# UI Rework 16: Ticketanlage und Bearbeitungsformulare
|
||||
|
||||
Epic: [Epic 1](epic-1.md)
|
||||
|
||||
Abhängigkeiten: [Issue 5](epic-1-issue-5.md), [Issue 15](epic-1-issue-15.md)
|
||||
|
||||
## Ziel und Grenzen
|
||||
|
||||
Ticketanlage und Bearbeitungsformulare als abgegrenzten Teil des vollständigen Frontend-Neubaus umsetzen. Grundlage: Vue 3/Inertia 2 mit Laravel, Tauri 2 für Apps, gemeinsames Repository, servergeladene Oberfläche, HTTPS, Hell/Dunkel/System und englische Texte über Übersetzungsschlüssel. Bestehende Fachlogik bleibt maßgeblich; Resources und Routen nur soweit für diese Oberfläche erforderlich anpassen. Keine zusätzlichen Produktmodule.
|
||||
|
||||
## Einstieg
|
||||
|
||||
Gezielt prüfen: [resources/js/pages/projects/TicketCreate.vue](../../resources/js/pages/projects/TicketCreate.vue); [resources/js/pages/projects/Ticket.vue](../../resources/js/pages/projects/Ticket.vue); [app/Http/Requests/StoreProjectTicketRequest.php](../../app/Http/Requests/StoreProjectTicketRequest.php); [app/Http/Requests/UpdateProjectTicketRequest.php](../../app/Http/Requests/UpdateProjectTicketRequest.php). Die Pfade sind Einstiegspunkte, keine Erlaubnis zu fachlichen Umbauten. [AGENTS.md](../../AGENTS.md) und die für den tatsächlich geänderten Bereich geltenden Skills beachten. Vor Codeänderungen die zur installierten Version passende Dokumentation prüfen.
|
||||
|
||||
Design: [Fluid Emerald Kanban](https://stitch.withgoogle.com/projects/9754281177402648015)
|
||||
Relevanter Stitch-Screen: Ticket Detail View — 934e3eb46764456195f0eb627e67a363. Zugriff über Stitch-MCP, Projekt 9754281177402648015.
|
||||
|
||||
## Implementierungsweg
|
||||
|
||||
1. Formulare für vorhandene Ticketattribute neu erstellen.
|
||||
2. Gemeinsame Feldkomponenten für Anlage und Bearbeitung verwenden.
|
||||
3. Projektkonfigurierte Felder, Autor-/Story-Point-Schalter und Validierung erhalten.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] Anlage und Bearbeitung speichern dieselben Attribute wie bisher.
|
||||
- [ ] Fehler und ungespeicherte Änderungen sind abgedeckt.
|
||||
- [ ] Gesperrte Aktionen bleiben serverseitig abgesichert.
|
||||
|
||||
## Prüfung
|
||||
|
||||
Vorhandene gezielte Abdeckung: [tests/Feature/Projects/ProjectTicketControllerTest.php](../../tests/Feature/Projects/ProjectTicketControllerTest.php). Nur bei Berührung des jeweiligen Verhaltens aktualisieren bzw. ausführen.
|
||||
|
||||
Gezielte Verhaltenstests für die Änderung; betroffene PHPUnit-Featuretests bei Controller-/Resource-/Route-Anpassungen. Oberfläche mit Tastatur und bei mobiler Breite in beiden Themes prüfen. Projektvorgaben für Lint/Build und bei Vue/TypeScript abschließend `npm run types:check` erfüllen. Keine ausschließlich optischen Momentaufnahmen als Ersatz für Verhaltenstests.
|
||||
|
||||
## Grenze der Agentensitzung
|
||||
|
||||
Nur dieses Ticket und bereits erfüllte Abhängigkeiten bearbeiten. Ein zusammenhängendes Ergebnis mit passenden Tests liefern; keine Nachbartickets mitziehen. Die Aufteilung zielt auf eine begrenzte Sitzung mit Reserve für Prüfung, garantiert aber kein festes Zeit- oder Tokenbudget. Wenn unerwartete Voraussetzungen den Umfang wesentlich vergrößern, rechtzeitig den Stand, betroffene Dateien, bereits ausgeführte Prüfungen und den konkret nötigen Folgeschritt festhalten.
|
||||
@@ -1,38 +0,0 @@
|
||||
# UI Rework 17: Akzeptanzkriterien-Checkliste
|
||||
|
||||
Epic: [Epic 1](epic-1.md)
|
||||
|
||||
Abhängigkeiten: [Issue 16](epic-1-issue-16.md)
|
||||
|
||||
## Ziel und Grenzen
|
||||
|
||||
Akzeptanzkriterien-Checkliste als abgegrenzten Teil des vollständigen Frontend-Neubaus umsetzen. Grundlage: Vue 3/Inertia 2 mit Laravel, Tauri 2 für Apps, gemeinsames Repository, servergeladene Oberfläche, HTTPS, Hell/Dunkel/System und englische Texte über Übersetzungsschlüssel. Bestehende Fachlogik bleibt maßgeblich; Resources und Routen nur soweit für diese Oberfläche erforderlich anpassen. Keine zusätzlichen Produktmodule.
|
||||
|
||||
## Einstieg
|
||||
|
||||
Gezielt prüfen: [resources/js/lib/acceptance-criteria.ts](../../resources/js/lib/acceptance-criteria.ts); [resources/js/types/models/acceptance-criteria-model.ts](../../resources/js/types/models/acceptance-criteria-model.ts); [resources/js/pages/projects/Ticket.vue](../../resources/js/pages/projects/Ticket.vue). Die Pfade sind Einstiegspunkte, keine Erlaubnis zu fachlichen Umbauten. [AGENTS.md](../../AGENTS.md) und die für den tatsächlich geänderten Bereich geltenden Skills beachten. Vor Codeänderungen die zur installierten Version passende Dokumentation prüfen.
|
||||
|
||||
Design: [Fluid Emerald Kanban](https://stitch.withgoogle.com/projects/9754281177402648015)
|
||||
Relevanter Stitch-Screen: Ticket Detail View — 934e3eb46764456195f0eb627e67a363. Zugriff über Stitch-MCP, Projekt 9754281177402648015.
|
||||
|
||||
## Implementierungsweg
|
||||
|
||||
1. Bestehende hierarchische Checkliste als neue Komponente aufbauen.
|
||||
2. Hinzufügen, Bearbeiten, Verschachteln und Abhaken entsprechend dem Bestand erhalten.
|
||||
3. In Anlage und Detail integrieren.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] Hierarchie und Reihenfolge bleiben beim Speichern erhalten.
|
||||
- [ ] Bestehende Kriterien gehen nicht verloren.
|
||||
- [ ] Touch und Tastatur funktionieren.
|
||||
|
||||
## Prüfung
|
||||
|
||||
Vorhandene gezielte Abdeckung: [tests/Feature/Projects/ProjectTicketControllerTest.php](../../tests/Feature/Projects/ProjectTicketControllerTest.php). Nur bei Berührung des jeweiligen Verhaltens aktualisieren bzw. ausführen.
|
||||
|
||||
Gezielte Verhaltenstests für die Änderung; betroffene PHPUnit-Featuretests bei Controller-/Resource-/Route-Anpassungen. Oberfläche mit Tastatur und bei mobiler Breite in beiden Themes prüfen. Projektvorgaben für Lint/Build und bei Vue/TypeScript abschließend `npm run types:check` erfüllen. Keine ausschließlich optischen Momentaufnahmen als Ersatz für Verhaltenstests.
|
||||
|
||||
## Grenze der Agentensitzung
|
||||
|
||||
Nur dieses Ticket und bereits erfüllte Abhängigkeiten bearbeiten. Ein zusammenhängendes Ergebnis mit passenden Tests liefern; keine Nachbartickets mitziehen. Die Aufteilung zielt auf eine begrenzte Sitzung mit Reserve für Prüfung, garantiert aber kein festes Zeit- oder Tokenbudget. Wenn unerwartete Voraussetzungen den Umfang wesentlich vergrößern, rechtzeitig den Stand, betroffene Dateien, bereits ausgeführte Prüfungen und den konkret nötigen Folgeschritt festhalten.
|
||||
@@ -1,37 +0,0 @@
|
||||
# UI Rework 18: Ticketbeziehungen und Hierarchie
|
||||
|
||||
Epic: [Epic 1](epic-1.md)
|
||||
|
||||
Abhängigkeiten: [Issue 15](epic-1-issue-15.md), [Issue 16](epic-1-issue-16.md)
|
||||
|
||||
## Ziel und Grenzen
|
||||
|
||||
Ticketbeziehungen und Hierarchie als abgegrenzten Teil des vollständigen Frontend-Neubaus umsetzen. Grundlage: Vue 3/Inertia 2 mit Laravel, Tauri 2 für Apps, gemeinsames Repository, servergeladene Oberfläche, HTTPS, Hell/Dunkel/System und englische Texte über Übersetzungsschlüssel. Bestehende Fachlogik bleibt maßgeblich; Resources und Routen nur soweit für diese Oberfläche erforderlich anpassen. Keine zusätzlichen Produktmodule.
|
||||
|
||||
## Einstieg
|
||||
|
||||
Gezielt prüfen: [resources/js/pages/projects/Ticket.vue](../../resources/js/pages/projects/Ticket.vue); [app/Http/Controllers/ProjectTicketLinkController.php](../../app/Http/Controllers/ProjectTicketLinkController.php); [resources/js/components/TicketTreeList.vue](../../resources/js/components/TicketTreeList.vue). Die Pfade sind Einstiegspunkte, keine Erlaubnis zu fachlichen Umbauten. [AGENTS.md](../../AGENTS.md) und die für den tatsächlich geänderten Bereich geltenden Skills beachten. Vor Codeänderungen die zur installierten Version passende Dokumentation prüfen.
|
||||
|
||||
Design: [Fluid Emerald Kanban](https://stitch.withgoogle.com/projects/9754281177402648015)
|
||||
|
||||
## Implementierungsweg
|
||||
|
||||
1. Eltern-/Kindanzeige und Ticketverknüpfungen neu gestalten.
|
||||
2. Bestehende Linktypen und Richtung verwenden.
|
||||
3. Erlaubte Hinzufüge-/Entfernaktionen integrieren.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] Bestehende Hierarchievalidierung bleibt wirksam.
|
||||
- [ ] Linkrichtung wird korrekt angezeigt.
|
||||
- [ ] Kein neuer Beziehungstyp wird eingeführt.
|
||||
|
||||
## Prüfung
|
||||
|
||||
Vorhandene gezielte Abdeckung: [tests/Feature/Projects/ProjectTicketLinkControllerTest.php](../../tests/Feature/Projects/ProjectTicketLinkControllerTest.php), [tests/Feature/Projects/ProjectTicketControllerTest.php](../../tests/Feature/Projects/ProjectTicketControllerTest.php). Nur bei Berührung des jeweiligen Verhaltens aktualisieren bzw. ausführen.
|
||||
|
||||
Gezielte Verhaltenstests für die Änderung; betroffene PHPUnit-Featuretests bei Controller-/Resource-/Route-Anpassungen. Oberfläche mit Tastatur und bei mobiler Breite in beiden Themes prüfen. Projektvorgaben für Lint/Build und bei Vue/TypeScript abschließend `npm run types:check` erfüllen. Keine ausschließlich optischen Momentaufnahmen als Ersatz für Verhaltenstests.
|
||||
|
||||
## Grenze der Agentensitzung
|
||||
|
||||
Nur dieses Ticket und bereits erfüllte Abhängigkeiten bearbeiten. Ein zusammenhängendes Ergebnis mit passenden Tests liefern; keine Nachbartickets mitziehen. Die Aufteilung zielt auf eine begrenzte Sitzung mit Reserve für Prüfung, garantiert aber kein festes Zeit- oder Tokenbudget. Wenn unerwartete Voraussetzungen den Umfang wesentlich vergrößern, rechtzeitig den Stand, betroffene Dateien, bereits ausgeführte Prüfungen und den konkret nötigen Folgeschritt festhalten.
|
||||
@@ -1,37 +0,0 @@
|
||||
# UI Rework 19: Blocker und Ticketänderungshistorie
|
||||
|
||||
Epic: [Epic 1](epic-1.md)
|
||||
|
||||
Abhängigkeiten: [Issue 15](epic-1-issue-15.md), [Issue 16](epic-1-issue-16.md)
|
||||
|
||||
## Ziel und Grenzen
|
||||
|
||||
Blocker und Ticketänderungshistorie als abgegrenzten Teil des vollständigen Frontend-Neubaus umsetzen. Grundlage: Vue 3/Inertia 2 mit Laravel, Tauri 2 für Apps, gemeinsames Repository, servergeladene Oberfläche, HTTPS, Hell/Dunkel/System und englische Texte über Übersetzungsschlüssel. Bestehende Fachlogik bleibt maßgeblich; Resources und Routen nur soweit für diese Oberfläche erforderlich anpassen. Keine zusätzlichen Produktmodule.
|
||||
|
||||
## Einstieg
|
||||
|
||||
Gezielt prüfen: [resources/js/pages/projects/Ticket.vue](../../resources/js/pages/projects/Ticket.vue); [resources/js/types/models/ticket-worklog-entry-model.ts](../../resources/js/types/models/ticket-worklog-entry-model.ts). Die Pfade sind Einstiegspunkte, keine Erlaubnis zu fachlichen Umbauten. [AGENTS.md](../../AGENTS.md) und die für den tatsächlich geänderten Bereich geltenden Skills beachten. Vor Codeänderungen die zur installierten Version passende Dokumentation prüfen.
|
||||
|
||||
Design: [Fluid Emerald Kanban](https://stitch.withgoogle.com/projects/9754281177402648015)
|
||||
|
||||
## Implementierungsweg
|
||||
|
||||
1. Blockieren/Entblockieren und Kommentarerfassung neu darstellen.
|
||||
2. Vorhandenes Worklog als lesbare Historie integrieren.
|
||||
3. Mobile Darstellung und lokale Datumsformatierung nutzen.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] Blockieren erfordert weiterhin Kommentar.
|
||||
- [ ] Historie zeigt echte Ereignisse und Akteure.
|
||||
- [ ] Keine neue Kommentar- oder Aktivitätsfunktion.
|
||||
|
||||
## Prüfung
|
||||
|
||||
Vorhandene gezielte Abdeckung: [tests/Feature/Projects/ProjectTicketControllerTest.php](../../tests/Feature/Projects/ProjectTicketControllerTest.php). Nur bei Berührung des jeweiligen Verhaltens aktualisieren bzw. ausführen.
|
||||
|
||||
Gezielte Verhaltenstests für die Änderung; betroffene PHPUnit-Featuretests bei Controller-/Resource-/Route-Anpassungen. Oberfläche mit Tastatur und bei mobiler Breite in beiden Themes prüfen. Projektvorgaben für Lint/Build und bei Vue/TypeScript abschließend `npm run types:check` erfüllen. Keine ausschließlich optischen Momentaufnahmen als Ersatz für Verhaltenstests.
|
||||
|
||||
## Grenze der Agentensitzung
|
||||
|
||||
Nur dieses Ticket und bereits erfüllte Abhängigkeiten bearbeiten. Ein zusammenhängendes Ergebnis mit passenden Tests liefern; keine Nachbartickets mitziehen. Die Aufteilung zielt auf eine begrenzte Sitzung mit Reserve für Prüfung, garantiert aber kein festes Zeit- oder Tokenbudget. Wenn unerwartete Voraussetzungen den Umfang wesentlich vergrößern, rechtzeitig den Stand, betroffene Dateien, bereits ausgeführte Prüfungen und den konkret nötigen Folgeschritt festhalten.
|
||||
@@ -1,36 +0,0 @@
|
||||
# UI Rework 2: Emerald-Designsystem und Hell/Dunkel/System
|
||||
|
||||
Epic: [Epic 1](epic-1.md)
|
||||
|
||||
Abhängigkeiten: [Issue 1](epic-1-issue-1.md)
|
||||
|
||||
## Ziel und Grenzen
|
||||
|
||||
Emerald-Designsystem und Hell/Dunkel/System als abgegrenzten Teil des vollständigen Frontend-Neubaus umsetzen. Grundlage: Vue 3/Inertia 2 mit Laravel, Tauri 2 für Apps, gemeinsames Repository, servergeladene Oberfläche, HTTPS, Hell/Dunkel/System und englische Texte über Übersetzungsschlüssel. Bestehende Fachlogik bleibt maßgeblich; Resources und Routen nur soweit für diese Oberfläche erforderlich anpassen. Keine zusätzlichen Produktmodule.
|
||||
|
||||
## Einstieg
|
||||
|
||||
Gezielt prüfen: [resources/css/app.css](../../resources/css/app.css); [resources/js/composables/useAppearance.ts](../../resources/js/composables/useAppearance.ts). Die Pfade sind Einstiegspunkte, keine Erlaubnis zu fachlichen Umbauten. [AGENTS.md](../../AGENTS.md) und die für den tatsächlich geänderten Bereich geltenden Skills beachten. Vor Codeänderungen die zur installierten Version passende Dokumentation prüfen.
|
||||
|
||||
Design: [Fluid Emerald Kanban](https://stitch.withgoogle.com/projects/9754281177402648015)
|
||||
|
||||
## Implementierungsweg
|
||||
|
||||
1. Farben, Typografie, Abstände, Radien und Fokuszustände aus Stitch als semantische Tokens definieren.
|
||||
2. Inter lokal ausliefern.
|
||||
3. Hell/Dunkel/System einschließlich initialem Laden umsetzen.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] Beide Themes sind lesbar.
|
||||
- [ ] Systemwechsel aktualisieren die Ansicht.
|
||||
- [ ] Reduzierte Bewegung wird berücksichtigt.
|
||||
- [ ] Keine Starterkit-Optik.
|
||||
|
||||
## Prüfung
|
||||
|
||||
Gezielte Verhaltenstests für die Änderung; betroffene PHPUnit-Featuretests bei Controller-/Resource-/Route-Anpassungen. Oberfläche mit Tastatur und bei mobiler Breite in beiden Themes prüfen. Projektvorgaben für Lint/Build und bei Vue/TypeScript abschließend `npm run types:check` erfüllen. Keine ausschließlich optischen Momentaufnahmen als Ersatz für Verhaltenstests.
|
||||
|
||||
## Grenze der Agentensitzung
|
||||
|
||||
Nur dieses Ticket und bereits erfüllte Abhängigkeiten bearbeiten. Ein zusammenhängendes Ergebnis mit passenden Tests liefern; keine Nachbartickets mitziehen. Die Aufteilung zielt auf eine begrenzte Sitzung mit Reserve für Prüfung, garantiert aber kein festes Zeit- oder Tokenbudget. Wenn unerwartete Voraussetzungen den Umfang wesentlich vergrößern, rechtzeitig den Stand, betroffene Dateien, bereits ausgeführte Prüfungen und den konkret nötigen Folgeschritt festhalten.
|
||||
@@ -1,38 +0,0 @@
|
||||
# UI Rework 20: Git-Bereich und Branch-Erstellung am Ticket
|
||||
|
||||
Epic: [Epic 1](epic-1.md)
|
||||
|
||||
Abhängigkeiten: [Issue 5](epic-1-issue-5.md), [Issue 15](epic-1-issue-15.md)
|
||||
|
||||
## Ziel und Grenzen
|
||||
|
||||
Git-Bereich und Branch-Erstellung am Ticket als abgegrenzten Teil des vollständigen Frontend-Neubaus umsetzen. Grundlage: Vue 3/Inertia 2 mit Laravel, Tauri 2 für Apps, gemeinsames Repository, servergeladene Oberfläche, HTTPS, Hell/Dunkel/System und englische Texte über Übersetzungsschlüssel. Bestehende Fachlogik bleibt maßgeblich; Resources und Routen nur soweit für diese Oberfläche erforderlich anpassen. Keine zusätzlichen Produktmodule.
|
||||
|
||||
## Einstieg
|
||||
|
||||
Gezielt prüfen: [resources/js/pages/projects/Ticket.vue](../../resources/js/pages/projects/Ticket.vue); [app/Http/Controllers/ProjectTicketBranchController.php](../../app/Http/Controllers/ProjectTicketBranchController.php); [resources/js/types/models/ticket-repository-branch-model.ts](../../resources/js/types/models/ticket-repository-branch-model.ts). Die Pfade sind Einstiegspunkte, keine Erlaubnis zu fachlichen Umbauten. [AGENTS.md](../../AGENTS.md) und die für den tatsächlich geänderten Bereich geltenden Skills beachten. Vor Codeänderungen die zur installierten Version passende Dokumentation prüfen.
|
||||
|
||||
Design: [Fluid Emerald Kanban](https://stitch.withgoogle.com/projects/9754281177402648015)
|
||||
|
||||
## Implementierungsweg
|
||||
|
||||
1. Branch-/Pull-Request-Anzeigen sowie bestehenden Branch-Erstellablauf in eigene neue Komponenten überführen.
|
||||
2. Repositoryauswahl, Namensvorschlag und vorhandene Prüfungen erhalten.
|
||||
3. Laufende Anfragen, fehlenden Git-Zugriff und Providerfehler als eigene Zustände abbilden. Den Weg zu vorhandenen Verbindungseinstellungen anbieten, Formulareingaben bei Fehlern erhalten und externe Git-Links entsprechend kennzeichnen.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] Fehlende Git-Verbindung und Providerfehler sind verständlich.
|
||||
- [ ] Branch-Erstellung funktioniert.
|
||||
- [ ] Fremde Git-Links öffnen extern.
|
||||
- [ ] Bestehende Zuordnungsregeln bleiben erhalten.
|
||||
|
||||
## Prüfung
|
||||
|
||||
Vorhandene gezielte Abdeckung: [tests/Feature/Projects/ProjectTicketBranchControllerTest.php](../../tests/Feature/Projects/ProjectTicketBranchControllerTest.php). Nur bei Berührung des jeweiligen Verhaltens aktualisieren bzw. ausführen.
|
||||
|
||||
Gezielte Verhaltenstests für die Änderung; betroffene PHPUnit-Featuretests bei Controller-/Resource-/Route-Anpassungen. Oberfläche mit Tastatur und bei mobiler Breite in beiden Themes prüfen. Projektvorgaben für Lint/Build und bei Vue/TypeScript abschließend `npm run types:check` erfüllen. Keine ausschließlich optischen Momentaufnahmen als Ersatz für Verhaltenstests.
|
||||
|
||||
## Grenze der Agentensitzung
|
||||
|
||||
Nur dieses Ticket und bereits erfüllte Abhängigkeiten bearbeiten. Ein zusammenhängendes Ergebnis mit passenden Tests liefern; keine Nachbartickets mitziehen. Die Aufteilung zielt auf eine begrenzte Sitzung mit Reserve für Prüfung, garantiert aber kein festes Zeit- oder Tokenbudget. Wenn unerwartete Voraussetzungen den Umfang wesentlich vergrößern, rechtzeitig den Stand, betroffene Dateien, bereits ausgeführte Prüfungen und den konkret nötigen Folgeschritt festhalten.
|
||||
@@ -1,37 +0,0 @@
|
||||
# UI Rework 21: Profil und Erscheinungsbild
|
||||
|
||||
Epic: [Epic 1](epic-1.md)
|
||||
|
||||
Abhängigkeiten: [Issue 4](epic-1-issue-4.md), [Issue 5](epic-1-issue-5.md)
|
||||
|
||||
## Ziel und Grenzen
|
||||
|
||||
Profil und Erscheinungsbild als abgegrenzten Teil des vollständigen Frontend-Neubaus umsetzen. Grundlage: Vue 3/Inertia 2 mit Laravel, Tauri 2 für Apps, gemeinsames Repository, servergeladene Oberfläche, HTTPS, Hell/Dunkel/System und englische Texte über Übersetzungsschlüssel. Bestehende Fachlogik bleibt maßgeblich; Resources und Routen nur soweit für diese Oberfläche erforderlich anpassen. Keine zusätzlichen Produktmodule.
|
||||
|
||||
## Einstieg
|
||||
|
||||
Gezielt prüfen: [resources/js/pages/settings/Profile.vue](../../resources/js/pages/settings/Profile.vue); [resources/js/pages/settings/Appearance.vue](../../resources/js/pages/settings/Appearance.vue). Die Pfade sind Einstiegspunkte, keine Erlaubnis zu fachlichen Umbauten. [AGENTS.md](../../AGENTS.md) und die für den tatsächlich geänderten Bereich geltenden Skills beachten. Vor Codeänderungen die zur installierten Version passende Dokumentation prüfen.
|
||||
|
||||
Design: [Fluid Emerald Kanban](https://stitch.withgoogle.com/projects/9754281177402648015)
|
||||
|
||||
## Implementierungsweg
|
||||
|
||||
1. Profilfelder, Avatar-Upload/Entfernen und Erscheinungsbild neu gestalten.
|
||||
2. Gemeinsame Einstellungsnavigation aufbauen.
|
||||
3. Datenschutz durch bestehende Rechte und Endpunkte erhalten.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] Avatar und Profiländerungen funktionieren.
|
||||
- [ ] Hell/Dunkel/System bleibt nach Neustart erhalten.
|
||||
- [ ] Uploadfehler und mobile Dateiauswahl sind berücksichtigt.
|
||||
|
||||
## Prüfung
|
||||
|
||||
Vorhandene gezielte Abdeckung: [tests/Feature/Settings/ProfileUpdateTest.php](../../tests/Feature/Settings/ProfileUpdateTest.php). Nur bei Berührung des jeweiligen Verhaltens aktualisieren bzw. ausführen.
|
||||
|
||||
Gezielte Verhaltenstests für die Änderung; betroffene PHPUnit-Featuretests bei Controller-/Resource-/Route-Anpassungen. Oberfläche mit Tastatur und bei mobiler Breite in beiden Themes prüfen. Projektvorgaben für Lint/Build und bei Vue/TypeScript abschließend `npm run types:check` erfüllen. Keine ausschließlich optischen Momentaufnahmen als Ersatz für Verhaltenstests.
|
||||
|
||||
## Grenze der Agentensitzung
|
||||
|
||||
Nur dieses Ticket und bereits erfüllte Abhängigkeiten bearbeiten. Ein zusammenhängendes Ergebnis mit passenden Tests liefern; keine Nachbartickets mitziehen. Die Aufteilung zielt auf eine begrenzte Sitzung mit Reserve für Prüfung, garantiert aber kein festes Zeit- oder Tokenbudget. Wenn unerwartete Voraussetzungen den Umfang wesentlich vergrößern, rechtzeitig den Stand, betroffene Dateien, bereits ausgeführte Prüfungen und den konkret nötigen Folgeschritt festhalten.
|
||||
@@ -1,37 +0,0 @@
|
||||
# UI Rework 22: Sicherheit und Anwendungsschlüssel
|
||||
|
||||
Epic: [Epic 1](epic-1.md)
|
||||
|
||||
Abhängigkeiten: [Issue 6](epic-1-issue-6.md), [Issue 7](epic-1-issue-7.md), [Issue 21](epic-1-issue-21.md)
|
||||
|
||||
## Ziel und Grenzen
|
||||
|
||||
Sicherheit und Anwendungsschlüssel als abgegrenzten Teil des vollständigen Frontend-Neubaus umsetzen. Grundlage: Vue 3/Inertia 2 mit Laravel, Tauri 2 für Apps, gemeinsames Repository, servergeladene Oberfläche, HTTPS, Hell/Dunkel/System und englische Texte über Übersetzungsschlüssel. Bestehende Fachlogik bleibt maßgeblich; Resources und Routen nur soweit für diese Oberfläche erforderlich anpassen. Keine zusätzlichen Produktmodule.
|
||||
|
||||
## Einstieg
|
||||
|
||||
Gezielt prüfen: [resources/js/pages/settings/Security.vue](../../resources/js/pages/settings/Security.vue); [resources/js/pages/settings/Applications.vue](../../resources/js/pages/settings/Applications.vue); [resources/js/components/TwoFactorSetupModal.vue](../../resources/js/components/TwoFactorSetupModal.vue); [resources/js/components/TwoFactorRecoveryCodes.vue](../../resources/js/components/TwoFactorRecoveryCodes.vue). Die Pfade sind Einstiegspunkte, keine Erlaubnis zu fachlichen Umbauten. [AGENTS.md](../../AGENTS.md) und die für den tatsächlich geänderten Bereich geltenden Skills beachten. Vor Codeänderungen die zur installierten Version passende Dokumentation prüfen.
|
||||
|
||||
Design: [Fluid Emerald Kanban](https://stitch.withgoogle.com/projects/9754281177402648015)
|
||||
|
||||
## Implementierungsweg
|
||||
|
||||
1. Passwortverwaltung, 2FA-Einrichtung, Recovery-Codes und vorhandene Anwendungsschlüssel neu darstellen.
|
||||
2. Sensible Bestätigungsabläufe erhalten.
|
||||
3. Zustände vor und nach 2FA-Bestätigung sowie beim Anzeigen und Erneuern von Recovery-Codes anhand der bestehenden Endpunkte abbilden. Schlüsselanzeige und Kopierfunktion an die vorhandene Freigabelogik binden; fehlgeschlagene Bestätigungen direkt am Formular erklären.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] 2FA-Aktivieren/Deaktivieren und Recovery-Codes funktionieren.
|
||||
- [ ] Schlüssel werden weiterhin nur nach bestehender Logik angezeigt.
|
||||
- [ ] Abbruch und Fehler sind sicher.
|
||||
|
||||
## Prüfung
|
||||
|
||||
Vorhandene gezielte Abdeckung: [tests/Feature/Settings/SecurityTest.php](../../tests/Feature/Settings/SecurityTest.php), [tests/Feature/Settings/UserApplicationTest.php](../../tests/Feature/Settings/UserApplicationTest.php). Nur bei Berührung des jeweiligen Verhaltens aktualisieren bzw. ausführen.
|
||||
|
||||
Gezielte Verhaltenstests für die Änderung; betroffene PHPUnit-Featuretests bei Controller-/Resource-/Route-Anpassungen. Oberfläche mit Tastatur und bei mobiler Breite in beiden Themes prüfen. Projektvorgaben für Lint/Build und bei Vue/TypeScript abschließend `npm run types:check` erfüllen. Keine ausschließlich optischen Momentaufnahmen als Ersatz für Verhaltenstests.
|
||||
|
||||
## Grenze der Agentensitzung
|
||||
|
||||
Nur dieses Ticket und bereits erfüllte Abhängigkeiten bearbeiten. Ein zusammenhängendes Ergebnis mit passenden Tests liefern; keine Nachbartickets mitziehen. Die Aufteilung zielt auf eine begrenzte Sitzung mit Reserve für Prüfung, garantiert aber kein festes Zeit- oder Tokenbudget. Wenn unerwartete Voraussetzungen den Umfang wesentlich vergrößern, rechtzeitig den Stand, betroffene Dateien, bereits ausgeführte Prüfungen und den konkret nötigen Folgeschritt festhalten.
|
||||
@@ -1,37 +0,0 @@
|
||||
# UI Rework 23: Persönliche Git-Verbindungen
|
||||
|
||||
Epic: [Epic 1](epic-1.md)
|
||||
|
||||
Abhängigkeiten: [Issue 21](epic-1-issue-21.md)
|
||||
|
||||
## Ziel und Grenzen
|
||||
|
||||
Persönliche Git-Verbindungen als abgegrenzten Teil des vollständigen Frontend-Neubaus umsetzen. Grundlage: Vue 3/Inertia 2 mit Laravel, Tauri 2 für Apps, gemeinsames Repository, servergeladene Oberfläche, HTTPS, Hell/Dunkel/System und englische Texte über Übersetzungsschlüssel. Bestehende Fachlogik bleibt maßgeblich; Resources und Routen nur soweit für diese Oberfläche erforderlich anpassen. Keine zusätzlichen Produktmodule.
|
||||
|
||||
## Einstieg
|
||||
|
||||
Gezielt prüfen: [resources/js/pages/settings/GitConnections.vue](../../resources/js/pages/settings/GitConnections.vue); [app/Http/Controllers/Settings/GitConnectionController.php](../../app/Http/Controllers/Settings/GitConnectionController.php). Die Pfade sind Einstiegspunkte, keine Erlaubnis zu fachlichen Umbauten. [AGENTS.md](../../AGENTS.md) und die für den tatsächlich geänderten Bereich geltenden Skills beachten. Vor Codeänderungen die zur installierten Version passende Dokumentation prüfen.
|
||||
|
||||
Design: [Fluid Emerald Kanban](https://stitch.withgoogle.com/projects/9754281177402648015)
|
||||
|
||||
## Implementierungsweg
|
||||
|
||||
1. Vorhandene Git-Verbindungen und Formulare neu gestalten.
|
||||
2. Provider- und Serverangaben klar darstellen.
|
||||
3. Validierungs- und Providerfehler in das neue Rückmeldesystem integrieren.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] Erstellen, Bearbeiten und Entfernen bleiben möglich.
|
||||
- [ ] Zugangsdaten werden nicht zusätzlich offengelegt.
|
||||
- [ ] Leere Listen und fehlende Berechtigungen funktionieren.
|
||||
|
||||
## Prüfung
|
||||
|
||||
Vorhandene gezielte Abdeckung: [tests/Feature/Settings/GitConnectionTest.php](../../tests/Feature/Settings/GitConnectionTest.php). Nur bei Berührung des jeweiligen Verhaltens aktualisieren bzw. ausführen.
|
||||
|
||||
Gezielte Verhaltenstests für die Änderung; betroffene PHPUnit-Featuretests bei Controller-/Resource-/Route-Anpassungen. Oberfläche mit Tastatur und bei mobiler Breite in beiden Themes prüfen. Projektvorgaben für Lint/Build und bei Vue/TypeScript abschließend `npm run types:check` erfüllen. Keine ausschließlich optischen Momentaufnahmen als Ersatz für Verhaltenstests.
|
||||
|
||||
## Grenze der Agentensitzung
|
||||
|
||||
Nur dieses Ticket und bereits erfüllte Abhängigkeiten bearbeiten. Ein zusammenhängendes Ergebnis mit passenden Tests liefern; keine Nachbartickets mitziehen. Die Aufteilung zielt auf eine begrenzte Sitzung mit Reserve für Prüfung, garantiert aber kein festes Zeit- oder Tokenbudget. Wenn unerwartete Voraussetzungen den Umfang wesentlich vergrößern, rechtzeitig den Stand, betroffene Dateien, bereits ausgeführte Prüfungen und den konkret nötigen Folgeschritt festhalten.
|
||||
@@ -1,37 +0,0 @@
|
||||
# UI Rework 24: Allgemeine Projekteinstellungen und Vererbung
|
||||
|
||||
Epic: [Epic 1](epic-1.md)
|
||||
|
||||
Abhängigkeiten: [Issue 9](epic-1-issue-9.md), [Issue 5](epic-1-issue-5.md)
|
||||
|
||||
## Ziel und Grenzen
|
||||
|
||||
Allgemeine Projekteinstellungen und Vererbung als abgegrenzten Teil des vollständigen Frontend-Neubaus umsetzen. Grundlage: Vue 3/Inertia 2 mit Laravel, Tauri 2 für Apps, gemeinsames Repository, servergeladene Oberfläche, HTTPS, Hell/Dunkel/System und englische Texte über Übersetzungsschlüssel. Bestehende Fachlogik bleibt maßgeblich; Resources und Routen nur soweit für diese Oberfläche erforderlich anpassen. Keine zusätzlichen Produktmodule.
|
||||
|
||||
## Einstieg
|
||||
|
||||
Gezielt prüfen: [resources/js/pages/projects/settings/General.vue](../../resources/js/pages/projects/settings/General.vue); [resources/js/components/projects/TemplateInheritancePanel.vue](../../resources/js/components/projects/TemplateInheritancePanel.vue); [app/Http/Controllers/ProjectController.php](../../app/Http/Controllers/ProjectController.php). Die Pfade sind Einstiegspunkte, keine Erlaubnis zu fachlichen Umbauten. [AGENTS.md](../../AGENTS.md) und die für den tatsächlich geänderten Bereich geltenden Skills beachten. Vor Codeänderungen die zur installierten Version passende Dokumentation prüfen.
|
||||
|
||||
Design: [Fluid Emerald Kanban](https://stitch.withgoogle.com/projects/9754281177402648015)
|
||||
|
||||
## Implementierungsweg
|
||||
|
||||
1. Einstellungsnavigation und allgemeine Projektseite neu aufbauen.
|
||||
2. Vererbung und lokale Overrides nachvollziehbar anzeigen.
|
||||
3. Bestehende Vorlagenwechsel-/Reset- und Löschbestätigungen erhalten.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] Geerbte und lokale Werte sind unterscheidbar.
|
||||
- [ ] Destruktive Aktionen erfordern bestehende Bestätigung.
|
||||
- [ ] Keine neue Vorlagenlogik.
|
||||
|
||||
## Prüfung
|
||||
|
||||
Vorhandene gezielte Abdeckung: [tests/Feature/Projects/ProjectTemplateOverrideTest.php](../../tests/Feature/Projects/ProjectTemplateOverrideTest.php), [tests/Feature/Projects/ProjectControllerTest.php](../../tests/Feature/Projects/ProjectControllerTest.php). Nur bei Berührung des jeweiligen Verhaltens aktualisieren bzw. ausführen.
|
||||
|
||||
Gezielte Verhaltenstests für die Änderung; betroffene PHPUnit-Featuretests bei Controller-/Resource-/Route-Anpassungen. Oberfläche mit Tastatur und bei mobiler Breite in beiden Themes prüfen. Projektvorgaben für Lint/Build und bei Vue/TypeScript abschließend `npm run types:check` erfüllen. Keine ausschließlich optischen Momentaufnahmen als Ersatz für Verhaltenstests.
|
||||
|
||||
## Grenze der Agentensitzung
|
||||
|
||||
Nur dieses Ticket und bereits erfüllte Abhängigkeiten bearbeiten. Ein zusammenhängendes Ergebnis mit passenden Tests liefern; keine Nachbartickets mitziehen. Die Aufteilung zielt auf eine begrenzte Sitzung mit Reserve für Prüfung, garantiert aber kein festes Zeit- oder Tokenbudget. Wenn unerwartete Voraussetzungen den Umfang wesentlich vergrößern, rechtzeitig den Stand, betroffene Dateien, bereits ausgeführte Prüfungen und den konkret nötigen Folgeschritt festhalten.
|
||||
@@ -1,37 +0,0 @@
|
||||
# UI Rework 25: Workflow-Einstellungen für Spalten und Status
|
||||
|
||||
Epic: [Epic 1](epic-1.md)
|
||||
|
||||
Abhängigkeiten: [Issue 24](epic-1-issue-24.md)
|
||||
|
||||
## Ziel und Grenzen
|
||||
|
||||
Workflow-Einstellungen für Spalten und Status als abgegrenzten Teil des vollständigen Frontend-Neubaus umsetzen. Grundlage: Vue 3/Inertia 2 mit Laravel, Tauri 2 für Apps, gemeinsames Repository, servergeladene Oberfläche, HTTPS, Hell/Dunkel/System und englische Texte über Übersetzungsschlüssel. Bestehende Fachlogik bleibt maßgeblich; Resources und Routen nur soweit für diese Oberfläche erforderlich anpassen. Keine zusätzlichen Produktmodule.
|
||||
|
||||
## Einstieg
|
||||
|
||||
Gezielt prüfen: [resources/js/pages/projects/settings/Board.vue](../../resources/js/pages/projects/settings/Board.vue); [app/Http/Controllers/ProjectBoardSettingsController.php](../../app/Http/Controllers/ProjectBoardSettingsController.php). Die Pfade sind Einstiegspunkte, keine Erlaubnis zu fachlichen Umbauten. [AGENTS.md](../../AGENTS.md) und die für den tatsächlich geänderten Bereich geltenden Skills beachten. Vor Codeänderungen die zur installierten Version passende Dokumentation prüfen.
|
||||
|
||||
Design: [Fluid Emerald Kanban](https://stitch.withgoogle.com/projects/9754281177402648015)
|
||||
|
||||
## Implementierungsweg
|
||||
|
||||
1. Spalten, Statuszuordnung, Namen und vorhandene Sortierung neu bearbeiten lassen.
|
||||
2. Backlogstatus und Löschbedingungen aus dem Bestand übernehmen.
|
||||
3. Übergangsmatrix als separate Komponente vorbereiten.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] Vorhandene CRUD-Aktionen funktionieren.
|
||||
- [ ] Gültige Positionen bleiben erhalten.
|
||||
- [ ] Genutzte Status und Spalten werden nicht unzulässig entfernt.
|
||||
|
||||
## Prüfung
|
||||
|
||||
Vorhandene gezielte Abdeckung: [tests/Feature/Projects/ProjectBoardSettingsControllerTest.php](../../tests/Feature/Projects/ProjectBoardSettingsControllerTest.php). Nur bei Berührung des jeweiligen Verhaltens aktualisieren bzw. ausführen.
|
||||
|
||||
Gezielte Verhaltenstests für die Änderung; betroffene PHPUnit-Featuretests bei Controller-/Resource-/Route-Anpassungen. Oberfläche mit Tastatur und bei mobiler Breite in beiden Themes prüfen. Projektvorgaben für Lint/Build und bei Vue/TypeScript abschließend `npm run types:check` erfüllen. Keine ausschließlich optischen Momentaufnahmen als Ersatz für Verhaltenstests.
|
||||
|
||||
## Grenze der Agentensitzung
|
||||
|
||||
Nur dieses Ticket und bereits erfüllte Abhängigkeiten bearbeiten. Ein zusammenhängendes Ergebnis mit passenden Tests liefern; keine Nachbartickets mitziehen. Die Aufteilung zielt auf eine begrenzte Sitzung mit Reserve für Prüfung, garantiert aber kein festes Zeit- oder Tokenbudget. Wenn unerwartete Voraussetzungen den Umfang wesentlich vergrößern, rechtzeitig den Stand, betroffene Dateien, bereits ausgeführte Prüfungen und den konkret nötigen Folgeschritt festhalten.
|
||||
@@ -1,37 +0,0 @@
|
||||
# UI Rework 26: Bearbeitung erlaubter Statusübergänge
|
||||
|
||||
Epic: [Epic 1](epic-1.md)
|
||||
|
||||
Abhängigkeiten: [Issue 25](epic-1-issue-25.md)
|
||||
|
||||
## Ziel und Grenzen
|
||||
|
||||
Bearbeitung erlaubter Statusübergänge als abgegrenzten Teil des vollständigen Frontend-Neubaus umsetzen. Grundlage: Vue 3/Inertia 2 mit Laravel, Tauri 2 für Apps, gemeinsames Repository, servergeladene Oberfläche, HTTPS, Hell/Dunkel/System und englische Texte über Übersetzungsschlüssel. Bestehende Fachlogik bleibt maßgeblich; Resources und Routen nur soweit für diese Oberfläche erforderlich anpassen. Keine zusätzlichen Produktmodule.
|
||||
|
||||
## Einstieg
|
||||
|
||||
Gezielt prüfen: [resources/js/pages/projects/settings/Board.vue](../../resources/js/pages/projects/settings/Board.vue); [app/Http/Controllers/ProjectBoardSettingsController.php](../../app/Http/Controllers/ProjectBoardSettingsController.php). Die Pfade sind Einstiegspunkte, keine Erlaubnis zu fachlichen Umbauten. [AGENTS.md](../../AGENTS.md) und die für den tatsächlich geänderten Bereich geltenden Skills beachten. Vor Codeänderungen die zur installierten Version passende Dokumentation prüfen.
|
||||
|
||||
Design: [Fluid Emerald Kanban](https://stitch.withgoogle.com/projects/9754281177402648015)
|
||||
|
||||
## Implementierungsweg
|
||||
|
||||
1. Vorhandene Übergangsregeln als übersichtliche, mobil bedienbare Oberfläche neu erstellen.
|
||||
2. Nur existierende Workflowfähigkeiten abbilden.
|
||||
3. Gespeicherten Zustand und Fehler klar anzeigen.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] Erlaubte und verbotene Übergänge werden korrekt gespeichert.
|
||||
- [ ] Board-Dropziele entsprechen der Konfiguration.
|
||||
- [ ] Keine neue Workflow-Engine.
|
||||
|
||||
## Prüfung
|
||||
|
||||
Vorhandene gezielte Abdeckung: [tests/Feature/Projects/ProjectBoardSettingsControllerTest.php](../../tests/Feature/Projects/ProjectBoardSettingsControllerTest.php), [tests/Feature/Projects/ProjectBoardControllerTest.php](../../tests/Feature/Projects/ProjectBoardControllerTest.php). Nur bei Berührung des jeweiligen Verhaltens aktualisieren bzw. ausführen.
|
||||
|
||||
Gezielte Verhaltenstests für die Änderung; betroffene PHPUnit-Featuretests bei Controller-/Resource-/Route-Anpassungen. Oberfläche mit Tastatur und bei mobiler Breite in beiden Themes prüfen. Projektvorgaben für Lint/Build und bei Vue/TypeScript abschließend `npm run types:check` erfüllen. Keine ausschließlich optischen Momentaufnahmen als Ersatz für Verhaltenstests.
|
||||
|
||||
## Grenze der Agentensitzung
|
||||
|
||||
Nur dieses Ticket und bereits erfüllte Abhängigkeiten bearbeiten. Ein zusammenhängendes Ergebnis mit passenden Tests liefern; keine Nachbartickets mitziehen. Die Aufteilung zielt auf eine begrenzte Sitzung mit Reserve für Prüfung, garantiert aber kein festes Zeit- oder Tokenbudget. Wenn unerwartete Voraussetzungen den Umfang wesentlich vergrößern, rechtzeitig den Stand, betroffene Dateien, bereits ausgeführte Prüfungen und den konkret nötigen Folgeschritt festhalten.
|
||||
@@ -1,37 +0,0 @@
|
||||
# UI Rework 27: Tickettypen, Prioritäten und Anzeigeoptionen
|
||||
|
||||
Epic: [Epic 1](epic-1.md)
|
||||
|
||||
Abhängigkeiten: [Issue 24](epic-1-issue-24.md)
|
||||
|
||||
## Ziel und Grenzen
|
||||
|
||||
Tickettypen, Prioritäten und Anzeigeoptionen als abgegrenzten Teil des vollständigen Frontend-Neubaus umsetzen. Grundlage: Vue 3/Inertia 2 mit Laravel, Tauri 2 für Apps, gemeinsames Repository, servergeladene Oberfläche, HTTPS, Hell/Dunkel/System und englische Texte über Übersetzungsschlüssel. Bestehende Fachlogik bleibt maßgeblich; Resources und Routen nur soweit für diese Oberfläche erforderlich anpassen. Keine zusätzlichen Produktmodule.
|
||||
|
||||
## Einstieg
|
||||
|
||||
Gezielt prüfen: [resources/js/pages/projects/settings/Metadata.vue](../../resources/js/pages/projects/settings/Metadata.vue); [app/Http/Controllers/ProjectTicketMetadataSettingsController.php](../../app/Http/Controllers/ProjectTicketMetadataSettingsController.php). Die Pfade sind Einstiegspunkte, keine Erlaubnis zu fachlichen Umbauten. [AGENTS.md](../../AGENTS.md) und die für den tatsächlich geänderten Bereich geltenden Skills beachten. Vor Codeänderungen die zur installierten Version passende Dokumentation prüfen.
|
||||
|
||||
Design: [Fluid Emerald Kanban](https://stitch.withgoogle.com/projects/9754281177402648015)
|
||||
|
||||
## Implementierungsweg
|
||||
|
||||
1. Typen, Prioritäten und bestehende Autor-/Story-Point-Optionen neu darstellen.
|
||||
2. Hinzufügen, Umbenennen, Reihenfolge und bestehende Ersetzungsregeln beim Löschen erhalten.
|
||||
3. Geschützte und bearbeitbare Einträge anhand vorhandener Berechtigungen unterscheiden. Vor dem Löschen die benötigte Ersatzzuordnung erklären und Validierungsfehler am betroffenen Eintrag anzeigen, ohne projektseitige Regeln zu verändern.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] Geschützte Typen bleiben geschützt.
|
||||
- [ ] Löschersatz und Reihenfolge funktionieren.
|
||||
- [ ] Inhalte bleiben projektkonfigurierbar.
|
||||
|
||||
## Prüfung
|
||||
|
||||
Vorhandene gezielte Abdeckung: [tests/Feature/Projects/ProjectTicketMetadataSettingsControllerTest.php](../../tests/Feature/Projects/ProjectTicketMetadataSettingsControllerTest.php). Nur bei Berührung des jeweiligen Verhaltens aktualisieren bzw. ausführen.
|
||||
|
||||
Gezielte Verhaltenstests für die Änderung; betroffene PHPUnit-Featuretests bei Controller-/Resource-/Route-Anpassungen. Oberfläche mit Tastatur und bei mobiler Breite in beiden Themes prüfen. Projektvorgaben für Lint/Build und bei Vue/TypeScript abschließend `npm run types:check` erfüllen. Keine ausschließlich optischen Momentaufnahmen als Ersatz für Verhaltenstests.
|
||||
|
||||
## Grenze der Agentensitzung
|
||||
|
||||
Nur dieses Ticket und bereits erfüllte Abhängigkeiten bearbeiten. Ein zusammenhängendes Ergebnis mit passenden Tests liefern; keine Nachbartickets mitziehen. Die Aufteilung zielt auf eine begrenzte Sitzung mit Reserve für Prüfung, garantiert aber kein festes Zeit- oder Tokenbudget. Wenn unerwartete Voraussetzungen den Umfang wesentlich vergrößern, rechtzeitig den Stand, betroffene Dateien, bereits ausgeführte Prüfungen und den konkret nötigen Folgeschritt festhalten.
|
||||
@@ -1,37 +0,0 @@
|
||||
# UI Rework 28: Benutzerdefinierte Felder und Linktypen
|
||||
|
||||
Epic: [Epic 1](epic-1.md)
|
||||
|
||||
Abhängigkeiten: [Issue 27](epic-1-issue-27.md)
|
||||
|
||||
## Ziel und Grenzen
|
||||
|
||||
Benutzerdefinierte Felder und Linktypen als abgegrenzten Teil des vollständigen Frontend-Neubaus umsetzen. Grundlage: Vue 3/Inertia 2 mit Laravel, Tauri 2 für Apps, gemeinsames Repository, servergeladene Oberfläche, HTTPS, Hell/Dunkel/System und englische Texte über Übersetzungsschlüssel. Bestehende Fachlogik bleibt maßgeblich; Resources und Routen nur soweit für diese Oberfläche erforderlich anpassen. Keine zusätzlichen Produktmodule.
|
||||
|
||||
## Einstieg
|
||||
|
||||
Gezielt prüfen: [resources/js/pages/projects/settings/Metadata.vue](../../resources/js/pages/projects/settings/Metadata.vue); [app/Http/Controllers/ProjectTicketMetadataSettingsController.php](../../app/Http/Controllers/ProjectTicketMetadataSettingsController.php). Die Pfade sind Einstiegspunkte, keine Erlaubnis zu fachlichen Umbauten. [AGENTS.md](../../AGENTS.md) und die für den tatsächlich geänderten Bereich geltenden Skills beachten. Vor Codeänderungen die zur installierten Version passende Dokumentation prüfen.
|
||||
|
||||
Design: [Fluid Emerald Kanban](https://stitch.withgoogle.com/projects/9754281177402648015)
|
||||
|
||||
## Implementierungsweg
|
||||
|
||||
1. Bestehende Felddefinitionen und Linktypen in separate neue Einstellungsabschnitte aufteilen.
|
||||
2. Vorhandene Feldtypen, Richtung und Gegenbezeichnungen erhalten.
|
||||
3. Anlage, Bearbeitung und vorhandene Löschabläufe über die bestehenden Endpunkte anbinden. Ungültige Definitionen mit konkreten Feldfehlern zurückmelden und bestehende Schutzregeln für bereits verwendete Felder und Linktypen in der Oberfläche nachvollziehbar machen.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] Keine neuen Feldtypen.
|
||||
- [ ] Vorhandene Feldwerte bleiben erhalten.
|
||||
- [ ] Linktypen und Gegenrichtung werden korrekt angezeigt und bearbeitet.
|
||||
|
||||
## Prüfung
|
||||
|
||||
Vorhandene gezielte Abdeckung: [tests/Feature/Projects/ProjectTicketMetadataSettingsControllerTest.php](../../tests/Feature/Projects/ProjectTicketMetadataSettingsControllerTest.php). Nur bei Berührung des jeweiligen Verhaltens aktualisieren bzw. ausführen.
|
||||
|
||||
Gezielte Verhaltenstests für die Änderung; betroffene PHPUnit-Featuretests bei Controller-/Resource-/Route-Anpassungen. Oberfläche mit Tastatur und bei mobiler Breite in beiden Themes prüfen. Projektvorgaben für Lint/Build und bei Vue/TypeScript abschließend `npm run types:check` erfüllen. Keine ausschließlich optischen Momentaufnahmen als Ersatz für Verhaltenstests.
|
||||
|
||||
## Grenze der Agentensitzung
|
||||
|
||||
Nur dieses Ticket und bereits erfüllte Abhängigkeiten bearbeiten. Ein zusammenhängendes Ergebnis mit passenden Tests liefern; keine Nachbartickets mitziehen. Die Aufteilung zielt auf eine begrenzte Sitzung mit Reserve für Prüfung, garantiert aber kein festes Zeit- oder Tokenbudget. Wenn unerwartete Voraussetzungen den Umfang wesentlich vergrößern, rechtzeitig den Stand, betroffene Dateien, bereits ausgeführte Prüfungen und den konkret nötigen Folgeschritt festhalten.
|
||||
@@ -1,37 +0,0 @@
|
||||
# UI Rework 29: Repository-Einstellungen des Projekts
|
||||
|
||||
Epic: [Epic 1](epic-1.md)
|
||||
|
||||
Abhängigkeiten: [Issue 23](epic-1-issue-23.md), [Issue 24](epic-1-issue-24.md)
|
||||
|
||||
## Ziel und Grenzen
|
||||
|
||||
Repository-Einstellungen des Projekts als abgegrenzten Teil des vollständigen Frontend-Neubaus umsetzen. Grundlage: Vue 3/Inertia 2 mit Laravel, Tauri 2 für Apps, gemeinsames Repository, servergeladene Oberfläche, HTTPS, Hell/Dunkel/System und englische Texte über Übersetzungsschlüssel. Bestehende Fachlogik bleibt maßgeblich; Resources und Routen nur soweit für diese Oberfläche erforderlich anpassen. Keine zusätzlichen Produktmodule.
|
||||
|
||||
## Einstieg
|
||||
|
||||
Gezielt prüfen: [resources/js/pages/projects/settings/Repositories.vue](../../resources/js/pages/projects/settings/Repositories.vue); [app/Http/Controllers/ProjectRepositorySettingsController.php](../../app/Http/Controllers/ProjectRepositorySettingsController.php). Die Pfade sind Einstiegspunkte, keine Erlaubnis zu fachlichen Umbauten. [AGENTS.md](../../AGENTS.md) und die für den tatsächlich geänderten Bereich geltenden Skills beachten. Vor Codeänderungen die zur installierten Version passende Dokumentation prüfen.
|
||||
|
||||
Design: [Fluid Emerald Kanban](https://stitch.withgoogle.com/projects/9754281177402648015)
|
||||
|
||||
## Implementierungsweg
|
||||
|
||||
1. Projekt-Repository-Verbindungen mit vorhandener Discovery, Auswahl und Bearbeitung neu aufbauen.
|
||||
2. Persönliche Verbindungen und Projektzuordnung klar unterscheiden.
|
||||
3. Ladezustände, leere Suchergebnisse, fehlende persönliche Verbindung und Providerfehler getrennt darstellen. Auf die vorhandenen persönlichen Git-Einstellungen verlinken und nach erfolgreicher Zuordnung die Projektliste aus der Serverantwort aktualisieren.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] Vorhandene Provider werden unterstützt.
|
||||
- [ ] Fehlende Verbindung und Providerfehler sind abgedeckt.
|
||||
- [ ] Tickets nutzen weiterhin dieselben Repositories.
|
||||
|
||||
## Prüfung
|
||||
|
||||
Vorhandene gezielte Abdeckung: [tests/Feature/Projects/ProjectRepositorySettingsControllerTest.php](../../tests/Feature/Projects/ProjectRepositorySettingsControllerTest.php). Nur bei Berührung des jeweiligen Verhaltens aktualisieren bzw. ausführen.
|
||||
|
||||
Gezielte Verhaltenstests für die Änderung; betroffene PHPUnit-Featuretests bei Controller-/Resource-/Route-Anpassungen. Oberfläche mit Tastatur und bei mobiler Breite in beiden Themes prüfen. Projektvorgaben für Lint/Build und bei Vue/TypeScript abschließend `npm run types:check` erfüllen. Keine ausschließlich optischen Momentaufnahmen als Ersatz für Verhaltenstests.
|
||||
|
||||
## Grenze der Agentensitzung
|
||||
|
||||
Nur dieses Ticket und bereits erfüllte Abhängigkeiten bearbeiten. Ein zusammenhängendes Ergebnis mit passenden Tests liefern; keine Nachbartickets mitziehen. Die Aufteilung zielt auf eine begrenzte Sitzung mit Reserve für Prüfung, garantiert aber kein festes Zeit- oder Tokenbudget. Wenn unerwartete Voraussetzungen den Umfang wesentlich vergrößern, rechtzeitig den Stand, betroffene Dateien, bereits ausgeführte Prüfungen und den konkret nötigen Folgeschritt festhalten.
|
||||
@@ -1,35 +0,0 @@
|
||||
# UI Rework 3: Neue grundlegende UI-Komponenten
|
||||
|
||||
Epic: [Epic 1](epic-1.md)
|
||||
|
||||
Abhängigkeiten: [Issue 1](epic-1-issue-1.md), [Issue 2](epic-1-issue-2.md)
|
||||
|
||||
## Ziel und Grenzen
|
||||
|
||||
Neue grundlegende UI-Komponenten als abgegrenzten Teil des vollständigen Frontend-Neubaus umsetzen. Grundlage: Vue 3/Inertia 2 mit Laravel, Tauri 2 für Apps, gemeinsames Repository, servergeladene Oberfläche, HTTPS, Hell/Dunkel/System und englische Texte über Übersetzungsschlüssel. Bestehende Fachlogik bleibt maßgeblich; Resources und Routen nur soweit für diese Oberfläche erforderlich anpassen. Keine zusätzlichen Produktmodule.
|
||||
|
||||
## Einstieg
|
||||
|
||||
Gezielt prüfen: [resources/js/components/ui](../../resources/js/components/ui). Die Pfade sind Einstiegspunkte, keine Erlaubnis zu fachlichen Umbauten. [AGENTS.md](../../AGENTS.md) und die für den tatsächlich geänderten Bereich geltenden Skills beachten. Vor Codeänderungen die zur installierten Version passende Dokumentation prüfen.
|
||||
|
||||
Design: [Fluid Emerald Kanban](https://stitch.withgoogle.com/projects/9754281177402648015)
|
||||
|
||||
## Implementierungsweg
|
||||
|
||||
1. Buttons, Eingaben, Auswahlfelder, Checkboxen, Badges und Avatare neu gestalten.
|
||||
2. Zugängliches Verhalten vorhandener Headless-Primitiven wiederverwenden.
|
||||
3. Öffentliche Props für Folgetickets stabil festlegen.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] Tastatur, Fokus, disabled, invalid und loading funktionieren.
|
||||
- [ ] Sichtbare Texte sind übersetzbar.
|
||||
- [ ] Komponenten funktionieren in beiden Themes.
|
||||
|
||||
## Prüfung
|
||||
|
||||
Gezielte Verhaltenstests für die Änderung; betroffene PHPUnit-Featuretests bei Controller-/Resource-/Route-Anpassungen. Oberfläche mit Tastatur und bei mobiler Breite in beiden Themes prüfen. Projektvorgaben für Lint/Build und bei Vue/TypeScript abschließend `npm run types:check` erfüllen. Keine ausschließlich optischen Momentaufnahmen als Ersatz für Verhaltenstests.
|
||||
|
||||
## Grenze der Agentensitzung
|
||||
|
||||
Nur dieses Ticket und bereits erfüllte Abhängigkeiten bearbeiten. Ein zusammenhängendes Ergebnis mit passenden Tests liefern; keine Nachbartickets mitziehen. Die Aufteilung zielt auf eine begrenzte Sitzung mit Reserve für Prüfung, garantiert aber kein festes Zeit- oder Tokenbudget. Wenn unerwartete Voraussetzungen den Umfang wesentlich vergrößern, rechtzeitig den Stand, betroffene Dateien, bereits ausgeführte Prüfungen und den konkret nötigen Folgeschritt festhalten.
|
||||
@@ -1,37 +0,0 @@
|
||||
# UI Rework 30: Projekt-Benachrichtigungseinstellungen
|
||||
|
||||
Epic: [Epic 1](epic-1.md)
|
||||
|
||||
Abhängigkeiten: [Issue 24](epic-1-issue-24.md)
|
||||
|
||||
## Ziel und Grenzen
|
||||
|
||||
Projekt-Benachrichtigungseinstellungen als abgegrenzten Teil des vollständigen Frontend-Neubaus umsetzen. Grundlage: Vue 3/Inertia 2 mit Laravel, Tauri 2 für Apps, gemeinsames Repository, servergeladene Oberfläche, HTTPS, Hell/Dunkel/System und englische Texte über Übersetzungsschlüssel. Bestehende Fachlogik bleibt maßgeblich; Resources und Routen nur soweit für diese Oberfläche erforderlich anpassen. Keine zusätzlichen Produktmodule.
|
||||
|
||||
## Einstieg
|
||||
|
||||
Gezielt prüfen: [resources/js/pages/projects/settings/Notifications.vue](../../resources/js/pages/projects/settings/Notifications.vue); [app/Http/Controllers/ProjectNotificationSettingsController.php](../../app/Http/Controllers/ProjectNotificationSettingsController.php). Die Pfade sind Einstiegspunkte, keine Erlaubnis zu fachlichen Umbauten. [AGENTS.md](../../AGENTS.md) und die für den tatsächlich geänderten Bereich geltenden Skills beachten. Vor Codeänderungen die zur installierten Version passende Dokumentation prüfen.
|
||||
|
||||
Design: [Fluid Emerald Kanban](https://stitch.withgoogle.com/projects/9754281177402648015)
|
||||
|
||||
## Implementierungsweg
|
||||
|
||||
1. Bestehende Aktionen, Trigger, Filter und Nachrichtenvorlagen neu gestalten.
|
||||
2. Vorhandene Delivery-Informationen und Validierung nutzen.
|
||||
3. Sensible Webhookfelder sorgfältig darstellen.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] Bestehende Benachrichtigungen lassen sich unverändert konfigurieren.
|
||||
- [ ] Keine neuen Trigger oder Push-Dienste.
|
||||
- [ ] Fehlerfälle sind getestet.
|
||||
|
||||
## Prüfung
|
||||
|
||||
Vorhandene gezielte Abdeckung: [tests/Feature/Projects/ProjectNotificationSettingsControllerTest.php](../../tests/Feature/Projects/ProjectNotificationSettingsControllerTest.php). Nur bei Berührung des jeweiligen Verhaltens aktualisieren bzw. ausführen.
|
||||
|
||||
Gezielte Verhaltenstests für die Änderung; betroffene PHPUnit-Featuretests bei Controller-/Resource-/Route-Anpassungen. Oberfläche mit Tastatur und bei mobiler Breite in beiden Themes prüfen. Projektvorgaben für Lint/Build und bei Vue/TypeScript abschließend `npm run types:check` erfüllen. Keine ausschließlich optischen Momentaufnahmen als Ersatz für Verhaltenstests.
|
||||
|
||||
## Grenze der Agentensitzung
|
||||
|
||||
Nur dieses Ticket und bereits erfüllte Abhängigkeiten bearbeiten. Ein zusammenhängendes Ergebnis mit passenden Tests liefern; keine Nachbartickets mitziehen. Die Aufteilung zielt auf eine begrenzte Sitzung mit Reserve für Prüfung, garantiert aber kein festes Zeit- oder Tokenbudget. Wenn unerwartete Voraussetzungen den Umfang wesentlich vergrößern, rechtzeitig den Stand, betroffene Dateien, bereits ausgeführte Prüfungen und den konkret nötigen Folgeschritt festhalten.
|
||||
@@ -1,37 +0,0 @@
|
||||
# UI Rework 31: Verwaltung wiederverwendbarer Projektvorlagen
|
||||
|
||||
Epic: [Epic 1](epic-1.md)
|
||||
|
||||
Abhängigkeiten: [Issue 9](epic-1-issue-9.md), [Issue 24](epic-1-issue-24.md)
|
||||
|
||||
## Ziel und Grenzen
|
||||
|
||||
Verwaltung wiederverwendbarer Projektvorlagen als abgegrenzten Teil des vollständigen Frontend-Neubaus umsetzen. Grundlage: Vue 3/Inertia 2 mit Laravel, Tauri 2 für Apps, gemeinsames Repository, servergeladene Oberfläche, HTTPS, Hell/Dunkel/System und englische Texte über Übersetzungsschlüssel. Bestehende Fachlogik bleibt maßgeblich; Resources und Routen nur soweit für diese Oberfläche erforderlich anpassen. Keine zusätzlichen Produktmodule.
|
||||
|
||||
## Einstieg
|
||||
|
||||
Gezielt prüfen: [resources/js/pages/project-templates/Index.vue](../../resources/js/pages/project-templates/Index.vue); [resources/js/pages/project-templates/Create.vue](../../resources/js/pages/project-templates/Create.vue); [resources/js/pages/project-templates/Edit.vue](../../resources/js/pages/project-templates/Edit.vue). Die Pfade sind Einstiegspunkte, keine Erlaubnis zu fachlichen Umbauten. [AGENTS.md](../../AGENTS.md) und die für den tatsächlich geänderten Bereich geltenden Skills beachten. Vor Codeänderungen die zur installierten Version passende Dokumentation prüfen.
|
||||
|
||||
Design: [Fluid Emerald Kanban](https://stitch.withgoogle.com/projects/9754281177402648015)
|
||||
|
||||
## Implementierungsweg
|
||||
|
||||
1. Vorlagenübersicht, Erstellung und Bearbeitung neu aufbauen.
|
||||
2. Auswirkungen auf vererbende Projekte und Bestätigungen darstellen.
|
||||
3. Interne Basisvorlagen weiterhin ausblenden.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] Vorhandene Erstell-/Bearbeitungsflüsse funktionieren.
|
||||
- [ ] Unveränderliche Slugs bleiben gesperrt.
|
||||
- [ ] Keine Änderungen an Vererbungsregeln.
|
||||
|
||||
## Prüfung
|
||||
|
||||
Vorhandene gezielte Abdeckung: [tests/Feature/Projects/ProjectTemplateControllerTest.php](../../tests/Feature/Projects/ProjectTemplateControllerTest.php), [tests/Feature/Projects/ProjectTemplateOverrideTest.php](../../tests/Feature/Projects/ProjectTemplateOverrideTest.php). Nur bei Berührung des jeweiligen Verhaltens aktualisieren bzw. ausführen.
|
||||
|
||||
Gezielte Verhaltenstests für die Änderung; betroffene PHPUnit-Featuretests bei Controller-/Resource-/Route-Anpassungen. Oberfläche mit Tastatur und bei mobiler Breite in beiden Themes prüfen. Projektvorgaben für Lint/Build und bei Vue/TypeScript abschließend `npm run types:check` erfüllen. Keine ausschließlich optischen Momentaufnahmen als Ersatz für Verhaltenstests.
|
||||
|
||||
## Grenze der Agentensitzung
|
||||
|
||||
Nur dieses Ticket und bereits erfüllte Abhängigkeiten bearbeiten. Ein zusammenhängendes Ergebnis mit passenden Tests liefern; keine Nachbartickets mitziehen. Die Aufteilung zielt auf eine begrenzte Sitzung mit Reserve für Prüfung, garantiert aber kein festes Zeit- oder Tokenbudget. Wenn unerwartete Voraussetzungen den Umfang wesentlich vergrößern, rechtzeitig den Stand, betroffene Dateien, bereits ausgeführte Prüfungen und den konkret nötigen Folgeschritt festhalten.
|
||||
@@ -1,36 +0,0 @@
|
||||
# UI Rework 32: Teams und Mitgliedschaft
|
||||
|
||||
Epic: [Epic 1](epic-1.md)
|
||||
|
||||
Abhängigkeiten: [Issue 4](epic-1-issue-4.md), [Issue 5](epic-1-issue-5.md)
|
||||
|
||||
## Ziel und Grenzen
|
||||
|
||||
Teams und Mitgliedschaft als abgegrenzten Teil des vollständigen Frontend-Neubaus umsetzen. Grundlage: Vue 3/Inertia 2 mit Laravel, Tauri 2 für Apps, gemeinsames Repository, servergeladene Oberfläche, HTTPS, Hell/Dunkel/System und englische Texte über Übersetzungsschlüssel. Bestehende Fachlogik bleibt maßgeblich; Resources und Routen nur soweit für diese Oberfläche erforderlich anpassen. Keine zusätzlichen Produktmodule.
|
||||
|
||||
## Einstieg
|
||||
|
||||
Gezielt prüfen: [resources/js/pages/teams/Index.vue](../../resources/js/pages/teams/Index.vue); [app/Http/Controllers/TeamController.php](../../app/Http/Controllers/TeamController.php). Die Pfade sind Einstiegspunkte, keine Erlaubnis zu fachlichen Umbauten. [AGENTS.md](../../AGENTS.md) und die für den tatsächlich geänderten Bereich geltenden Skills beachten. Vor Codeänderungen die zur installierten Version passende Dokumentation prüfen.
|
||||
|
||||
Design: [Fluid Emerald Kanban](https://stitch.withgoogle.com/projects/9754281177402648015)
|
||||
|
||||
## Implementierungsweg
|
||||
|
||||
1. Teamübersicht, Teamverwaltung und Mitgliedsauswahl im neuen Design umsetzen.
|
||||
2. Alle bestehenden Rechte und Aktionen übernehmen.
|
||||
3. Mobile Bedienung sicherstellen.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] Team- und Mitgliedsaktionen entsprechen dem Bestand.
|
||||
- [ ] Fehlende Rechte und Validierungsfehler sind abgedeckt.
|
||||
|
||||
## Prüfung
|
||||
|
||||
Vorhandene gezielte Abdeckung: [tests/Feature/Teams/TeamControllerTest.php](../../tests/Feature/Teams/TeamControllerTest.php). Nur bei Berührung des jeweiligen Verhaltens aktualisieren bzw. ausführen.
|
||||
|
||||
Gezielte Verhaltenstests für die Änderung; betroffene PHPUnit-Featuretests bei Controller-/Resource-/Route-Anpassungen. Oberfläche mit Tastatur und bei mobiler Breite in beiden Themes prüfen. Projektvorgaben für Lint/Build und bei Vue/TypeScript abschließend `npm run types:check` erfüllen. Keine ausschließlich optischen Momentaufnahmen als Ersatz für Verhaltenstests.
|
||||
|
||||
## Grenze der Agentensitzung
|
||||
|
||||
Nur dieses Ticket und bereits erfüllte Abhängigkeiten bearbeiten. Ein zusammenhängendes Ergebnis mit passenden Tests liefern; keine Nachbartickets mitziehen. Die Aufteilung zielt auf eine begrenzte Sitzung mit Reserve für Prüfung, garantiert aber kein festes Zeit- oder Tokenbudget. Wenn unerwartete Voraussetzungen den Umfang wesentlich vergrößern, rechtzeitig den Stand, betroffene Dateien, bereits ausgeführte Prüfungen und den konkret nötigen Folgeschritt festhalten.
|
||||
@@ -1,37 +0,0 @@
|
||||
# UI Rework 33: Projektzugriff und Rechtezuweisung
|
||||
|
||||
Epic: [Epic 1](epic-1.md)
|
||||
|
||||
Abhängigkeiten: [Issue 8](epic-1-issue-8.md), [Issue 32](epic-1-issue-32.md)
|
||||
|
||||
## Ziel und Grenzen
|
||||
|
||||
Projektzugriff und Rechtezuweisung als abgegrenzten Teil des vollständigen Frontend-Neubaus umsetzen. Grundlage: Vue 3/Inertia 2 mit Laravel, Tauri 2 für Apps, gemeinsames Repository, servergeladene Oberfläche, HTTPS, Hell/Dunkel/System und englische Texte über Übersetzungsschlüssel. Bestehende Fachlogik bleibt maßgeblich; Resources und Routen nur soweit für diese Oberfläche erforderlich anpassen. Keine zusätzlichen Produktmodule.
|
||||
|
||||
## Einstieg
|
||||
|
||||
Gezielt prüfen: [resources/js/pages/projects/Access.vue](../../resources/js/pages/projects/Access.vue); [app/Http/Controllers/ProjectController.php](../../app/Http/Controllers/ProjectController.php). Die Pfade sind Einstiegspunkte, keine Erlaubnis zu fachlichen Umbauten. [AGENTS.md](../../AGENTS.md) und die für den tatsächlich geänderten Bereich geltenden Skills beachten. Vor Codeänderungen die zur installierten Version passende Dokumentation prüfen.
|
||||
|
||||
Design: [Fluid Emerald Kanban](https://stitch.withgoogle.com/projects/9754281177402648015)
|
||||
|
||||
## Implementierungsweg
|
||||
|
||||
1. Direkte Benutzerrechte und Teamrechte neu darstellen.
|
||||
2. Effektive Rechte und Bearbeitungsgrenzen nachvollziehbar machen.
|
||||
3. Bestehende Zuweisungsendpunkte weiterverwenden.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] Keine neue Rollenlogik.
|
||||
- [ ] Rechte gelten weiterhin additiv nach Bestand.
|
||||
- [ ] Unberechtigte Zuweisungen werden serverseitig abgewiesen.
|
||||
|
||||
## Prüfung
|
||||
|
||||
Vorhandene gezielte Abdeckung: [tests/Feature/Projects/ProjectControllerTest.php](../../tests/Feature/Projects/ProjectControllerTest.php), [tests/Feature/Permissions/PermissionServiceTest.php](../../tests/Feature/Permissions/PermissionServiceTest.php). Nur bei Berührung des jeweiligen Verhaltens aktualisieren bzw. ausführen.
|
||||
|
||||
Gezielte Verhaltenstests für die Änderung; betroffene PHPUnit-Featuretests bei Controller-/Resource-/Route-Anpassungen. Oberfläche mit Tastatur und bei mobiler Breite in beiden Themes prüfen. Projektvorgaben für Lint/Build und bei Vue/TypeScript abschließend `npm run types:check` erfüllen. Keine ausschließlich optischen Momentaufnahmen als Ersatz für Verhaltenstests.
|
||||
|
||||
## Grenze der Agentensitzung
|
||||
|
||||
Nur dieses Ticket und bereits erfüllte Abhängigkeiten bearbeiten. Ein zusammenhängendes Ergebnis mit passenden Tests liefern; keine Nachbartickets mitziehen. Die Aufteilung zielt auf eine begrenzte Sitzung mit Reserve für Prüfung, garantiert aber kein festes Zeit- oder Tokenbudget. Wenn unerwartete Voraussetzungen den Umfang wesentlich vergrößern, rechtzeitig den Stand, betroffene Dateien, bereits ausgeführte Prüfungen und den konkret nötigen Folgeschritt festhalten.
|
||||
@@ -1,37 +0,0 @@
|
||||
# UI Rework 34: Benutzeradministration
|
||||
|
||||
Epic: [Epic 1](epic-1.md)
|
||||
|
||||
Abhängigkeiten: [Issue 4](epic-1-issue-4.md), [Issue 5](epic-1-issue-5.md)
|
||||
|
||||
## Ziel und Grenzen
|
||||
|
||||
Benutzeradministration als abgegrenzten Teil des vollständigen Frontend-Neubaus umsetzen. Grundlage: Vue 3/Inertia 2 mit Laravel, Tauri 2 für Apps, gemeinsames Repository, servergeladene Oberfläche, HTTPS, Hell/Dunkel/System und englische Texte über Übersetzungsschlüssel. Bestehende Fachlogik bleibt maßgeblich; Resources und Routen nur soweit für diese Oberfläche erforderlich anpassen. Keine zusätzlichen Produktmodule.
|
||||
|
||||
## Einstieg
|
||||
|
||||
Gezielt prüfen: [resources/js/pages/admin/users/Index.vue](../../resources/js/pages/admin/users/Index.vue); [resources/js/pages/admin/users/Create.vue](../../resources/js/pages/admin/users/Create.vue); [resources/js/pages/admin/users/Edit.vue](../../resources/js/pages/admin/users/Edit.vue). Die Pfade sind Einstiegspunkte, keine Erlaubnis zu fachlichen Umbauten. [AGENTS.md](../../AGENTS.md) und die für den tatsächlich geänderten Bereich geltenden Skills beachten. Vor Codeänderungen die zur installierten Version passende Dokumentation prüfen.
|
||||
|
||||
Design: [Fluid Emerald Kanban](https://stitch.withgoogle.com/projects/9754281177402648015)
|
||||
|
||||
## Implementierungsweg
|
||||
|
||||
1. Übersicht, Benutzeranlage und Bearbeitung vollständig neu gestalten.
|
||||
2. Vorhandene Felder, Adminoptionen und erzwungenen Passwortwechsel erhalten.
|
||||
3. Verfügbare Aktionen aus den bestehenden Berechtigungen ableiten, sensible Änderungen bestätigen lassen und Validierungsfehler direkt den Eingaben zuordnen. Nach Speichern oder Abbruch zur passenden Übersicht zurückführen, ohne neue Rollen oder Verwaltungsaktionen einzuführen.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] Bestehende Adminaktionen funktionieren.
|
||||
- [ ] Nicht-Admins erhalten keinen Zugriff.
|
||||
- [ ] Formfehler und sensible Aktionen sind abgedeckt.
|
||||
|
||||
## Prüfung
|
||||
|
||||
Vorhandene gezielte Abdeckung: [tests/Feature/Admin/UserManagementControllerTest.php](../../tests/Feature/Admin/UserManagementControllerTest.php). Nur bei Berührung des jeweiligen Verhaltens aktualisieren bzw. ausführen.
|
||||
|
||||
Gezielte Verhaltenstests für die Änderung; betroffene PHPUnit-Featuretests bei Controller-/Resource-/Route-Anpassungen. Oberfläche mit Tastatur und bei mobiler Breite in beiden Themes prüfen. Projektvorgaben für Lint/Build und bei Vue/TypeScript abschließend `npm run types:check` erfüllen. Keine ausschließlich optischen Momentaufnahmen als Ersatz für Verhaltenstests.
|
||||
|
||||
## Grenze der Agentensitzung
|
||||
|
||||
Nur dieses Ticket und bereits erfüllte Abhängigkeiten bearbeiten. Ein zusammenhängendes Ergebnis mit passenden Tests liefern; keine Nachbartickets mitziehen. Die Aufteilung zielt auf eine begrenzte Sitzung mit Reserve für Prüfung, garantiert aber kein festes Zeit- oder Tokenbudget. Wenn unerwartete Voraussetzungen den Umfang wesentlich vergrößern, rechtzeitig den Stand, betroffene Dateien, bereits ausgeführte Prüfungen und den konkret nötigen Folgeschritt festhalten.
|
||||
@@ -1,36 +0,0 @@
|
||||
# UI Rework 35: Öffentliche Einstiegsseite
|
||||
|
||||
Epic: [Epic 1](epic-1.md)
|
||||
|
||||
Abhängigkeiten: [Issue 2](epic-1-issue-2.md), [Issue 3](epic-1-issue-3.md), [Issue 6](epic-1-issue-6.md)
|
||||
|
||||
## Ziel und Grenzen
|
||||
|
||||
Öffentliche Einstiegsseite als abgegrenzten Teil des vollständigen Frontend-Neubaus umsetzen. Grundlage: Vue 3/Inertia 2 mit Laravel, Tauri 2 für Apps, gemeinsames Repository, servergeladene Oberfläche, HTTPS, Hell/Dunkel/System und englische Texte über Übersetzungsschlüssel. Bestehende Fachlogik bleibt maßgeblich; Resources und Routen nur soweit für diese Oberfläche erforderlich anpassen. Keine zusätzlichen Produktmodule.
|
||||
|
||||
## Einstieg
|
||||
|
||||
Gezielt prüfen: [resources/js/pages/Welcome.vue](../../resources/js/pages/Welcome.vue). Die Pfade sind Einstiegspunkte, keine Erlaubnis zu fachlichen Umbauten. [AGENTS.md](../../AGENTS.md) und die für den tatsächlich geänderten Bereich geltenden Skills beachten. Vor Codeänderungen die zur installierten Version passende Dokumentation prüfen.
|
||||
|
||||
Design: [Fluid Emerald Kanban](https://stitch.withgoogle.com/projects/9754281177402648015)
|
||||
Relevanter Stitch-Screen: Landing Page - Open Source & Self-Hosted — 41a744db2b1f4e01a9c597835b58759e. Zugriff über Stitch-MCP, Projekt 9754281177402648015.
|
||||
|
||||
## Implementierungsweg
|
||||
|
||||
1. Neue kompakte öffentliche Einstiegsseite mit KanbanGit-Branding und Anmeldung entwickeln.
|
||||
2. Stitch-Landingpage ausschließlich als Stilreferenz nutzen.
|
||||
3. Nur vorhandene Funktionen beschreiben.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] Keine erfundenen Produktfunktionen, Tarife oder Statistiken.
|
||||
- [ ] Responsiv und lokalisierbar.
|
||||
- [ ] Einstieg in Anmeldung funktioniert.
|
||||
|
||||
## Prüfung
|
||||
|
||||
Gezielte Verhaltenstests für die Änderung; betroffene PHPUnit-Featuretests bei Controller-/Resource-/Route-Anpassungen. Oberfläche mit Tastatur und bei mobiler Breite in beiden Themes prüfen. Projektvorgaben für Lint/Build und bei Vue/TypeScript abschließend `npm run types:check` erfüllen. Keine ausschließlich optischen Momentaufnahmen als Ersatz für Verhaltenstests.
|
||||
|
||||
## Grenze der Agentensitzung
|
||||
|
||||
Nur dieses Ticket und bereits erfüllte Abhängigkeiten bearbeiten. Ein zusammenhängendes Ergebnis mit passenden Tests liefern; keine Nachbartickets mitziehen. Die Aufteilung zielt auf eine begrenzte Sitzung mit Reserve für Prüfung, garantiert aber kein festes Zeit- oder Tokenbudget. Wenn unerwartete Voraussetzungen den Umfang wesentlich vergrößern, rechtzeitig den Stand, betroffene Dateien, bereits ausgeführte Prüfungen und den konkret nötigen Folgeschritt festhalten.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user