Skip to content

Commit a4d7511

Browse files
committed
Improve adding packages from external registries with a Stimulus controller and AJAX requests
1 parent e764646 commit a4d7511

File tree

6 files changed

+166
-30
lines changed

6 files changed

+166
-30
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { Controller } from '@hotwired/stimulus';
2+
3+
/* stimulusFetch: 'lazy' */
4+
export default class extends Controller {
5+
static targets = ['form', 'packageList'];
6+
static values = {
7+
packageUrl: String,
8+
};
9+
10+
formTarget: HTMLFormElement;
11+
packageListTarget: HTMLElement;
12+
packageUrlValue: string;
13+
14+
submitForm(event: Event) {
15+
event.preventDefault();
16+
17+
const formData = new FormData(this.formTarget);
18+
19+
const data = [];
20+
for (const [key, value] of formData) {
21+
data.push(`${encodeURIComponent(key)}=${encodeURIComponent(value.toString())}`);
22+
}
23+
const body = data.join("&");
24+
25+
fetch(this.formTarget.getAttribute('action'), {
26+
method: 'POST',
27+
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
28+
body,
29+
})
30+
.then(response => response.json())
31+
.then((data: PackageResultResponse) => {
32+
for (const result of data.results) {
33+
const packageUrl = this.packageUrlValue.replace('place/holder', result.packageName);
34+
35+
const packageName = !result.error ? `<a href="${packageUrl}" target="_blank">${result.packageName}</a>` : result.packageName;
36+
const registryName = result.registryName ? result.registryName : '';
37+
let message = result.message;
38+
if (result.error) {
39+
message = `<div class="text-warning">${message}</div>`;
40+
} else if (result.created) {
41+
message = `<div class="text-success">${message}</div>`;
42+
}
43+
44+
const listItem = document.createElement('div');
45+
listItem.innerHTML = `
46+
<div class="row gap-md-2 my-2">
47+
<div class="col-md-3 col-lg-2">${packageName}</div>
48+
<div class="col-md-3 col-lg-2">${registryName}</div>
49+
<div class="col-md-6 col-lg-8">${message}</div>
50+
</div>
51+
`;
52+
53+
this.packageListTarget.appendChild(listItem);
54+
}
55+
});
56+
57+
const packagesInput: HTMLFormElement = this.formTarget.querySelector('[name="package_add_mirroring_form[packages]"]');
58+
packagesInput.value = '';
59+
}
60+
}
61+
62+
interface PackageResultResponse {
63+
results: PackageResult[];
64+
}
65+
66+
interface PackageResult {
67+
packageName: string;
68+
registryName: string;
69+
message: string;
70+
created: boolean;
71+
error: boolean;
72+
}

src/Controller/Dashboard/DashboardPackagesController.php

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ public function statistics(string $packageName): Response
140140

141141
#[Route('/dashboard/packages/add-mirroring', name: 'dashboard_packages_add_mirroring')]
142142
#[IsGranted('ROLE_ADMIN')]
143-
public function addMirror(Request $request): Response
143+
public function addMirroring(Request $request): Response
144144
{
145145
$form = $this->createForm(PackageAddMirroringFormType::class);
146146

@@ -149,25 +149,43 @@ public function addMirror(Request $request): Response
149149
if ($form->isSubmitted() && $form->isValid()) {
150150
$registry = $form->get('registry')->getData();
151151

152-
$packageNames = explode(PHP_EOL, $form->get('packages')->getData());
153-
$packageNames = array_map('trim', $packageNames);
152+
$packageNamesInput = $form->get('packages')->getData();
153+
$packageNames = preg_split('#(\s|,)+#', $packageNamesInput);
154154

155155
$results = [];
156156

157157
foreach ($packageNames as $packageName) {
158-
if (null !== $this->packageRepository->findOneBy(['name' => $packageName])) {
158+
if (!preg_match('#[a-z0-9_.-]+/[a-z0-9_.-]+#', $packageName)) {
159159
$results[] = [
160+
'packageName' => $packageName,
161+
'registryName' => null,
162+
'created' => false,
160163
'error' => true,
161-
'message' => "The package $packageName already exists and was skipped",
164+
'message' => "The package name $packageName is invalid.",
165+
];
166+
167+
continue;
168+
}
169+
170+
if (null !== $this->packageRepository->findOneBy(['name' => $packageName])) {
171+
$results[] = [
172+
'packageName' => $packageName,
173+
'registryName' => null,
174+
'created' => false,
175+
'error' => false,
176+
'message' => "The package $packageName already exists and was skipped.",
162177
];
163178

164179
continue;
165180
}
166181

167182
if (!$this->metadataResolver->provides($packageName, $registry)) {
168183
$results[] = [
184+
'packageName' => $packageName,
185+
'registryName' => $registry->getName(),
186+
'created' => false,
169187
'error' => true,
170-
'message' => "The package $packageName could not be found and was skipped",
188+
'message' => "The package $packageName could not be found and was skipped.",
171189
];
172190

173191
continue;
@@ -183,14 +201,17 @@ public function addMirror(Request $request): Response
183201
$this->messenger->dispatch(new UpdatePackage($package->getId()));
184202

185203
$results[] = [
204+
'packageName' => $packageName,
205+
'registryName' => $registry->getName(),
206+
'created' => true,
186207
'error' => false,
187-
'message' => "The package $packageName was created successfully",
208+
'message' => "The package $packageName was created successfully.",
188209
];
189210

190211
$this->entityManager->flush();
191212
}
192213

193-
return $this->render('dashboard/packages/add_mirroring_results.html.twig', [
214+
return $this->json([
194215
'results' => $results,
195216
]);
196217
}

templates/dashboard/packages/add_mirroring.html.twig

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,30 @@
33
{% block page_title %}Mirror packages from registry{% endblock %}
44

55
{% block page_content %}
6-
{{ form_start(form) }}
7-
{{ form_row(form.packages, {attr: {rows: 10}}) }}
6+
<div {{ stimulus_controller('packages-mirroring', {packageUrl: dashboard_path('dashboard_packages_info', {packageName: 'place/holder'})}) }}>
7+
<div
8+
class="border-top border-bottom my-3"
9+
{{ stimulus_target('packages-mirroring', 'packageList') }}
10+
>
11+
</div>
812

9-
{{ form_row(form.registry) }}
13+
{{ form_start(form, {
14+
attr: {
15+
'action': dashboard_path('dashboard_packages_add_mirroring'),
16+
'data-action': 'packages-mirroring#submitForm',
17+
'data-packages-mirroring-target': 'form',
18+
},
19+
}) }}
20+
<div class="row">
21+
<div class="col-lg-8">
22+
{{ form_row(form.packages, {attr: {rows: 5}}) }}
23+
</div>
24+
<div class="col-lg-4">
25+
{{ form_row(form.registry) }}
26+
</div>
27+
</div>
1028

11-
<button class="btn btn-primary">Add packages</button>
12-
{{ form_end(form) }}
29+
<button class="btn btn-primary">Add packages</button>
30+
{{ form_end(form) }}
31+
</div>
1332
{% endblock %}

templates/dashboard/packages/add_mirroring_results.html.twig

Lines changed: 0 additions & 15 deletions
This file was deleted.

tests/FunctionalTests/DashboardPackagesControllerTest.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use CodedMonkey\Dirigent\Doctrine\Entity\User;
66
use CodedMonkey\Dirigent\Doctrine\Repository\PackageRepository;
7+
use CodedMonkey\Dirigent\Doctrine\Repository\RegistryRepository;
78
use CodedMonkey\Dirigent\Doctrine\Repository\UserRepository;
89
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
910

@@ -62,6 +63,44 @@ public function testAddVcsRepository(): void
6263
$packageRepository->remove($package, true);
6364
}
6465

66+
public function testAddMirroring(): void
67+
{
68+
$client = static::createClient();
69+
70+
/** @var UserRepository $userRepository */
71+
$userRepository = $client->getContainer()->get(UserRepository::class);
72+
73+
/** @var User $user */
74+
$user = $userRepository->findOneByUsername('owner');
75+
$client->loginUser($user);
76+
77+
$registry = $client->getContainer()->get(RegistryRepository::class)->findOneBy(['name' => 'Packagist']);
78+
79+
$client->request('GET', '/?routeName=dashboard_packages_add_mirroring');
80+
$client->submitForm('Add packages', [
81+
'package_add_mirroring_form[packages]' => 'psr/cache',
82+
'package_add_mirroring_form[registry]' => $registry->getId(),
83+
]);
84+
85+
$this->assertResponseStatusCodeSame(200);
86+
87+
// todo the submit request should be invoked with ajax, and this assertion should be performed on the initial request
88+
// however, the assertion is performed on the ajax response making it invalid
89+
// $this->assertAnySelectorTextSame(
90+
// '.text-success',
91+
// 'The package psr/cache was created successfully.',
92+
// 'A message showing the package was created must be shown.',
93+
// );
94+
95+
/** @var PackageRepository $packageRepository */
96+
$packageRepository = $client->getContainer()->get(PackageRepository::class);
97+
98+
$package = $packageRepository->findOneByName('psr/cache');
99+
self::assertNotNull($package, 'A package was created.');
100+
101+
$packageRepository->remove($package, true);
102+
}
103+
65104
public function testEdit(): void
66105
{
67106
$client = static::createClient();

tsconfig.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
"include": ["assets/**/*"],
33
"compilerOptions": {
44
"noImplicitAny": true,
5-
"module": "es6",
6-
"target": "es5",
5+
"module": "esnext",
6+
"target": "es6",
77
"jsx": "react",
88
"allowJs": true,
99
"moduleResolution": "node"

0 commit comments

Comments
 (0)