Compare commits

..
3 Commits
Author SHA1 Message Date
Twyco 1b2349e977 feat(KAN-18): redesign foundational UI components
CI/CD / test_and_check_app (push) Successful in 4m25s
CI/CD / Publish immutable image (push) Skipped
CI/CD / test_and_check_app (pull_request) Canceled after 0s
CI/CD / Publish immutable image (pull_request) Canceled after 0s
2026-09-09 15:31:04 +02:00
Twyco 95db152b91 feat(KAN-17): add Emerald design system and resilient themes
CI/CD / test_and_check_app (push) Successful in 4m13s
CI/CD / Publish immutable image (push) Skipped
CI/CD / test_and_check_app (pull_request) Canceled after 0s
CI/CD / Publish immutable image (pull_request) Canceled after 0s
2026-09-09 14:29:16 +02:00
Twyco 5194ed0a17 feat(KAN-16): add English localization foundation
CI/CD / test_and_check_app (push) Successful in 4m9s
CI/CD / Publish immutable image (push) Skipped
CI/CD / Publish immutable image (pull_request) Canceled after 0s
CI/CD / test_and_check_app (pull_request) Canceled after 3m45s
2026-09-09 14:02:20 +02:00
199 changed files with 13261 additions and 20899 deletions
+3
View File
@@ -33,6 +33,9 @@
vendor
**/.DS_Store
.editorconfig
.env.example
.env.mariadb
.env.mariadb.example
.env.production
LICENSE
README.md
+4
View File
@@ -0,0 +1,4 @@
MARIADB_DATABASE=kanbangit
MARIADB_USER=kanbangit
MARIADB_PASSWORD=
MARIADB_ROOT_PASSWORD=
+1 -18
View File
@@ -2,13 +2,7 @@ name: CI/CD
on:
push:
branches:
- main
tags:
- "v*"
pull_request:
branches:
- main
permissions:
contents: read
@@ -17,19 +11,9 @@ env:
REGISTRY: git.twyco.de
jobs:
preconditions:
if: >
!(contains(github.event.head_commit.message, '[no-ci]') || contains(github.event.head_commit.message, '[skip-ci]'))
runs-on: ubuntu-latest
steps:
- name: Conditions met
run: |
echo "Conditions for CI/CD are met. Proceeding with the workflow."
test_and_check_app:
name: test_and_check_app
runs-on: ubuntu-latest
needs:
- preconditions
steps:
- name: Check out repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
@@ -43,7 +27,6 @@ jobs:
github.event_name == 'push' &&
(github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
needs:
- preconditions
- test_and_check_app
runs-on: ubuntu-latest
steps:
+3 -5
View File
@@ -55,14 +55,12 @@ COPY --from=node_binary /usr/local/ /usr/local/
COPY . /var/www
RUN composer install --prefer-dist --no-interaction --no-progress \
&& corepack enable pnpm \
&& pnpm install --frozen-lockfile \
&& pnpm run build
&& npm ci \
&& npm run build
FROM build AS test
RUN cp .env.example .env \
&& php artisan key:generate\
RUN touch .env \
&& composer audit --locked --no-interaction \
&& COMPOSER_ALLOW_SUPERUSER=1 composer ci:check
@@ -47,7 +47,6 @@ class BuildProjectBacklogPayload
'tickets' => TicketResource::collection(
Ticket::query()
->with([
'assignee',
'ticketPriority',
'ticketStatus.allowedTargetStatuses.boardColumn',
'ticketStatus.boardColumn',
@@ -29,7 +29,6 @@ class BuildProjectBoardPayload
'ticketStatuses',
'tickets' => fn ($query) => $query
->with([
'assignee',
'ticketPriority',
'ticketStatus.allowedTargetStatuses.boardColumn',
'ticketStatus.boardColumn',
@@ -41,8 +41,8 @@ class ListAssignedTicketsByVisibleProject
->where('assignee_user_id', $assignee->id)
->whereIn('project_id', $visibleProjectIds)
->with([
'acceptanceCriteria',
'assignee',
'project',
'ticketPriority',
'ticketStatus.boardColumn',
'ticketType',
@@ -1,24 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Tickets\ListAssignedTicketsByVisibleProject;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class DashboardController extends Controller
{
public function __construct(
private readonly ListAssignedTicketsByVisibleProject $listAssignedTicketsByVisibleProject,
) {}
public function __invoke(Request $request): Response
{
$user = $request->user();
return Inertia::render('Dashboard', [
'assignedTicketsByProject' => $this->listAssignedTicketsByVisibleProject->handle($user, $user),
]);
}
}
@@ -4,7 +4,6 @@ namespace App\Http\Middleware;
use App\Actions\Permissions\PermissionService;
use App\Enums\Permission;
use App\Models\Project;
use Illuminate\Http\Request;
use Inertia\Middleware;
@@ -61,36 +60,6 @@ class HandleInertiaRequests extends Middleware
'user' => $user,
],
'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true',
'navigation' => fn (): array => [
'project' => $this->projectNavigation($request),
],
];
}
/**
* @return array{id: int, name: string, key: string, canManage: bool, canCreateTickets: bool}|null
*/
private function projectNavigation(Request $request): ?array
{
$project = $request->route('project');
$user = $request->user();
if (! $project instanceof Project || $user === null) {
return null;
}
$permissions = $this->permissionService->effectivePermissions($user, $project);
if (! in_array(Permission::ProjectRead, $permissions, true)) {
return null;
}
return [
'id' => $project->id,
'name' => $project->name,
'key' => $project->key,
'canManage' => in_array(Permission::ProjectAdmin, $permissions, true),
'canCreateTickets' => in_array(Permission::ProjectWrite, $permissions, true),
];
}
}
+6 -6
View File
@@ -47,15 +47,15 @@
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
"@php artisan key:generate",
"@php artisan migrate --force",
"pnpm install",
"pnpm run build"
"npm install",
"npm run build"
],
"dev": [
"Composer\\Config::disableProcessTimeout",
"@php artisan dev"
],
"dev:ssr": [
"pnpm run build:ssr",
"npm run build:ssr",
"Composer\\Config::disableProcessTimeout",
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"php artisan inertia:start-ssr\" --names=server,queue,logs,ssr --kill-others"
],
@@ -67,9 +67,9 @@
],
"ci:check": [
"Composer\\Config::disableProcessTimeout",
"pnpm audit --audit-level=high",
"pnpm run lint:check",
"pnpm run types:check",
"npm audit --audit-level=high",
"npm run lint:check",
"npm run types:check",
"@test"
],
"test": [
+35
View File
@@ -0,0 +1,35 @@
# 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.
+39
View File
@@ -0,0 +1,39 @@
# 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.
+36
View File
@@ -0,0 +1,36 @@
# 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.
+38
View File
@@ -0,0 +1,38 @@
# 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.
+40
View File
@@ -0,0 +1,40 @@
# 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.
+37
View File
@@ -0,0 +1,37 @@
# 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.
+38
View File
@@ -0,0 +1,38 @@
# 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.
+38
View File
@@ -0,0 +1,38 @@
# 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.
+38
View File
@@ -0,0 +1,38 @@
# 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.
+37
View File
@@ -0,0 +1,37 @@
# 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.
+37
View File
@@ -0,0 +1,37 @@
# 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.
+36
View File
@@ -0,0 +1,36 @@
# 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.
+38
View File
@@ -0,0 +1,38 @@
# 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.
+37
View File
@@ -0,0 +1,37 @@
# 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.
+37
View File
@@ -0,0 +1,37 @@
# 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.
+37
View File
@@ -0,0 +1,37 @@
# 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.
+37
View File
@@ -0,0 +1,37 @@
# 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.
+37
View File
@@ -0,0 +1,37 @@
# 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.
+37
View File
@@ -0,0 +1,37 @@
# 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.
+37
View File
@@ -0,0 +1,37 @@
# 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.
+37
View File
@@ -0,0 +1,37 @@
# 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.
+37
View File
@@ -0,0 +1,37 @@
# 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.
+35
View File
@@ -0,0 +1,35 @@
# 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.
+37
View File
@@ -0,0 +1,37 @@
# 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.
+37
View File
@@ -0,0 +1,37 @@
# 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.
+36
View File
@@ -0,0 +1,36 @@
# 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.
+37
View File
@@ -0,0 +1,37 @@
# 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.
+37
View File
@@ -0,0 +1,37 @@
# 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.
+36
View File
@@ -0,0 +1,36 @@
# 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.
+37
View File
@@ -0,0 +1,37 @@
# UI Rework 36: Tauri-Grundgerüst und macOS-Entwicklungsbuild
Epic: [Epic 1](epic-1.md)
Abhängigkeiten: Keine.
## Ziel und Grenzen
Tauri-Grundgerüst und macOS-Entwicklungsbuild 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: `native/` (in diesem Ticket neu anzulegen); [.gitignore](../../.gitignore). 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)
Technische Referenz: [Tauri 2](https://v2.tauri.app/), [Remote-Inhalte und native Berechtigungen](https://v2.tauri.app/security/capabilities/).
## Implementierungsweg
1. Isoliertes Tauri-2-Projekt im gemeinsamen Repository anlegen.
2. Minimale lokale Startansicht und Server-WebView erstellen.
3. Entwicklungsbuild auf macOS mit vorhandenem KanbanGit prüfen.
## Akzeptanzkriterien
- [ ] Laravel bleibt am bisherigen Ort.
- [ ] macOS-App startet und zeigt die Serveroberfläche.
- [ ] Keine allgemeine native API für Remote-Inhalte.
## Prüfung
Zunächst den im Ticket genannten Plattformbuild und die beschriebenen Abläufe prüfen. Für neue App-Logik gezielte Tests für Navigation, Zustände oder URL-Verarbeitung ergänzen; Plattformverhalten im passenden Simulator bzw. auf einem geeigneten Gerät prüfen. Bei Vue/TypeScript-Änderungen abschließend `npm run types:check`; bei Backend-Anpassungen betroffene PHPUnit-Tests.
## 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.
+38
View File
@@ -0,0 +1,38 @@
# UI Rework 37: Serverauswahl, Sitzungen und Verbindungsfehler
Epic: [Epic 1](epic-1.md)
Abhängigkeiten: [Issue 36](epic-1-issue-36.md), [Issue 6](epic-1-issue-6.md), [Issue 7](epic-1-issue-7.md)
## Ziel und Grenzen
Serverauswahl, Sitzungen und Verbindungsfehler 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: `native` (entsteht ab Issue 36); [resources/js/pages/auth](../../resources/js/pages/auth). 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)
Technische Referenz: [Tauri 2](https://v2.tauri.app/), [Remote-Inhalte und native Berechtigungen](https://v2.tauri.app/security/capabilities/).
## Implementierungsweg
1. Eine aktive, frei wählbare HTTPS-Installation speichern.
2. Serverwechsel und lokale Fehleransicht umsetzen.
3. Sitzungen, Logout, 2FA, Start ohne Verbindung und Formularfehler prüfen.
## Akzeptanzkriterien
- [ ] Kein Zertifikats-Bypass.
- [ ] Sitzungserhalt folgt Laravel.
- [ ] Serverwechsel übernimmt keine fremden Zugangsdaten.
- [ ] Bestehende Eingaben werden bei Verbindungsfehler nicht durch einen leeren Bildschirm ersetzt.
## Prüfung
Zunächst den im Ticket genannten Plattformbuild und die beschriebenen Abläufe prüfen. Für neue App-Logik gezielte Tests für Navigation, Zustände oder URL-Verarbeitung ergänzen; Plattformverhalten im passenden Simulator bzw. auf einem geeigneten Gerät prüfen. Bei Vue/TypeScript-Änderungen abschließend `npm run types:check`; bei Backend-Anpassungen betroffene PHPUnit-Tests.
## 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.
+37
View File
@@ -0,0 +1,37 @@
# UI Rework 38: Android-App und Plattformanpassung
Epic: [Epic 1](epic-1.md)
Abhängigkeiten: [Issue 37](epic-1-issue-37.md)
## Ziel und Grenzen
Android-App und Plattformanpassung 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: `native (Android-Plattformprojekt)` (entsteht ab Issue 36). 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)
Technische Referenz: [Tauri 2](https://v2.tauri.app/).
## Implementierungsweg
1. Android-Ziel konfigurieren und Entwicklungsbuild erstellen.
2. Zurück-Taste, Systemleisten, Safe Areas und Bildschirmtastatur anpassen.
3. Bestehende Server-/Anmeldeabläufe prüfen.
## Akzeptanzkriterien
- [ ] App startet auf Emulator und geeignetem Gerät.
- [ ] Login, 2FA, Navigation und Wiederaufnahme funktionieren.
- [ ] Keine eigenständige zweite Fachoberfläche.
## Prüfung
Zunächst den im Ticket genannten Plattformbuild und die beschriebenen Abläufe prüfen. Für neue App-Logik gezielte Tests für Navigation, Zustände oder URL-Verarbeitung ergänzen; Plattformverhalten im passenden Simulator bzw. auf einem geeigneten Gerät prüfen. Bei Vue/TypeScript-Änderungen abschließend `npm run types:check`; bei Backend-Anpassungen betroffene PHPUnit-Tests.
## 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.
+38
View File
@@ -0,0 +1,38 @@
# UI Rework 39: iOS/iPadOS-App für lokale Xcode-Builds
Epic: [Epic 1](epic-1.md)
Abhängigkeiten: [Issue 37](epic-1-issue-37.md)
## Ziel und Grenzen
iOS/iPadOS-App für lokale Xcode-Builds 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: `native (Apple-Plattformprojekt)` (entsteht ab Issue 36). 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)
Technische Referenz: [Tauri 2](https://v2.tauri.app/).
## Implementierungsweg
1. IOS/iPadOS-Ziel für lokalen Xcode-Build konfigurieren.
2. WKWebView-Sitzungen, Safe Areas, Tastatur und Größenwechsel prüfen.
3. Benötigte lokale Einrichtung im Ticket dokumentieren.
## Akzeptanzkriterien
- [ ] Simulatorbuild funktioniert.
- [ ] Gerätebuild nutzt vom Nutzer bereitgestellte Signierung.
- [ ] Kein TestFlight- oder Store-Upload.
- [ ] IPad bleibt bedienbar.
## Prüfung
Zunächst den im Ticket genannten Plattformbuild und die beschriebenen Abläufe prüfen. Für neue App-Logik gezielte Tests für Navigation, Zustände oder URL-Verarbeitung ergänzen; Plattformverhalten im passenden Simulator bzw. auf einem geeigneten Gerät prüfen. Bei Vue/TypeScript-Änderungen abschließend `npm run types:check`; bei Backend-Anpassungen betroffene PHPUnit-Tests.
## 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.
+35
View File
@@ -0,0 +1,35 @@
# UI Rework 4: Responsive Anwendungsnavigation
Epic: [Epic 1](epic-1.md)
Abhängigkeiten: [Issue 3](epic-1-issue-3.md)
## Ziel und Grenzen
Responsive Anwendungsnavigation 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/layouts](../../resources/js/layouts); [resources/js/components/AppSidebar.vue](../../resources/js/components/AppSidebar.vue); [resources/js/types/navigation.ts](../../resources/js/types/navigation.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. Neue Sidebar, obere Werkzeugleiste, Projektnavigation und mobile Navigation aufbauen.
2. Nur vorhandene und erlaubte Bereiche verlinken.
3. Aktuelle Auswahl und kompakte Bildschirmgrößen berücksichtigen.
## Akzeptanzkriterien
- [ ] Alle bestehenden Bereiche bleiben erreichbar.
- [ ] Rollen sehen nur erlaubte Links.
- [ ] Browser-Zurück und mobile Navigation 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.
+37
View File
@@ -0,0 +1,37 @@
# UI Rework 40: App-Links, externe Links und Dateiauswahl
Epic: [Epic 1](epic-1.md)
Abhängigkeiten: [Issue 20](epic-1-issue-20.md), [Issue 21](epic-1-issue-21.md), [Issue 38](epic-1-issue-38.md), [Issue 39](epic-1-issue-39.md)
## Ziel und Grenzen
App-Links, externe Links und Dateiauswahl 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: `native` (entsteht ab Issue 36); [resources/js/components](../../resources/js/components). 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)
Technische Referenz: [Tauri 2](https://v2.tauri.app/), [Remote-Inhalte und native Berechtigungen](https://v2.tauri.app/security/capabilities/).
## Implementierungsweg
1. Interne App-Links zur ausgewählten Installation und externe Browserlinks umsetzen.
2. Avatar-Dateiauswahl, Zwischenablage und Browser-Zurück plattformübergreifend absichern.
3. Native Schnittstellen eng begrenzen.
## Akzeptanzkriterien
- [ ] Fremde Links wechseln nicht ungefragt den Server.
- [ ] Externe Git-Links funktionieren.
- [ ] Uploads und Kopieren funktionieren auf allen App-Plattformen.
## Prüfung
Zunächst den im Ticket genannten Plattformbuild und die beschriebenen Abläufe prüfen. Für neue App-Logik gezielte Tests für Navigation, Zustände oder URL-Verarbeitung ergänzen; Plattformverhalten im passenden Simulator bzw. auf einem geeigneten Gerät prüfen. Bei Vue/TypeScript-Änderungen abschließend `npm run types:check`; bei Backend-Anpassungen betroffene PHPUnit-Tests.
## 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.
+37
View File
@@ -0,0 +1,37 @@
# UI Rework 41: Signierter macOS-Release-Download
Epic: [Epic 1](epic-1.md)
Abhängigkeiten: [Issue 40](epic-1-issue-40.md)
## Ziel und Grenzen
Signierter macOS-Release-Download 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: `native` (entsteht ab Issue 36); [.github/workflows](../../.github/workflows). 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)
Technische Referenz: [Tauri 2](https://v2.tauri.app/), [macOS-Signierung](https://v2.tauri.app/distribute/sign/macos/).
## Implementierungsweg
1. Reproduzierbaren macOS-Releasebuild mit Signierung und Notarisierung vorbereiten.
2. Zertifikate nur als externe Secrets erwarten.
3. Installationsartefakt für Git-Releases erzeugen.
## Akzeptanzkriterien
- [ ] Builds verwenden keine eingecheckten Schlüssel.
- [ ] Signiertes Artefakt lässt sich mit bereitgestellten Voraussetzungen installieren.
- [ ] Fehlende Secrets werden klar gemeldet.
## Prüfung
Zunächst den im Ticket genannten Plattformbuild und die beschriebenen Abläufe prüfen. Für neue App-Logik gezielte Tests für Navigation, Zustände oder URL-Verarbeitung ergänzen; Plattformverhalten im passenden Simulator bzw. auf einem geeigneten Gerät prüfen. Bei Vue/TypeScript-Änderungen abschließend `npm run types:check`; bei Backend-Anpassungen betroffene PHPUnit-Tests.
## 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.
+38
View File
@@ -0,0 +1,38 @@
# UI Rework 42: Signiertes Android-APK für Git-Releases
Epic: [Epic 1](epic-1.md)
Abhängigkeiten: [Issue 40](epic-1-issue-40.md)
## Ziel und Grenzen
Signiertes Android-APK für Git-Releases 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: `native` (entsteht ab Issue 36); [.github/workflows](../../.github/workflows). 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)
Technische Referenz: [Tauri 2](https://v2.tauri.app/), [Android-Releasevorbereitung](https://developer.android.com/studio/publish/preparing).
## Implementierungsweg
1. Reproduzierbaren signierten APK-Releasebuild konfigurieren.
2. Dauerhafte Signing-Identität über Secrets beziehen.
3. Versionierung und Upgrade vorhandener Installationen berücksichtigen.
## Akzeptanzkriterien
- [ ] APK ist direkt installierbar.
- [ ] Updates behalten Daten/Sitzungen entsprechend Plattformverhalten.
- [ ] Schlüssel fehlen im Repository.
- [ ] Kein Play-Store-Upload.
## Prüfung
Zunächst den im Ticket genannten Plattformbuild und die beschriebenen Abläufe prüfen. Für neue App-Logik gezielte Tests für Navigation, Zustände oder URL-Verarbeitung ergänzen; Plattformverhalten im passenden Simulator bzw. auf einem geeigneten Gerät prüfen. Bei Vue/TypeScript-Änderungen abschließend `npm run types:check`; bei Backend-Anpassungen betroffene PHPUnit-Tests.
## 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.
+39
View File
@@ -0,0 +1,39 @@
# UI Rework 43: Getrennte Prüf- und Build-Pipelines
Epic: [Epic 1](epic-1.md)
Abhängigkeiten: [Issue 39](epic-1-issue-39.md), [Issue 41](epic-1-issue-41.md), [Issue 42](epic-1-issue-42.md)
## Ziel und Grenzen
Getrennte Prüf- und Build-Pipelines 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: [Dockerfile](../../Dockerfile); [.github/workflows/ci_cd.yml](../../.github/workflows/ci_cd.yml); [.github/tests/test_and_check_app/action.yml](../../.github/tests/test_and_check_app/action.yml). 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)
Technische Referenz: [Tauri 2](https://v2.tauri.app/).
## Implementierungsweg
1. Laravel/Web-Prüfungen von Tauri-Builds trennen.
2. Passende Linux-/macOS-Runner festlegen.
3. Änderungen nach Pfaden zuordnen, gemeinsame Änderungen vollständig prüfen.
4. Bestehendes Serverrelease erhalten.
## Akzeptanzkriterien
- [ ] Serverimage enthält keine native Toolchain.
- [ ] Erforderliche Prüfungen werden nicht übersprungen.
- [ ] macOS/Android-Artefakte sind getrennt.
- [ ] iOS bleibt Selbstbau.
## Prüfung
Zunächst den im Ticket genannten Plattformbuild und die beschriebenen Abläufe prüfen. Für neue App-Logik gezielte Tests für Navigation, Zustände oder URL-Verarbeitung ergänzen; Plattformverhalten im passenden Simulator bzw. auf einem geeigneten Gerät prüfen. Bei Vue/TypeScript-Änderungen abschließend `npm run types:check`; bei Backend-Anpassungen betroffene PHPUnit-Tests.
## 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.
+37
View File
@@ -0,0 +1,37 @@
# UI Rework 44: Web-Abnahme und Entfernung des alten Frontends
Epic: [Epic 1](epic-1.md)
Abhängigkeiten: [Issue 1](epic-1-issue-1.md), [Issue 2](epic-1-issue-2.md), [Issue 3](epic-1-issue-3.md), [Issue 4](epic-1-issue-4.md), [Issue 5](epic-1-issue-5.md), [Issue 6](epic-1-issue-6.md), [Issue 7](epic-1-issue-7.md), [Issue 8](epic-1-issue-8.md), [Issue 9](epic-1-issue-9.md), [Issue 10](epic-1-issue-10.md), [Issue 11](epic-1-issue-11.md), [Issue 12](epic-1-issue-12.md), [Issue 13](epic-1-issue-13.md), [Issue 14](epic-1-issue-14.md), [Issue 15](epic-1-issue-15.md), [Issue 16](epic-1-issue-16.md), [Issue 17](epic-1-issue-17.md), [Issue 18](epic-1-issue-18.md), [Issue 19](epic-1-issue-19.md), [Issue 20](epic-1-issue-20.md), [Issue 21](epic-1-issue-21.md), [Issue 22](epic-1-issue-22.md), [Issue 23](epic-1-issue-23.md), [Issue 24](epic-1-issue-24.md), [Issue 25](epic-1-issue-25.md), [Issue 26](epic-1-issue-26.md), [Issue 27](epic-1-issue-27.md), [Issue 28](epic-1-issue-28.md), [Issue 29](epic-1-issue-29.md), [Issue 30](epic-1-issue-30.md), [Issue 31](epic-1-issue-31.md), [Issue 32](epic-1-issue-32.md), [Issue 33](epic-1-issue-33.md), [Issue 34](epic-1-issue-34.md), [Issue 35](epic-1-issue-35.md)
## Ziel und Grenzen
Web-Abnahme und Entfernung des alten Frontends 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](../../resources/js); [resources/css](../../resources/css); [tests/Feature](../../tests/Feature). 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. Alle vorhandenen Seiten und Aktionen gegen die Bestandsliste prüfen.
2. Desktop/Tablet/Mobil sowie Hell/Dunkel/System visuell und funktional abnehmen.
3. Nur nicht mehr referenzierte alte UI-Dateien entfernen.
4. Größere neue Befunde in eigene Folgeaufgaben abgrenzen.
## Akzeptanzkriterien
- [ ] Keine Starterkit-Seite oder unübersetzte neue UI-Texte.
- [ ] Keine verlorenen Bestandsfunktionen.
- [ ] Keine bestehenden Tests entfernt.
- [ ] Relevante Tests, Build, Lint und abschließender Typecheck bestehen.
## 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.
+36
View File
@@ -0,0 +1,36 @@
# UI Rework 45: Plattformübergreifende App-Abnahme
Epic: [Epic 1](epic-1.md)
Abhängigkeiten: [Issue 40](epic-1-issue-40.md), [Issue 41](epic-1-issue-41.md), [Issue 42](epic-1-issue-42.md), [Issue 43](epic-1-issue-43.md), [Issue 44](epic-1-issue-44.md)
## Ziel und Grenzen
Plattformübergreifende App-Abnahme 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: `native` (entsteht ab Issue 36); [resources/js](../../resources/js). 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. macOS, Android und iOS/iPadOS mit fertigem Webfrontend prüfen.
2. Serverwechsel, Sessionablauf, Serverupdate, Verbindungsabbruch, Links und Dateiauswahl abnehmen.
3. Dokumentierte reproduzierbare Befunde mit klaren Reproduktionsschritten festhalten.
## Akzeptanzkriterien
- [ ] Alle vereinbarten App-Abläufe bestehen.
- [ ] Keine Produktionsdaten als Testfixtures verändert.
- [ ] Releasewege entsprechen Vereinbarung.
- [ ] Größere Defekte werden nicht im Abnahmeticket versteckt.
## Prüfung
Zunächst den im Ticket genannten Plattformbuild und die beschriebenen Abläufe prüfen. Für neue App-Logik gezielte Tests für Navigation, Zustände oder URL-Verarbeitung ergänzen; Plattformverhalten im passenden Simulator bzw. auf einem geeigneten Gerät prüfen. Bei Vue/TypeScript-Änderungen abschließend `npm run types:check`; bei Backend-Anpassungen betroffene PHPUnit-Tests.
## 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.
+35
View File
@@ -0,0 +1,35 @@
# UI Rework 5: Dialoge, Rückmeldungen und Eingabeschutz
Epic: [Epic 1](epic-1.md)
Abhängigkeiten: [Issue 3](epic-1-issue-3.md), [Issue 4](epic-1-issue-4.md)
## Ziel und Grenzen
Dialoge, Rückmeldungen und Eingabeschutz 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/composables](../../resources/js/composables). 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. Dialoge, Menüs, Leer-, Lade- und Fehlerzustände vereinheitlichen.
2. Formfehler und Wiederholaktionen definieren.
3. Ungespeicherte Eingaben gegen Navigation und fehlgeschlagene Requests absichern.
## Akzeptanzkriterien
- [ ] Keine verlorenen Eingaben bei Requestfehlern.
- [ ] Dialogfokus wird korrekt zurückgegeben.
- [ ] Netzwerkfehler und serverseitige Validierung sind unterscheidbar.
## 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.
+37
View File
@@ -0,0 +1,37 @@
# UI Rework 6: Anmeldung, E-Mail-Verifikation und 2FA-Challenge
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
Anmeldung, E-Mail-Verifikation und 2FA-Challenge 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/auth/Login.vue](../../resources/js/pages/auth/Login.vue); [resources/js/pages/auth/VerifyEmail.vue](../../resources/js/pages/auth/VerifyEmail.vue); [resources/js/pages/auth/TwoFactorChallenge.vue](../../resources/js/pages/auth/TwoFactorChallenge.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. Die drei Ansichten vollständig neu erstellen.
2. Bestehende Fortify-Routen und Sessionflüsse verwenden.
3. Mobile Tastatur, Autofill und Recovery-Codes berücksichtigen.
## Akzeptanzkriterien
- [ ] Normale und zweistufige Anmeldung funktionieren.
- [ ] Fehler erhalten passende Eingaben.
- [ ] Bestehende Autorisierungsregeln bleiben wirksam.
## Prüfung
Vorhandene gezielte Abdeckung: [tests/Feature/Auth/AuthenticationTest.php](../../tests/Feature/Auth/AuthenticationTest.php), [tests/Feature/Auth/TwoFactorChallengeTest.php](../../tests/Feature/Auth/TwoFactorChallengeTest.php), [tests/Feature/Auth/EmailVerificationTest.php](../../tests/Feature/Auth/EmailVerificationTest.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.
+37
View File
@@ -0,0 +1,37 @@
# UI Rework 7: Passwort- und Bestätigungsseiten
Epic: [Epic 1](epic-1.md)
Abhängigkeiten: [Issue 6](epic-1-issue-6.md)
## Ziel und Grenzen
Passwort- und Bestätigungsseiten 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/auth/ForgotPassword.vue](../../resources/js/pages/auth/ForgotPassword.vue); [resources/js/pages/auth/ResetPassword.vue](../../resources/js/pages/auth/ResetPassword.vue); [resources/js/pages/auth/ConfirmPassword.vue](../../resources/js/pages/auth/ConfirmPassword.vue); [resources/js/pages/auth/ForcePasswordChange.vue](../../resources/js/pages/auth/ForcePasswordChange.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. Passwort-Reset, sensible Aktionsbestätigung und erzwungenen Passwortwechsel neu gestalten.
2. Bestehende Fortify-Verträge erhalten.
3. Zustände abgelaufener oder ungültiger Links berücksichtigen.
## Akzeptanzkriterien
- [ ] Alle vier Flüsse funktionieren mit bestehenden Endpunkten.
- [ ] Sensible Werte werden nicht in Entwürfen gespeichert.
- [ ] Erfolg und Fehler sind klar.
## Prüfung
Vorhandene gezielte Abdeckung: [tests/Feature/Auth/PasswordResetTest.php](../../tests/Feature/Auth/PasswordResetTest.php), [tests/Feature/Auth/PasswordConfirmationTest.php](../../tests/Feature/Auth/PasswordConfirmationTest.php), [tests/Feature/Settings/SecurityTest.php](../../tests/Feature/Settings/SecurityTest.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.
+37
View File
@@ -0,0 +1,37 @@
# UI Rework 8: Projektübersicht und Projekteinstieg
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
Projektübersicht und Projekteinstieg 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/Index.vue](../../resources/js/pages/projects/Index.vue); [resources/js/pages/projects/Show.vue](../../resources/js/pages/projects/Show.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. Projektübersicht und Projekteinstieg neu aufbauen.
2. Erlaubte Aktionen und direkte Wege zu Board, Backlog und Einstellungen anbieten.
3. Vorhandene Resources nutzen.
## Akzeptanzkriterien
- [ ] Nur sichtbare Projekte erscheinen.
- [ ] Leerzustand und eingeschränkte Rechte sind abgedeckt.
- [ ] Keine Mockup-Funktionen ohne Backend.
## Prüfung
Vorhandene gezielte Abdeckung: [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.
+37
View File
@@ -0,0 +1,37 @@
# UI Rework 9: Projektanlage, Duplikation und Vorlagenauswahl
Epic: [Epic 1](epic-1.md)
Abhängigkeiten: [Issue 8](epic-1-issue-8.md)
## Ziel und Grenzen
Projektanlage, Duplikation und Vorlagenauswahl 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/Create.vue](../../resources/js/pages/projects/Create.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. Neuen Erstellablauf für leeres Projekt, Duplikat und Vorlage bauen.
2. Bedingte Felder und Kopieroptionen aus dem Bestand übernehmen.
3. Backend-Verträge nur bei Darstellungsbedarf anpassen.
## Akzeptanzkriterien
- [ ] Alle drei vorhandenen Erstellwege bleiben korrekt.
- [ ] Validierung ist verständlich.
- [ ] Operative Ticketdaten werden weiterhin nicht dupliziert.
## Prüfung
Vorhandene gezielte Abdeckung: [tests/Feature/Projects/ProjectControllerTest.php](../../tests/Feature/Projects/ProjectControllerTest.php), [tests/Feature/Projects/ProjectTemplateControllerTest.php](../../tests/Feature/Projects/ProjectTemplateControllerTest.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.
+15
View File
@@ -0,0 +1,15 @@
# Epic 1: KanbanGit UI Rework und Tauri-Apps
## Scope
Die vorhandene, aus dem Laravel-Starterkit entstandene Oberfläche vollständig durch eine neue Vue/Inertia-Oberfläche im Stil von [Fluid Emerald Kanban](https://stitch.withgoogle.com/projects/9754281177402648015) ersetzen. Alle bestehenden Produktfunktionen erhalten ein neues, auf Desktop und Mobilgeräte abgestimmtes Erscheinungsbild.
Eine gemeinsame servergeladene Oberfläche versorgt Browser sowie Tauri-Apps für macOS, iOS/iPadOS und Android. Backend und Clients bleiben im selben Repository. Selbst gehostete Installationen werden über eine frei wählbare HTTPS-Adresse angebunden; jeweils eine Installation ist aktiv.
Zum Zielumfang gehören Hell/Dunkel/System, eine Lokalisierungsstruktur mit zunächst englischen Texten und bessere Frontend-Bedienung einschließlich Drag-and-drop für bestehende Statuswechsel. Anpassungen an Resources, Inertia-Daten und erforderlichen Routen sind zulässig.
Ausgeschlossen sind neue fachliche Backend-Funktionen, zusätzliche Mockup-Module, Offline-Datensynchronisierung, persistierte freie Kartenreihenfolge und Hintergrund-Push. macOS und Android werden als signierte Git-Release-Downloads vorbereitet; iOS/iPadOS wird zunächst selbst über Xcode gebaut. Stores folgen später.
## Abschluss
Die neue Oberfläche deckt die bestehenden Funktionen ab, funktioniert auf allen vereinbarten Plattformen und hat das alte Starterkit-Frontend vollständig abgelöst. Die Umsetzung ist in eigenständige, begrenzte Agentensitzungen aufgeteilt.
-1
View File
@@ -25,7 +25,6 @@ const prettierOptions = {
const eslintConfig = [
{
ignores: [
'bootstrap/ssr/**',
'**/dist/**',
'node_modules/**',
'.github/**',
+7269
View File
File diff suppressed because it is too large Load Diff
-8
View File
@@ -2,13 +2,6 @@
"$schema": "https://www.schemastore.org/package.json",
"private": true,
"type": "module",
"devEngines": {
"packageManager": {
"name": "pnpm",
"version": "11.26.0+sha512.fc0e2bf890b9f983611f1ab68c0637bce914390653699f83c1a78b005ed25f2c81e77920c8fd8eee2ecf0b58b28cdcb00a84d97f69bcf7c56b2f344710238664",
"onFail": "error"
}
},
"scripts": {
"build": "vite build",
"build:ssr": "vite build && vite build --ssr",
@@ -46,7 +39,6 @@
"vue-tsc": "^2.2.4"
},
"dependencies": {
"@inertiajs/core": "2.3.18",
"@inertiajs/vue3": "^2.3.7",
"@vueuse/core": "^12.8.2",
"class-variance-authority": "^0.7.1",
+1
View File
@@ -19,6 +19,7 @@
</source>
<php>
<env name="APP_ENV" value="testing"/>
<env name="APP_KEY" value="base64:11ujGYe5vzbwqcdFDVQC6PuDgd7c8PKD9kWt9HH9Bvs="/>
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
<env name="AVATAR_FILESYSTEM_DISK" value="public"/>
<env name="AVATAR_URLS_TEMPORARY" value="false"/>
-4832
View File
File diff suppressed because it is too large Load Diff
-4
View File
@@ -1,4 +0,0 @@
minimumReleaseAge: 10080
allowBuilds:
unrs-resolver: true
vue-demi: true
+10 -11
View File
@@ -1,17 +1,16 @@
<script setup lang="ts">
import { Columns3 } from 'lucide-vue-next';
import AppLogoIcon from '@/components/AppLogoIcon.vue';
</script>
<template>
<span class="flex min-w-0 items-center gap-3">
<span
class="bg-brand text-brand-foreground flex size-8 shrink-0 items-center justify-center rounded-md"
<div
class="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-md"
>
<AppLogoIcon class="size-5 fill-current text-white dark:text-black" />
</div>
<div class="ml-1 grid flex-1 text-left text-sm">
<span class="mb-0.5 truncate font-semibold leading-tight"
>Laravel Starter Kit</span
>
<Columns3 class="size-5" aria-hidden="true" />
</span>
<span
class="text-title truncate font-semibold group-data-[collapsible=icon]:hidden"
>KanbanGit</span
>
</span>
</div>
</template>
+90 -86
View File
@@ -1,10 +1,19 @@
<script setup lang="ts">
import { Link, router } from '@inertiajs/vue3';
import { Settings2, X } from 'lucide-vue-next';
import { computed, onMounted, onUnmounted } from 'vue';
import { Link, usePage } from '@inertiajs/vue3';
import {
BookOpen,
Folder,
FolderGit2,
LayoutGrid,
Layers3,
Shield,
Users,
} from 'lucide-vue-next';
import { computed } from 'vue';
import AppLogo from '@/components/AppLogo.vue';
import NavFooter from '@/components/NavFooter.vue';
import NavMain from '@/components/NavMain.vue';
import { Button } from '@/components/ui/button';
import NavUser from '@/components/NavUser.vue';
import {
Sidebar,
SidebarContent,
@@ -13,98 +22,93 @@ import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from '@/components/ui/sidebar';
import { useAppNavigation } from '@/composables/useAppNavigation';
import { useCurrentUrl } from '@/composables/useCurrentUrl';
import { useLocalization } from '@/composables/useLocalization';
import { dashboard } from '@/routes';
import { edit as profile } from '@/routes/profile';
import { show } from '@/routes/projects';
import { index as adminUsers } from '@/routes/admin/users';
import { index as projectTemplatesIndex } from '@/routes/project-templates';
import { index as projectsIndex } from '@/routes/projects';
import { index as teamsIndex } from '@/routes/teams';
import type { NavItem } from '@/types';
const { t } = useLocalization();
const { currentUrl } = useCurrentUrl();
const { project, workspaceItems, projectItems, projectSettingsItems } =
useAppNavigation();
const { isMobile, setOpenMobile } = useSidebar();
const accountItems = computed(() => [
{
title: t('navigation.settings'),
href: profile(),
icon: Settings2,
isActive: currentUrl.value.startsWith('/settings/'),
},
]);
let removeNavigationListener: (() => void) | undefined;
onMounted(() => {
removeNavigationListener = router.on('navigate', () =>
setOpenMobile(false),
);
const page = usePage();
const mainNavItems = computed<NavItem[]>(() => {
const items: NavItem[] = [
{
title: 'Dashboard',
href: dashboard(),
icon: LayoutGrid,
},
{
title: 'Projects',
href: projectsIndex(),
icon: Folder,
},
];
if (page.props.auth.permissions.projectCreate) {
items.push({
title: 'Project templates',
href: projectTemplatesIndex(),
icon: Layers3,
});
}
if (page.props.auth.permissions.globalAdmin) {
items.push({
title: 'Users',
href: adminUsers(),
icon: Shield,
});
}
if (page.props.auth.permissions.canManageTeams) {
items.push({
title: 'Teams',
href: teamsIndex(),
icon: Users,
});
}
return items;
});
onUnmounted(() => removeNavigationListener?.());
const footerNavItems: NavItem[] = [
{
title: 'Repository',
href: 'https://github.com/laravel/vue-starter-kit',
icon: FolderGit2,
},
{
title: 'Documentation',
href: 'https://laravel.com/docs/starter-kits#vue',
icon: BookOpen,
},
];
</script>
<template>
<Sidebar id="app-navigation" collapsible="icon" variant="sidebar">
<SidebarHeader class="border-sidebar-border gap-4 border-b p-3">
<div class="flex items-center gap-2">
<SidebarMenu class="min-w-0">
<SidebarMenuItem>
<SidebarMenuButton
size="lg"
as-child
tooltip="KanbanGit"
>
<Link :href="dashboard()" aria-label="KanbanGit"
><AppLogo
/></Link>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
<Button
v-if="isMobile"
variant="ghost"
size="icon"
:aria-label="t('navigation.close')"
@click="setOpenMobile(false)"
>
<X aria-hidden="true" />
</Button>
</div>
<Link
v-if="project"
:href="show(project.id)"
class="bg-sidebar-accent flex min-w-0 items-center gap-3 rounded-md p-3 group-data-[collapsible=icon]:hidden"
>
<span
class="text-label bg-brand/15 text-primary shrink-0 rounded-md px-2 py-1"
>{{ project.key }}</span
>
<span class="truncate font-semibold" :title="project.name">{{
project.name
}}</span>
</Link>
<Sidebar collapsible="icon" variant="inset">
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" as-child>
<Link :href="dashboard()">
<AppLogo />
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<nav :aria-label="t('navigation.main')">
<NavMain
:items="workspaceItems"
:label="t('navigation.workspace')"
/>
<NavMain
v-if="projectItems.length"
:items="projectItems"
:label="t('navigation.project')"
/>
<NavMain
v-if="projectSettingsItems.length"
:items="projectSettingsItems"
:label="t('navigation.projectSettings')"
/>
</nav>
<NavMain :items="mainNavItems" />
</SidebarContent>
<SidebarFooter class="border-sidebar-border border-t p-0">
<NavMain :items="accountItems" :label="t('navigation.account')" />
<SidebarFooter>
<NavFooter :items="footerNavItems" />
<NavUser />
</SidebarFooter>
</Sidebar>
<slot />
</template>
+13 -131
View File
@@ -1,145 +1,27 @@
<script setup lang="ts">
import { Link, usePage } from '@inertiajs/vue3';
import { Monitor, Moon, Plus, Sun } from 'lucide-vue-next';
import { computed } from 'vue';
import Breadcrumbs from '@/components/Breadcrumbs.vue';
import UserMenuContent from '@/components/UserMenuContent.vue';
import { UserAvatar } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { SidebarTrigger } from '@/components/ui/sidebar';
import { useAppNavigation } from '@/composables/useAppNavigation';
import {
normalizeAppearance,
useAppearance,
} from '@/composables/useAppearance';
import { useCurrentUrl } from '@/composables/useCurrentUrl';
import { useLocalization } from '@/composables/useLocalization';
import { create as createProject } from '@/routes/projects';
import { index as board } from '@/routes/projects/board';
import { create as createTicket } from '@/routes/projects/tickets';
import type { BreadcrumbItem } from '@/types';
withDefaults(defineProps<{ breadcrumbs?: BreadcrumbItem[] }>(), {
breadcrumbs: () => [],
});
const page = usePage();
const { project } = useAppNavigation();
const { isCurrentUrl } = useCurrentUrl();
const { t } = useLocalization();
const { appearance, updateAppearance } = useAppearance();
const themeIcon = computed(
() => ({ light: Sun, dark: Moon, system: Monitor })[appearance.value],
withDefaults(
defineProps<{
breadcrumbs?: BreadcrumbItem[];
}>(),
{
breadcrumbs: () => [],
},
);
const createAction = computed(() => {
if (project.value?.canCreateTickets) {
return {
title: t('navigation.createTicket'),
href: createTicket(project.value.id, {
query: {
source: isCurrentUrl(board(project.value.id))
? 'board'
: 'backlog',
},
}),
};
}
if (!project.value && page.props.auth.permissions.projectCreate) {
return { title: t('navigation.createProject'), href: createProject() };
}
return null;
});
</script>
<template>
<header
class="bg-background md:px-page sticky top-0 z-20 flex min-h-16 min-w-0 shrink-0 items-center justify-between gap-2 border-b px-3 md:gap-4"
class="border-sidebar-border/70 group-has-data-[collapsible=icon]/sidebar-wrapper:h-12 flex h-16 shrink-0 items-center gap-2 border-b px-6 transition-[width,height] ease-linear md:px-4"
>
<div class="flex min-w-0 flex-1 items-center gap-2">
<SidebarTrigger class="shrink-0" />
<Breadcrumbs v-if="breadcrumbs.length" :breadcrumbs="breadcrumbs" />
<span v-else class="truncate font-semibold">KanbanGit</span>
</div>
<div class="flex shrink-0 items-center gap-1 sm:gap-2">
<Button
v-if="createAction"
as-child
:aria-label="createAction.title"
class="px-3"
>
<Link :href="createAction.href"
><Plus aria-hidden="true" /><span
class="hidden sm:inline"
>{{ createAction.title }}</span
></Link
>
</Button>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button
variant="ghost"
size="icon"
:aria-label="t('appearance.label')"
><component :is="themeIcon" aria-hidden="true"
/></Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>{{
t('appearance.label')
}}</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup
:model-value="appearance"
@update:model-value="
(value) =>
updateAppearance(normalizeAppearance(value))
"
>
<DropdownMenuRadioItem value="light"
><Sun aria-hidden="true" />{{
t('appearance.light')
}}</DropdownMenuRadioItem
>
<DropdownMenuRadioItem value="dark"
><Moon aria-hidden="true" />{{
t('appearance.dark')
}}</DropdownMenuRadioItem
>
<DropdownMenuRadioItem value="system"
><Monitor aria-hidden="true" />{{
t('appearance.system')
}}</DropdownMenuRadioItem
>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button
variant="ghost"
size="icon"
:aria-label="
t('navigation.userMenu', {
name: page.props.auth.user.name,
})
"
data-test="sidebar-menu-button"
>
<UserAvatar :user="page.props.auth.user" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" class="min-w-60">
<UserMenuContent :user="page.props.auth.user" />
</DropdownMenuContent>
</DropdownMenu>
<div class="flex items-center gap-2">
<SidebarTrigger class="-ml-1" />
<template v-if="breadcrumbs && breadcrumbs.length > 0">
<Breadcrumbs :breadcrumbs="breadcrumbs" />
</template>
</div>
</header>
</template>
@@ -1,78 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue';
import FormFeedback from '@/components/FormFeedback.vue';
import { Button } from '@/components/ui/button';
import { useLocalization } from '@/composables/useLocalization';
import type { FormFeedbackState } from '@/lib/form-feedback';
import { login } from '@/routes/index';
const props = defineProps<{
state: FormFeedbackState;
responseStatus?: number | null;
loadingTitle?: string;
successTitle?: string;
}>();
defineEmits<{ retry: [] }>();
const { t } = useLocalization();
const expired = computed(
() => props.state === 'server' && props.responseStatus === 419,
);
const limited = computed(
() => props.state === 'server' && props.responseStatus === 429,
);
const title = computed(() => {
if (expired.value) {
return t('auth.feedback.expiredTitle');
}
if (limited.value) {
return t('auth.feedback.limitedTitle');
}
if (props.state === 'loading') {
return props.loadingTitle ?? t('auth.feedback.pending');
}
if (props.state === 'success') {
return props.successTitle ?? t('auth.feedback.completed');
}
if (props.state === 'validation') {
return t('auth.feedback.validationTitle');
}
return undefined;
});
const description = computed(() => {
if (expired.value) {
return t('auth.feedback.expiredDescription');
}
if (limited.value) {
return t('auth.feedback.limitedDescription');
}
if (props.state === 'validation') {
return t('auth.feedback.validationDescription');
}
if (props.state === 'network') {
return t('auth.feedback.networkDescription');
}
if (props.state === 'server') {
return t('auth.feedback.serverDescription');
}
if (props.state === 'cancelled') {
return t('auth.feedback.cancelledDescription');
}
return undefined;
});
</script>
<template>
<div class="gap-card grid" v-show="state !== 'idle'">
<FormFeedback
:state="state"
:title="title"
:description="description"
:retryable="!expired"
@retry="$emit('retry')"
/>
<Button v-if="expired" as-child variant="outline">
<!-- A fresh document renews the session/CSRF state and drops in-memory secrets. -->
<a :href="login.url()">{{ t('auth.backToLogin') }}</a>
</Button>
</div>
</template>
@@ -1,209 +0,0 @@
<script setup lang="ts">
import { Form } from '@inertiajs/vue3';
import { ChevronDown, ChevronRight } from 'lucide-vue-next';
import { move } from '@/actions/App/Http/Controllers/ProjectBoardController';
import InputError from '@/components/InputError.vue';
import { TicketCard } from '@/components/tickets';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { useLocalization } from '@/composables/useLocalization';
import { ticketStatusLabel } from '@/lib/tickets';
import type {
BacklogTicketTreeEmits,
BacklogTicketTreeProps,
} from '@/types/backlog';
import type { TicketModel } from '@/types/models/ticket-model';
const props = defineProps<BacklogTicketTreeProps>();
const emit = defineEmits<BacklogTicketTreeEmits>();
const { t } = useLocalization();
const allowedTargetStatuses = (ticket: TicketModel) =>
(ticket.ticket_status?.allowed_target_statuses ?? []).filter(
(status) => status.id !== ticket.ticket_status_id,
);
</script>
<template>
<ul class="grid min-w-0 gap-3">
<li v-for="node in nodes" :key="node.ticket.id" class="min-w-0">
<div class="grid min-w-0 grid-cols-[2.75rem_minmax(0,1fr)] gap-2">
<div class="flex justify-center pt-1">
<button
v-if="node.children.length > 0"
type="button"
class="border-border bg-card text-muted-foreground hover:border-primary hover:text-primary focus-visible:ring-ring/35 flex size-10 items-center justify-center rounded-md border transition-colors focus-visible:outline-none focus-visible:ring-2"
:aria-expanded="!collapsedTicketIds.has(node.ticket.id)"
:aria-controls="`backlog-children-${node.ticket.id}`"
:aria-label="
t(
collapsedTicketIds.has(node.ticket.id)
? 'backlog.tree.expand'
: 'backlog.tree.collapse',
{ key: node.ticket.ticket_key },
)
"
@click="emit('toggle', node.ticket.id)"
>
<ChevronRight
v-if="collapsedTicketIds.has(node.ticket.id)"
class="size-4"
aria-hidden="true"
/>
<ChevronDown v-else class="size-4" aria-hidden="true" />
</button>
<span
v-else
class="bg-border mt-4 size-1.5 rounded-full"
aria-hidden="true"
/>
</div>
<div class="grid min-w-0 gap-2">
<Badge
v-if="!node.matchesFilters"
variant="secondary"
class="w-fit"
>
{{ t('backlog.filters.parentContext') }}
</Badge>
<TicketCard
:project-id="projectId"
:ticket="node.ticket"
layout="list"
:class="{ 'opacity-80': !node.matchesFilters }"
>
<template v-if="canEditTickets" #actions>
<Form
v-bind="
move.form({
project: projectId,
ticket: node.ticket.id,
})
"
v-slot="{ processing, errors }"
:error-bag="`backlog-ticket-${node.ticket.id}`"
:options="{ preserveScroll: true }"
:on-before="() => pendingTicketId === null"
class="grid min-w-0 gap-2"
@start="emit('moveStart', node.ticket.id)"
@finish="emit('moveFinish', node.ticket.id)"
>
<input
type="hidden"
name="redirect_to"
value="backlog"
/>
<label
:for="`backlog-ticket-status-${node.ticket.id}`"
class="text-caption text-muted-foreground font-semibold"
>
{{
t('backlog.moveTicket', {
key: node.ticket.ticket_key,
})
}}
</label>
<select
:id="`backlog-ticket-status-${node.ticket.id}`"
name="ticket_status_id"
:disabled="
pendingTicketId !== null ||
processing ||
allowedTargetStatuses(node.ticket)
.length === 0
"
:aria-invalid="
!!errors.ticket_status_id || undefined
"
:aria-describedby="
errors.ticket_status_id
? `backlog-ticket-status-error-${node.ticket.id}`
: undefined
"
class="border-input bg-card text-body min-h-control focus-visible:ring-ring/35 w-full min-w-0 rounded-md border px-3 py-2 focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-60"
>
<option
v-if="
allowedTargetStatuses(node.ticket)
.length === 0
"
value=""
>
{{ t('board.noMoves') }}
</option>
<option
v-for="target in allowedTargetStatuses(
node.ticket,
)"
:key="target.id"
:value="target.id"
>
{{
ticketStatusLabel(
target,
t('tickets.noStatus'),
)
}}
</option>
</select>
<InputError
:id="`backlog-ticket-status-error-${node.ticket.id}`"
:message="errors.ticket_status_id"
/>
<Button
type="submit"
variant="outline"
:disabled="
pendingTicketId !== null ||
processing ||
allowedTargetStatuses(node.ticket)
.length === 0
"
>
{{
t(
processing
? 'board.updatingStatus'
: 'board.updateStatus',
)
}}
</Button>
<p
v-if="
allowedTargetStatuses(node.ticket)
.length === 0
"
class="text-caption text-muted-foreground"
>
{{ t('board.noMovesDescription') }}
</p>
</Form>
</template>
</TicketCard>
</div>
</div>
<div
v-if="
node.children.length > 0 &&
!collapsedTicketIds.has(node.ticket.id)
"
:id="`backlog-children-${node.ticket.id}`"
class="border-border ml-[1.375rem] mt-3 border-l pl-2 sm:pl-4"
>
<BacklogTicketTree
:nodes="node.children"
:project-id="projectId"
:can-edit-tickets="canEditTickets"
:collapsed-ticket-ids="collapsedTicketIds"
:pending-ticket-id="pendingTicketId"
@toggle="emit('toggle', $event)"
@move-start="emit('moveStart', $event)"
@move-finish="emit('moveFinish', $event)"
/>
</div>
</li>
</ul>
</template>
+6 -25
View File
@@ -9,48 +9,29 @@ import {
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb';
import type { BreadcrumbItem as BreadcrumbItemType } from '@/types';
import { useLocalization } from '@/composables/useLocalization';
type Props = {
breadcrumbs: BreadcrumbItemType[];
};
defineProps<Props>();
const { t } = useLocalization();
</script>
<template>
<Breadcrumb class="min-w-0" :aria-label="t('navigation.breadcrumbs')">
<BreadcrumbList class="flex-nowrap">
<Breadcrumb>
<BreadcrumbList>
<template v-for="(item, index) in breadcrumbs" :key="index">
<BreadcrumbItem
:class="[
'min-w-0',
index !== breadcrumbs.length - 1
? 'hidden md:inline-flex'
: '',
]"
>
<BreadcrumbItem>
<template v-if="index === breadcrumbs.length - 1">
<BreadcrumbPage class="truncate" :title="item.title">{{
item.title
}}</BreadcrumbPage>
<BreadcrumbPage>{{ item.title }}</BreadcrumbPage>
</template>
<template v-else>
<BreadcrumbLink as-child>
<Link
:href="item.href"
class="max-w-40 truncate"
:title="item.title"
>{{ item.title }}</Link
>
<Link :href="item.href">{{ item.title }}</Link>
</BreadcrumbLink>
</template>
</BreadcrumbItem>
<BreadcrumbSeparator
v-if="index !== breadcrumbs.length - 1"
class="hidden md:block"
/>
<BreadcrumbSeparator v-if="index !== breadcrumbs.length - 1" />
</template>
</BreadcrumbList>
</Breadcrumb>
-72
View File
@@ -1,72 +0,0 @@
<script setup lang="ts">
import { CircleAlert, CircleCheck, Inbox, LoaderCircle } from 'lucide-vue-next';
import { computed } from 'vue';
import { Button } from '@/components/ui/button';
import { useLocalization } from '@/composables/useLocalization';
const props = withDefaults(
defineProps<{
state: 'empty' | 'loading' | 'error' | 'success';
title?: string;
description?: string;
retryable?: boolean;
}>(),
{ retryable: false },
);
defineEmits<{ retry: [] }>();
const { t } = useLocalization();
const icons = {
empty: Inbox,
loading: LoaderCircle,
error: CircleAlert,
success: CircleCheck,
};
const defaultTitles = {
empty: 'feedback.empty',
loading: 'common.loading',
error: 'common.error',
success: 'feedback.saved',
} as const;
const heading = computed(() => props.title ?? t(defaultTitles[props.state]));
</script>
<template>
<div
:role="state === 'error' ? 'alert' : 'status'"
:aria-busy="state === 'loading' || undefined"
:data-feedback="state"
class="gap-card bg-card p-card text-body text-card-foreground flex min-w-0 items-start rounded-md border"
:class="{
'border-destructive/40': state === 'error',
'border-success/40': state === 'success',
}"
>
<component
:is="icons[state]"
aria-hidden="true"
class="mt-0.5 size-5 shrink-0"
:class="{
'text-primary animate-spin motion-reduce:animate-none':
state === 'loading',
'text-destructive': state === 'error',
'text-success-foreground': state === 'success',
'text-muted-foreground': state === 'empty',
}"
/>
<div class="gap-stack grid min-w-0 flex-1 break-words">
<p class="font-semibold">{{ heading }}</p>
<p v-if="description" class="text-muted-foreground">
{{ description }}
</p>
<div v-if="$slots.default"><slot /></div>
<Button
v-if="retryable && state === 'error'"
type="button"
variant="outline"
class="justify-self-start"
@click="$emit('retry')"
>{{ t('common.retry') }}</Button
>
</div>
</div>
</template>
-76
View File
@@ -1,76 +0,0 @@
<script setup lang="ts">
import { computed, nextTick, useTemplateRef, watch } from 'vue';
import FeedbackState from '@/components/FeedbackState.vue';
import { useLocalization } from '@/composables/useLocalization';
import type { FormFeedbackState } from '@/lib/form-feedback';
const props = withDefaults(
defineProps<{
state: FormFeedbackState;
title?: string;
description?: string;
retryable?: boolean;
}>(),
{ retryable: true },
);
defineEmits<{ retry: [] }>();
const { t } = useLocalization();
const feedback = useTemplateRef<HTMLDivElement>('feedback');
watch(
() => props.state,
async (state) => {
if (['validation', 'network', 'server', 'cancelled'].includes(state)) {
await nextTick();
if (props.state === state) {
feedback.value?.focus();
}
}
},
);
const titles = {
idle: 'feedback.saved',
loading: 'forms.saving',
success: 'feedback.saved',
validation: 'forms.validationTitle',
network: 'forms.networkTitle',
server: 'forms.serverTitle',
cancelled: 'forms.cancelledTitle',
} as const;
const descriptions = {
validation: 'forms.validationDescription',
network: 'forms.networkDescription',
server: 'forms.serverDescription',
cancelled: 'forms.cancelledDescription',
} as const;
const description = computed(() =>
props.state in descriptions
? t(descriptions[props.state as keyof typeof descriptions])
: undefined,
);
</script>
<template>
<div
ref="feedback"
v-show="state !== 'idle'"
:data-form-state="state"
tabindex="-1"
class="focus-visible:ring-ring/35 rounded-md outline-none focus-visible:ring-2"
>
<FeedbackState
v-if="state !== 'idle'"
:state="
state === 'loading' || state === 'success' ? state : 'error'
"
:title="props.title ?? t(titles[state])"
:description="props.description ?? description"
:retryable="
props.retryable !== false &&
(state === 'network' ||
state === 'server' ||
state === 'cancelled')
"
@retry="$emit('retry')"
/>
</div>
</template>
+2 -2
View File
@@ -5,8 +5,8 @@ defineProps<{
</script>
<template>
<div v-show="message" role="alert">
<p class="text-body text-destructive">
<div v-show="message">
<p class="text-sm text-red-600 dark:text-red-500">
{{ message }}
</p>
</div>
+6 -29
View File
@@ -12,46 +12,23 @@ import type { NavItem } from '@/types';
defineProps<{
items: NavItem[];
label: string;
}>();
const { isCurrentUrl } = useCurrentUrl();
</script>
<template>
<SidebarGroup class="px-3 py-2">
<SidebarGroupLabel class="text-label text-muted-foreground uppercase">{{
label
}}</SidebarGroupLabel>
<SidebarMenu role="list" :aria-label="label">
<SidebarGroup class="px-2 py-0">
<SidebarGroupLabel>Platform</SidebarGroupLabel>
<SidebarMenu>
<SidebarMenuItem v-for="item in items" :key="item.title">
<SidebarMenuButton
as-child
:is-active="
item.isActive ??
isCurrentUrl(
item.href,
undefined,
item.match === 'section',
)
"
:is-active="isCurrentUrl(item.href)"
:tooltip="item.title"
class="min-h-control data-[active=true]:border-brand border-l-2 border-transparent px-3 data-[active=true]:font-semibold group-data-[collapsible=icon]:min-h-8"
>
<Link
:href="item.href"
:aria-current="
(item.isActive ??
isCurrentUrl(
item.href,
undefined,
item.match === 'section',
))
? 'page'
: undefined
"
>
<component :is="item.icon" aria-hidden="true" />
<Link :href="item.href">
<component :is="item.icon" />
<span>{{ item.title }}</span>
</Link>
</SidebarMenuButton>
+4 -11
View File
@@ -2,7 +2,6 @@
import { Eye, EyeOff } from 'lucide-vue-next';
import { ref, useTemplateRef } from 'vue';
import type { HTMLAttributes } from 'vue';
import { useLocalization } from '@/composables/useLocalization';
import { Input } from '@/components/ui/input';
import { cn } from '@/lib/utils';
@@ -10,14 +9,12 @@ defineOptions({ inheritAttrs: false });
const props = defineProps<{
class?: HTMLAttributes['class'];
disabled?: boolean;
}>();
const model = defineModel<string | number | null>();
const showPassword = ref(false);
const inputRef = useTemplateRef('inputRef');
const { t } = useLocalization();
defineExpose({
$el: inputRef,
@@ -31,8 +28,7 @@ defineExpose({
ref="inputRef"
v-model="model"
:type="showPassword ? 'text' : 'password'"
:class="cn('pr-12', props.class)"
:disabled="disabled"
:class="cn('pr-10', props.class)"
v-bind="$attrs"
/>
<button
@@ -40,14 +36,11 @@ defineExpose({
@click="showPassword = !showPassword"
:class="
cn(
'text-muted-foreground hover:text-foreground focus-visible:ring-ring w-control absolute inset-y-0 right-0 flex items-center justify-center rounded-r-md focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50',
'text-muted-foreground hover:text-foreground focus-visible:ring-ring absolute inset-y-0 right-0 flex items-center rounded-r-md px-3 focus-visible:outline-none focus-visible:ring-[3px]',
)
"
:aria-label="
t(showPassword ? 'auth.password.hide' : 'auth.password.show')
"
:aria-pressed="showPassword"
:disabled="disabled"
:aria-label="showPassword ? 'Hide password' : 'Show password'"
:tabindex="-1"
>
<EyeOff v-if="showPassword" class="size-4" />
<Eye v-else class="size-4" />
@@ -1,89 +0,0 @@
<script setup lang="ts">
import { Link } from '@inertiajs/vue3';
import { ChevronDown, Settings2 } from 'lucide-vue-next';
import { computed } from 'vue';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { useAppNavigation } from '@/composables/useAppNavigation';
import { useCurrentUrl } from '@/composables/useCurrentUrl';
import { useLocalization } from '@/composables/useLocalization';
const { projectItems, projectSettingsItems } = useAppNavigation();
const { isCurrentUrl } = useCurrentUrl();
const { t } = useLocalization();
const settingsActive = computed(() =>
projectSettingsItems.value.some((item) => isCurrentUrl(item.href)),
);
</script>
<template>
<div
v-if="projectItems.length"
class="bg-card md:px-page flex min-w-0 items-center gap-2 border-b px-4"
>
<nav
:aria-label="t('navigation.project')"
class="flex min-w-0 flex-1 gap-4 overflow-x-auto"
>
<Link
v-for="item in projectItems"
:key="item.title"
:href="item.href"
:aria-current="isCurrentUrl(item.href) ? 'page' : undefined"
:class="[
'min-h-control text-body flex shrink-0 items-center gap-2 border-b-2 px-1 py-3 font-medium outline-offset-[-3px]',
isCurrentUrl(item.href)
? 'border-brand text-primary'
: 'text-muted-foreground hover:border-border hover:text-foreground border-transparent',
]"
>
<component
:is="item.icon"
class="size-4"
aria-hidden="true"
/>{{ item.title }}
</Link>
</nav>
<DropdownMenu v-if="projectSettingsItems.length">
<DropdownMenuTrigger as-child>
<Button
variant="ghost"
size="sm"
:aria-label="t('navigation.projectSettings')"
:class="{
'bg-accent text-accent-foreground': settingsActive,
}"
>
<Settings2 aria-hidden="true" /><span
class="hidden sm:inline"
>{{ t('navigation.projectSettings') }}</span
><ChevronDown class="hidden sm:block" aria-hidden="true" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
v-for="item in projectSettingsItems"
:key="item.title"
as-child
>
<Link
:href="item.href"
:aria-current="
isCurrentUrl(item.href) ? 'page' : undefined
"
:class="{ 'bg-accent': isCurrentUrl(item.href) }"
>
<component :is="item.icon" aria-hidden="true" />{{
item.title
}}
</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</template>
+27 -75
View File
@@ -1,93 +1,45 @@
<script setup lang="ts">
import { Link } from '@inertiajs/vue3';
import { ChevronDown, ChevronRight } from 'lucide-vue-next';
import { reactive } from 'vue';
import { Badge } from '@/components/ui/badge';
import { useLocalization } from '@/composables/useLocalization';
import { show as showTicket } from '@/routes/projects/tickets';
import type { TicketTreeListProps } from '@/types/ticket-tree';
import type { TicketTreeNodeModel } from '@/types/models/ticket-tree-node-model';
defineProps<TicketTreeListProps>();
const { t } = useLocalization();
const collapsedTicketIds = reactive(new Set<number>());
function toggleTicket(ticketId: number): void {
if (collapsedTicketIds.has(ticketId)) {
collapsedTicketIds.delete(ticketId);
} else {
collapsedTicketIds.add(ticketId);
}
}
defineProps<{
nodes: TicketTreeNodeModel[];
projectId: number;
}>();
</script>
<template>
<ul class="grid min-w-0 gap-3">
<li v-for="node in nodes" :key="node.id" class="min-w-0">
<div class="grid min-w-0 grid-cols-[2.75rem_minmax(0,1fr)] gap-2">
<div class="flex justify-center pt-1">
<button
v-if="node.children.length > 0"
type="button"
class="border-border bg-card text-muted-foreground hover:border-primary hover:text-primary focus-visible:ring-ring/35 flex size-10 items-center justify-center rounded-md border transition-colors focus-visible:outline-none focus-visible:ring-2"
:aria-expanded="!collapsedTicketIds.has(node.id)"
:aria-controls="`ticket-tree-children-${node.id}`"
:aria-label="
t(
collapsedTicketIds.has(node.id)
? 'tickets.tree.expand'
: 'tickets.tree.collapse',
{ key: node.ticket_key },
)
<ul class="space-y-3">
<li v-for="node in nodes" :key="node.id">
<div class="border-border rounded-xl border px-4 py-4">
<div class="flex flex-wrap items-center gap-2">
<Link
:href="
showTicket({
project: projectId,
ticket: node.id,
})
"
@click="toggleTicket(node.id)"
class="text-foreground hover:text-primary font-medium transition"
>
<ChevronRight
v-if="collapsedTicketIds.has(node.id)"
class="size-4"
aria-hidden="true"
/>
<ChevronDown v-else class="size-4" aria-hidden="true" />
</button>
<span
v-else
class="bg-border mt-4 size-1.5 rounded-full"
aria-hidden="true"
/>
{{ node.ticket_key }}
</Link>
<Badge v-if="node.ticket_type_name" variant="secondary">
{{ node.ticket_type_name }}
</Badge>
</div>
<Link
:href="
showTicket({
project: projectId,
ticket: node.id,
})
"
class="bg-card text-card-foreground hover:border-primary focus-visible:ring-ring/35 gap-stack group grid min-w-0 rounded-md border p-4 transition-[border-color,box-shadow] focus-visible:outline-none focus-visible:ring-2"
>
<div class="flex min-w-0 flex-wrap items-center gap-2">
<Badge variant="outline">
{{ node.ticket_key }}
</Badge>
<Badge v-if="node.ticket_type_name" variant="secondary">
{{ node.ticket_type_name }}
</Badge>
</div>
<span
class="text-body group-hover:text-primary wrap-anywhere min-w-0 font-semibold"
>
{{ node.title }}
</span>
</Link>
<div class="text-foreground mt-2 text-sm">
{{ node.title }}
</div>
</div>
<div
v-if="
node.children.length > 0 && !collapsedTicketIds.has(node.id)
"
:id="`ticket-tree-children-${node.id}`"
class="border-border ml-[1.375rem] mt-3 border-l pl-2 sm:pl-4"
v-if="node.children.length > 0"
class="border-border mt-3 border-l pl-4"
>
<TicketTreeList
:nodes="node.children"
+23 -12
View File
@@ -1,22 +1,33 @@
<script setup lang="ts">
import UserAvatar from '@/components/ui/avatar/UserAvatar.vue';
import { computed } from 'vue';
import { useInitials } from '@/composables/useInitials';
import type { User } from '@/types';
import UserAvatar from '@/components/ui/avatar/UserAvatar.vue';
withDefaults(defineProps<{ user: User; showEmail?: boolean }>(), {
type Props = {
user: User;
showEmail?: boolean;
};
const props = withDefaults(defineProps<Props>(), {
showEmail: false,
});
const { getInitials } = useInitials();
// Compute whether we should show the avatar image
const showAvatar = computed(
() => props.user.avatar && props.user.avatar !== '',
);
</script>
<template>
<div class="flex min-w-0 items-center gap-2">
<UserAvatar :user="user" />
<div class="grid min-w-0 flex-1 text-left text-sm leading-tight">
<span class="truncate font-medium">{{ user.name }}</span>
<span
v-if="showEmail"
class="text-muted-foreground truncate text-xs"
>{{ user.email }}</span
>
</div>
<UserAvatar />
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-medium">{{ user.name }}</span>
<span v-if="showEmail" class="text-muted-foreground truncate text-xs">{{
user.email
}}</span>
</div>
</template>
+2 -4
View File
@@ -11,7 +11,6 @@ import UserInfo from '@/components/UserInfo.vue';
import { logout } from '@/routes/index';
import { edit } from '@/routes/profile/index';
import type { User } from '@/types';
import { useLocalization } from '@/composables/useLocalization';
type Props = {
user: User;
@@ -22,7 +21,6 @@ const handleLogout = () => {
};
defineProps<Props>();
const { t } = useLocalization();
</script>
<template>
@@ -36,7 +34,7 @@ const { t } = useLocalization();
<DropdownMenuItem :as-child="true">
<Link class="block w-full cursor-pointer" :href="edit()" prefetch>
<Settings class="mr-2 h-4 w-4" />
{{ t('navigation.settings') }}
Settings
</Link>
</DropdownMenuItem>
</DropdownMenuGroup>
@@ -50,7 +48,7 @@ const { t } = useLocalization();
data-test="logout-button"
>
<LogOut class="mr-2 h-4 w-4" />
{{ t('navigation.logout') }}
Log out
</Link>
</DropdownMenuItem>
</template>
@@ -1,320 +0,0 @@
<script setup lang="ts">
import {
ClipboardCheck,
IndentDecrease,
IndentIncrease,
Plus,
Trash2,
} from 'lucide-vue-next';
import { computed, nextTick, ref, useId, watch } from 'vue';
import { Button } from '@/components/ui/button';
import InputError from '@/components/InputError.vue';
import { useLocalization } from '@/composables/useLocalization';
import {
canIndentAcceptanceCriterion,
indentAcceptanceCriterion,
removeAcceptanceCriterion,
syncAcceptanceCriteriaDescendantCheckedState,
} from '@/lib/acceptance-criteria';
import type { AcceptanceCriteriaModel } from '@/types/models/acceptance-criteria-model';
const props = withDefaults(
defineProps<{
modelValue: AcceptanceCriteriaModel;
readonly?: boolean;
disabled?: boolean;
persistToggles?: boolean;
allowEditing?: boolean;
saving?: boolean;
dirty?: boolean;
error?: string;
}>(),
{
readonly: false,
disabled: false,
persistToggles: false,
allowEditing: true,
saving: undefined,
dirty: false,
error: '',
},
);
const emit = defineEmits<{
'update:modelValue': [items: AcceptanceCriteriaModel];
toggle: [index: number, checked: boolean];
}>();
const { t } = useLocalization();
const id = useId();
const root = ref<HTMLElement>();
const editing = ref(!props.persistToggles);
const completed = computed(
() => props.modelValue.filter((item) => item.checked).length,
);
const locked = computed(() => props.readonly || props.disabled);
const update = (items: AcceptanceCriteriaModel): void => {
if (!locked.value) {
emit('update:modelValue', items);
}
};
const focusRow = async (index: number): Promise<void> => {
await nextTick();
const inputs =
root.value?.querySelectorAll<HTMLTextAreaElement>('textarea');
if (inputs?.[index]) {
inputs[index].focus();
} else {
root.value
?.querySelector<HTMLButtonElement>('[data-add-criterion]')
?.focus();
}
};
const add = (): void => {
update([...props.modelValue, { text: '', checked: false, indent: 0 }]);
void focusRow(props.modelValue.length);
};
const remove = (index: number): void => {
update(removeAcceptanceCriterion(props.modelValue, index));
void focusRow(Math.max(0, index - 1));
};
watch(
() => props.modelValue,
async () => {
await nextTick();
root.value
?.querySelectorAll<HTMLTextAreaElement>('textarea')
.forEach((input) => {
input.setCustomValidity(
input.value.trim() === '' ? t('criteria.required') : '',
);
});
},
);
const changeText = (index: number, event: Event): void => {
const text = (event.target as HTMLTextAreaElement).value;
update(
props.modelValue.map((item, position) =>
position === index ? { ...item, text } : item,
),
);
};
const toggle = (index: number, event: Event): void => {
const target = event.target as HTMLInputElement;
const checked = target.checked;
target.checked = props.modelValue[index].checked;
if (locked.value) {
return;
}
if (props.persistToggles && !editing.value && !props.dirty) {
emit('toggle', index, checked);
} else {
update(
syncAcceptanceCriteriaDescendantCheckedState(
props.modelValue,
index,
checked,
),
);
}
};
const onKeydown = (index: number, event: KeyboardEvent): void => {
if (!event.altKey || !['ArrowLeft', 'ArrowRight'].includes(event.key)) {
return;
}
event.preventDefault();
update(
indentAcceptanceCriterion(
props.modelValue,
index,
event.key === 'ArrowRight' ? 1 : -1,
),
);
};
</script>
<template>
<section
ref="root"
class="border-border bg-card min-w-0 space-y-4 rounded-xl border p-4 sm:p-5"
:aria-labelledby="`${id}-title`"
:aria-busy="disabled"
>
<div class="flex flex-wrap items-center justify-between gap-3">
<h3
:id="`${id}-title`"
class="flex items-center gap-2 font-semibold"
>
<ClipboardCheck
class="text-primary size-5"
aria-hidden="true"
/>
{{ t('criteria.title') }}
</h3>
<span class="text-muted-foreground text-sm" aria-live="polite">{{
t('criteria.progress', { completed, total: modelValue.length })
}}</span>
<Button
v-if="persistToggles && allowEditing && !readonly"
type="button"
variant="outline"
:disabled="
disabled ||
(editing &&
modelValue.some((item) => item.text.trim() === ''))
"
:aria-pressed="editing"
@click="editing = !editing"
>
{{ t(editing ? 'criteria.preview' : 'criteria.edit') }}
</Button>
</div>
<p v-if="!modelValue.length" class="text-muted-foreground text-sm">
{{ t('criteria.empty') }}
</p>
<ol v-else class="space-y-3">
<li
v-for="(item, index) in modelValue"
:key="item.id ?? `new-${index}`"
:data-indent="item.indent"
class="border-border min-w-0"
:class="{ 'border-s-2 ps-2': item.indent > 0 }"
:style="{ marginInlineStart: `${item.indent * 0.5}rem` }"
>
<div class="flex min-w-0 items-start gap-2">
<label
class="focus-within:ring-ring flex min-h-11 w-11 shrink-0 cursor-pointer items-center justify-center rounded-md focus-within:ring-2"
:class="{ 'cursor-not-allowed opacity-60': locked }"
>
<input
:checked="item.checked"
type="checkbox"
:disabled="locked"
:aria-label="
t('criteria.complete', {
number: index + 1,
text: item.text,
})
"
class="border-input accent-primary size-5 rounded"
@change="toggle(index, $event)"
/>
</label>
<div class="min-w-0 flex-1">
<textarea
v-if="editing && !readonly"
:value="item.text"
:aria-label="
t('criteria.item', { number: index + 1 })
"
:aria-describedby="`${id}-hint ${id}-error`"
:disabled="disabled"
:aria-invalid="Boolean(error)"
required
rows="2"
class="border-input bg-background focus-visible:ring-ring min-h-11 w-full rounded-md border px-3 py-2 text-sm outline-none focus-visible:ring-2 disabled:opacity-60"
@input="changeText(index, $event)"
@keydown="onKeydown(index, $event)"
/>
<p
v-else
class="wrap-anywhere min-h-11 whitespace-pre-wrap py-2.5 text-sm"
:class="{
'text-muted-foreground line-through':
item.checked,
}"
>
{{ item.text }}
</p>
<div
v-if="editing && !readonly"
class="flex flex-wrap gap-1"
>
<Button
type="button"
variant="ghost"
size="icon"
:disabled="disabled || item.indent === 0"
:aria-label="
t('criteria.outdent', { number: index + 1 })
"
@click="
update(
indentAcceptanceCriterion(
modelValue,
index,
-1,
),
)
"
><IndentDecrease aria-hidden="true"
/></Button>
<Button
type="button"
variant="ghost"
size="icon"
:disabled="
disabled ||
!canIndentAcceptanceCriterion(
modelValue,
index,
)
"
:aria-label="
t('criteria.indent', { number: index + 1 })
"
@click="
update(
indentAcceptanceCriterion(
modelValue,
index,
1,
),
)
"
><IndentIncrease aria-hidden="true"
/></Button>
<Button
type="button"
variant="ghost"
size="icon"
:disabled="disabled"
:aria-label="
t('criteria.remove', { number: index + 1 })
"
@click="remove(index)"
><Trash2 aria-hidden="true"
/></Button>
</div>
</div>
</div>
</li>
</ol>
<template v-if="editing && !readonly">
<Button
data-add-criterion
type="button"
variant="outline"
:disabled="disabled"
@click="add"
><Plus aria-hidden="true" />{{ t('criteria.add') }}</Button
>
<p :id="`${id}-hint`" class="text-muted-foreground text-xs">
{{ t('criteria.hint') }}
</p>
</template>
<p
v-if="persistToggles && dirty"
class="text-muted-foreground text-sm"
role="status"
>
{{ t('criteria.unsaved') }}
</p>
<p
v-if="saving ?? disabled"
class="text-muted-foreground text-sm"
role="status"
>
{{ t('criteria.saving') }}
</p>
<InputError :id="`${id}-error`" :message="error" role="alert" />
</section>
</template>
@@ -1,107 +0,0 @@
<script setup lang="ts">
import { Link } from '@inertiajs/vue3';
import { ArrowUpRight } from 'lucide-vue-next';
import TicketMetadata from '@/components/tickets/TicketMetadata.vue';
import { Badge } from '@/components/ui/badge';
import { useLocalization } from '@/composables/useLocalization';
import { show as showTicket } from '@/routes/projects/tickets';
import type {
TicketPresentationEmits,
TicketPresentationProps,
} from '@/types/ticket-presentation';
withDefaults(defineProps<TicketPresentationProps>(), {
layout: 'card',
showDescription: true,
showStatus: true,
});
const emit = defineEmits<TicketPresentationEmits>();
const { t } = useLocalization();
</script>
<template>
<article
:class="[
'bg-card text-card-foreground min-w-0 rounded-md border',
layout === 'card' ? 'shadow-sm' : '',
ticket.is_blocked ? 'border-warning' : 'border-border',
]"
>
<div
:class="
layout === 'list'
? 'flex min-w-0 flex-col lg:flex-row lg:items-stretch'
: ''
"
>
<Link
:href="showTicket({ project: projectId, ticket: ticket.id })"
:aria-label="
t('tickets.open', {
key: ticket.ticket_key,
title: ticket.title,
})
"
:class="[
'focus-visible:ring-ring/35 gap-card min-h-control hover:bg-accent/35 group flex min-w-0 flex-1 flex-col p-4 transition-[background-color,box-shadow] focus-visible:outline-none focus-visible:ring-2',
layout === 'list'
? 'rounded-t-md lg:rounded-l-md lg:rounded-tr-none'
: 'rounded-t-md',
]"
@click="emit('open', ticket.id)"
>
<div class="flex min-w-0 items-start justify-between gap-3">
<Badge
variant="outline"
class="wrap-anywhere max-w-full whitespace-normal"
>
{{ ticket.ticket_key }}
</Badge>
<ArrowUpRight
class="text-muted-foreground group-hover:text-primary size-4 shrink-0"
aria-hidden="true"
/>
</div>
<h3
class="text-body group-hover:text-primary wrap-anywhere min-w-0 font-semibold"
>
{{ ticket.title }}
</h3>
<TicketMetadata :ticket="ticket" :show-status="showStatus" />
<p
v-if="showDescription && ticket.description"
class="text-caption text-muted-foreground wrap-anywhere line-clamp-3"
>
{{ ticket.description }}
</p>
<p
v-if="ticket.is_blocked && ticket.blocked_comment"
class="text-caption text-warning-foreground bg-warning wrap-anywhere rounded-sm px-3 py-2"
>
{{
t('tickets.blockerComment', {
comment: ticket.blocked_comment,
})
}}
</p>
</Link>
<div
v-if="$slots.actions"
:class="[
'border-t p-4',
layout === 'list'
? 'lg:flex lg:w-80 lg:items-center lg:border-l lg:border-t-0'
: '',
]"
>
<slot name="actions" />
</div>
</div>
</article>
</template>
@@ -1,89 +0,0 @@
<script setup lang="ts">
import InputError from '@/components/InputError.vue';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
withDefaults(
defineProps<{
id: string;
label: string;
kind?: 'text' | 'number' | 'textarea' | 'select';
options?: { value: string | number; label: string }[];
error?: string;
hint?: string;
disabled?: boolean;
required?: boolean;
autofocus?: boolean;
placeholder?: string;
rows?: number;
min?: number;
step?: string;
}>(),
{ kind: 'text', rows: 5 },
);
const model = defineModel<string | number | null>({ required: true });
const controlClass =
'min-h-control border-input bg-card focus-visible:border-ring focus-visible:ring-ring/35 w-full min-w-0 rounded-md border px-3 py-2 text-base shadow-xs outline-none transition-[border-color,box-shadow] hover:border-primary/45 focus-visible:ring-2 disabled:cursor-not-allowed disabled:bg-muted disabled:opacity-60 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/25 md:text-body';
</script>
<template>
<div class="grid min-w-0 content-start gap-2">
<Label :for="id">{{ label }}</Label>
<select
v-if="kind === 'select'"
:id="id"
v-model="model"
:name="id"
:disabled="disabled"
:required="required"
:aria-invalid="Boolean(error)"
:aria-describedby="`${id}-hint ${id}-error`"
:class="controlClass"
>
<option
v-for="option in options"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
<textarea
v-else-if="kind === 'textarea'"
:id="id"
v-model="model"
:name="id"
:rows="rows"
:disabled="disabled"
:required="required"
:placeholder="placeholder"
:aria-invalid="Boolean(error)"
:aria-describedby="`${id}-hint ${id}-error`"
:class="controlClass"
/>
<Input
v-else
:id="id"
v-model="model"
:name="id"
:type="kind"
:disabled="disabled"
:required="required"
:autofocus="autofocus"
:placeholder="placeholder"
:min="min"
:step="step"
:maxlength="kind === 'text' ? 255 : undefined"
:invalid="Boolean(error)"
:aria-describedby="`${id}-hint ${id}-error`"
/>
<p
:id="`${id}-hint`"
class="text-muted-foreground text-xs empty:hidden"
>
{{ hint }}
</p>
<InputError :id="`${id}-error`" :message="error" />
</div>
</template>
@@ -1,157 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue';
import TicketFormField from '@/components/tickets/TicketFormField.vue';
import { useLocalization } from '@/composables/useLocalization';
import { ticketStatusLabel } from '@/lib/tickets';
import type { ProjectTicketCreatePageProps } from '@/types';
const props = defineProps<
Pick<
ProjectTicketCreatePageProps,
| 'project'
| 'ticketTypes'
| 'ticketPriorities'
| 'users'
| 'parentTicketOptions'
> & {
errors: Record<string, string>;
disabled?: boolean;
autofocus?: boolean;
ticketStatusOptions?: ProjectTicketCreatePageProps['ticketStatusOptions'];
}
>();
const title = defineModel<string>('title', { required: true });
const description = defineModel<string>('description', { required: true });
const type = defineModel<string | number>('type', { required: true });
const priority = defineModel<string | number>('priority', { required: true });
const assignee = defineModel<string | number>('assignee', { required: true });
const parent = defineModel<string | number>('parent', { required: true });
const points = defineModel<string | number | null>('points', {
required: true,
});
const status = defineModel<string | number>('status');
const { t } = useLocalization();
const typeOptions = computed(() =>
props.ticketTypes.map((item) => ({ value: item.id, label: item.name })),
);
const priorityOptions = computed(() =>
props.ticketPriorities.map((item) => ({
value: item.id,
label: item.name,
})),
);
const userOptions = computed(() => [
{ value: '', label: t('tickets.unassigned') },
...props.users.map((item) => ({ value: item.id, label: item.name })),
]);
const parentOptions = computed(() => [
{ value: '', label: t('ticketDetail.noParent') },
...props.parentTicketOptions.map((item) => ({
value: item.id,
label: `${item.ticket_key} - ${item.title}`,
})),
]);
const statusOptions = computed(
() =>
props.ticketStatusOptions?.map((item) => ({
value: item.id,
label: ticketStatusLabel(item, t('ticketDetail.notSet')),
})) ?? [],
);
</script>
<template>
<div class="grid min-w-0 gap-6">
<TicketFormField
id="title"
v-model="title"
:label="t('ticketDetail.title')"
:error="errors.title"
:disabled="disabled"
:autofocus="autofocus"
:placeholder="t('ticketForm.titlePlaceholder')"
required
/>
<TicketFormField
id="description"
v-model="description"
kind="textarea"
:label="t('ticketDetail.description')"
:error="errors.description"
:disabled="disabled"
:placeholder="t('ticketForm.descriptionPlaceholder')"
/>
<slot name="content" />
<fieldset
class="bg-muted/20 grid min-w-0 gap-5 rounded-xl border p-4 sm:p-5"
:disabled="disabled"
>
<legend class="px-2 text-sm font-semibold">
{{ t('ticketDetail.metadata') }}
</legend>
<div class="grid min-w-0 gap-5 sm:grid-cols-2">
<TicketFormField
id="ticket_type_id"
v-model="type"
kind="select"
:label="t('ticketDetail.type')"
:options="typeOptions"
:error="errors.ticket_type_id"
:disabled="disabled"
required
/>
<TicketFormField
id="ticket_priority_id"
v-model="priority"
kind="select"
:label="t('ticketDetail.priority')"
:options="priorityOptions"
:error="errors.ticket_priority_id"
:disabled="disabled"
required
/>
<TicketFormField
v-if="ticketStatusOptions && status !== undefined"
id="ticket_status_id"
v-model="status"
kind="select"
:label="t('ticketDetail.status')"
:options="statusOptions"
:error="errors.ticket_status_id"
:disabled="disabled"
required
/>
<TicketFormField
id="assignee_user_id"
v-model="assignee"
kind="select"
:label="t('ticketDetail.assignee')"
:options="userOptions"
:error="errors.assignee_user_id"
:disabled="disabled"
/>
<TicketFormField
id="parent_ticket_id"
v-model="parent"
kind="select"
:label="t('ticketDetail.parent')"
:options="parentOptions"
:error="errors.parent_ticket_id"
:disabled="disabled"
/>
<TicketFormField
v-if="project.show_story_points"
id="story_points"
v-model="points"
kind="number"
:label="t('ticketDetail.storyPoints')"
:error="errors.story_points"
:disabled="disabled"
:min="0"
step="1"
/>
<slot name="metadata" />
</div>
</fieldset>
</div>
</template>
@@ -1,88 +0,0 @@
<script setup lang="ts">
import { Link } from '@inertiajs/vue3';
import { ArrowUpRight } from 'lucide-vue-next';
import TicketMetadata from '@/components/tickets/TicketMetadata.vue';
import { Badge } from '@/components/ui/badge';
import { useLocalization } from '@/composables/useLocalization';
import { show as showTicket } from '@/routes/projects/tickets';
import type {
TicketPresentationEmits,
TicketPresentationProps,
} from '@/types/ticket-presentation';
withDefaults(defineProps<TicketPresentationProps>(), {
showDescription: true,
showStatus: true,
});
const emit = defineEmits<TicketPresentationEmits>();
const { t } = useLocalization();
</script>
<template>
<li
:class="[
'bg-card text-card-foreground min-w-0 rounded-md border',
ticket.is_blocked ? 'border-warning' : 'border-border',
]"
>
<div class="flex min-w-0 flex-col lg:flex-row lg:items-stretch">
<Link
:href="showTicket({ project: projectId, ticket: ticket.id })"
:aria-label="
t('tickets.open', {
key: ticket.ticket_key,
title: ticket.title,
})
"
class="focus-visible:ring-ring/35 gap-card min-h-control hover:bg-accent/35 group flex min-w-0 flex-1 flex-col rounded-t-md p-4 transition-[background-color,box-shadow] focus-visible:outline-none focus-visible:ring-2 lg:rounded-l-md lg:rounded-tr-none"
@click="emit('open', ticket.id)"
>
<div class="flex min-w-0 items-start gap-3">
<Badge
variant="outline"
class="wrap-anywhere max-w-full whitespace-normal"
>
{{ ticket.ticket_key }}
</Badge>
<h3
class="text-body group-hover:text-primary wrap-anywhere min-w-0 flex-1 font-semibold"
>
{{ ticket.title }}
</h3>
<ArrowUpRight
class="text-muted-foreground group-hover:text-primary size-4 shrink-0"
aria-hidden="true"
/>
</div>
<TicketMetadata :ticket="ticket" :show-status="showStatus" />
<p
v-if="showDescription && ticket.description"
class="text-caption text-muted-foreground wrap-anywhere line-clamp-2"
>
{{ ticket.description }}
</p>
<p
v-if="ticket.is_blocked && ticket.blocked_comment"
class="text-caption text-warning-foreground bg-warning wrap-anywhere rounded-sm px-3 py-2"
>
{{
t('tickets.blockerComment', {
comment: ticket.blocked_comment,
})
}}
</p>
</Link>
<div
v-if="$slots.actions"
class="border-t p-4 lg:flex lg:w-80 lg:items-center lg:border-l lg:border-t-0"
>
<slot name="actions" />
</div>
</div>
</li>
</template>
@@ -1,91 +0,0 @@
<script setup lang="ts">
import { CircleUserRound, OctagonAlert } from 'lucide-vue-next';
import { computed } from 'vue';
import { Badge } from '@/components/ui/badge';
import { UserAvatar } from '@/components/ui/avatar';
import { useLocalization } from '@/composables/useLocalization';
import { ticketStatusLabel } from '@/lib/tickets';
import type { TicketModel } from '@/types/models/ticket-model';
const props = withDefaults(
defineProps<{
showStatus?: boolean;
ticket: TicketModel;
}>(),
{
showStatus: true,
},
);
const { t } = useLocalization();
const statusLabel = computed(() =>
ticketStatusLabel(props.ticket.ticket_status, t('tickets.noStatus')),
);
</script>
<template>
<div class="gap-stack flex min-w-0 flex-wrap items-center">
<Badge
v-if="ticket.is_blocked"
variant="warning"
:aria-label="t('tickets.blocked')"
>
<OctagonAlert aria-hidden="true" />
{{ t('tickets.blocked') }}
</Badge>
<Badge
variant="secondary"
:aria-label="
t('tickets.typeLabel', { name: ticket.ticket_type.name })
"
:data-ticket-type-id="ticket.ticket_type.id"
>
{{ ticket.ticket_type.name }}
</Badge>
<Badge
variant="outline"
:aria-label="
t('tickets.priorityLabel', {
name: ticket.ticket_priority.name,
})
"
:data-ticket-priority-id="ticket.ticket_priority.id"
>
{{ ticket.ticket_priority.name }}
</Badge>
<Badge
v-if="showStatus"
variant="outline"
:aria-label="t('tickets.statusLabel', { name: statusLabel })"
:data-ticket-status-id="ticket.ticket_status?.id"
>
{{ statusLabel }}
</Badge>
<span
v-if="ticket.assignee"
class="text-caption text-muted-foreground flex min-w-0 items-center gap-1.5"
:aria-label="
t('tickets.assignedTo', { name: ticket.assignee.name })
"
>
<UserAvatar
:user="ticket.assignee"
size="sm"
:label="ticket.assignee.name"
/>
<span class="max-w-40 truncate">{{ ticket.assignee.name }}</span>
</span>
<span
v-else
class="text-caption text-muted-foreground flex items-center gap-1.5"
>
<CircleUserRound class="size-4" aria-hidden="true" />
{{ t('tickets.unassigned') }}
</span>
</div>
</template>
@@ -1,518 +0,0 @@
<script setup lang="ts">
import { useLocalization } from '@/composables/useLocalization';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { useForm, useRemember } from '@inertiajs/vue3';
import { computed, ref, toRef, unref, useTemplateRef, nextTick } from 'vue';
import ProjectTicketController from '@/actions/App/Http/Controllers/ProjectTicketController';
import InputError from '@/components/InputError.vue';
import AcceptanceCriteriaChecklist from '@/components/tickets/AcceptanceCriteriaChecklist.vue';
import FormFeedback from '@/components/FormFeedback.vue';
import { Button } from '@/components/ui/button';
import TicketFormField from '@/components/tickets/TicketFormField.vue';
import TicketFormFields from '@/components/tickets/TicketFormFields.vue';
import { Label } from '@/components/ui/label';
import { useFormProtection } from '@/composables/useFormProtection';
import {
normalizeAcceptanceCriteria,
syncAcceptanceCriteriaDescendantCheckedState,
} from '@/lib/acceptance-criteria';
import type { TicketDetailContentProps } from '@/types/ticket-detail';
const props = defineProps<TicketDetailContentProps>();
const { t } = useLocalization();
const editor = useTemplateRef<HTMLDetailsElement>('editor');
async function editParent(): Promise<void> {
if (!props.canEditTicket || !editor.value) {
return;
}
editor.value.open = true;
await nextTick();
editor.value
?.querySelector<HTMLSelectElement>('#parent_ticket_id')
?.focus();
}
defineExpose({ editParent });
const canEditAuthor = computed(
() => props.canEditTicket && props.canManageProject,
);
const canViewAuthor = computed(
() => props.project.show_author || props.canManageProject,
);
const draftKey = `projects:${props.project.id}:tickets:${props.ticket.id}:edit`;
const acceptanceCriteriaDraft = useRemember(
{ items: normalizeAcceptanceCriteria(props.ticket.acceptance_criteria) },
`${draftKey}:criteria`,
);
const acceptanceCriteriaItems = toRef(unref(acceptanceCriteriaDraft), 'items');
const savedAcceptanceCriteria = ref(
normalizeAcceptanceCriteria(props.ticket.acceptance_criteria),
);
const isPersistingAcceptanceCriteriaToggle = ref(false);
const acceptanceCriteriaToggleError = ref('');
const savedFormData = () => ({
acceptance_criteria: normalizeAcceptanceCriteria(
savedAcceptanceCriteria.value,
),
assignee_user_id: props.ticket.assignee_user_id ?? '',
author_user_id: props.ticket.author_user_id ?? '',
blocked_comment: props.ticket.blocked_comment ?? '',
description: props.ticket.description ?? '',
field_values: props.ticketFieldDefinitions.map((ticketFieldDefinition) => {
const currentFieldValue = props.ticket.field_values?.find(
(fieldValue) =>
fieldValue.ticket_field_definition_id ===
ticketFieldDefinition.id,
);
return {
ticket_field_definition_id: ticketFieldDefinition.id,
value_number: currentFieldValue?.value_number ?? '',
value_user_id: currentFieldValue?.value_user_id ?? '',
};
}),
is_blocked: props.ticket.is_blocked,
parent_ticket_id: props.ticket.parent_ticket_id ?? '',
ticket_priority_id: props.ticket.ticket_priority_id,
ticket_type_id: props.ticket.ticket_type_id,
title: props.ticket.title,
story_points: props.ticket.story_points,
});
const form = useForm(draftKey, savedFormData());
const hasUnsavedAcceptanceCriteriaDraft = computed(
() =>
JSON.stringify(acceptanceCriteriaItems.value) !==
JSON.stringify(savedAcceptanceCriteria.value),
);
const isDirty = computed(
() =>
props.canEditTicket &&
(form.isDirty || hasUnsavedAcceptanceCriteriaDraft.value),
);
const protection = useFormProtection(form, { isDirty: () => isDirty.value });
const reset = (): void => {
if (
form.processing ||
isPersistingAcceptanceCriteriaToggle.value ||
!protection.confirmLeave()
) {
return;
}
form.reset();
form.clearErrors();
acceptanceCriteriaItems.value = normalizeAcceptanceCriteria(
savedAcceptanceCriteria.value,
);
acceptanceCriteriaToggleError.value = '';
protection.clearFeedback();
};
const acceptanceCriteriaError = computed(() => {
return (
form.errors.acceptance_criteria ??
Object.entries(form.errors).find(([key]) =>
key.startsWith('acceptance_criteria.'),
)?.[1] ??
''
);
});
const buildSavedTicketPayload = (
acceptanceCriteria = savedAcceptanceCriteria.value,
) => ({
acceptance_criteria: acceptanceCriteria,
assignee_user_id: props.ticket.assignee_user_id,
author_user_id: props.ticket.author_user_id,
blocked_comment: props.ticket.blocked_comment,
description: props.ticket.description,
field_values: (props.ticket.field_values ?? []).map((fieldValue) => ({
ticket_field_definition_id: fieldValue.ticket_field_definition_id,
value_number: fieldValue.value_number,
value_user_id: fieldValue.value_user_id,
})),
is_blocked: props.ticket.is_blocked,
parent_ticket_id: props.ticket.parent_ticket_id,
story_points: props.ticket.story_points,
ticket_priority_id: props.ticket.ticket_priority_id,
ticket_type_id: props.ticket.ticket_type_id,
title: props.ticket.title,
});
const persistAcceptanceCriteriaToggle = async (
index: number,
checked: boolean,
): Promise<void> => {
if (
!props.canEditTicket ||
form.processing ||
isPersistingAcceptanceCriteriaToggle.value ||
hasUnsavedAcceptanceCriteriaDraft.value
) {
return;
}
const nextAcceptanceCriteria = syncAcceptanceCriteriaDescendantCheckedState(
savedAcceptanceCriteria.value,
index,
checked,
);
const csrfToken = document
.querySelector('meta[name="csrf-token"]')
?.getAttribute('content');
isPersistingAcceptanceCriteriaToggle.value = true;
acceptanceCriteriaToggleError.value = '';
try {
const response = await fetch(
ProjectTicketController.update.url({
project: props.project.id,
ticket: props.ticket.id,
}),
{
method: 'PATCH',
credentials: 'same-origin',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken ?? '',
'X-Requested-With': 'XMLHttpRequest',
},
body: JSON.stringify(
buildSavedTicketPayload(nextAcceptanceCriteria),
),
},
);
if (!response.ok) {
throw new Error('Checklist toggle update failed.');
}
savedAcceptanceCriteria.value = nextAcceptanceCriteria;
acceptanceCriteriaItems.value = normalizeAcceptanceCriteria(
nextAcceptanceCriteria,
);
form.acceptance_criteria = nextAcceptanceCriteria;
form.defaults('acceptance_criteria', nextAcceptanceCriteria);
} catch (_error) {
acceptanceCriteriaToggleError.value = t('criteria.saveError');
} finally {
isPersistingAcceptanceCriteriaToggle.value = false;
}
};
const submit = (): void => {
if (
!props.canEditTicket ||
form.processing ||
isPersistingAcceptanceCriteriaToggle.value
) {
return;
}
form.acceptance_criteria = acceptanceCriteriaItems.value;
protection.submit(
ProjectTicketController.update({
project: props.project.id,
ticket: props.ticket.id,
}),
{
preserveScroll: true,
onSuccess: () => {
savedAcceptanceCriteria.value = normalizeAcceptanceCriteria(
props.ticket.acceptance_criteria,
);
acceptanceCriteriaItems.value = normalizeAcceptanceCriteria(
savedAcceptanceCriteria.value,
);
Object.assign(form, savedFormData());
form.defaults();
},
},
);
};
</script>
<template>
<div class="flex min-w-0 flex-col gap-6">
<section
id="ticket-description"
aria-labelledby="ticket-description-title"
class="scroll-mt-6"
>
<Card>
<CardHeader
><h2
id="ticket-description-title"
class="text-lg font-semibold"
>
{{ t('ticketDetail.description') }}
</h2></CardHeader
>
<CardContent>
<p
class="wrap-anywhere whitespace-pre-wrap text-sm leading-7"
:class="{
'text-muted-foreground': !ticket.description,
}"
>
{{
ticket.description ||
t('ticketDetail.noDescription')
}}
</p>
</CardContent>
</Card>
</section>
<div id="ticket-checklist" class="scroll-mt-6">
<AcceptanceCriteriaChecklist
:model-value="savedAcceptanceCriteria"
:readonly="!canEditTicket"
:disabled="
form.processing ||
isPersistingAcceptanceCriteriaToggle ||
hasUnsavedAcceptanceCriteriaDraft
"
:saving="
form.processing || isPersistingAcceptanceCriteriaToggle
"
:allow-editing="false"
persist-toggles
:dirty="hasUnsavedAcceptanceCriteriaDraft"
:error="acceptanceCriteriaToggleError"
@toggle="persistAcceptanceCriteriaToggle"
/>
</div>
<details
v-if="canEditTicket"
id="ticket-edit"
ref="editor"
class="bg-card group rounded-xl border shadow-sm"
>
<summary
class="min-h-control focus-visible:ring-ring cursor-pointer rounded-xl px-6 py-4 font-semibold focus-visible:outline-none focus-visible:ring-2"
>
{{ t('ticketDetail.edit') }}
</summary>
<div class="border-t p-4 sm:p-6">
<p class="text-muted-foreground mb-6 text-sm">
{{ t('ticketDetail.editDescription') }}
</p>
<form class="grid min-w-0 gap-6" @submit.prevent="submit">
<FormFeedback
:state="protection.state.value"
@retry="submit"
/>
<TicketFormFields
v-model:title="form.title"
v-model:description="form.description"
v-model:type="form.ticket_type_id"
v-model:priority="form.ticket_priority_id"
v-model:assignee="form.assignee_user_id"
v-model:parent="form.parent_ticket_id"
v-model:points="form.story_points"
:project="project"
:ticket-types="ticketTypes"
:ticket-priorities="ticketPriorities"
:users="users"
:parent-ticket-options="parentTicketOptions"
:errors="form.errors"
:disabled="
form.processing ||
isPersistingAcceptanceCriteriaToggle
"
>
<template #content>
<AcceptanceCriteriaChecklist
v-model="acceptanceCriteriaItems"
:disabled="
form.processing ||
isPersistingAcceptanceCriteriaToggle
"
:error="acceptanceCriteriaError"
/>
</template>
<template #metadata>
<TicketFormField
v-if="canViewAuthor"
id="author_user_id"
v-model="form.author_user_id"
kind="select"
:label="t('ticketDetail.author')"
:hint="
!canEditAuthor
? t('ticketForm.authorRestricted')
: undefined
"
:options="[
{
value: '',
label: t('ticketDetail.noAuthor'),
},
...users.map((user) => ({
value: user.id,
label: user.name,
})),
]"
:error="form.errors.author_user_id"
:disabled="!canEditAuthor || form.processing"
/>
</template>
</TicketFormFields>
<fieldset
class="grid min-w-0 gap-4 rounded-xl border p-4 sm:p-5"
:disabled="
form.processing ||
isPersistingAcceptanceCriteriaToggle
"
>
<legend class="px-2 text-sm font-semibold">
{{ t('ticketForm.blocker') }}
</legend>
<div class="min-h-control flex items-center gap-3">
<input
id="is_blocked"
v-model="form.is_blocked"
type="checkbox"
:aria-invalid="Boolean(form.errors.is_blocked)"
aria-describedby="is_blocked-error"
class="border-input text-primary focus-visible:ring-ring size-4 rounded focus-visible:ring-2"
/>
<Label for="is_blocked">{{
t('ticketDetail.blocked')
}}</Label>
</div>
<InputError
id="is_blocked-error"
:message="form.errors.is_blocked"
/>
<TicketFormField
id="blocked_comment"
v-model="form.blocked_comment"
kind="textarea"
:rows="3"
:label="t('ticketDetail.blockerComment')"
:placeholder="t('ticketDetail.blockerPlaceholder')"
:error="form.errors.blocked_comment"
:required="form.is_blocked"
/>
</fieldset>
<fieldset
class="grid min-w-0 gap-4 rounded-xl border p-4 sm:p-5"
:disabled="
form.processing ||
isPersistingAcceptanceCriteriaToggle
"
>
<legend class="px-2 text-sm font-semibold">
{{ t('ticketDetail.customFields') }}
</legend>
<p class="text-muted-foreground text-sm">
{{ t('ticketDetail.customFieldsDescription') }}
</p>
<InputError :message="form.errors.field_values" />
<p
v-if="!ticketFieldDefinitions.length"
class="text-muted-foreground text-sm"
>
{{ t('ticketDetail.noCustomFields') }}
</p>
<div v-else class="grid min-w-0 gap-5 sm:grid-cols-2">
<template
v-for="(
definition, index
) in ticketFieldDefinitions"
:key="definition.id"
>
<TicketFormField
v-if="definition.field_type === 'user'"
:id="`field-${definition.id}`"
v-model="
form.field_values[index].value_user_id
"
kind="select"
:label="definition.label"
:options="[
{
value: '',
label: t('tickets.unassigned'),
},
...users.map((user) => ({
value: user.id,
label: user.name,
})),
]"
:error="
form.errors[
`field_values.${index}.value_user_id`
] ??
form.errors[
`field_values.${index}.ticket_field_definition_id`
]
"
/>
<TicketFormField
v-else
:id="`field-${definition.id}`"
v-model="
form.field_values[index].value_number
"
kind="number"
step="0.1"
:label="definition.label"
:error="
form.errors[
`field_values.${index}.value_number`
] ??
form.errors[
`field_values.${index}.ticket_field_definition_id`
]
"
/>
</template>
</div>
</fieldset>
<div
class="flex flex-wrap items-center gap-3 border-t pt-5"
>
<Button
type="submit"
:disabled="
form.processing ||
isPersistingAcceptanceCriteriaToggle
"
>{{
form.processing
? t('ticketForm.saving')
: t('ticketDetail.saveChanges')
}}</Button
>
<Button
type="button"
variant="outline"
:disabled="
form.processing ||
isPersistingAcceptanceCriteriaToggle ||
!isDirty
"
@click="reset"
>{{ t('ticketForm.reset') }}</Button
>
<p role="status" class="text-muted-foreground text-sm">
{{
isDirty
? t('ticketForm.unsaved')
: t('ticketForm.saved')
}}
</p>
</div>
</form>
</div>
</details>
</div>
</template>
@@ -1,658 +0,0 @@
<script setup lang="ts">
import { useLocalization } from '@/composables/useLocalization';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Link, useForm, usePage } from '@inertiajs/vue3';
import { CheckCircle2, GitBranchPlus, XCircle } from 'lucide-vue-next';
import { computed, ref, watch } from 'vue';
import ProjectTicketBranchController from '@/actions/App/Http/Controllers/ProjectTicketBranchController';
import FormFeedback from '@/components/FormFeedback.vue';
import FeedbackState from '@/components/FeedbackState.vue';
import InputError from '@/components/InputError.vue';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { useFormProtection } from '@/composables/useFormProtection';
import { edit as editGitConnections } from '@/routes/git-connections';
import type { BranchNameValidationModel } from '@/types/models/branch-name-validation-model';
import type { RepositoryConnectionModel } from '@/types/models/repository-connection-model';
import type {
TicketDetailGitProps,
TicketBranchFormData,
} from '@/types/ticket-detail';
const props = defineProps<TicketDetailGitProps>();
const { t } = useLocalization();
const appName = String(usePage().props.name);
const draftKey = `projects:${props.project.id}:tickets:${props.ticket.id}:edit`;
const visibleRepositoryBranches = computed(() =>
props.repositoryBranches.filter(
(repositoryBranch) =>
repositoryBranch.branches.length > 0 ||
repositoryBranch.error !== null ||
!repositoryBranch.repository_connection.has_personal_access,
),
);
const visibleRepositoryPullRequests = computed(() =>
props.repositoryPullRequests.filter(
(repositoryPullRequest) =>
repositoryPullRequest.pull_requests.length > 0 ||
repositoryPullRequest.error !== null ||
!repositoryPullRequest.repository_connection.has_personal_access,
),
);
const linkedRepositoryConnections = computed<RepositoryConnectionModel[]>(() =>
props.repositoryBranches.map(
(repositoryBranch) => repositoryBranch.repository_connection,
),
);
const canCreateBranches = computed(
() => props.canEditTicket && linkedRepositoryConnections.value.length > 0,
);
const defaultBranchRepositoryConnectionId = computed(() =>
String(
linkedRepositoryConnections.value.find(
(repositoryConnection) => repositoryConnection.has_personal_access,
)?.id ??
linkedRepositoryConnections.value[0]?.id ??
'',
),
);
const isCreateBranchDialogOpen = ref(false);
const isCheckingBranchName = ref(false);
const branchNameValidation = ref<BranchNameValidationModel | null>(null);
const branchCreationForm = useForm<TicketBranchFormData>(`${draftKey}:branch`, {
branch_name: props.suggestedBranchName,
repository_connection_id: defaultBranchRepositoryConnectionId.value,
});
const branchProtection = useFormProtection(branchCreationForm, {
isDirty: () => isCreateBranchDialogOpen.value && branchCreationForm.isDirty,
});
const updateBranchDialogOpen = (open: boolean): void => {
if (!open && !branchProtection.confirmLeave()) {
return;
}
isCreateBranchDialogOpen.value = open;
};
const selectedBranchRepositoryConnection = computed(
(): RepositoryConnectionModel | null =>
linkedRepositoryConnections.value.find(
(repositoryConnection) =>
String(repositoryConnection.id) ===
branchCreationForm.repository_connection_id,
) ?? null,
);
const selectedBranchRepositoryHasPersonalAccess = computed(
() =>
selectedBranchRepositoryConnection.value?.has_personal_access ?? false,
);
const initializeBranchCreationForm = (): void => {
if (branchCreationForm.isDirty) {
return;
}
branchCreationForm.clearErrors();
branchCreationForm.branch_name = props.suggestedBranchName;
branchCreationForm.repository_connection_id =
defaultBranchRepositoryConnectionId.value;
branchNameValidation.value = null;
isCheckingBranchName.value = false;
};
watch(
() => isCreateBranchDialogOpen.value,
(isOpen) => {
if (!isOpen) {
branchCreationForm.clearErrors();
branchNameValidation.value = null;
isCheckingBranchName.value = false;
return;
}
initializeBranchCreationForm();
},
);
watch(
[
() => isCreateBranchDialogOpen.value,
() => branchCreationForm.repository_connection_id,
() => branchCreationForm.branch_name,
],
(
[isOpen, repositoryConnectionId, branchName],
_previousValues,
onCleanup,
) => {
if (!isOpen) {
return;
}
const repositoryConnection = selectedBranchRepositoryConnection.value;
if (repositoryConnection === null || repositoryConnectionId === '') {
branchNameValidation.value = null;
isCheckingBranchName.value = false;
return;
}
if (!repositoryConnection.has_personal_access) {
branchNameValidation.value = {
is_valid: false,
message: t('ticketDetail.connectGitFirst'),
};
isCheckingBranchName.value = false;
return;
}
if (branchName.trim() === '') {
branchNameValidation.value = {
is_valid: false,
message: t('ticketDetail.branchRequired'),
};
isCheckingBranchName.value = false;
return;
}
const abortController = new AbortController();
const timeoutId = window.setTimeout(async () => {
isCheckingBranchName.value = true;
try {
const response = await fetch(
ProjectTicketBranchController.validate.url(
{
project: props.project.id,
ticket: props.ticket.id,
},
{
query: {
branch_name: branchName,
repository_connection_id:
repositoryConnectionId,
},
},
),
{
credentials: 'same-origin',
headers: {
Accept: 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
method: 'GET',
signal: abortController.signal,
},
);
if (!response.ok) {
throw new Error('Branch validation failed.');
}
branchNameValidation.value =
(await response.json()) as BranchNameValidationModel;
} catch (error) {
if (
error instanceof DOMException &&
error.name === 'AbortError'
) {
return;
}
branchNameValidation.value = {
is_valid: false,
message: t('ticketDetail.branchValidationFailed', {
app: appName,
}),
};
} finally {
if (!abortController.signal.aborted) {
isCheckingBranchName.value = false;
}
}
}, 300);
onCleanup(() => {
abortController.abort();
window.clearTimeout(timeoutId);
isCheckingBranchName.value = false;
});
},
{ immediate: true },
);
const submitBranchCreation = (): void => {
if (
!canCreateBranches.value ||
!selectedBranchRepositoryHasPersonalAccess.value ||
branchNameValidation.value?.is_valid !== true
) {
return;
}
branchProtection.submit(
ProjectTicketBranchController.store({
project: props.project.id,
ticket: props.ticket.id,
}),
{
onSuccess: () => {
isCreateBranchDialogOpen.value = false;
},
preserveScroll: true,
},
);
};
</script>
<template>
<section
id="ticket-git"
aria-labelledby="ticket-git-title"
class="wrap-anywhere flex min-w-0 scroll-mt-6 flex-col gap-4"
>
<h2 id="ticket-git-title" class="text-lg font-semibold">
{{ t('ticketDetail.git') }}
</h2>
<div class="grid min-w-0 gap-6">
<Card class="h-fit">
<CardHeader
class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"
>
<div class="space-y-1.5">
<CardTitle>{{ t('ticketDetail.branches') }}</CardTitle>
<CardDescription>{{
t('ticketDetail.branchesDescription')
}}</CardDescription>
</div>
<Dialog
:open="isCreateBranchDialogOpen"
@update:open="updateBranchDialogOpen"
>
<Button
v-if="canCreateBranches"
type="button"
variant="outline"
class="gap-2"
@click="isCreateBranchDialogOpen = true"
>
<GitBranchPlus class="size-4" />{{
t('ticketDetail.createBranch')
}}</Button
>
<DialogContent class="sm:max-w-lg">
<DialogHeader class="space-y-3">
<DialogTitle>{{
t('ticketDetail.createBranchTitle')
}}</DialogTitle>
<DialogDescription>{{
t('ticketDetail.createBranchDescription')
}}</DialogDescription>
</DialogHeader>
<form
class="grid gap-4"
@submit.prevent="submitBranchCreation"
>
<FormFeedback
:state="branchProtection.state.value"
@retry="submitBranchCreation"
/>
<div class="grid min-w-0 gap-2">
<Label for="repository_connection_id">{{
t('ticketDetail.repository')
}}</Label>
<select
id="repository_connection_id"
v-model="
branchCreationForm.repository_connection_id
"
:disabled="
branchCreationForm.processing
"
class="min-h-control border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 w-full min-w-0 rounded-md border px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-60"
>
<option
v-for="repositoryConnection in linkedRepositoryConnections"
:key="repositoryConnection.id"
:value="
String(repositoryConnection.id)
"
>
{{
`${repositoryConnection.repository_full_name}${repositoryConnection.has_personal_access ? '' : ' (connect personal Git access)'}`
}}
</option>
</select>
<InputError
:message="
branchCreationForm.errors
.repository_connection_id
"
/>
</div>
<div class="grid min-w-0 gap-2">
<Label for="branch_name">{{
t('ticketDetail.branchName')
}}</Label>
<Input
id="branch_name"
v-model="branchCreationForm.branch_name"
:disabled="
branchCreationForm.processing ||
!selectedBranchRepositoryHasPersonalAccess
"
/>
<InputError
:message="
branchCreationForm.errors
.branch_name
"
/>
</div>
<div
class="rounded-xl border px-4 py-3"
:class="
branchNameValidation?.is_valid
? 'border-emerald-200 bg-emerald-50 text-emerald-900 dark:border-emerald-900/70 dark:bg-emerald-950/40 dark:text-emerald-100'
: 'border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-900/70 dark:bg-amber-950/40 dark:text-amber-100'
"
>
<div class="flex items-start gap-3">
<CheckCircle2
v-if="
branchNameValidation?.is_valid
"
class="mt-0.5 size-4 shrink-0"
/>
<XCircle
v-else-if="
branchNameValidation !== null
"
class="mt-0.5 size-4 shrink-0"
/>
<p class="text-sm">
{{
isCheckingBranchName
? t(
'ticketDetail.checkingBranch',
)
: (branchNameValidation?.message ??
t(
'ticketDetail.selectRepository',
))
}}
</p>
</div>
</div>
<div
v-if="
!selectedBranchRepositoryHasPersonalAccess &&
selectedBranchRepositoryConnection !==
null
"
class="border-border rounded-xl border border-dashed px-4 py-4"
>
<p class="text-muted-foreground text-sm">
{{ t('ticketDetail.branchAccess') }}
</p>
<Button
as-child
variant="outline"
size="sm"
class="mt-3"
>
<Link :href="editGitConnections()">{{
t('ticketDetail.openGitConnections')
}}</Link>
</Button>
</div>
<DialogFooter class="gap-2">
<Button
type="button"
variant="secondary"
@click="updateBranchDialogOpen(false)"
>{{ t('common.cancel') }}</Button
>
<Button
type="submit"
:disabled="
branchCreationForm.processing ||
isCheckingBranchName ||
branchNameValidation?.is_valid !==
true
"
>{{
t('ticketDetail.createNow')
}}</Button
>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</CardHeader>
<CardContent class="space-y-4">
<FeedbackState
v-if="visibleRepositoryBranches.length === 0"
state="empty"
:title="t('tickets.noBranches')"
:description="t('tickets.noBranchesDescription')"
/>
<div v-else class="space-y-3">
<div
v-for="repositoryBranch in visibleRepositoryBranches"
:key="repositoryBranch.repository_connection.id"
class="border-border rounded-xl border px-4 py-4"
>
<div class="flex flex-wrap items-center gap-2">
<Badge variant="secondary">
{{
repositoryBranch.repository_connection
.provider_label
}}
</Badge>
<span class="font-medium">
{{
repositoryBranch.repository_connection
.repository_full_name
}}
</span>
</div>
<p class="text-muted-foreground mt-2 text-sm">
{{
repositoryBranch.repository_connection
.base_url
}}
</p>
<div
v-if="
!repositoryBranch.repository_connection
.has_personal_access
"
class="mt-3 space-y-3"
>
<p class="text-muted-foreground text-sm">
{{ t('ticketDetail.inspectBranches') }}
</p>
<Button as-child variant="outline" size="sm">
<Link :href="editGitConnections()">{{
t('ticketDetail.openGitConnections')
}}</Link>
</Button>
</div>
<div
v-else-if="repositoryBranch.error"
class="mt-3 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900 dark:border-amber-900/70 dark:bg-amber-950/40 dark:text-amber-100"
>
{{ repositoryBranch.error }}
</div>
<div v-else class="mt-3 flex flex-wrap gap-2">
<Badge
v-for="branch in repositoryBranch.branches"
:key="branch.name"
variant="outline"
class="px-3 py-1"
>
{{ branch.name }}
</Badge>
</div>
</div>
</div>
</CardContent>
</Card>
<Card class="h-fit">
<CardHeader>
<CardTitle>{{ t('ticketDetail.pullRequests') }}</CardTitle>
<CardDescription>{{
t('ticketDetail.pullRequestsDescription')
}}</CardDescription>
</CardHeader>
<CardContent class="space-y-4">
<FeedbackState
v-if="visibleRepositoryPullRequests.length === 0"
state="empty"
:title="t('tickets.noPullRequests')"
:description="t('tickets.noPullRequestsDescription')"
/>
<div v-else class="space-y-3">
<div
v-for="repositoryPullRequest in visibleRepositoryPullRequests"
:key="
repositoryPullRequest.repository_connection.id
"
class="border-border rounded-xl border px-4 py-4"
>
<div class="flex flex-wrap items-center gap-2">
<Badge variant="secondary">
{{
repositoryPullRequest
.repository_connection
.provider_label
}}
</Badge>
<span class="font-medium">
{{
repositoryPullRequest
.repository_connection
.repository_full_name
}}
</span>
</div>
<p class="text-muted-foreground mt-2 text-sm">
{{
repositoryPullRequest.repository_connection
.base_url
}}
</p>
<div
v-if="
!repositoryPullRequest.repository_connection
.has_personal_access
"
class="mt-3 space-y-3"
>
<p class="text-muted-foreground text-sm">
{{ t('ticketDetail.inspectPullRequests') }}
</p>
<Button as-child variant="outline" size="sm">
<Link :href="editGitConnections()">{{
t('ticketDetail.openGitConnections')
}}</Link>
</Button>
</div>
<div
v-else-if="repositoryPullRequest.error"
class="mt-3 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900 dark:border-amber-900/70 dark:bg-amber-950/40 dark:text-amber-100"
>
{{ repositoryPullRequest.error }}
</div>
<div v-else class="mt-3 space-y-3">
<div
v-for="pullRequest in repositoryPullRequest.pull_requests"
:key="pullRequest.html_url"
class="border-border/70 rounded-xl border px-4 py-3"
>
<div
class="flex flex-wrap items-center gap-2"
>
<Badge variant="outline">
#{{ pullRequest.number }}
</Badge>
<Badge variant="secondary">
{{ pullRequest.status }}
</Badge>
<a
:href="pullRequest.html_url"
target="_blank"
rel="noreferrer"
class="text-foreground hover:text-primary font-medium transition"
>
{{ pullRequest.title }}
</a>
</div>
<p
class="text-muted-foreground mt-2 text-sm"
>
{{
t('ticketDetail.sourceBranch', {
name: pullRequest.source_branch,
})
}}
</p>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
</section>
</template>
@@ -1,79 +0,0 @@
<script setup lang="ts">
import { Link } from '@inertiajs/vue3';
import { ArrowLeft, Columns3, ListTree } from 'lucide-vue-next';
import TicketMetadata from '@/components/tickets/TicketMetadata.vue';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { useLocalization } from '@/composables/useLocalization';
import { index as backlogIndex } from '@/routes/projects/backlog';
import { index as boardIndex } from '@/routes/projects/board';
import { show } from '@/routes/projects';
import type { TicketDetailHeaderProps } from '@/types/ticket-detail';
defineProps<TicketDetailHeaderProps>();
const { t } = useLocalization();
const sections = [
'description',
'checklist',
'metadata',
'relationships',
'history',
'git',
] as const;
</script>
<template>
<header class="flex min-w-0 flex-col gap-5">
<div class="flex flex-wrap items-center justify-between gap-3">
<Button as-child variant="ghost" class="max-w-full"
><Link :href="show(project.id)"
><ArrowLeft aria-hidden="true" />{{
t('ticketDetail.backToProject')
}}</Link
></Button
>
<div class="flex flex-wrap gap-2">
<Button as-child variant="outline"
><Link :href="backlogIndex(project.id)"
><ListTree aria-hidden="true" />{{
t('ticketDetail.openBacklog')
}}</Link
></Button
>
<Button as-child variant="outline"
><Link :href="boardIndex(project.id)"
><Columns3 aria-hidden="true" />{{
t('ticketDetail.openBoard')
}}</Link
></Button
>
</div>
</div>
<div class="flex min-w-0 flex-col gap-3">
<div class="flex flex-wrap items-center gap-2">
<span class="text-primary font-mono text-sm font-semibold">{{
ticket.ticket_key
}}</span
><Badge v-if="!canEditTicket" variant="outline">{{
t('ticketDetail.readOnly')
}}</Badge>
</div>
<h1
class="wrap-anywhere text-2xl font-semibold leading-tight tracking-tight sm:text-3xl"
>
{{ ticket.title }}
</h1>
<TicketMetadata :ticket="ticket" />
</div>
<nav
:aria-label="t('ticketDetail.sections')"
class="flex flex-wrap gap-1 border-b pb-3"
>
<a
v-for="section in sections"
:key="section"
:href="`#ticket-${section}`"
class="min-h-control text-muted-foreground hover:bg-accent hover:text-accent-foreground focus-visible:ring-ring inline-flex items-center rounded-lg px-3 text-sm transition-colors focus-visible:outline-none focus-visible:ring-2"
>{{ t(`ticketDetail.${section}`) }}</a
>
</nav>
</header>
</template>
@@ -1,70 +0,0 @@
<script setup lang="ts">
import { useLocalization } from '@/composables/useLocalization';
import {
Card,
CardContent,
CardDescription,
CardHeader,
} from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import type { TicketDetailHistoryProps } from '@/types/ticket-detail';
defineProps<TicketDetailHistoryProps>();
const { t, date } = useLocalization();
const formatDateTime = (value: string): string =>
date(new Date(value), { dateStyle: 'medium', timeStyle: 'short' });
</script>
<template>
<section
id="ticket-history"
aria-labelledby="ticket-history-title"
class="wrap-anywhere min-w-0 scroll-mt-6"
>
<Card class="h-fit">
<CardHeader>
<h2 id="ticket-history-title" class="text-lg font-semibold">
{{ t('ticketDetail.history') }}
</h2>
<CardDescription>{{
t('ticketDetail.historyDescription')
}}</CardDescription>
</CardHeader>
<CardContent class="space-y-4">
<div
v-if="(ticket.worklog_entries?.length ?? 0) === 0"
class="border-border text-muted-foreground rounded-xl border border-dashed px-4 py-6 text-sm"
>
{{ t('ticketDetail.noHistory') }}
</div>
<div v-else class="flex flex-col gap-4">
<div
v-for="worklogEntry in ticket.worklog_entries"
:key="worklogEntry.id"
class="border-border rounded-xl border px-4 py-4"
>
<div class="flex flex-wrap items-center gap-2">
<Badge variant="outline">
{{ worklogEntry.entry_type }}
</Badge>
<span class="text-foreground text-sm font-medium">
{{
worklogEntry.actor?.name ??
t('ticketDetail.system')
}}
</span>
</div>
<p class="text-foreground mt-3 text-sm">
{{ worklogEntry.summary }}
</p>
<p class="text-muted-foreground mt-2 text-xs">
{{ formatDateTime(worklogEntry.created_at) }}
</p>
</div>
</div>
</CardContent>
</Card>
</section>
</template>
@@ -1,165 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue';
import { UserAvatar } from '@/components/ui/avatar';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { useLocalization } from '@/composables/useLocalization';
import { ticketStatusLabel } from '@/lib/tickets';
import type { TicketDetailMetadataProps } from '@/types/ticket-detail';
const props = defineProps<TicketDetailMetadataProps>();
const { t, date, number } = useLocalization();
const fields = computed(() =>
props.ticketFieldDefinitions.map((definition) => {
const value = props.ticket.field_values?.find(
(field) => field.ticket_field_definition_id === definition.id,
);
return {
id: definition.id,
label: definition.label,
value:
definition.field_type === 'user'
? (value?.user_value?.name ?? t('tickets.unassigned'))
: value?.value_number != null
? number(value.value_number)
: t('ticketDetail.notSet'),
};
}),
);
</script>
<template>
<section
id="ticket-metadata"
aria-labelledby="ticket-metadata-title"
class="min-w-0 scroll-mt-6"
>
<Card class="bg-muted/30">
<CardHeader
><h2 id="ticket-metadata-title" class="text-lg font-semibold">
{{ t('ticketDetail.metadata') }}
</h2></CardHeader
>
<CardContent class="wrap-anywhere flex min-w-0 flex-col gap-6">
<dl
class="grid min-w-0 gap-5 text-sm sm:grid-cols-2 xl:grid-cols-1"
>
<div class="space-y-2">
<dt class="text-muted-foreground">
{{ t('ticketDetail.status') }}
</dt>
<dd>
{{
ticketStatusLabel(
ticket.ticket_status,
t('tickets.noStatus'),
)
}}
</dd>
</div>
<div class="space-y-2">
<dt class="text-muted-foreground">
{{ t('ticketDetail.type') }}
</dt>
<dd>{{ ticket.ticket_type.name }}</dd>
</div>
<div class="space-y-2">
<dt class="text-muted-foreground">
{{ t('ticketDetail.priority') }}
</dt>
<dd>
<Badge variant="outline">{{
ticket.ticket_priority.name
}}</Badge>
</dd>
</div>
<div class="space-y-2">
<dt class="text-muted-foreground">
{{ t('ticketDetail.assignee') }}
</dt>
<dd class="flex items-center gap-2">
<UserAvatar
v-if="ticket.assignee"
:user="ticket.assignee"
size="sm"
/>{{
ticket.assignee?.name ?? t('tickets.unassigned')
}}
</dd>
</div>
<div
v-if="project.show_author || canManageProject"
class="space-y-2"
>
<dt class="text-muted-foreground">
{{ t('ticketDetail.author') }}
</dt>
<dd class="flex items-center gap-2">
<UserAvatar
v-if="ticket.author"
:user="ticket.author"
size="sm"
/>{{
ticket.author?.name ??
t('ticketDetail.noAuthor')
}}
</dd>
</div>
<div v-if="project.show_story_points" class="space-y-2">
<dt class="text-muted-foreground">
{{ t('ticketDetail.storyPoints') }}
</dt>
<dd>
{{
ticket.story_points != null
? number(ticket.story_points)
: t('ticketDetail.notSet')
}}
</dd>
</div>
<div
v-for="field in fields"
:key="field.id"
class="space-y-2"
>
<dt class="text-muted-foreground">{{ field.label }}</dt>
<dd>{{ field.value }}</dd>
</div>
</dl>
<div
v-if="ticket.is_blocked"
class="border-warning/30 bg-warning/10 space-y-2 rounded-lg border p-4 text-sm"
>
<p class="font-semibold">{{ t('tickets.blocked') }}</p>
<p class="whitespace-pre-wrap">
{{ ticket.blocked_comment }}
</p>
</div>
<dl
class="text-muted-foreground grid gap-4 border-t pt-5 text-xs"
>
<div class="space-y-1">
<dt>{{ t('ticketDetail.created') }}</dt>
<dd>
<time :datetime="ticket.created_at">{{
date(new Date(ticket.created_at), {
dateStyle: 'medium',
timeStyle: 'short',
})
}}</time>
</dd>
</div>
<div class="space-y-1">
<dt>{{ t('ticketDetail.updated') }}</dt>
<dd>
<time :datetime="ticket.updated_at">{{
date(new Date(ticket.updated_at), {
dateStyle: 'medium',
timeStyle: 'short',
})
}}</time>
</dd>
</div>
</dl>
</CardContent>
</Card>
</section>
</template>
@@ -1,305 +0,0 @@
<script setup lang="ts">
import { Link, useForm } from '@inertiajs/vue3';
import { ArrowRight, GitFork, Link2, Plus } from 'lucide-vue-next';
import { computed, nextTick, useTemplateRef } from 'vue';
import { store } from '@/actions/App/Http/Controllers/ProjectTicketLinkController';
import FormFeedback from '@/components/FormFeedback.vue';
import TicketTreeList from '@/components/TicketTreeList.vue';
import TicketFormField from '@/components/tickets/TicketFormField.vue';
import TicketRelationshipLink from '@/components/tickets/detail/TicketRelationshipLink.vue';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { useFormProtection } from '@/composables/useFormProtection';
import { useLocalization } from '@/composables/useLocalization';
import { show as showTicket } from '@/routes/projects/tickets';
import type { TicketDetailRelationshipsProps } from '@/types/ticket-detail';
const props = defineProps<TicketDetailRelationshipsProps>();
defineEmits<{ editParent: [] }>();
const { t } = useLocalization();
const linksHeading = useTemplateRef<HTMLHeadingElement>('linksHeading');
const currentParentTicket = computed(
() =>
props.ticket.parent_ticket ??
props.parentTicketOptions.find(
(parent) => parent.id === props.ticket.parent_ticket_id,
) ??
null,
);
const canAddLink = computed(
() =>
props.canEditTicket &&
props.ticketLinkTypes.length > 0 &&
props.ticketLinkOptions.length > 0,
);
const form = useForm({
ticket_link_type_id: props.ticketLinkTypes[0]?.id ?? '',
linked_ticket_id: '',
});
const protection = useFormProtection(form, {
isDirty: () => props.canEditTicket && form.isDirty,
});
const selectedType = computed(() =>
props.ticketLinkTypes.find(
(type) => String(type.id) === String(form.ticket_link_type_id),
),
);
const selectedTicket = computed(() =>
props.ticketLinkOptions.find(
(ticket) => String(ticket.id) === String(form.linked_ticket_id),
),
);
async function focusLinks(): Promise<void> {
await nextTick();
linksHeading.value?.focus();
}
function submit(): void {
if (!canAddLink.value) {
return;
}
protection.submit(
store({ project: props.project.id, ticket: props.ticket.id }),
{
preserveScroll: true,
onSuccess: () => {
form.linked_ticket_id = '';
form.clearErrors();
void focusLinks();
},
},
);
}
</script>
<template>
<section
id="ticket-relationships"
aria-labelledby="ticket-relationships-title"
class="wrap-anywhere flex min-w-0 scroll-mt-6 flex-col gap-4"
>
<h2 id="ticket-relationships-title" class="text-lg font-semibold">
{{ t('ticketDetail.relationships') }}
</h2>
<Card class="min-w-0 gap-5 overflow-hidden">
<CardHeader
class="flex flex-wrap items-center justify-between gap-3"
>
<h3 class="flex items-center gap-2 font-semibold">
<GitFork class="text-primary size-5" aria-hidden="true" />{{
t('ticketRelationships.hierarchy')
}}
</h3>
<Button
v-if="canEditTicket"
type="button"
variant="outline"
size="sm"
@click="$emit('editParent')"
>{{ t('ticketRelationships.editParent') }}</Button
>
</CardHeader>
<CardContent class="grid min-w-0 gap-5">
<div class="grid min-w-0 gap-2">
<h4
class="text-muted-foreground text-xs font-semibold uppercase tracking-wide"
>
{{ t('ticketDetail.parent') }}
</h4>
<Link
v-if="currentParentTicket"
:href="
showTicket({
project: project.id,
ticket: currentParentTicket.id,
})
"
class="border-primary/20 bg-primary/5 hover:border-primary focus-visible:ring-ring/35 group grid min-w-0 gap-2 rounded-xl border p-4 outline-none transition-colors focus-visible:ring-2"
>
<span
class="text-primary font-mono text-xs font-semibold"
>{{ currentParentTicket.ticket_key }}</span
>
<span
class="group-hover:text-primary text-sm font-medium"
>{{ currentParentTicket.title }}</span
>
</Link>
<p
v-else
class="text-muted-foreground rounded-xl border border-dashed p-4 text-sm"
>
{{ t('ticketDetail.noParent') }}
</p>
<p
v-if="canEditTicket"
class="text-muted-foreground text-xs"
>
{{ t('ticketRelationships.parentHelp') }}
</p>
</div>
<div
v-if="ticket.ticket_type?.is_parent"
class="grid min-w-0 gap-3 border-t pt-5"
>
<div class="flex flex-wrap items-center gap-2">
<h4 class="text-sm font-semibold">
{{ t('ticketDetail.children') }}
</h4>
<Badge variant="secondary">{{
childTicketTree.length
}}</Badge>
</div>
<p class="text-muted-foreground text-xs">
{{ t('ticketRelationships.childrenHelp') }}
</p>
<p
v-if="childTicketTree.length === 0"
class="text-muted-foreground rounded-xl border border-dashed p-4 text-sm"
>
{{ t('ticketDetail.noChildren') }}
</p>
<TicketTreeList
v-else
:nodes="childTicketTree"
:project-id="project.id"
/>
</div>
</CardContent>
</Card>
<Card class="min-w-0 gap-5 overflow-hidden">
<CardHeader>
<h3
ref="linksHeading"
tabindex="-1"
class="focus-visible:ring-ring/35 flex flex-wrap items-center gap-2 rounded-md font-semibold outline-none focus-visible:ring-2"
>
<Link2 class="text-primary size-5" aria-hidden="true" />{{
t('ticketDetail.linkedTickets')
}}<Badge variant="secondary">{{
linkedTickets.length
}}</Badge>
</h3>
<p class="text-muted-foreground text-sm">
{{ t('ticketDetail.linkedTicketsDescription') }}
</p>
</CardHeader>
<CardContent class="grid min-w-0 gap-5">
<p
v-if="linkedTickets.length === 0"
class="text-muted-foreground rounded-xl border border-dashed p-4 text-sm"
>
{{ t('ticketDetail.noLinks') }}
</p>
<ul v-else class="grid min-w-0 gap-3">
<TicketRelationshipLink
v-for="link in linkedTickets"
:key="link.id"
:link="link"
:project-id="project.id"
:ticket-id="ticket.id"
:ticket-key="ticket.ticket_key"
:can-edit="canEditTicket"
@removed="focusLinks"
/>
</ul>
<form
v-if="canAddLink"
class="bg-muted/30 grid min-w-0 gap-4 rounded-xl border p-4"
@submit.prevent="submit"
>
<h4 class="flex items-center gap-2 text-sm font-semibold">
<Plus class="size-4" aria-hidden="true" />{{
t('ticketRelationships.addLink')
}}
</h4>
<FormFeedback
:state="protection.state.value"
@retry="submit"
/>
<fieldset
:disabled="form.processing"
class="grid min-w-0 gap-4 sm:grid-cols-[minmax(0,1fr)_minmax(0,2fr)]"
>
<TicketFormField
id="ticket_link_type_id"
v-model="form.ticket_link_type_id"
kind="select"
:label="t('ticketDetail.linkType')"
:options="
ticketLinkTypes.map((type) => ({
value: type.id,
label: type.label,
}))
"
:error="form.errors.ticket_link_type_id"
:disabled="form.processing"
required
/>
<TicketFormField
id="linked_ticket_id"
v-model="form.linked_ticket_id"
kind="select"
:label="t('ticketDetail.linkedTicket')"
:options="[
{
value: '',
label: t(
'ticketRelationships.selectTicket',
),
},
...ticketLinkOptions.map((ticket) => ({
value: ticket.id,
label: `${ticket.ticket_key} — ${ticket.title}`,
})),
]"
:error="form.errors.linked_ticket_id"
:disabled="form.processing"
required
/>
</fieldset>
<p
v-if="selectedType && selectedTicket"
aria-live="polite"
class="text-muted-foreground flex flex-wrap items-center gap-2 text-sm"
>
<span class="font-mono text-xs">{{
ticket.ticket_key
}}</span
><span>{{ selectedType.label }}</span
><ArrowRight class="size-4" aria-hidden="true" /><span
class="font-mono text-xs"
>{{ selectedTicket.ticket_key }}</span
>
</p>
<p class="text-muted-foreground text-xs">
{{ t('ticketRelationships.directionHelp') }}
</p>
<Button
type="submit"
:disabled="form.processing || !selectedTicket"
class="sm:justify-self-start"
>{{
form.processing
? t('forms.saving')
: t('ticketDetail.saveLink')
}}</Button
>
</form>
<p
v-else-if="canEditTicket"
class="text-muted-foreground text-sm"
>
{{
t(
ticketLinkTypes.length === 0
? 'ticketRelationships.noTypes'
: 'ticketRelationships.noTargets',
)
}}
</p>
</CardContent>
</Card>
</section>
</template>
@@ -1,116 +0,0 @@
<script setup lang="ts">
import { Link, useForm } from '@inertiajs/vue3';
import { ArrowDownLeft, ArrowUpRight, Unlink } from 'lucide-vue-next';
import { computed } from 'vue';
import { destroy } from '@/actions/App/Http/Controllers/ProjectTicketLinkController';
import FormFeedback from '@/components/FormFeedback.vue';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { useFormProtection } from '@/composables/useFormProtection';
import { useLocalization } from '@/composables/useLocalization';
import { show } from '@/routes/projects/tickets';
import type { TicketLinkModel } from '@/types/models/ticket-link-model';
const props = defineProps<{
link: TicketLinkModel;
projectId: number;
ticketId: number;
ticketKey: string;
canEdit: boolean;
}>();
const emit = defineEmits<{ removed: [] }>();
const { t } = useLocalization();
const outgoing = computed(() => props.link.source_ticket_id === props.ticketId);
const form = useForm({});
const protection = useFormProtection(form);
function remove(): void {
if (!props.canEdit) {
return;
}
protection.submit(
destroy({
project: props.projectId,
ticket: props.ticketId,
ticketLink: props.link.id,
}),
{
preserveScroll: true,
onSuccess: () => emit('removed'),
},
);
}
</script>
<template>
<li
class="grid min-w-0 gap-3 rounded-xl border p-4"
:data-ticket-link="link.id"
>
<div class="flex min-w-0 flex-wrap items-center gap-2">
<Badge
variant="outline"
class="text-muted-foreground max-w-full whitespace-normal"
>
<ArrowUpRight
v-if="outgoing"
class="size-3.5 shrink-0"
aria-hidden="true"
/><ArrowDownLeft
v-else
class="size-3.5 shrink-0"
aria-hidden="true"
/>
{{
t(
outgoing
? 'ticketRelationships.outgoing'
: 'ticketRelationships.incoming',
)
}}
</Badge>
<span class="text-muted-foreground font-mono text-xs">{{
ticketKey
}}</span>
<span class="text-sm font-medium">{{
link.ticket_link_type.label
}}</span>
</div>
<div class="flex min-w-0 flex-wrap items-start justify-between gap-3">
<Link
:href="
show({ project: projectId, ticket: link.linked_ticket.id })
"
class="hover:text-primary focus-visible:ring-ring/35 grid min-w-0 flex-1 basis-40 gap-1 rounded-md outline-none transition-colors focus-visible:ring-2"
>
<span class="text-primary font-mono text-xs font-semibold">{{
link.linked_ticket.ticket_key
}}</span>
<span class="wrap-anywhere text-sm font-medium">{{
link.linked_ticket.title
}}</span>
</Link>
<form v-if="canEdit" @submit.prevent="remove">
<Button
type="submit"
variant="ghost"
size="sm"
class="text-muted-foreground hover:text-destructive"
:aria-label="
t('ticketRelationships.removeLink', {
key: link.linked_ticket.ticket_key,
})
"
:disabled="form.processing"
><Unlink class="size-4" aria-hidden="true" />{{
t('ticketDetail.removeLink')
}}</Button
>
</form>
</div>
<FormFeedback
v-if="canEdit"
:state="protection.state.value"
@retry="remove"
/>
</li>
</template>
-2
View File
@@ -1,2 +0,0 @@
export { default as TicketCard } from './TicketCard.vue';
export { default as TicketListItem } from './TicketListItem.vue';
@@ -1,67 +1,53 @@
<script setup lang="ts">
import type { DialogContentEmits, DialogContentProps } from "reka-ui";
import type { HTMLAttributes } from "vue";
import { reactiveOmit } from "@vueuse/core";
import { X } from "lucide-vue-next";
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { X } from "lucide-vue-next"
import {
DialogClose,
DialogContent,
DialogPortal,
useForwardPropsEmits,
} from "reka-ui";
import { cn } from "@/lib/utils";
import DialogOverlay from "./DialogOverlay.vue";
import { useDialogFocus } from "@/composables/useDialogFocus";
import { useLocalization } from "@/composables/useLocalization";
DialogClose,
DialogContent,
DialogPortal,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
import DialogOverlay from "./DialogOverlay.vue"
defineOptions({
inheritAttrs: false,
});
inheritAttrs: false,
})
const props = withDefaults(
defineProps<
DialogContentProps & {
class?: HTMLAttributes["class"];
showCloseButton?: boolean;
}
>(),
{
showCloseButton: true,
},
);
const emits = defineEmits<DialogContentEmits>();
const props = withDefaults(defineProps<DialogContentProps & { class?: HTMLAttributes["class"], showCloseButton?: boolean }>(), {
showCloseButton: true,
})
const emits = defineEmits<DialogContentEmits>()
const delegatedProps = reactiveOmit(props, "class", "showCloseButton");
const { restoreFocus } = useDialogFocus();
const { t } = useLocalization();
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits);
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DialogPortal>
<DialogOverlay />
<DialogContent
data-slot="dialog-content"
v-bind="{ ...$attrs, ...forwarded }"
@close-auto-focus="restoreFocus"
:class="
cn(
'bg-card text-card-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid max-h-[calc(100dvh-2rem)] w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-column overflow-y-auto overscroll-contain rounded-md border p-page shadow-lg duration-200 motion-reduce:animate-none sm:max-w-lg',
props.class,
)
"
>
<slot />
<DialogPortal>
<DialogOverlay />
<DialogContent
data-slot="dialog-content"
v-bind="{ ...$attrs, ...forwarded }"
:class="
cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
props.class,
)"
>
<slot />
<DialogClose
v-if="showCloseButton"
data-slot="dialog-close"
class="ring-offset-background focus-visible:ring-ring absolute top-1 right-1 flex size-control items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<X />
<span class="sr-only">{{ t("common.close") }}</span>
</DialogClose>
</DialogContent>
</DialogPortal>
<DialogClose
v-if="showCloseButton"
data-slot="dialog-close"
class="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<X />
<span class="sr-only">Close</span>
</DialogClose>
</DialogContent>
</DialogPortal>
</template>
@@ -1,17 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div
data-slot="dialog-header"
:class="cn('flex flex-col gap-stack pr-6 text-left', props.class)"
>
<slot />
</div>
<div
data-slot="dialog-header"
:class="cn('flex flex-col gap-2 text-center sm:text-left', props.class)"
>
<slot />
</div>
</template>
@@ -1,71 +1,59 @@
<script setup lang="ts">
import type { DialogContentEmits, DialogContentProps } from "reka-ui";
import type { HTMLAttributes } from "vue";
import { reactiveOmit } from "@vueuse/core";
import { X } from "lucide-vue-next";
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { X } from "lucide-vue-next"
import {
DialogClose,
DialogContent,
DialogOverlay,
DialogPortal,
useForwardPropsEmits,
} from "reka-ui";
import { cn } from "@/lib/utils";
import { useDialogFocus } from "@/composables/useDialogFocus";
import { useLocalization } from "@/composables/useLocalization";
DialogClose,
DialogContent,
DialogOverlay,
DialogPortal,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
defineOptions({
inheritAttrs: false,
});
inheritAttrs: false,
})
const props = defineProps<
DialogContentProps & { class?: HTMLAttributes["class"] }
>();
const emits = defineEmits<DialogContentEmits>();
const props = defineProps<DialogContentProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<DialogContentEmits>()
const delegatedProps = reactiveOmit(props, "class");
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits);
const { restoreFocus } = useDialogFocus();
const { t } = useLocalization();
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DialogPortal>
<DialogOverlay
class="fixed inset-0 z-50 grid place-items-center overflow-y-auto bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
>
<DialogContent
:class="
cn(
'relative z-50 my-4 grid w-full max-w-[calc(100%-2rem)] gap-column rounded-md border bg-card p-page text-card-foreground shadow-lg duration-200 sm:max-w-lg',
props.class,
)
"
v-bind="{ ...$attrs, ...forwarded }"
@close-auto-focus="restoreFocus"
@pointer-down-outside="
(event) => {
const originalEvent = event.detail.originalEvent;
const target = originalEvent.target as HTMLElement;
if (
originalEvent.offsetX > target.clientWidth ||
originalEvent.offsetY > target.clientHeight
) {
event.preventDefault();
}
}
"
>
<slot />
<DialogPortal>
<DialogOverlay
class="fixed inset-0 z-50 grid place-items-center overflow-y-auto bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
>
<DialogContent
:class="
cn(
'relative z-50 grid w-full max-w-lg my-8 gap-4 border border-border bg-background p-6 shadow-lg duration-200 sm:rounded-lg md:w-full',
props.class,
)
"
v-bind="{ ...$attrs, ...forwarded }"
@pointer-down-outside="(event) => {
const originalEvent = event.detail.originalEvent;
const target = originalEvent.target as HTMLElement;
if (originalEvent.offsetX > target.clientWidth || originalEvent.offsetY > target.clientHeight) {
event.preventDefault();
}
}"
>
<slot />
<DialogClose
class="absolute top-1 right-1 flex size-control items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring"
>
<X class="w-4 h-4" />
<span class="sr-only">{{ t("common.close") }}</span>
</DialogClose>
</DialogContent>
</DialogOverlay>
</DialogPortal>
<DialogClose
class="absolute top-4 right-4 p-0.5 transition-colors rounded-md hover:bg-secondary"
>
<X class="w-4 h-4" />
<span class="sr-only">Close</span>
</DialogClose>
</DialogContent>
</DialogOverlay>
</DialogPortal>
</template>
@@ -1,48 +1,39 @@
<script setup lang="ts">
import type {
DropdownMenuCheckboxItemEmits,
DropdownMenuCheckboxItemProps,
} from "reka-ui";
import type { HTMLAttributes } from "vue";
import { reactiveOmit } from "@vueuse/core";
import { Check } from "lucide-vue-next";
import type { DropdownMenuCheckboxItemEmits, DropdownMenuCheckboxItemProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { Check } from "lucide-vue-next"
import {
DropdownMenuCheckboxItem,
DropdownMenuItemIndicator,
useForwardPropsEmits,
} from "reka-ui";
import { cn } from "@/lib/utils";
DropdownMenuCheckboxItem,
DropdownMenuItemIndicator,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<
DropdownMenuCheckboxItemProps & { class?: HTMLAttributes["class"] }
>();
const emits = defineEmits<DropdownMenuCheckboxItemEmits>();
const props = defineProps<DropdownMenuCheckboxItemProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<DropdownMenuCheckboxItemEmits>()
const delegatedProps = reactiveOmit(props, "class");
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits);
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DropdownMenuCheckboxItem
data-slot="dropdown-menu-checkbox-item"
v-bind="forwarded"
:class="
cn(
'focus:bg-accent focus:text-accent-foreground relative flex min-h-control cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
props.class,
)
"
>
<span
class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"
>
<DropdownMenuItemIndicator>
<slot name="indicator-icon">
<Check class="size-4" />
</slot>
</DropdownMenuItemIndicator>
</span>
<slot />
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
data-slot="dropdown-menu-checkbox-item"
v-bind="forwarded"
:class=" cn(
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
props.class,
)"
>
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuItemIndicator>
<slot name="indicator-icon">
<Check class="size-4" />
</slot>
</DropdownMenuItemIndicator>
</span>
<slot />
</DropdownMenuCheckboxItem>
</template>
@@ -1,49 +1,39 @@
<script setup lang="ts">
import type {
DropdownMenuContentEmits,
DropdownMenuContentProps,
} from "reka-ui";
import type { HTMLAttributes } from "vue";
import { reactiveOmit } from "@vueuse/core";
import type { DropdownMenuContentEmits, DropdownMenuContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
DropdownMenuContent,
DropdownMenuPortal,
useForwardPropsEmits,
} from "reka-ui";
import { cn } from "@/lib/utils";
DropdownMenuContent,
DropdownMenuPortal,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
defineOptions({
inheritAttrs: false,
});
inheritAttrs: false,
})
const props = withDefaults(
defineProps<
DropdownMenuContentProps & { class?: HTMLAttributes["class"] }
>(),
{
sideOffset: 4,
},
);
const emits = defineEmits<DropdownMenuContentEmits>();
defineProps<DropdownMenuContentProps & { class?: HTMLAttributes["class"] }>(),
{
sideOffset: 4,
},
)
const emits = defineEmits<DropdownMenuContentEmits>()
const delegatedProps = reactiveOmit(props, "class");
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits);
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DropdownMenuPortal>
<DropdownMenuContent
data-slot="dropdown-menu-content"
v-bind="{ ...$attrs, ...forwarded }"
:class="
cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--reka-dropdown-menu-content-available-height) min-w-[8rem] max-w-[calc(100vw-2rem)] origin-(--reka-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',
props.class,
)
"
>
<slot />
</DropdownMenuContent>
</DropdownMenuPortal>
<DropdownMenuPortal>
<DropdownMenuContent
data-slot="dropdown-menu-content"
v-bind="{ ...$attrs, ...forwarded }"
:class="cn('bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--reka-dropdown-menu-content-available-height) min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md', props.class)"
>
<slot />
</DropdownMenuContent>
</DropdownMenuPortal>
</template>
@@ -1,41 +1,31 @@
<script setup lang="ts">
import type { DropdownMenuItemProps } from "reka-ui";
import type { HTMLAttributes } from "vue";
import { reactiveOmit } from "@vueuse/core";
import { DropdownMenuItem, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils";
import type { DropdownMenuItemProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DropdownMenuItem, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
const props = withDefaults(
defineProps<
DropdownMenuItemProps & {
class?: HTMLAttributes["class"];
inset?: boolean;
variant?: "default" | "destructive";
}
>(),
{
variant: "default",
},
);
const props = withDefaults(defineProps<DropdownMenuItemProps & {
class?: HTMLAttributes["class"]
inset?: boolean
variant?: "default" | "destructive"
}>(), {
variant: "default",
})
const delegatedProps = reactiveOmit(props, "inset", "variant", "class");
const delegatedProps = reactiveOmit(props, "inset", "variant", "class")
const forwardedProps = useForwardProps(delegatedProps);
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<DropdownMenuItem
data-slot="dropdown-menu-item"
:data-inset="inset ? '' : undefined"
:data-variant="variant"
v-bind="forwardedProps"
:class="
cn(
'focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex min-h-control cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
props.class,
)
"
>
<slot />
</DropdownMenuItem>
<DropdownMenuItem
data-slot="dropdown-menu-item"
:data-inset="inset ? '' : undefined"
:data-variant="variant"
v-bind="forwardedProps"
:class="cn('focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4', props.class)"
>
<slot />
</DropdownMenuItem>
</template>
@@ -1,49 +1,40 @@
<script setup lang="ts">
import type {
DropdownMenuRadioItemEmits,
DropdownMenuRadioItemProps,
} from "reka-ui";
import type { HTMLAttributes } from "vue";
import { reactiveOmit } from "@vueuse/core";
import { Circle } from "lucide-vue-next";
import type { DropdownMenuRadioItemEmits, DropdownMenuRadioItemProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { Circle } from "lucide-vue-next"
import {
DropdownMenuItemIndicator,
DropdownMenuRadioItem,
useForwardPropsEmits,
} from "reka-ui";
import { cn } from "@/lib/utils";
DropdownMenuItemIndicator,
DropdownMenuRadioItem,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<
DropdownMenuRadioItemProps & { class?: HTMLAttributes["class"] }
>();
const props = defineProps<DropdownMenuRadioItemProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<DropdownMenuRadioItemEmits>();
const emits = defineEmits<DropdownMenuRadioItemEmits>()
const delegatedProps = reactiveOmit(props, "class");
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits);
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DropdownMenuRadioItem
data-slot="dropdown-menu-radio-item"
v-bind="forwarded"
:class="
cn(
'focus:bg-accent focus:text-accent-foreground relative flex min-h-control cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
props.class,
)
"
>
<span
class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"
>
<DropdownMenuItemIndicator>
<slot name="indicator-icon">
<Circle class="size-2 fill-current" />
</slot>
</DropdownMenuItemIndicator>
</span>
<slot />
</DropdownMenuRadioItem>
<DropdownMenuRadioItem
data-slot="dropdown-menu-radio-item"
v-bind="forwarded"
:class="cn(
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
props.class,
)"
>
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuItemIndicator>
<slot name="indicator-icon">
<Circle class="size-2 fill-current" />
</slot>
</DropdownMenuItemIndicator>
</span>
<slot />
</DropdownMenuRadioItem>
</template>
@@ -1,34 +1,27 @@
<script setup lang="ts">
import type {
DropdownMenuSubContentEmits,
DropdownMenuSubContentProps,
} from "reka-ui";
import type { HTMLAttributes } from "vue";
import { reactiveOmit } from "@vueuse/core";
import { DropdownMenuSubContent, useForwardPropsEmits } from "reka-ui";
import { cn } from "@/lib/utils";
import type { DropdownMenuSubContentEmits, DropdownMenuSubContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
DropdownMenuSubContent,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<
DropdownMenuSubContentProps & { class?: HTMLAttributes["class"] }
>();
const emits = defineEmits<DropdownMenuSubContentEmits>();
const props = defineProps<DropdownMenuSubContentProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<DropdownMenuSubContentEmits>()
const delegatedProps = reactiveOmit(props, "class");
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits);
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DropdownMenuSubContent
data-slot="dropdown-menu-sub-content"
v-bind="forwarded"
:class="
cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--reka-dropdown-menu-content-available-height) min-w-[8rem] max-w-[calc(100vw-2rem)] origin-(--reka-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-lg',
props.class,
)
"
>
<slot />
</DropdownMenuSubContent>
<DropdownMenuSubContent
data-slot="dropdown-menu-sub-content"
v-bind="forwarded"
:class="cn('bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg', props.class)"
>
<slot />
</DropdownMenuSubContent>
</template>
@@ -1,34 +1,30 @@
<script setup lang="ts">
import type { DropdownMenuSubTriggerProps } from "reka-ui";
import type { HTMLAttributes } from "vue";
import { reactiveOmit } from "@vueuse/core";
import { ChevronRight } from "lucide-vue-next";
import { DropdownMenuSubTrigger, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils";
import type { DropdownMenuSubTriggerProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { ChevronRight } from "lucide-vue-next"
import {
DropdownMenuSubTrigger,
useForwardProps,
} from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<
DropdownMenuSubTriggerProps & {
class?: HTMLAttributes["class"];
inset?: boolean;
}
>();
const props = defineProps<DropdownMenuSubTriggerProps & { class?: HTMLAttributes["class"], inset?: boolean }>()
const delegatedProps = reactiveOmit(props, "class", "inset");
const forwardedProps = useForwardProps(delegatedProps);
const delegatedProps = reactiveOmit(props, "class", "inset")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<DropdownMenuSubTrigger
data-slot="dropdown-menu-sub-trigger"
v-bind="forwardedProps"
:class="
cn(
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex min-h-control cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
props.class,
)
"
>
<slot />
<ChevronRight class="ml-auto size-4" />
</DropdownMenuSubTrigger>
<DropdownMenuSubTrigger
data-slot="dropdown-menu-sub-trigger"
v-bind="forwardedProps"
:class="cn(
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
props.class,
)"
>
<slot />
<ChevronRight class="ml-auto size-4" />
</DropdownMenuSubTrigger>
</template>

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