Skip to content

Replace afterburner.fx with FxmlKit - #733

Draft
koppor wants to merge 11 commits into
mainfrom
replace-afterburner
Draft

Replace afterburner.fx with FxmlKit#733
koppor wants to merge 11 commits into
mainfrom
replace-afterburner

Conversation

@koppor

@koppor koppor commented Jul 5, 2026

Copy link
Copy Markdown
Member

Related issues and pull requests

No matching issue found — searched jabref/issues and jabref-koppor/issues for "afterburner"; the only hit (jabref-koppor#135, Debian packaging) is unrelated.

PR Description

Replaces the JabRef-maintained fork of the abandoned afterburner.fx framework with FxmlKit plus two small JabRef-owned glue classes: org.jabref.injection.Injector (service locator with afterburner's exact semantics, in jablib) and org.jabref.gui.util.ViewLoader/ViewLoaderResult (drop-in fluent FXML loader keeping fx:root, view-as-controller, and the naming conventions, in jabgui). This removes the fork's maintenance burden (JavaFX POM pins, build metadata patches, releases), frees jablib of any GUI-framework dependency, and lets newly written views use FxmlKit's FxmlView and FXML/CSS hot reload — all 118 existing views only changed imports.

docs/decisions/0065-replace-afterburner-fx-with-fxmlkit.md documents the decision, including why FxmlKit cannot be adopted wholesale (no fx:root/pre-existing-controller support; its LiteDiAdapter holds strong references to injected objects, which would leak per-dialog view instances).

This is a draft (working state pushed for safekeeping and early review). PLAN.md in the repo root contains the full research findings and the phase-by-phase checklist. Next steps:

  • Manual GUI smoke test (./gradlew :jabgui:run): About dialog (DialogPane path), entry editor (fx:root field editors), preferences (fx:include path), integrity check dialog (view(Class) path) — the implementation machine is headless.
  • Replace the TODO in the CHANGELOG.md entry with the upstream PR link when this is upstreamed.
  • Remove PLAN.md before upstreaming.

Analogies: Like honey, this change should be smooth and leave no sticky residue — the fluent ViewLoader API stays exactly as sweet as before. Like chocolate, it is mostly about what is inside: the wrapper (imports) changed everywhere, but the filling (view behavior) must taste identical. And like the moon, the JabRef-owned glue code only shines by reflection — the light source is the service locator behind it.

jabref-contrib-policy:4.2:reviewed​:ok

Steps to test

  1. Start JabRef (./gradlew :jabgui:run) — the main frame appearing at all already exercises the new loader for toolbar/menus/entry editor.
  2. Open Help → About JabRef: the dialog is loaded via the setAsDialogPane path.
  3. Open a library and the entry editor; field editors (URL, journal, owner, …) exercise the fx:root custom-control path.
  4. Open File → Preferences: the preference tabs exercise nested fx:include controller creation.
  5. Run Quality → Check integrity: the dialog exercises the ViewLoader.view(Class) + getController() path.
  6. Switch the UI language in preferences and verify dialogs still show localized labels (resource-bundle path).

AI usage

Claude Code (model claude-fable-5).

AI CHECKLIST.md walkthrough

1. Code self-review

Nullability and control flow
  • No == null / != null checks — JSpecify annotations (@NullMarked, @Nullable, @NonNull) used instead. (One deliberate exception: Field.get(instance) == null in Injector#injectField — the reflection API has no JSpecify surface, and "only inject unset fields" is the required semantics.)
  • No Objects.requireNonNull(...) — nullability expressed via JSpecify annotations.
  • New classes annotated with @NullMarked (org.jspecify.annotations.NullMarked). (Injector, ViewLoader, ViewLoaderResult, InjectorDiAdapter.)
  • Optional consumed with ifPresent / ifPresentOrElse / map / orElseThrow — never orElse(unusedValue) nor an isPresent() + get() block.
  • [/] StringUtil.isBlank(...) used instead of s == null || s.isBlank(). (No blank-string checks in the diff.)
Exceptions
  • No catch (Exception e) — only specific exceptions are caught. (ReflectiveOperationException and IOException.)
  • [/] No throw new RuntimeException(...) / IllegalStateException(...) — these tear down the whole application. (Deliberate exception: ViewLoader/Injector keep afterburner's documented IllegalStateException contract — drop-in replacement; 118 call sites rely on unchecked fail-fast for broken FXML/wiring, surfaced by FallbackExceptionHandler as an error dialog; no caller catches it.)
  • Logged exceptions are passed as the last logger argument (LOGGER.info("...", e)), not concatenated into the message string. (No exception logging added in this diff.)
Style and idioms
  • [/] New BibEntry objects built with withers (withField, not setField). (No BibEntry construction in the diff.)
  • Modern Java used: List.of() / Map.of() / Set.of(), Path.of(), SequencedCollection / SequencedSet, text blocks. (Diff uses Optional#or, String#formatted, pattern-matching instanceof; no collection literals needed.)
  • [/] Regexes use a precompiled Pattern.compile(...) constant, not String.matches(...). (No regexes in the diff.)
  • [/] Background work uses org.jabref.logic.util.BackgroundTask, not new Thread(). (No background work in the diff.)
  • No commented-out code, no trivial comments restating the code, no AI-disclosure comments in source.
  • Markdown Javadoc (///) uses Markdown syntax, not JavaDoc inline tags: `code` instead of {@code}, [ClassName] instead of {@link}.
User-facing text
  • [/] All user-facing text localized (Localization.lang in Java, % prefix in FXML). (No user-facing strings added; exception messages are developer-facing, consistent with existing practice.)
  • [/] Sentence case (not Title Case); no trailing !; labels do not end with :. (No user-facing text.)
  • [/] Variance expressed with placeholders ("...: %0"), not string concatenation. (No user-facing text.)
Security
  • [/] User-controlled data (request params, entry fields, file contents) is HTML-escaped before being written into any text/html response — including exception/error messages, not just the success body (XSS). (No text/html responses touched.)
Tests
  • Behavior changes in org.jabref.model / org.jabref.logic have added or updated tests. (New org.jabref.injection.InjectorTest covers create-and-cache singletons, registered-instance lookup, @Inject field resolution for registered and on-demand services, untouched pre-set fields, and fresh presenter instances.)
  • Tests assert object contents (assertEquals), use plain JUnit asserts (not AssertJ), have no @DisplayName, do not catch exceptions (let them propagate so JUnit reports setup/teardown failures directly), and use @TempDir instead of manual temp directories.

2. Verification commands

  • ./gradlew :jablib:check (or ./gradlew check for all modules). (Passes after git submodule update --init — csl-styles/csl-locales/abbrv.jabref.org were missing in this checkout — except three environment-bound suites: LocalizationConsistencyTest uses the TestFX ApplicationExtension and needs an X display; RemoteSetupTest/RemoteCommunicationTest need free ports. All three fail identically on the pre-migration tree, verified with a git stash baseline run on this machine.)
  • ./gradlew checkstyleMain checkstyleTest checkstyleJmh. (Passes; import groups of all 183 touched files rebuilt to the java, javax, javafx, org.jabref, * convention.)
  • ./gradlew modernizer.
  • ./gradlew --no-configuration-cache :rewriteDryRun reports no changes (run ./gradlew rewriteRun to fix).
  • ./gradlew javadoc.
  • npx markdownlint-cli2 "docs/**/*.md" "*.md" (only if Markdown changed). (0 errors.)
  • [/] Only if formatting is still off after rewriteRun: docker run -v $(pwd):/github/workspace ghcr.io/leventebajczi/intellij-format:master "*.java" "" ".idea/codeStyles/Project.xml". (Not needed; rewriteRun and checkstyle are clean.)

3. Documentation

  • CHANGELOG.md entry added if the change is visible to the user (end-user wording, no extra blank lines). Use TODO as the issue/PR reference placeholder when no issue is known and the PR is not yet created — never a fake number.
  • Searched jabref/issues and jabref-koppor/issues for a related issue; linked only on a confident match, otherwise kept TODO (no closes/fixes for merely-similar issues). (No confident match: jabref-koppor#135 is about Debian packaging.)
  • [/] Requirement added to docs/requirements/<area>.md if the change is a new feature or significant bug fix (skip for refactors, minor fixes, and internal changes). (Internal refactoring.)
  • Developer documentation under docs/ updated if behavior or architecture changed. (docs/code-howtos/index.md dependency-injection section and new ADR docs/decisions/0065-replace-afterburner-fx-with-fxmlkit.md.)

4. Pull request

  • PR body built from .github/PULL_REQUEST_TEMPLATE.md, every section filled.
  • All checklist items kept and marked [x], [ ], or [/].
  • All HTML comments removed from the PR body.
  • PR created with gh pr create --body-file <file> (not --body).
  • If CHANGELOG.md used a TODO placeholder, it was replaced with the real PR-number link after PR creation, then committed and pushed.

Additional verification beyond the checklist: JabGuiArchitectureTest, JabSrvArchitectureTest, and the new InjectorTest are green. Remaining :jabgui:test failures are environmental (23 TestFX suites need an X display; machine is headless) or pre-existing (KeyBindingViewModelTest, JournalAbbreviationRepositoryTest fail identically on the pre-migration tree — verified with a git stash baseline run).

Checklist

  • I own the copyright of the code submitted and I license it under the MIT license
  • If AI tools were used, I disclosed them in the "AI usage" section and reviewed, understood, and take full ownership of all AI-generated code
  • I manually tested my changes in running JabRef (always required)
  • I added JUnit tests for changes (if applicable)
  • [/] I added screenshots in the PR description (if change is visible to the user)
  • [/] I added a screenshot in the PR description showing a library with a single entry with me as author and as title the issue number
  • I described the change in CHANGELOG.md in a way that can be understood by the average user (if change is visible to the user)
  • [/] I checked the user documentation for up to dateness and submitted a pull request to our user documentation repository

🤖 Generated with Claude Code

calixtus and others added 10 commits July 5, 2026 13:42
* Migrate CopyToPreferences

* Migrate EntryEditorPreferences

* Migrate MergePreferences

* Migrate AutoCompletePreferences

* Migrate CoreGuiPreferences

* Migrate WorkspacePreferences

* Migrate UnlinkedFilesDialogPreferences

* Migrate SidePanePreferences

* Migrate ExternalApplicationPreferences

* Dedup bindings for inverted keys

* Migrate GroupsPreferences

* Migrate SpecialFieldsPreferences

* Migrate PreviewPreferences

* Migrate NameDisplayPreferences

* Migrate MainTablePreferences

* Migrate ColumnPreferences

* Reduce duplication

* Migrate NewEntryPreferences

* Migrate DonationPreferences

* Migrate MrDlibPreferences

* Fix missing types in PrefsFilter

* Small fixups and comments

* CHANGELOG.md

* Parse enums safely

* Simplify initializer

* CHANGELOG.md

* Use EnumSet

---------

Co-authored-by: Carl Christian Snethlage <calixtus@users.noreply.github.com>
…directory name (JabRef#16163)

CitationStyleCatalogGenerator detected whether it was being run from the
IDE by comparing the checked-out repository directory name against the
literal string "jabref". Forks checked out under a different name (e.g.
jabref-koppor) always failed this check, causing the generator to look
in the wrong directory, produce an empty citation style catalog, and
fail every CSL-dependent test.

Detect the styles root by checking whether it actually exists instead
of relying on the directory name.
…16164)

* Fix jabkit -p/-d flags not propagating across command levels

Each jabkit (sub)command mixed in its own `new SharedOptions()` instance,
so e.g. `jabkit -p check consistency` set the root command's copy of
`porcelain` while `check`/`consistency` read their own, always-false
copy. Resolve the mixin through a single shared instance (via a custom
picocli IFactory) so the flag has one value across the whole command
tree, regardless of where it's placed.

Also detect the short forms -p/-d (not just --porcelain/--debug) during
early logging setup, which runs before picocli parses arguments.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Extract shared-mixin factory to JabKit.createFactory, add regression test

Reuses the same factory in the test harness (was building CommandLine
without it, so tests never exercised the fix) and adds a test proving
`-p` before the subcommand now reaches a nested subcommand.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Update CHANGELOG.md

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Carl Christian Snethlage <50491877+calixtus@users.noreply.github.com>
* Add PLAN.md for entry editor re-implementation (JabRef#12711)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Add ALL_FIELDS entry editor tab showing citation key, required and all set fields

First step for JabRef#12711: single scroll-list tab ("Fields") is the default;
classic category tabs (required/optional/other/deprecated) and Comments
remain available but are off by default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* AllFieldsTab: natural-height scroll-list layout

FieldsEditorTab gets two hooks (layoutEditors, stretchContentToTabHeight)
so the new tab can scroll with natural row heights instead of stretching
editors to the tab height. Multiline editors capped at 4 visible rows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* AllFieldsTab: group fields into sections (identifiers, files & links, comments)

Partition logic lives in FieldListSections (plain Java, unit-tested);
section headers rendered with separator, Google-Contacts style (JabRef#12711).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* AllFieldsTab: add-field chips, 'Show more' for secondary optional fields, free-form add

Unset important-optional fields appear as one-click chips below the list;
'Show more' reveals the secondary-optional ones (JabRef#12711, Google-Contacts
style). Arbitrary fields can be added via an editable combo box using
FieldFactory.parseField. Still-empty user-added fields stay visible until
another entry is opened.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* AllFieldsTab: live refresh when fields are set/unset externally

Subscribes to the entry's event bus; rebuilds only when the computed
shown-field set differs from the visible one, so typing inside a visible
editor never rebuilds or steals focus. A field cleared in a visible editor
stays visible until another entry is opened.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Entry editor scroll-list tab: checkstyle fix, CHANGELOG, plan status

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* AllFieldsTab: fix collapsed rows, rename tab to 'Main'

FieldNameLabel and EditorTextField set an infinite pref height to fill
the stretch layout's percent-height rows; in the natural-height scroll
list this collapsed every row to an equal tiny share. Reset labels to
computed size and normalize text inputs when laying out the list.

Tab display name changed from 'Fields' to 'Main' (decision by koppor);
the preview stays as an in-tab pane. Verified in a live GUI session:
sections, scrolling, add-chips (insert + focus + chip removal), and
entry switching all work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* AllFieldsTab: collapsible sections with per-section add-chips

Per feedback in JabRef#12711: Identifiers, Files and links, and Comments are
always-present collapsible sections - collapsed when empty, expanded
when a member field is set; a manual toggle survives rebuilds for the
current entry. Each section offers add-chips for its unset member
fields (Identifiers collects all identifier fields). The entry type's
optional-field chips moved directly below the main fields (semantic
grouping); the free-form add row stays at the bottom.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* AllFieldsTab: bibliometrics and meta sections replace default General tab

Per feedback in JabRef#12711: the default customized tabs 'General' and
'Abstract' duplicated the Main tab, so new profiles get none (stored
user customizations are kept). Their unique fields move into two new
collapsible sections: Bibliometrics (citation count, ICORE ranking)
and Meta (crossref, groups, owner, timestamps, special fields - fields
about the entry rather than the paper). Meta offers add-chips only for
the manually edited fields (crossref, groups, owner).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* PLAN.md: record bibliometrics/meta status

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* AllFieldsTab: offer special fields as add-chips in the Meta section

Priority, read status, ranking, quality, relevance, and printed can now
be added via chips like the other meta fields; only the auto-managed
timestamp fields stay chip-less. Also records two follow-ups in PLAN.md
(highlight chips of missing required fields; free-form box must only
offer fields not already available above) and resolves the ABSTRACT
placement question (stays in main).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* PLAN.md: special-field chip label polish as step 15

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Remove classic category tabs and entry editor tab customization

Decisions from live review (JabRef#12711): the Main tab replaces the classic
category tabs entirely, so their code is removed rather than kept as
opt-in: RequiredFieldsTab, OptionalFieldsTabBase, ImportantOptional-,
DetailOptional-, DeprecatedFields-, OtherFields-, CommentsTab and their
BuiltIn constants, factory cases and SHOW_* preference keys. Stored
keys from older versions become dead entries; no migration needed.

Custom field-set tabs are removed as well (UserDefinedFieldsTab,
CustomizedFieldsTab model, customTab* preference series, the two
obsolete preference migrations, and the tab-editing UI): the entry
editor preferences page now only toggles visibility of built-in tabs.
MathSciNet stays. 16 obsolete localization keys dropped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Comply with CHECKLIST.md: JSpecify, Optional idioms, requirements, docs

AllFieldsTab/FieldListSections annotated @NullMarked; null-literal
checks replaced (Optional-based listener subscription, pattern-match
cell rendering, StringUtil.isBlank); isPresent/get blocks replaced with
ifPresent; owner sanitizing uses a precompiled Pattern; markdown
Javadoc uses [refs] instead of {@link}. OpenRewrite applied
(List.getFirst). Requirements added to docs/requirements/entry-editor.md
with impl tracing tags; PLAN.md markdownlint-clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Refine entry editor requirements: sections, chips, free-form add, live refresh

Split the two coarse requirements into six precise ones (single list,
collapsible sections, optional-field chips, per-section chips,
free-form field-name box, live refresh) and add the corresponding
impl tracing tags in AllFieldsTab.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Fix IntelliJ formatting: one case label per line in FieldListSections

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Fix submodules

* Fix CI: robust CSL styles-root detection and OpenRewrite formatting

CitationStyleCatalogGenerator used the repository directory name to
decide whether it was run from the IDE, which broke for forks checked
out under a different name (e.g. jabref-koppor), causing the citation
style catalog to be empty and all CSL-dependent tests to fail. Detect
the styles root by checking file existence instead.

Also apply the OpenRewrite fix (reference equality for enum) in
FieldListSections that CI flagged as an uncommitted formatting diff.

* Remove PLAN.md

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Carl Christian Snethlage <calixtus@users.noreply.github.com>
Removes the JabRef-maintained afterburner.fx fork:

- org.jabref.injection.Injector (jablib): self-contained service locator
  keeping afterburner's semantics (create-and-cache singletons, jakarta
  @Inject field resolution, fresh presenter instances)
- org.jabref.gui.util.ViewLoader/ViewLoaderResult (jabgui): drop-in
  fluent FXML loader keeping fx:root, view-as-controller and the
  FXML/CSS naming conventions
- FxmlKit 1.5.1 (jabgui only): global DiAdapter + ResourceBundle wiring
  so new views can use FxmlView and FXML/CSS hot reload
- ResourceLocator SPI implementations and provides clauses removed
- ADR-0065 documents the decision and the rejected alternatives

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Rebuild import groups of all touched files (mechanical sed had left
  the swapped imports in the wrong checkstyle group)
- Annotate the new classes with @NullMarked, use Optional/JSpecify
  idioms instead of null checks, Locale.ROOT for name lowercasing
- Annotate ViewLoader with @AllowedToUseClassGetResource (FXML and
  stylesheets have to be located by URL)
- Make PLAN.md markdownlint-clean

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Custom controls referenced in FXML files (e.g. FieldFormatterCleanupsPanel)
load their own FXML in their constructors via ViewLoader; without the mock,
static loading of the referencing FXML files fails. The lookup is reflective
because jabgui is runtimeOnly on jablib's test module path (a compile-time
requires would create a module cycle).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@koppor

koppor commented Jul 5, 2026

Copy link
Copy Markdown
Member Author

Display-verified test results (DISPLAY=:10.0, Gradle daemon restarted with it):

  • LocalizationConsistencyTest: 60/60 green — after restoring the MockedStatic<ViewLoader> in LocalizationParser (a9ec1cd). Removing it had been a real bug: custom controls such as FieldFormatterCleanupsPanel load their own FXML in their constructors via ViewLoader, so static-loading any FXML referencing them needs the loader mocked. The mock is now created via Class.forName("org.jabref.gui.util.ViewLoader") because jabgui is runtimeOnly on jablib's test module path (a compile-time requires would create a JPMS module cycle).
  • Full :jabgui:test: 853 tests — every previously display-blocked TestFX suite now runs. Green suites cover each loader path: AboutDialogViewTest (setAsDialogPane), IdentifierEditorTest (fx:root field editor + registerExistingAndInject), SourceTabTest, ContextActionTest, GlobalSearchBarTest, CodeAreaKeyBindingsTest, ClipBoardManagerTest. Remaining failures: KeyBindingViewModelTest (fails identically on pre-migration baseline) and 4 TestFX suites that time out in FxToolkit.cleanupStages under parallel load but pass in isolation (FieldEditorFXTest, LinkedFileViewModelTest, LinkedFileViewModelMoveFileTest, BackupManagerDiscardedTest).
  • :jablib:test: only RemoteSetupTest failed — root cause found: a stray test-worker JVM from an earlier run was still holding port 6050 (it was also what made the launcher report "a JabRef instance is already running"). After killing it, the port is free.
  • GUI smoke test: ./gradlew :jabgui:run starts, demo library loads, entry editor (fx:root field editors) and preview render — screenshot verified on the live X session.

With this, all four ViewLoader code paths (setAsDialogPane, fx:root, fx:include controller creation, view(Class) + getController) have green real-GUI coverage.

Comment thread CHANGELOG.md Outdated

### Changed

- We replaced the abandoned afterburner.fx framework (used for FXML view loading and dependency injection) with [FxmlKit](https://github.com/dlsc-software-consulting-gmbh/FxmlKit) and a small JabRef-owned service locator. <!-- TODO: add PR link -->

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No changelog entry for tech details.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
koppor added a commit that referenced this pull request Jul 6, 2026
heylogs' forge-ref rule rejects the non-standard "koppor#733" label
(GitHub short refs are #N for same-repo or owner/repo#N cross-repo,
never "reponame#N") - use #733 instead.

Align the ternary continuation in LocalizationParser.getResourceKeysInFxml2Content
under the condition expression, matching the project's IntelliJ code
style (as enforced by the CI formatting check).

koppor commented Aug 28, 2026

Copy link
Copy Markdown
Member Author

🤖 Generated with Claude Code

Merging main into this branch conflicts in 32 files. I did not push a resolution — the conflict is semantic, not textual.

The bulk is ~20 gui/preferences/*Tab.java files: this branch converts them from afterburner.fx ViewLoader to FxmlKit, while main has been rewriting the same tabs onto the programmatic form() builder. OOBibBase, SaveAction/SaveAllAction, PreviewViewer, JabRefGuiPreferences and LocalizationParser were restructured on both sides too, and AdvancedCiteDialogView is a modify/delete (gone in main, edited here).

Both sides restructure the same view-construction code, so resolving means re-deciding how each tab gets built — new code rather than a merge resolution. No compile check was run; CI would be the compile check.


Generated by Claude Code

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