Skip to content

Maintainer wanted model, badge, action and admin UI. #8870

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/lib/frontend/templates/package.dart
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ d.Node renderPkgHeader(PackagePageData data) {
packageName: package.name!,
publisherId: package.publisherId,
published: data.version.created!,
isMaintainerWanted: package.isMaintainerWanted ?? false,
isNullSafe: isNullSafe,
isDart3Compatible:
pkgView.tags.contains(PackageVersionTags.isDart3Compatible),
Expand Down
3 changes: 3 additions & 0 deletions app/lib/frontend/templates/package_misc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ final nameMatchBadgeNode = packageBadgeNode(
color: 'name-match',
);

/// Renders the maintainer-wanted badged used by package listing and package page.
final maintainerWantedBadgeNode = packageBadgeNode(label: 'Maintainer wanted');

/// Renders the null-safe badge used by package listing and package page.
d.Node nullSafeBadgeNode({String? title}) {
return packageBadgeNode(
Expand Down
13 changes: 13 additions & 0 deletions app/lib/frontend/templates/views/pkg/admin_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,19 @@ d.Node packageAdminPageNode({
),
),
],
d.a(name: 'maintainer-wanted'),
d.h3(text: 'Maintainer wanted'),
d.markdown(
'A package that\'s marked as *maintainWanted* will be featured with an '
'extra badge on the package page and in the search results.'),
d.div(
classes: ['-pub-form-checkbox-row'],
child: material.checkbox(
id: '-admin-is-maintainer-wanted-checkbox',
label: 'Mark "maintainWanted"',
checked: package.isMaintainerWanted ?? false,
),
),
_automatedPublishing(package),
d.a(name: 'version-retraction'),
d.h2(text: 'Version retraction'),
Expand Down
2 changes: 2 additions & 0 deletions app/lib/frontend/templates/views/pkg/header.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ d.Node packageHeaderNode({
required String packageName,
required String? publisherId,
required DateTime published,
required bool isMaintainerWanted,
required bool isNullSafe,
required bool isDart3Compatible,
required bool isDart3Incompatible,
Expand All @@ -22,6 +23,7 @@ d.Node packageHeaderNode({
d.span(child: d.xAgoTimestamp(published)),
d.text(' '),
if (publisherId != null) ..._publisher(publisherId),
if (isMaintainerWanted) maintainerWantedBadgeNode,
if (isNullSafe && !isDart3Compatible) nullSafeBadgeNode(),
if (isDart3Compatible) dart3CompatibleNode,
if (isDart3Incompatible) dart3IncompatibleNode,
Expand Down
2 changes: 2 additions & 0 deletions app/lib/frontend/templates/views/pkg/package_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ d.Node _packageItem(
required bool isNameMatch,
}) {
final isFlutterFavorite = view.tags.contains(PackageTags.isFlutterFavorite);
final isMaintainerWanted = view.tags.contains(PackageTags.isMaintainerWanted);
final isNullSafe = view.tags.contains(PackageVersionTags.isNullSafe);
final isDart3Compatible =
view.tags.contains(PackageVersionTags.isDart3Compatible);
Expand Down Expand Up @@ -139,6 +140,7 @@ d.Node _packageItem(
child: licenseNode,
),
if (isFlutterFavorite) flutterFavoriteBadgeNode,
if (isMaintainerWanted) maintainerWantedBadgeNode,
if (isNullSafe && !isDart3Compatible) nullSafeBadgeNode(),
if (isDart3Compatible) dart3CompatibleNode,
if (isDart3Incompatible) dart3IncompatibleNode,
Expand Down
11 changes: 10 additions & 1 deletion app/lib/package/backend.dart
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@ class PackageBackend {
isDiscontinued: p.isDiscontinued,
replacedBy: p.replacedBy,
isUnlisted: p.isUnlisted,
isMaintainerWanted: p.isMaintainerWanted ?? false,
);
}

Expand Down Expand Up @@ -463,6 +464,13 @@ class PackageBackend {
p.isUnlisted = options.isUnlisted!;
optionsChanges.add('unlisted');
}
if ((options.isMaintainerWanted ?? false) &&
(options.isMaintainerWanted ?? false) !=
(p.isMaintainerWanted ?? false)) {
p.updateMaintainerWanted(
isMaintainerWanted: options.isMaintainerWanted ?? false);
optionsChanges.add('maintainerWanted');
}

if (optionsChanges.isEmpty) {
return;
Expand All @@ -471,7 +479,8 @@ class PackageBackend {
p.updated = clock.now().toUtc();
_logger.info('Updating $package options: '
'isDiscontinued: ${p.isDiscontinued} '
'isUnlisted: ${p.isUnlisted}');
'isUnlisted: ${p.isUnlisted} '
'isMaintainerWanted: ${p.isMaintainerWanted}');
tx.insert(p);
tx.insert(await AuditLogRecord.packageOptionsUpdated(
agent: authenticatedUser,
Expand Down
20 changes: 20 additions & 0 deletions app/lib/package/models.dart
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,16 @@ class Package extends db.ExpandoModel<String> {
@db.DateTimeProperty()
DateTime? adminDeletedAt;

/// `true` if a package admin wants to advertise that they are looking for new maintainer(s).
///
/// Note: the value expires and resets to false after a set period (e.g. 6 months after setting it).
@db.BoolProperty(required: false)
bool? isMaintainerWanted;

/// The timestamp when the [isMaintainerWanted] flag was set.
@db.DateTimeProperty()
DateTime? maintainerWantedStartedAt;

/// Tags that are assigned to this package.
///
/// The permissions required to assign a tag typically depends on the tag.
Expand Down Expand Up @@ -188,6 +198,7 @@ class Package extends db.ExpandoModel<String> {
..isUnlisted = false
..isModerated = false
..isAdminDeleted = false
..isMaintainerWanted = false
..assignedTags = []
..deletedVersions = [];
}
Expand Down Expand Up @@ -380,6 +391,7 @@ class Package extends db.ExpandoModel<String> {
],
if (isUnlisted) PackageTags.isUnlisted,
if (publisherId != null) PackageTags.publisherTag(publisherId!),
if (isMaintainerWanted ?? false) PackageTags.isMaintainerWanted,
};
}

Expand Down Expand Up @@ -419,6 +431,14 @@ class Package extends db.ExpandoModel<String> {
adminDeletedAt = isAdminDeleted ? clock.now().toUtc() : null;
updated = clock.now().toUtc();
}

void updateMaintainerWanted({
required bool isMaintainerWanted,
}) {
this.isMaintainerWanted = isMaintainerWanted;
maintainerWantedStartedAt = isMaintainerWanted ? clock.now().toUtc() : null;
updated = clock.now().toUtc();
}
}

/// Describes the various categories of latest releases.
Expand Down
20 changes: 20 additions & 0 deletions app/lib/shared/integrity.dart
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,11 @@ class IntegrityChecker {
isAdminDeleted: p.isAdminDeleted,
adminDeletedAt: p.adminDeletedAt,
);
yield* _checkMaintainerWantedFlags(
package: p.name!,
isMaintainerWanted: p.isMaintainerWanted,
maintainerWantedStartedAt: p.maintainerWantedStartedAt,
);
if (p.isModerated) {
_packagesWithIsModeratedFlag.add(p.name!);
}
Expand Down Expand Up @@ -1056,3 +1061,18 @@ Stream<String> _checkAdminDeletedFlags({
yield '$kind "$id" has `isAdminDeleted = false` but `adminDeletedAt` is not null.';
}
}

/// Check that `isMaintainerWanted` and `maintainerWantedStartedAt` are consistent.
Stream<String> _checkMaintainerWantedFlags({
required String package,
required bool? isMaintainerWanted,
required DateTime? maintainerWantedStartedAt,
}) async* {
isMaintainerWanted ??= false;
if (isMaintainerWanted && maintainerWantedStartedAt == null) {
yield 'Package "$package" has `isMaintainerWanted = true` but `maintainerWantedStartedAt` is null.';
}
if (!isMaintainerWanted && maintainerWantedStartedAt != null) {
yield 'Package "$package" has `isMaintainerWanted = false` but `maintainerWantedStartedAt` is not null.';
}
}
1 change: 1 addition & 0 deletions app/lib/tool/test_profile/importer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ Future<void> importProfile({
isDiscontinued: testPackage.isDiscontinued,
replacedBy: testPackage.replacedBy,
isUnlisted: testPackage.isUnlisted,
isMaintainerWanted: testPackage.isMaintainerWanted,
));

if (testPackage.retractedVersions != null) {
Expand Down
2 changes: 2 additions & 0 deletions app/lib/tool/test_profile/models.dart
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ class TestPackage {
final bool? isDiscontinued;
final String? replacedBy;
final bool? isUnlisted;
final bool? isMaintainerWanted;
final bool? isFlutterFavorite;
final List<String>? retractedVersions;
final int? likeCount;
Expand All @@ -92,6 +93,7 @@ class TestPackage {
this.isDiscontinued,
this.replacedBy,
this.isUnlisted,
this.isMaintainerWanted,
this.isFlutterFavorite,
this.retractedVersions,
this.likeCount,
Expand Down
3 changes: 3 additions & 0 deletions app/lib/tool/test_profile/models.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 22 additions & 0 deletions app/test/frontend/golden/pkg_admin_page.html
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,28 @@ <h3>Unlisted</h3>
<label for="-admin-is-unlisted-checkbox">Mark "unlisted"</label>
</div>
</div>
<a name="maintainer-wanted"></a>
<h3>Maintainer wanted</h3>
<p>
A package that's marked as
<em>maintainWanted</em>
will be featured with an extra badge on the package page and in the search results.
</p>
<div class="-pub-form-checkbox-row">
<div class="mdc-form-field">
<div class="mdc-checkbox">
<input id="-admin-is-maintainer-wanted-checkbox" class="mdc-checkbox__native-control" type="checkbox"/>
<div class="mdc-checkbox__background">
<svg class="mdc-checkbox__checkmark" viewBox="0 0 24 24">
<path class="mdc-checkbox__checkmark-path" fill="none" d="M1.73,12.91 8.1,19.28 22.79,4.59"/>
</svg>
<div class="mdc-checkbox__mixedmark"></div>
</div>
<div class="mdc-checkbox__ripple"></div>
</div>
<label for="-admin-is-maintainer-wanted-checkbox">Mark "maintainWanted"</label>
</div>
</div>
<a name="automated-publishing"></a>
<h2>Automated publishing</h2>
<p>
Expand Down
1 change: 1 addition & 0 deletions app/test/package/api_export/api_exporter_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ Future<void> _testExportedApiSynchronization(
'isDiscontinued': false,
'replacedBy': null,
'isUnlisted': false,
'isMaintainerWanted': false,
},
);
expect(
Expand Down
17 changes: 17 additions & 0 deletions app/test/package/backend_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ void main() {
expect(p.isDiscontinued, isTrue);
expect(p.replacedBy, isNull);
expect(p.isUnlisted, isFalse);
expect(p.isMaintainerWanted ?? false, isFalse);
});
});

Expand Down Expand Up @@ -434,13 +435,15 @@ void main() {
expect(p.isDiscontinued, isTrue);
expect(p.replacedBy, 'neon');
expect(p.isUnlisted, isFalse);
expect(p.isMaintainerWanted ?? false, isFalse);

await packageBackend.updateOptions(
'oxygen', PkgOptions(isDiscontinued: true));
final p1 = (await packageBackend.lookupPackage('oxygen'))!;
expect(p1.isDiscontinued, isTrue);
expect(p1.replacedBy, isNull);
expect(p1.isUnlisted, isFalse);
expect(p1.isMaintainerWanted ?? false, isFalse);

// check audit log record
final page = await auditBackend.listRecordsForPackage('oxygen');
Expand All @@ -455,6 +458,7 @@ void main() {
expect(p2.isDiscontinued, isFalse);
expect(p2.replacedBy, isNull);
expect(p2.isUnlisted, isFalse);
expect(p2.isMaintainerWanted ?? false, isFalse);
});
});

Expand All @@ -466,6 +470,19 @@ void main() {
expect(p.isDiscontinued, isFalse);
expect(p.replacedBy, isNull);
expect(p.isUnlisted, isTrue);
expect(p.isMaintainerWanted ?? false, isFalse);
});
});

testWithProfile('maintainerWanted', fn: () async {
await withFakeAuthRequestContext(adminAtPubDevEmail, () async {
await packageBackend.updateOptions(
'oxygen', PkgOptions(isMaintainerWanted: true));
final p = (await packageBackend.lookupPackage('oxygen'))!;
expect(p.isDiscontinued, isFalse);
expect(p.replacedBy, isNull);
expect(p.isUnlisted, isFalse);
expect(p.isMaintainerWanted, isTrue);
});
});
});
Expand Down
2 changes: 2 additions & 0 deletions pkg/_pub_shared/lib/data/package_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,13 @@ class PkgOptions {
final bool? isDiscontinued;
final String? replacedBy;
final bool? isUnlisted;
final bool? isMaintainerWanted;

PkgOptions({
this.isDiscontinued,
this.replacedBy,
this.isUnlisted,
this.isMaintainerWanted,
});

factory PkgOptions.fromJson(Map<String, dynamic> json) =>
Expand Down
2 changes: 2 additions & 0 deletions pkg/_pub_shared/lib/data/package_api.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions pkg/_pub_shared/lib/search/tags.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ abstract class PackageTags {
/// Package is marked unlisted, discontinued, or is a legacy package.
static const String isUnlisted = 'is:unlisted';

/// Package is marked maintainerWanted.
static const String isMaintainerWanted = 'is:maintainer-wanted';

/// Package is shown, regardless of its unlisted status.
static const String showUnlisted = 'show:unlisted';

Expand Down
2 changes: 1 addition & 1 deletion pkg/pub_integration/lib/src/test_browser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ extension PageExt on Page {
/// Returns the [property] value of the first element by [selector].
Future<String> propertyValue(String selector, String property) async {
final h = await $(selector);
return await h.propertyValue(property);
return (await h.propertyValue(property)).toString();
}
}

Expand Down
20 changes: 18 additions & 2 deletions pkg/pub_integration/test/pkg_admin_page_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,6 @@ void main() {
// github publishing
await user.withBrowserPage((page) async {
await page.gotoOrigin('/packages/test_pkg/admin');
await page.takeScreenshots(
prefix: 'package-page/admin-page', selector: 'body');

await page.waitAndClick('#-pkg-admin-automated-github-enabled');
await page.waitForLayout([
Expand All @@ -70,6 +68,24 @@ void main() {
final value = await page.propertyValue(
'#-pkg-admin-automated-github-repository', 'value');
expect(value, githubRepository);

await page.takeScreenshots(
prefix: 'package-page/admin-page', selector: 'body');
});

// maintainer wanted
await user.withBrowserPage((page) async {
await page.gotoOrigin('/packages/test_pkg/admin');
final valueBefore = await page.propertyValue(
'#-admin-is-maintainer-wanted-checkbox', 'checked');
expect(valueBefore, 'false');

await page.waitAndClick('#-admin-is-maintainer-wanted-checkbox');
await page.waitAndClickOnDialogOk();
await page.reload();
final valueAfter = await page.propertyValue(
'#-admin-is-maintainer-wanted-checkbox', 'checked');
expect(valueAfter, 'true');
});

// visit activity log page
Expand Down
Loading