diff --git a/resources/js/locales/en.ts b/resources/js/locales/en.ts index f0bcc11..311535d 100644 --- a/resources/js/locales/en.ts +++ b/resources/js/locales/en.ts @@ -583,6 +583,8 @@ export const en = { 'profile.delete.confirmAction': 'Permanently delete account', 'common.cancel': 'Cancel', 'common.close': 'Close', + 'common.edit': 'Edit', + 'common.remove': 'Remove', 'common.retry': 'Try again', 'feedback.empty': 'Nothing here yet', 'feedback.saved': 'Changes saved', @@ -874,6 +876,81 @@ export const en = { 'inheritance.area.ticketLinkTypes': 'Ticket link types', 'inheritance.area.repositoryConnections': 'Repository connections', 'inheritance.area.notificationActions': 'Notification actions', + 'repositorySettings.pageTitle': '{project} repository settings', + 'repositorySettings.title': 'Project repositories', + 'repositorySettings.description': + 'Choose the repositories this project uses for ticket branches and pull requests.', + 'repositorySettings.projectScope': + 'Repository links belong to the project. Every team member keeps using the same linked repositories, while access is granted through their own personal Git connections.', + 'repositorySettings.linkedRepositories': 'Linked repositories', + 'repositorySettings.linkAction': 'Link repository', + 'repositorySettings.empty': 'No project repositories yet', + 'repositorySettings.emptyDescription': + 'Link a repository from one of your personal Git connections to make it available to project tickets.', + 'repositorySettings.personalAccessReady': 'Personal access ready', + 'repositorySettings.personalAccessMissing': 'Personal access needed', + 'repositorySettings.ticketUsage': + 'Tickets use this project link to discover matching branches and pull requests.', + 'repositorySettings.openRepository': + 'Open {repository} in the Git provider (opens in a new tab)', + 'repositorySettings.open': 'Open repository', + 'repositorySettings.connectAccess': 'Connect Git access', + 'repositorySettings.editRepository': + 'Change project repository {repository}', + 'repositorySettings.removeRepository': + 'Remove project repository {repository}', + 'repositorySettings.personalSources': 'Personal Git access', + 'repositorySettings.personalSourcesDescription': + 'These account connections are private to you and are only used to discover repositories you can link to this project.', + 'repositorySettings.manageConnections': 'Manage Git connections', + 'repositorySettings.refresh': 'Refresh repositories', + 'repositorySettings.refreshing': 'Loading repositories…', + 'repositorySettings.refreshingDescription': + 'The latest repositories are being requested from your Git providers.', + 'repositorySettings.noConnections': 'No personal Git connection', + 'repositorySettings.noConnectionsDescription': + 'Add a personal Git connection before linking or changing a project repository.', + 'repositorySettings.detectedSources': 'Detected personal sources', + 'repositorySettings.unavailable': 'Provider unavailable', + 'repositorySettings.repositoryCount': '{count} repositories', + 'repositorySettings.linkTitle': 'Link a project repository', + 'repositorySettings.linkDescription': + 'Choose a personal connection, find a repository, and add it to this project.', + 'repositorySettings.editTitle': 'Change project repository', + 'repositorySettings.editDescription': + 'Replace this project link with another repository. Tickets will use the updated link after saving.', + 'repositorySettings.removeTitle': 'Remove repository', + 'repositorySettings.removeDescription': + 'Remove this repository from the project? Tickets will no longer discover its branches or pull requests.', + 'repositorySettings.linked': 'Repository linked to the project.', + 'repositorySettings.updated': 'Project repository updated.', + 'repositorySettings.removed': 'Repository removed from the project.', + 'repositorySettings.matchingConnectionMissing': + 'Matching personal access is missing', + 'repositorySettings.matchingConnectionMissingDescription': + 'The project link remains available to the team, but this account cannot query it until you add a matching provider connection. You can still choose a different accessible repository below.', + 'repositorySettings.personalConnection': 'Personal Git connection', + 'repositorySettings.personalConnectionHelp': + 'Used for discovery only; it is not shared with the project.', + 'repositorySettings.providerError': + 'Repositories could not be loaded from this provider', + 'repositorySettings.searchLabel': 'Find a repository', + 'repositorySettings.searchPlaceholder': + 'Search by repository, owner, or namespace', + 'repositorySettings.noRepositories': 'No repositories available', + 'repositorySettings.noRepositoriesDescription': + 'This connection is working, but the provider returned no repositories you can access.', + 'repositorySettings.noSearchResults': 'No matching repositories', + 'repositorySettings.noSearchResultsDescription': + 'No repository matches “{search}”. Try a different name or clear the search.', + 'repositorySettings.repositoryLabel': 'Available repositories', + 'repositorySettings.forbidden': 'Project access denied', + 'repositorySettings.forbiddenDescription': + 'You no longer have permission to manage repositories for this project. Close this dialog and refresh the page.', + 'repositorySettings.linking': 'Linking repository…', + 'repositorySettings.saving': 'Saving repository…', + 'repositorySettings.save': 'Save repository', + 'repositorySettings.removing': 'Removing repository…', 'gitConnections.description': 'Connect your personal Git account to browse repositories, branches, and pull requests you can access.', 'gitConnections.privacy': diff --git a/resources/js/pages/projects/settings/Repositories.vue b/resources/js/pages/projects/settings/Repositories.vue index 3c2687e..aa758bd 100644 --- a/resources/js/pages/projects/settings/Repositories.vue +++ b/resources/js/pages/projects/settings/Repositories.vue @@ -1,282 +1,336 @@ diff --git a/tests/Feature/Projects/ProjectRepositorySettingsControllerTest.php b/tests/Feature/Projects/ProjectRepositorySettingsControllerTest.php index 2da038c..4a4a7b0 100644 --- a/tests/Feature/Projects/ProjectRepositorySettingsControllerTest.php +++ b/tests/Feature/Projects/ProjectRepositorySettingsControllerTest.php @@ -63,6 +63,58 @@ class ProjectRepositorySettingsControllerTest extends TestCase ); } + /** + * Ensure repository discovery distinguishes empty provider results from provider failures. + */ + public function test_repository_settings_expose_personal_access_and_provider_states(): void + { + $user = User::factory()->create(); + $project = Project::factory()->create(); + $emptyGitConnection = UserGitConnection::factory()->for($user)->create([ + 'account_login' => 'empty-account', + 'base_url' => 'https://empty.git.test', + 'provider' => GitProviderKey::Gitea, + ]); + UserGitConnection::factory()->for($user)->create([ + 'account_login' => 'broken-account', + 'base_url' => 'https://broken.git.test', + 'provider' => GitProviderKey::Gitea, + ]); + RepositoryConnection::factory()->for($project)->create([ + 'base_url' => $emptyGitConnection->base_url, + 'provider' => GitProviderKey::Gitea, + ]); + + $this->syncProjectPermissions($project, $user, ['edit']); + + Http::preventStrayRequests(); + Http::fake([ + 'https://empty.git.test/api/v1/user/repos*' => Http::response([]), + 'https://broken.git.test/api/v1/user/repos*' => Http::response([], 502), + ]); + + $this->actingAs($user) + ->get(route('projects.settings.repositories', $project)) + ->assertOk() + ->assertInertia(fn (Assert $page) => $page + ->component('projects/settings/Repositories') + ->has('availableRepositorySources', 2) + ->where('availableRepositorySources', function ($sources): bool { + $sourcesByAccount = collect($sources)->keyBy('account_login'); + $emptySource = $sourcesByAccount->get('empty-account'); + $brokenSource = $sourcesByAccount->get('broken-account'); + + return data_get($emptySource, 'provider') === GitProviderKey::Gitea->value + && data_get($emptySource, 'provider_label') === GitProviderKey::Gitea->label() + && data_get($emptySource, 'repositories') === [] + && data_get($emptySource, 'error') === null + && data_get($brokenSource, 'repositories') === [] + && data_get($brokenSource, 'error') === config('app.name').' could not load repositories from this Git connection right now.'; + }) + ->where('repositoryConnections.0.has_personal_access', true), + ); + } + /** * Ensure users without edit permission cannot manage repository settings. */