Skip to content

[Port to dtq-dev] 117 better control of user deletion in user administration page#1335

Open
kosarko wants to merge 1 commit into
dataquest-dev:dtq-devfrom
ufal:backport-140-to-dtq-dev
Open

[Port to dtq-dev] 117 better control of user deletion in user administration page#1335
kosarko wants to merge 1 commit into
dataquest-dev:dtq-devfrom
ufal:backport-140-to-dtq-dev

Conversation

@kosarko

@kosarko kosarko commented Jun 29, 2026

Copy link
Copy Markdown

Port of ufal#140 by @amadulhaxxani to dtq-dev.

Summary by CodeRabbit

  • New Features

    • Added clearer delete warnings and protections when removing a user account.
    • Delete actions now show a warning or disabled state when the selected account is the current user.
  • Bug Fixes

    • Prevented self-deletion from proceeding normally and added a dedicated error message for that case.
    • Improved delete behavior so actions wait until the signed-in user is known.
  • Documentation

    • Added new translated messages for delete warnings and self-deletion notices.

* Disable self-delete and add delete warnings

Prevent administrators from deleting their own account by disabling the delete button for the current authenticated user and showing a tooltip. Add a warningLabel input to the confirmation modal and render it when present. Implement logic in EPeopleRegistryComponent to determine the current user, compute a contextual delete warning (submitter, admin, or both) by querying workspace/workflow submissions, search, and group membership, and open the confirmation modal with that warning. Handle delete responses to show a friendly notification for backend 400 self-delete errors and use a generic failure notification otherwise. Update templates, component imports and helper methods (isCurrentUser, getDeleteWarningLabel, hasSubmittedItems, isAdministrator, isSelfDeletionError, showSelfDeleteNotification), and extend unit tests to cover disabled self-delete UI, composed warning labels, and notification behavior.

* Prevent self-delete and show delete warnings

Disable direct self-deletion and surface contextual warnings when deleting an EPerson. Template: render the delete button as disabled for the currently authenticated user and show a tooltip explaining self-delete restrictions; otherwise show the normal delete button. Component: track the current authenticated user, compute a combined warning label based on whether the EPerson is an administrator and/or has submitted items (checks workspace, workflow and archived submissions), present that warning in the confirmation modal, and handle deletion results including a friendly notification for backend self-delete errors. Added helper methods (isCurrentUser, getDeleteWarningLabel, hasSubmittedItems, isAdministrator, isSelfDeletionError, showSelfDeleteNotification) and adjusted delete flow to update canDelete$ appropriately. Tests: updated and added specs and mocks to cover the disabled self-delete UI, combined warning label usage, and the friendly self-delete error notification; added required service mocks and test setup changes.

* Add EPerson delete warnings and self-delete message

Add four i18n keys to en.json5 and cs.json5 for EPerson deletion flows: a forbidden self-delete message and warnings for deleting users who are submitters, administrators, or both. Provides localized English and Czech strings to surface these messages in the admin access-control UI.

* Guard EPerson delete until auth ID resolved

Prevent delete actions from running before the current authenticated user id is available. Add a template condition to hide the delete button until currentAuthenticatedUserId is set, add an early-return guard in deleteEPerson when the id is missing, and add a unit test ensuring the modal is not opened and delete is not called before the id is resolved.

* Hide delete button until auth user ID resolved

Prevent delete actions before the current authenticated user ID is available. Add *ngIf to the delete button in the template, add an early return guard in the component's delete flow when currentAuthenticatedUserId is not set, and include unit tests verifying the button is hidden and the delete/modal are not invoked until the ID is resolved.

* Restrict delete button visibility and update tests

Template: Wrap self-delete tooltip/button in a guard that checks epersonDto.ableToDelete and currentAuthenticatedUserId so delete UI is only rendered when deletion is allowed; simplify the enabledDeleteButton markup by removing a duplicated *ngIf on the inner button. Spec: remove unused DebugElement import, rename the test from 'should be disabled' to 'should be hidden', and update assertions to expect no delete buttons to be present (reflecting the new visibility behavior).

* Detect Administrator group across paginated pages

Add support for detecting Administrator group membership across paginated group lists and a unit test for it.

- Introduce hasAdministratorGroupOnPage(groupsHref, currentPage) which fetches group pages (elementsPerPage=100) and recursively checks subsequent pages when the Administrator group is not found on the current page.
- isAdministrator now delegates to hasAdministratorGroupOnPage starting at page 1.
- Preserve error handling to return false on failures.
- Add a unit test that simulates a two-page group response (administrator present on the second page) and verifies the component detects administrator membership and displays the correct warning label.

* Detect Administrator group across pages

Refactor group membership check to traverse paginated group lists until an "Administrator" group is found or pages are exhausted. Introduces a recursive hasAdministratorGroupOnPage that requests pages, validates payload/pageInfo, checks for the admin group, and requests the next page when needed. Adds a unit test covering detection on later pages and a minor spec setup tweak (setting currentAuthenticatedUserId) to correctly exercise deletion behavior.

* Fix Czech typo in delete warning

Corrects a spelling mistake in src/assets/i18n/cs.json5 for key admin.access-control.epeople.delete.warning.submitter: changed 'repositáři' to 'repozitáři' to improve Czech translation accuracy.

* Make disabled delete button unfocusable

Add tabindex="-1" to the disabled delete button in the epeople registry template so it cannot receive keyboard focus. This ensures the parent span (which provides the tooltip) remains the sole focus target for the current-user case, improving accessibility and preventing duplicate focusable elements.

* Make disabled self-delete button unfocusable

Add tabindex="-1" to the disabled delete button in eperson-form.component.html so the button cannot receive keyboard focus when showing the current user. The tooltip remains on the parent span, improving keyboard navigation and accessibility for the self-delete warning.

* Remove stray 'after' attribute from eperson template

Remove an unintended "after" attribute from a <span> in eperson-form.component.html. The change cleans up the template by leaving only the intended *ngIf binding for conditional rendering of the delete/impersonation UI, preventing potential template parsing or linting issues.

* code cleanup - remove duplicities

* cleanup

* copilot comments

* copilot's comment

---------

Co-authored-by: Ondrej Kosarko <kosarko@ufal.mff.cuni.cz>
(cherry picked from commit d0b5ecf)
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Introduces EPersonDeleteGuardService centralizing EPerson self-deletion detection, warning label computation, and error handling. Both EPeopleRegistryComponent and EPersonFormComponent integrate the guard, track the authenticated user ID, conditionally disable the delete button for the current user, and display contextual warning labels in the confirmation modal. English and Czech i18n keys are added for the new messages.

Changes

EPerson Self-Delete Guard Feature

Layer / File(s) Summary
EPersonDeleteGuardService implementation and tests
src/app/access-control/epeople-registry/eperson-delete-guard.service.ts, src/app/access-control/epeople-registry/eperson-delete-guard.service.spec.ts
New injectable service with isCurrentUser, getDeleteWarningLabel (combining workspace/workflow/archived item probes and admin authorization), isSelfDeletionError, showSelfDeleteNotification, and hasSubmittedItems/isAdministrator internals. Each probe degrades to false on error. Full Jasmine spec covers all paths including error degradation.
ConfirmationModal optional warningLabel
src/app/shared/confirmation-modal/confirmation-modal.component.ts, ...component.html, ...component.spec.ts
warningLabel input made optional (string | undefined); template conditionally renders a translated warning paragraph; tests assert presence and absence of the warning element.
Module wiring
src/app/access-control/access-control.module.ts
EPersonDeleteGuardService imported and registered in providers.
EPeopleRegistryComponent self-delete integration
src/app/access-control/epeople-registry/epeople-registry.component.ts, ...component.html, ...component.spec.ts
Component subscribes to authenticated user ID on init, rewrites deleteEPerson to use the guard for self-delete short-circuit and warning label fetching, adds isCurrentUser helper. Template disables the delete button with tooltip for the current user. Spec adds delete-flow, warning label, and notification tests.
EPersonFormComponent self-delete integration
src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts, ...component.html, ...component.spec.ts
Component injects EPersonDeleteGuardService, tracks currentAuthenticatedUserId, rewrites delete() with guard-based self-delete routing and warning label fetching, adds isCurrentUser. Template gates and disables the delete button for the current user. Spec extends deletion tests for warning label composition, error paths, and notification assertions.
i18n translations
src/assets/i18n/en.json5, src/assets/i18n/cs.json5
English and Czech keys added for self-delete forbidden message and contextual warnings for submitter, administrator, and combined roles.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • milanmajchrak
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is far too brief and omits the required template sections and checklist items. Add Problem description, Analysis, Problems, the sync-verification checklist if i18n changed, Manual Testing, and Copilot review.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: improving user deletion controls in the administration page.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/app/access-control/epeople-registry/epeople-registry.component.ts (1)

245-247: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a doc comment for isCurrentUser().

This new public method is missing TypeDoc/JSDoc.
As per coding guidelines, **/*.ts: All new/modified public methods and classes require TypeDoc/JSDoc comments.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/access-control/epeople-registry/epeople-registry.component.ts` around
lines 245 - 247, Add a TypeDoc/JSDoc comment for the public
isCurrentUser(ePerson: EPerson) method in EpeopleRegistryComponent. Briefly
describe that it checks whether the provided EPerson matches the currently
authenticated user, and keep the comment aligned with the existing public API
documentation style used in this component.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/access-control/epeople-registry/epeople-registry.component.html`:
- Around line 81-89: The focusable self-delete wrapper is missing the accessible
name and disabled semantics because the `tabindex="0"` span is the real keyboard
target while the nested button is removed from tab order. Update the wrapper in
the epeople registry template so the translated label and disabled state are
applied to the outer element instead of the inner button, and make the same
change in the matching self-delete control in eperson-form so both use the
wrapper as the accessible target.

In
`@src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts`:
- Around line 553-555: Add a JSDoc/TypeDoc comment for the public isCurrentUser
method in EpersonFormComponent. Document that it checks whether the given
EPerson matches the currently authenticated user, and keep the comment directly
above isCurrentUser so the new public API is documented per the coding
guidelines.
- Around line 515-525: Restore the authorization-backed canDelete$ stream in
eperson-form.component by avoiding permanent replacement with constant
observableOf(true/false) values inside the delete flow. Update the logic around
the delete/confirm handling so the original isAuthorized(FeatureID.CanDelete,
...) observable is preserved or re-created after the modal closes, and only use
temporary disabled/enabled state during the action. Ensure the finalize block
and the delete pipeline in the component keep canDelete$ tied to authorization
rather than leaving it as a static observable.

---

Nitpick comments:
In `@src/app/access-control/epeople-registry/epeople-registry.component.ts`:
- Around line 245-247: Add a TypeDoc/JSDoc comment for the public
isCurrentUser(ePerson: EPerson) method in EpeopleRegistryComponent. Briefly
describe that it checks whether the provided EPerson matches the currently
authenticated user, and keep the comment aligned with the existing public API
documentation style used in this component.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3dcd07e2-6997-4719-9fc7-11bb7962d6fc

📥 Commits

Reviewing files that changed from the base of the PR and between fecfa07 and fb81a2f.

📒 Files selected for processing (14)
  • src/app/access-control/access-control.module.ts
  • src/app/access-control/epeople-registry/epeople-registry.component.html
  • src/app/access-control/epeople-registry/epeople-registry.component.spec.ts
  • src/app/access-control/epeople-registry/epeople-registry.component.ts
  • src/app/access-control/epeople-registry/eperson-delete-guard.service.spec.ts
  • src/app/access-control/epeople-registry/eperson-delete-guard.service.ts
  • src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html
  • src/app/access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts
  • src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts
  • src/app/shared/confirmation-modal/confirmation-modal.component.html
  • src/app/shared/confirmation-modal/confirmation-modal.component.spec.ts
  • src/app/shared/confirmation-modal/confirmation-modal.component.ts
  • src/assets/i18n/cs.json5
  • src/assets/i18n/en.json5

Comment on lines +81 to +89
<span tabindex="0" [ngbTooltip]="selfDeleteWarningLabel | translate">
<button [dsBtnDisabled]="true"
tabindex="-1"
[attr.aria-label]="selfDeleteWarningLabel | translate"
class="delete-button btn btn-outline-danger btn-sm access-control-deleteEPersonButton"
type="button">
<i class="fas fa-trash-alt fa-fw"></i>
</button>
</span>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== epeople-registry.component.html slice ==\n'
sed -n '70,100p' src/app/access-control/epeople-registry/epeople-registry.component.html

printf '\n== eperson-form.component.html slice ==\n'
sed -n '1,140p' src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html

printf '\n== search for similar wrapper pattern ==\n'
rg -n --no-heading 'tabindex="0"|ngbTooltip|dsBtnDisabled="true"|aria-label=.*selfDeleteWarningLabel' src/app/access-control/epeople-registry

Repository: dataquest-dev/dspace-angular

Length of output: 7821


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

paths = [
    Path("src/app/access-control/epeople-registry/epeople-registry.component.html"),
    Path("src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html"),
]
for p in paths:
    print(f"\n## {p}")
    text = p.read_text(encoding="utf-8")
    for i, line in enumerate(text.splitlines(), 1):
        if "tabindex=\"0\"" in line or "ngbTooltip" in line or "dsBtnDisabled=\"true\"" in line or "selfDeleteWarningLabel" in line or "delete-button" in line:
            start = max(1, i - 3)
            end = min(len(text.splitlines()), i + 4)
            print(f"-- lines {start}-{end}")
            for j in range(start, end + 1):
                print(f"{j:4}: {text.splitlines()[j-1]}")
PY

Repository: dataquest-dev/dspace-angular

Length of output: 4918


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n --no-heading 'selfDeleteWarningLabel|currentAuthenticatedUserId|isCurrentUser|tabindex="0"|ngbTooltip|dsBtnDisabled="true"' src/app/access-control/epeople-registry

Repository: dataquest-dev/dspace-angular

Length of output: 4805


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect whether the wrapper is intended to be keyboard-focusable tooltip trigger,
# and whether there is any existing accessibility pattern in nearby templates.
python3 - <<'PY'
from pathlib import Path

for rel in [
    "src/app/access-control/epeople-registry/epeople-registry.component.html",
    "src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html",
]:
    p = Path(rel)
    if not p.exists():
        print(rel, "MISSING")
        continue
    lines = p.read_text(encoding="utf-8").splitlines()
    print(f"\nFILE: {rel}")
    for idx, line in enumerate(lines, 1):
        if any(tok in line for tok in ["ngbTooltip", "tabindex=\"0\"", "aria-label", "aria-disabled", "dsBtnDisabled", "delete-button", "selfDeleteWarningLabel"]):
            a = max(1, idx - 4)
            b = min(len(lines), idx + 4)
            print(f"\n--- around line {idx} ---")
            for j in range(a, b + 1):
                print(f"{j:4}: {lines[j-1]}")
PY

Repository: dataquest-dev/dspace-angular

Length of output: 8383


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '70,100p' src/app/access-control/epeople-registry/epeople-registry.component.html
printf '\n---\n'
sed -n '1,140p' src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html
printf '\n---\n'
rg -n --no-heading 'selfDeleteWarningLabel|currentAuthenticatedUserId|isCurrentUser|tabindex="0"|ngbTooltip|dsBtnDisabled="true"|aria-label|aria-disabled' src/app/access-control/epeople-registry

Repository: dataquest-dev/dspace-angular

Length of output: 12995


Label the focusable self-delete wrapper. The tabindex="0" <span> is the actual keyboard target, but it has no accessible name or disabled state; the translated label is only on the nested button, which is removed from the tab order. Move the label/disabled semantics to the wrapper here and in src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/access-control/epeople-registry/epeople-registry.component.html`
around lines 81 - 89, The focusable self-delete wrapper is missing the
accessible name and disabled semantics because the `tabindex="0"` span is the
real keyboard target while the nested button is removed from tab order. Update
the wrapper in the epeople registry template so the translated label and
disabled state are applied to the outer element instead of the inner button, and
make the same change in the matching self-delete control in eperson-form so both
use the wrapper as the accessible target.

Comment on lines +515 to +525
this.canDelete$ = observableOf(false);
return this.epersonService.deleteEPerson(eperson).pipe(
getFirstCompletedRemoteData(),
map((restResponse: RemoteData<NoContent>) => ({ restResponse, eperson, attempted: true }))
);
}

return observableOf({ restResponse: null, eperson, attempted: false });
}),
finalize(() => this.canDelete$ = observableOf(true))
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Restore the authorization-backed canDelete$ stream after the modal closes.

Lines 515-524 replace canDelete$ with constant observables. After the first cancel/confirm cycle, the original isAuthorized(FeatureID.CanDelete, ...) check is gone, so the delete UI can stay enabled even for states that were not authorized.

💡 Minimal fix
+  private createCanDeleteStream(): Observable<boolean> {
+    return this.activeEPerson$.pipe(
+      switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined))
+    );
+  }
+
   initialisePage() {
@@
-    this.canDelete$ = this.activeEPerson$.pipe(
-      switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined))
-    );
+    this.canDelete$ = this.createCanDeleteStream();
@@
-              finalize(() => this.canDelete$ = observableOf(true))
+              finalize(() => this.canDelete$ = this.createCanDeleteStream())
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
this.canDelete$ = observableOf(false);
return this.epersonService.deleteEPerson(eperson).pipe(
getFirstCompletedRemoteData(),
map((restResponse: RemoteData<NoContent>) => ({ restResponse, eperson, attempted: true }))
);
}
return observableOf({ restResponse: null, eperson, attempted: false });
}),
finalize(() => this.canDelete$ = observableOf(true))
);
this.canDelete$ = observableOf(false);
return this.epersonService.deleteEPerson(eperson).pipe(
getFirstCompletedRemoteData(),
map((restResponse: RemoteData<NoContent>) => ({ restResponse, eperson, attempted: true }))
);
}
return observableOf({ restResponse: null, eperson, attempted: false });
}),
finalize(() => this.canDelete$ = this.createCanDeleteStream())
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts`
around lines 515 - 525, Restore the authorization-backed canDelete$ stream in
eperson-form.component by avoiding permanent replacement with constant
observableOf(true/false) values inside the delete flow. Update the logic around
the delete/confirm handling so the original isAuthorized(FeatureID.CanDelete,
...) observable is preserved or re-created after the modal closes, and only use
temporary disabled/enabled state during the action. Ensure the finalize block
and the delete pipeline in the component keep canDelete$ tied to authorization
rather than leaving it as a static observable.

Comment on lines +553 to +555
isCurrentUser(ePerson: EPerson): boolean {
return this.deleteGuard.isCurrentUser(ePerson, this.currentAuthenticatedUserId);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add JSDoc for isCurrentUser.

This new public method is undocumented. As per coding guidelines, **/*.ts: All new/modified public methods and classes require TypeDoc/JSDoc comments.

📝 Suggested comment
+  /**
+   * Whether the given EPerson is the currently authenticated user.
+   */
   isCurrentUser(ePerson: EPerson): boolean {
     return this.deleteGuard.isCurrentUser(ePerson, this.currentAuthenticatedUserId);
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
isCurrentUser(ePerson: EPerson): boolean {
return this.deleteGuard.isCurrentUser(ePerson, this.currentAuthenticatedUserId);
}
/**
* Whether the given EPerson is the currently authenticated user.
*/
isCurrentUser(ePerson: EPerson): boolean {
return this.deleteGuard.isCurrentUser(ePerson, this.currentAuthenticatedUserId);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts`
around lines 553 - 555, Add a JSDoc/TypeDoc comment for the public isCurrentUser
method in EpersonFormComponent. Document that it checks whether the given
EPerson matches the currently authenticated user, and keep the comment directly
above isCurrentUser so the new public API is documented per the coding
guidelines.

Source: Coding guidelines

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants