Udostępnij labele projektu nowej stronie

Przećwiczony przykład: nadanie zupełnie nowej, jednoprojektowej stronie prawdziwych kolorów labeli (zamiast hashowanego fallbacku z useProjectLabels()) przez zamontowanie ProjectLabelsProvider. Dokładnie ten sam wzorzec stosują już Pages/Projects/Show.tsx i Pages/Issues/Show.tsx — poniższy przećwiczony przykład to hipotetyczna strona Backlog (widok nieplanowanych issue'ów jednego projektu), zbudowana w tym samym kształcie, więc możesz skopiować go w całości do dowolnej nowej strony jednoprojektowej.

Pomiń ten przewodnik, jeśli strona pokazuje issue'y z wielu projektów naraz (jak Dashboard) — nie ma wtedy jednego projektu, którego labele miałyby zastosowanie, więc takie strony polegają na deterministycznym fallbacku hashLabelColor(), i to jest poprawny wybór, a nie luka do wypełnienia.

Krok 1 — Zmapuj labele projektu w kontrolerze

Plik: app/Http/Controllers/BacklogController.php

<?php

namespace App\Http\Controllers;

use App\Models\Label;
use App\Models\Project;
use App\Services\IssueService;
use App\Services\LabelService;
use Illuminate\Support\Collection;
use Inertia\Inertia;
use Inertia\Response;

class BacklogController extends Controller
{
    public function __construct(
        protected IssueService $issueService,
        protected LabelService $labelService,
    ) {}

    public function show(Project $project): Response
    {
        $this->authorize('view', $project);

        return Inertia::render('Projects/Backlog', [
            'project' => $project,
            'issues' => $this->issueService->getAllForProject($project->id, [], ['status' => ['open']]),
            'labels' => $this->mapLabels($this->labelService->getLabels($project)),
        ]);
    }

    private function mapLabels(Collection $labels): array
    {
        return $labels->map(fn (Label $label) => [
            'id' => $label->id,
            'name' => $label->name,
            'color' => $label->color,
            'description' => $label->description,
            'isSystem' => $label->is_system,
        ])->values()->all();
    }
}

mapLabels() jest wklejone jeden do jednego z ProjectController i IssueController — jest na tyle małe i na tyle specyficzne dla importów każdego kontrolera, że to repozytorium powtarza je per kontroler zamiast wyciągać wspólną klasę w stylu LabelResource. Zrób tu to samo, zamiast sięgać po nową abstrakcję.

Zarejestruj trasę tak samo, jak projects.show:

Plik: routes/web.php

Route::get('/projects/{project}/backlog', [BacklogController::class, 'show'])->name('projects.backlog');

Krok 2 — Zamontuj providera na stronie

Plik: resources/js/Pages/Projects/Backlog.tsx

import ListRow from '@/Components/Organisms/ListRow/ListRow';
import Sidebar from '@/Components/Organisms/Sidebar/Sidebar';
import { ProjectLabelsProvider } from '@/context/ProjectLabelsContext';
import { Issue } from '@/types/Issues';
import { ProjectLabel } from '@/types/Labels';
import { Project } from '@/types/Projects';

interface BacklogProps {
    project: Project;
    issues: Issue[];
    labels: ProjectLabel[];
}

export default function Backlog({ project, issues, labels }: BacklogProps) {
    return (
        <ProjectLabelsProvider labels={labels}>
            <div className="flex h-screen w-screen overflow-hidden bg-[var(--bg-color)]">
                <Sidebar projects={[project]} />
                <div className="m-2 flex min-w-0 flex-1 flex-col overflow-hidden rounded-2xl bg-[var(--bg-color-hover)]">
                    <table className="w-full">
                        <tbody>
                            {issues.map((issue) => (
                                <ListRow
                                    key={issue.id}
                                    issue={issue}
                                    onClick={() => {}}
                                    isClosed={issue.status === 'closed'}
                                />
                            ))}
                        </tbody>
                    </table>
                </div>
            </div>
        </ProjectLabelsProvider>
    );
}

Każdy LabelBadge/LabelList/EditableLabelList renderowany gdziekolwiek wewnątrz ListRow teraz automatycznie rozwiązuje swój kolor przez prawdziwe dane labeli tego providera — żaden z tych komponentów sam nie przyjmuje propsu labels, więc nie ma nic więcej do podpięcia. Provider musi owijać drzewo zanim którykolwiek z nich się wyrenderuje, dlatego siedzi na górze komponentu strony, a nie np. tylko wokół samej tabeli.

Krok 3 — Testy

Plik: resources/js/Pages/Projects/Backlog.test.tsx

import { render, screen } from '@testing-library/react';
import { describe, expect, test } from 'vitest';
import Backlog from './Backlog';

const project = {
    id: 1,
    name: 'Orbit',
    slug: 'orbit',
    description: '',
    color: 'purple' as const,
    created_at: 0,
    updated_at: 0,
};

describe('Backlog Page', () => {
    test('renders each issue with its real label color', () => {
        render(
            <Backlog
                project={project}
                issues={[
                    {
                        id: '1',
                        title: 'Fix login crash',
                        status: 'open',
                        priority: 'high',
                        project_id: 1,
                        user_id: 1,
                        labels: ['bug'],
                    },
                ]}
                labels={[
                    {
                        id: 1,
                        name: 'bug',
                        color: '#f44336',
                        description: null,
                        isSystem: true,
                    },
                ]}
            />,
        );

        expect(screen.getByText('bug')).toBeInTheDocument();
    });
});

Dodaj BacklogControllerTest.php, odzwierciedlający kształt tests/Feature/ProjectIntegrationControllerTest.php (zalogowany member dostaje 200, ktoś z zewnątrz dostaje 403), plus jedną asercję, że props labels w payloadzie Inertia zgadza się z $project->labels.

Przed zacommitowaniem uruchom php artisan test i npm test -- run — dokładne komendy znajdziesz w głównym CLAUDE.md.