Skip to content

Commit 62d6807

Browse files
Create the form to transfer package ownership
1 parent 99eee3b commit 62d6807

File tree

5 files changed

+197
-5
lines changed

5 files changed

+197
-5
lines changed

css/app.scss

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1082,12 +1082,19 @@ input:focus:invalid:focus, textarea:focus:invalid:focus, select:focus:invalid:fo
10821082
margin-bottom: 4px;
10831083
margin-right: 4px;
10841084
}
1085-
.package .details #add-maintainer {
1085+
.package .details #add-maintainer{
10861086
margin-top: 3px;
10871087
margin-bottom: 7px;
10881088
float: right;
10891089
font-size: 34px;
10901090
}
1091+
.package .details #transfer-package {
1092+
margin-top: 7px;
1093+
margin-bottom: 7px;
1094+
margin-right: 4px;
1095+
float: right;
1096+
font-size: 34px;
1097+
}
10911098
.package .details #remove-maintainer {
10921099
float: right;
10931100
font-size: 35px;
@@ -1884,3 +1891,43 @@ body {
18841891
}
18851892
}
18861893
}
1894+
1895+
#transfer-package-form {
1896+
.maintainers-collection-wrapper {
1897+
margin-bottom: 15px;
1898+
1899+
label {
1900+
font-weight: bold;
1901+
margin-bottom: 10px;
1902+
}
1903+
1904+
.maintainers-list {
1905+
list-style: none;
1906+
padding: 0;
1907+
margin-bottom: 10px;
1908+
1909+
li {
1910+
display: flex;
1911+
gap: 10px;
1912+
margin-bottom: 10px;
1913+
align-items: center;
1914+
1915+
input[type="text"] {
1916+
flex: 1;
1917+
padding: 6px 12px;
1918+
font-size: 14px;
1919+
border: 1px solid #ccc;
1920+
border-radius: 4px;
1921+
}
1922+
1923+
.btn-danger {
1924+
flex-shrink: 0;
1925+
}
1926+
}
1927+
}
1928+
1929+
.add-maintainer-item {
1930+
margin-top: 5px;
1931+
}
1932+
}
1933+
}

js/view.js

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,23 @@ const init = function ($) {
77
var versionCache = {},
88
ongoingRequest = false;
99

10-
$('#add-maintainer').on('click', function (e) {
10+
const togglePackageForm = function (selector) {
1111
$('#remove-maintainer-form').addClass('hidden');
12-
$('#add-maintainer-form').removeClass('hidden');
12+
$('#add-maintainer-form').addClass('hidden');
13+
$('#transfer-package-form').addClass('hidden');
14+
$(selector).removeClass('hidden');
15+
}
16+
17+
$('#add-maintainer').on('click', function (e) {
18+
togglePackageForm('#add-maintainer-form');
1319
e.preventDefault();
1420
});
1521
$('#remove-maintainer').on('click', function (e) {
16-
$('#add-maintainer-form').addClass('hidden');
17-
$('#remove-maintainer-form').removeClass('hidden');
22+
togglePackageForm('#remove-maintainer-form');
23+
e.preventDefault();
24+
});
25+
$('#transfer-package').on('click', function (e) {
26+
togglePackageForm('#transfer-package-form');
1827
e.preventDefault();
1928
});
2029

@@ -227,6 +236,38 @@ const init = function ($) {
227236
$(versionsList).css('max-height', 'inherit');
228237
});
229238
}
239+
240+
// Handle add/remove buttons for transfer package form
241+
$('.add-maintainer-item').on('click', function (e) {
242+
e.preventDefault();
243+
244+
var list = $('.maintainers-list');
245+
var prototype = list.data('prototype');
246+
var index = list.find('li').length + 1;
247+
248+
var newForm = prototype.replace(/__name__/g, index);
249+
var newItem = $('<li></li>').append(newForm);
250+
addMaintainerRemoveButton(newItem);
251+
list.append(newItem);
252+
});
253+
254+
$('.maintainers-list').find('li').each(function(index) {
255+
addMaintainerRemoveButton($(this));
256+
});
257+
258+
function addMaintainerRemoveButton(item) {
259+
var removeButton = $('<button type="button" class="btn btn-danger btn-sm"><i class="glyphicon glyphicon-remove"></i></button>');
260+
removeButton.on('click', function(e) {
261+
e.preventDefault();
262+
263+
if ($('.maintainers-list').find('li').length === 1) {
264+
return;
265+
}
266+
267+
item.remove();
268+
});
269+
item.append(removeButton);
270+
}
230271
};
231272

232273
if (document.querySelector('#view-package-page')) {

src/Controller/PackageController.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@
2727
use App\Entity\Vendor;
2828
use App\Entity\Version;
2929
use App\Form\Model\MaintainerRequest;
30+
use App\Form\Model\TransferPackageRequest;
3031
use App\Form\Type\AbandonedType;
3132
use App\Form\Type\AddMaintainerRequestType;
3233
use App\Form\Type\PackageType;
3334
use App\Form\Type\RemoveMaintainerRequestType;
35+
use App\Form\Type\TransferPackageRequestType;
3436
use App\Model\DownloadManager;
3537
use App\Model\FavoriteManager;
3638
use App\Model\PackageManager;
@@ -692,6 +694,7 @@ public function viewPackageAction(Request $req, string $name, CsrfTokenManagerIn
692694

693695
$data['addMaintainerForm'] = $this->createAddMaintainerForm($package)->createView();
694696
$data['removeMaintainerForm'] = $this->createRemoveMaintainerForm($package)->createView();
697+
$data['transferPackageForm'] = $this->createTransferPackageForm($package)->createView();
695698
$data['deleteForm'] = $this->createDeletePackageForm($package)->createView();
696699
} else {
697700
$data['hasVersionSecurityAdvisories'] = [];
@@ -986,6 +989,41 @@ public function removeMaintainerAction(Request $req, #[MapEntity] Package $packa
986989
]);
987990
}
988991

992+
993+
#[Route(path: '/packages/{name:package}/transfer/', name: 'transfer_package', requirements: ['name' => Package::PACKAGE_NAME_REGEX])]
994+
public function transferPackageAction(Request $req, #[MapEntity] Package $package, #[CurrentUser] User $user, LoggerInterface $logger): RedirectResponse
995+
{
996+
$this->denyAccessUnlessGranted(PackageActions::TransferPackage->value, $package);
997+
998+
$oldMaintainers = $package->getMaintainers()->toArray();
999+
1000+
$form = $this->createTransferPackageForm($package);
1001+
$form->handleRequest($req);
1002+
1003+
if ($form->isSubmitted() && $form->isValid()) {
1004+
try {
1005+
$em = $this->getEM();
1006+
$newMaintainers = $form->getData()->getMaintainers()->toArray();
1007+
$result = $this->packageManager->transferPackage($package, $oldMaintainers, $newMaintainers);
1008+
$em->flush();
1009+
1010+
if ($result) {
1011+
$usernames = array_map(fn (User $user) => $user->getUsername(), $newMaintainers);
1012+
$this->addFlash('success', sprintf('Package has been transferred to %s', implode(', ', $usernames)));
1013+
} else {
1014+
$this->addFlash('warning', 'Package maintainers are identical and have not been changed');
1015+
}
1016+
1017+
return $this->redirectToRoute('view_package', ['name' => $package->getName()]);
1018+
} catch (\Exception $e) {
1019+
$logger->critical($e->getMessage(), ['exception', $e]);
1020+
$this->addFlash('error', 'The package could not be transferred.');
1021+
}
1022+
}
1023+
1024+
return $this->redirectToRoute('view_package', ['name' => $package->getName()]);
1025+
}
1026+
9891027
#[Route(path: '/packages/{name:package}/edit', name: 'edit_package', requirements: ['name' => '[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+?'])]
9901028
public function editAction(Request $req, #[MapEntity] Package $package, #[CurrentUser] ?User $user = null): Response
9911029
{
@@ -1623,6 +1661,17 @@ private function createRemoveMaintainerForm(Package $package): FormInterface
16231661
]);
16241662
}
16251663

1664+
/**
1665+
* @return FormInterface<TransferPackageRequest>
1666+
*/
1667+
private function createTransferPackageForm(Package $package): FormInterface
1668+
{
1669+
$transferRequest = new TransferPackageRequest();
1670+
$transferRequest->setMaintainers($package->getMaintainers());
1671+
1672+
return $this->createForm(TransferPackageRequestType::class, $transferRequest);
1673+
}
1674+
16261675
/**
16271676
* @return FormInterface<array{}>
16281677
*/

src/Model/PackageManager.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
use Algolia\AlgoliaSearch\Exceptions\AlgoliaException;
1616
use Algolia\AlgoliaSearch\SearchClient;
17+
use App\Entity\AuditRecord;
1718
use App\Entity\Dependent;
1819
use App\Entity\Download;
1920
use App\Entity\EmptyReferenceCache;
@@ -239,4 +240,30 @@ public function notifyNewMaintainer(User $user, Package $package): bool
239240

240241
return true;
241242
}
243+
244+
/*
245+
* @param User[] $oldMaintainers
246+
* @param User[] $newMaintainers
247+
*/
248+
public function transferPackage(Package $package, array $oldMaintainers, array $newMaintainers): bool
249+
{
250+
$normalizedOldMaintainers = array_values(array_map(fn (User $user) => $user->getId(), $oldMaintainers));
251+
sort($normalizedOldMaintainers, SORT_NUMERIC);
252+
253+
$normalizedMaintainers = array_values(array_map(fn (User $user) => $user->getId(), $newMaintainers));
254+
sort($normalizedMaintainers, SORT_NUMERIC);
255+
256+
if ($normalizedMaintainers === $normalizedOldMaintainers) {
257+
return false;
258+
}
259+
260+
$package->getMaintainers()->clear();
261+
foreach ($newMaintainers as $maintainer) {
262+
$package->addMaintainer($maintainer);
263+
}
264+
265+
$this->doctrine->getManager()->persist(AuditRecord::packageTransferred($package, null, $oldMaintainers, $newMaintainers));
266+
267+
return true;
268+
}
242269
}

templates/package/view_package.html.twig

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@
146146
{% endfor %}
147147
{% if is_granted('remove_maintainer', package) %}<a title="Remove Maintainer" id="remove-maintainer" href="{{ path('remove_maintainer', {'name': package.name}) }}"><i class="glyphicon glyphicon-remove"></i></a>{% endif %}
148148
{% if is_granted('add_maintainer', package) %}<a title="Add Maintainer" id="add-maintainer" href="{{ path('add_maintainer', {'name': package.name}) }}"><i class="glyphicon glyphicon-plus"></i></a>{% endif %}
149+
{% if is_granted('transfer_package', package) %}<a title="Transfer Package" id="transfer-package" href="{{ path('transfer_package', {'name': package.name}) }}"><i class="glyphicon glyphicon-transfer"></i></a>{% endif %}
149150
</p>
150151

151152
<h5>Details</h5>
@@ -319,6 +320,33 @@
319320
</div>
320321
{% endif %}
321322

323+
{% if is_granted('transfer_package', package) %}
324+
<div class="row">
325+
{{ form_start(transferPackageForm, {
326+
attr: { id: 'transfer-package-form', class: 'col-sm-6 col-md-3 col-md-offset-9 ' ~ (show_transfer_package_form|default(false) ? '': 'hidden') },
327+
action: path('transfer_package', { 'name': package.name })
328+
}) }}
329+
<div>
330+
<h4>Transfer Ownership</h4>
331+
<div class="maintainers-collection-wrapper">
332+
{{ form_errors(transferPackageForm.maintainers) }}
333+
<ul class="maintainers-list" data-prototype="{{ form_widget(transferPackageForm.maintainers.vars.prototype)|e('html_attr') }}">
334+
{% for maintainerField in transferPackageForm.maintainers %}
335+
<li>
336+
{{ form_errors(maintainerField) }}
337+
{{ form_widget(maintainerField) }}
338+
</li>
339+
{% endfor %}
340+
</ul>
341+
<button type="button" class="btn btn-info btn-sm add-maintainer-item">Add Another Maintainer</button>
342+
</div>
343+
{{ form_rest(transferPackageForm) }}
344+
<input class="btn btn-block btn-success btn-lg" type="submit" value="Transfer Ownership" />
345+
</div>
346+
{{ form_end(transferPackageForm) }}
347+
</div>
348+
{% endif %}
349+
322350
{% if versions|length %}
323351
<div class="row versions-section">
324352
<div class="version-details col-md-9">

0 commit comments

Comments
 (0)