Skip to content

Commit 73bc711

Browse files
committed
feat(repository): add bulk import functionality with UI, validation, and webhook handling
1 parent 080fe59 commit 73bc711

16 files changed

Lines changed: 780 additions & 19 deletions

app/Domains/Organization/Http/Controllers/OrganizationController.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use App\Domains\Organization\Actions\CreateOrganizationAction;
88
use App\Domains\Organization\Contracts\Data\OrganizationData;
99
use App\Domains\Organization\Requests\StoreOrganizationRequest;
10+
use App\Domains\Repository\Contracts\Enums\GitProvider;
1011
use App\Http\Controllers\Controller;
1112
use App\Models\Organization;
1213
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
@@ -34,10 +35,16 @@ public function show(Organization $organization): Response
3435
/** @var \App\Models\User $user */
3536
$user = auth()->user();
3637

38+
$configuredProviders = $user->gitCredentials()
39+
->pluck('provider')
40+
->map(fn (GitProvider $provider) => $provider->value)
41+
->toArray();
42+
3743
return Inertia::render('organizations/show', [
3844
'organization' => OrganizationData::fromModel($organization),
3945
'stats' => $this->buildStats->handle($organization),
4046
'onboarding' => $this->buildOnboarding->handle($organization, $user),
47+
'configuredProviders' => $configuredProviders,
4148
]);
4249
}
4350

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
namespace App\Domains\Repository\Actions;
4+
5+
use App\Domains\Repository\Contracts\Data\BulkImportResultData;
6+
use App\Domains\Repository\Contracts\Enums\GitProvider;
7+
use App\Domains\Repository\Jobs\SyncRepositoryJob;
8+
use App\Models\Organization;
9+
use App\Models\Repository;
10+
11+
class BulkCreateRepositoriesAction
12+
{
13+
public function __construct(
14+
protected ExtractRepositoryNameAction $extractRepositoryName,
15+
protected RegisterWebhookAction $registerWebhook,
16+
) {}
17+
18+
/**
19+
* Bulk create repositories for an organization.
20+
*
21+
* @param array<int, array{repo_identifier: string}> $repositories
22+
*/
23+
public function handle(
24+
Organization $organization,
25+
GitProvider $provider,
26+
array $repositories,
27+
string $userUuid,
28+
): BulkImportResultData {
29+
$existingIdentifiers = Repository::query()
30+
->where('organization_uuid', $organization->uuid)
31+
->where('provider', $provider)
32+
->pluck('repo_identifier')
33+
->toArray();
34+
35+
$created = 0;
36+
$skipped = 0;
37+
$webhooksFailed = 0;
38+
39+
foreach ($repositories as $repoData) {
40+
$identifier = $repoData['repo_identifier'];
41+
42+
if (in_array($identifier, $existingIdentifiers)) {
43+
$skipped++;
44+
45+
continue;
46+
}
47+
48+
$name = $this->extractRepositoryName->handle($identifier, $provider);
49+
50+
$repository = Repository::create([
51+
'organization_uuid' => $organization->uuid,
52+
'credential_user_uuid' => $userUuid,
53+
'name' => $name,
54+
'provider' => $provider,
55+
'repo_identifier' => $identifier,
56+
]);
57+
58+
SyncRepositoryJob::dispatch($repository);
59+
60+
if (! $this->registerWebhook->handle($repository)) {
61+
$webhooksFailed++;
62+
}
63+
64+
$created++;
65+
}
66+
67+
return new BulkImportResultData(
68+
created: $created,
69+
skipped: $skipped,
70+
webhooksFailed: $webhooksFailed,
71+
);
72+
}
73+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace App\Domains\Repository\Contracts\Data;
4+
5+
use Spatie\LaravelData\Data;
6+
7+
class BulkImportResultData extends Data
8+
{
9+
public function __construct(
10+
public int $created,
11+
public int $skipped,
12+
public int $webhooksFailed,
13+
) {}
14+
15+
public function statusMessage(): string
16+
{
17+
$parts = [];
18+
19+
if ($this->created > 0) {
20+
$parts[] = $this->created.' '.($this->created === 1 ? 'repository' : 'repositories').' imported';
21+
}
22+
23+
if ($this->skipped > 0) {
24+
$parts[] = $this->skipped.' already connected';
25+
}
26+
27+
if ($this->webhooksFailed > 0) {
28+
$parts[] = $this->webhooksFailed.' webhook '.($this->webhooksFailed === 1 ? 'registration' : 'registrations').' failed';
29+
}
30+
31+
if (empty($parts)) {
32+
return 'No repositories were imported.';
33+
}
34+
35+
return implode(', ', $parts).'.';
36+
}
37+
}

app/Domains/Repository/Contracts/Data/RepositorySuggestionData.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public function __construct(
1313
public string $fullName,
1414
public bool $isPrivate,
1515
public ?string $description,
16+
public bool $isConnected = false,
1617
) {}
1718

1819
/**

app/Domains/Repository/Http/Controllers/Api/RepositorySuggestionController.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use App\Domains\Repository\Services\GitProviders\GitHubProvider;
88
use App\Http\Controllers\Controller;
99
use App\Models\Organization;
10+
use App\Models\Repository;
1011
use App\Models\UserGitCredential;
1112
use Illuminate\Http\JsonResponse;
1213
use Illuminate\Http\Request;
@@ -41,6 +42,23 @@ public function index(Request $request, Organization $organization): JsonRespons
4142
),
4243
};
4344

45+
$connectedIdentifiers = Repository::query()
46+
->where('organization_uuid', $organization->uuid)
47+
->where('provider', $provider)
48+
->pluck('repo_identifier')
49+
->toArray();
50+
51+
$repositories = array_map(
52+
fn (RepositorySuggestionData $repo) => new RepositorySuggestionData(
53+
name: $repo->name,
54+
fullName: $repo->fullName,
55+
isPrivate: $repo->isPrivate,
56+
description: $repo->description,
57+
isConnected: in_array($repo->fullName, $connectedIdentifiers),
58+
),
59+
$repositories,
60+
);
61+
4462
return response()->json(['repositories' => $repositories]);
4563
} catch (\Exception $e) {
4664
return response()->json(['error' => $e->getMessage()], 500);

app/Domains/Repository/Http/Controllers/RepositoryController.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44

55
use App\Domains\Organization\Contracts\Data\OrganizationData;
66
use App\Domains\Package\Contracts\Data\PackageData;
7+
use App\Domains\Repository\Actions\BulkCreateRepositoriesAction;
78
use App\Domains\Repository\Actions\DeleteWebhookAction;
89
use App\Domains\Repository\Actions\ExtractRepositoryNameAction;
910
use App\Domains\Repository\Actions\RegisterWebhookAction;
1011
use App\Domains\Repository\Contracts\Data\RepositoryData;
1112
use App\Domains\Repository\Contracts\Data\SyncLogData;
1213
use App\Domains\Repository\Contracts\Enums\GitProvider;
14+
use App\Domains\Repository\Http\Requests\BulkStoreRepositoryRequest;
1315
use App\Domains\Repository\Http\Requests\StoreRepositoryRequest;
1416
use App\Domains\Repository\Jobs\SyncRepositoryJob;
1517
use App\Http\Controllers\Controller;
@@ -81,6 +83,26 @@ public function store(StoreRepositoryRequest $request, Organization $organizatio
8183
->with('status', $message);
8284
}
8385

86+
public function bulkStore(
87+
BulkStoreRepositoryRequest $request,
88+
Organization $organization,
89+
BulkCreateRepositoriesAction $bulkCreateAction,
90+
): RedirectResponse {
91+
/** @var \App\Models\User $user */
92+
$user = $request->user();
93+
94+
$result = $bulkCreateAction->handle(
95+
organization: $organization,
96+
provider: GitProvider::from($request->validated('provider')),
97+
repositories: $request->validated('repositories'),
98+
userUuid: $user->uuid,
99+
);
100+
101+
return redirect()
102+
->route('organizations.repositories.index', $organization)
103+
->with('status', $result->statusMessage());
104+
}
105+
84106
public function show(Organization $organization, Repository $repository): Response
85107
{
86108
$repository->load('organization');
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace App\Domains\Repository\Http\Requests;
4+
5+
use App\Domains\Repository\Contracts\Enums\GitProvider;
6+
use Illuminate\Contracts\Validation\ValidationRule;
7+
use Illuminate\Foundation\Http\FormRequest;
8+
use Illuminate\Validation\Rule;
9+
10+
class BulkStoreRepositoryRequest extends FormRequest
11+
{
12+
/**
13+
* @return array<string, ValidationRule|array<mixed>|string>
14+
*/
15+
public function rules(): array
16+
{
17+
return [
18+
'provider' => ['required', 'string', Rule::enum(GitProvider::class)],
19+
'repositories' => ['required', 'array', 'min:1', 'max:50'],
20+
'repositories.*.repo_identifier' => ['required', 'string', 'max:500'],
21+
];
22+
}
23+
}

resources/js/components/add-repository-dialog.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,18 +68,22 @@ export default function AddRepositoryDialog({
6868
const repoListContainerRef = useRef<HTMLDivElement>(null);
6969

7070
const handleClose = (): void => {
71+
onClose();
72+
};
73+
74+
useEffect(() => {
75+
if (!isOpen) {
76+
return;
77+
}
78+
7179
setProvider(defaultProvider);
72-
setRepositories([]);
7380
setSelectedRepo('');
7481
setRepoIdentifier('');
7582
setSearchQuery('');
76-
onClose();
77-
};
83+
}, [isOpen]);
7884

7985
useEffect(() => {
8086
if (!isOpen || provider === 'git') {
81-
setRepositories([]);
82-
setSelectedRepo('');
8387
return;
8488
}
8589

0 commit comments

Comments
 (0)