Skip to content
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

DPMMA-2608 Local Autosave and Recovery Feature for GrapesJS Builder #13610

Open
wants to merge 19 commits into
base: 5.x
Choose a base branch
from

Conversation

patrykgruszka
Copy link
Member

@patrykgruszka patrykgruszka commented Apr 8, 2024

Q A
Bug fix? (use the a.b branch) [ N ]
New feature/enhancement? (use the a.x branch) [ Y ]
Deprecations? [ N ]
BC breaks? (use the c.x branch) [ N ]
Automated tests included? [ N ] (only JS and CSS changes)
Related user documentation PR URL mautic/mautic-documentation#...
Related developer documentation PR URL mautic/developer-documentation#...
Issue(s) addressed Fixes #...

Description:

This pull request introduces a local autosave backup and recovery functionality for the GrapesJS builder used in Mautic for emails and pages.

Features:

  • Functionality to store a local copy of the builder content in the user's browser local storage each time the content is modified.
  • When an email or page is accessed in Mautic, the editor will check if a different version exists in the browser's local storage. If a different version is found, a prompt will be displayed to the user, giving them the option to restore the autosaved version. If the user chooses to restore, the contents of the editor will be replaced with the autosaved version from the local storage.
  • The copy is associated with the type (page/email), theme (e.g. blank/paprika) and the entity ID
  • The storage can contain last 10 copies to prevent web storage from exceeding its 10MiB per domain limit.

Steps to test this PR:

  1. Open this PR on Gitpod or pull down for testing locally (see docs on testing PRs here)
  2. Open the email or page builder, either by using an existing object or creating a new one.
  3. Choose a theme for the page or email.
  4. Open the builder and make some changes.
  5. Close the browser tab without saving any changes.
  6. Reopen the builder, ensuring that the type (page/email), theme, and ID match the previous object or a new one.
  7. Verify that the copy is available and click the "Restore the backup" button.
    image
  8. Confirm that the content has been successfully restored.

This PR includes the commit 6486428e389d6626398558e05c512db4a1fe522b from the 5.0 branch to prevent future conflicts.

@patrykgruszka patrykgruszka added ready-to-test PR's that are ready to test code-review-needed PR's that require a code review before merging landing-pages Anything related to landing pages email Anything related to email builder-grapesjs Anything related to the GrapesJS email or landing page builders labels Apr 8, 2024
@jos0405
Copy link
Contributor

jos0405 commented Apr 15, 2024

Hi, I started to test this.
the gitpod comes with GrapesJS turned off. If I turn it on, it's asking for Froala to be turned on?
If I clear the cache, I get an error.
image

@patrykgruszka
Copy link
Member Author

@jos0405 have you tried deleting everything in the cache directory after changing the builder to grapesjs and before clearing the cache?

rm -rf var/cache/*
ddev exec php bin/console c:c

@jos0405
Copy link
Contributor

jos0405 commented Apr 16, 2024

Thank you, that works.
However after saving a template and coming back I couldn't open the email, got this:

/html/app/bundles/CoreBundle/Helper/InputHelper.php:420 at
Mautic\CoreBundle\ErrorHandler\ErrorHandler -> handleError ( 8, 'PHP Notice - iconv(): Detected an illegal character in input string', '/var/www/html/app/bundles/CoreBundle/Helper/InputHelper.php', 420 )
/html/app/bundles/CoreBundle/Helper/InputHelper.php:420 at iconv ( 'UTF-8', 'Windows-1252', '<title> {subject} </title>

Hello World!

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid officia consequatur placeat reprehenderit excepturi, tempore, id quos quaerat ab fuga. 

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Inventore, voluptate. Hello World!

⁠⁠⁠⁠⁠⁠⁠This is a good change.

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dignissimos alias rerum nemo ducimus modi perspiciatis.

{unsubscribe_text} | {webview_text}
<style>#outlook a{padding:0;}.ExternalClass *{line-height:100%;}body{margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;font-family:"Open Sans", Helvetica, Arial, sans-serif !important;font-size:14px;line-height:1.6;text-align:left;color:#414141;}table, td{border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;}img{border:0;height:auto;line-height:100%;outline:none;text-decoration:none;-ms-interpolation-mode:bicubic;}p{display:block;margin:13px 0;}div[data-slot="text"]{font-size:14px !important;line-height:1.6 !important;text-align:left !important;color:#414141 !important;margin-bottom:10px !important;}div[style="clear:both"]{margin-bottom:20px !important;}h1, h2, h3, h4, h5, h6{margin:0 !important;margin-bottom:10px !important;}.outlook-group-fix{width:100% !important;}#ik0w{background-color:#ffffff;}#i04s{Margin:0px auto;border-radius:4px;max-width:600px;}#i3wc{width:100%;border-radius:4px;}#iw25{direction:ltr;padding:20px 0;text-align:center;vertical-align:top;}#ihvk{font-size:13px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;}#i4s0y{background-color:#FFFFFF;vertical-align:top;padding:20px 20px;}#iksmj{padding:0;word-break:break-word;}#irq88{font-family:'Open Sans', Helvetica, Arial, sans-serif;font-size:14px;line-height:1.6;text-align:left;color:#414141;}#irvk4{font-size:13px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;}#io1rt{vertical-align:top;}#id2mb{padding:20px 20px;word-break:break-word;}#iarci{font-family:'Open Sans', Helvetica, Arial, sans-serif !important;font-size:12px !important;line-height:1.4 !important;text-align:left !important;color:#999999 !important;}@media only screen and (min-width:480px){.mj-column-per-100{width:100% !important;}}</style>' )
/html/app/bundles/CoreBundle/Helper/InputHelper.php:142 at Mautic\CoreBundle\Helper\InputHelper :: html ( '<title> {subject} </title>

Hello World!

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid officia consequatur placeat reprehenderit excepturi, tempore, id quos quaerat ab fuga. 

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Inventore, voluptate. Hello World!

⁠⁠⁠⁠⁠⁠⁠This is a good change.

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dignissimos alias rerum nemo ducimus modi perspiciatis.

{unsubscribe_text} | {webview_text}
<style>#outlook a{padding:0;}.ExternalClass *{line-height:100%;}body{margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;font-family:"Open Sans", Helvetica, Arial, sans-serif !important;font-size:14px;line-height:1.6;text-align:left;color:#414141;}table, td{border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;}img{border:0;height:auto;line-height:100%;outline:none;text-decoration:none;-ms-interpolation-mode:bicubic;}p{display:block;margin:13px 0;}div[data-slot="text"]{font-size:14px !important;line-height:1.6 !important;text-align:left !important;color:#414141 !important;margin-bottom:10px !important;}div[style="clear:both"]{margin-bottom:20px !important;}h1, h2, h3, h4, h5, h6{margin:0 !important;margin-bottom:10px !important;}.outlook-group-fix{width:100% !important;}#ik0w{background-color:#ffffff;}#i04s{Margin:0px auto;border-radius:4px;max-width:600px;}#i3wc{width:100%;border-radius:4px;}#iw25{direction:ltr;padding:20px 0;text-align:center;vertical-align:top;}#ihvk{font-size:13px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;}#i4s0y{background-color:#FFFFFF;vertical-align:top;padding:20px 20px;}#iksmj{padding:0;word-break:break-word;}#irq88{font-family:'Open Sans', Helvetica, Arial, sans-serif;font-size:14px;line-height:1.6;text-align:left;color:#414141;}#irvk4{font-size:13px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;}#io1rt{vertical-align:top;}#id2mb{padding:20px 20px;word-break:break-word;}#iarci{font-family:'Open Sans', Helvetica, Arial, sans-serif !important;font-size:12px !important;line-height:1.4 !important;text-align:left !important;color:#999999 !important;}@media only screen and (min-width:480px){.mj-column-per-100{width:100% !important;}}</style>', false )
/html/app/bundles/CoreBundle/Form/EventListener/CleanFormSubscriber.php:35 at Mautic\CoreBundle\Helper\InputHelper :: _ ( array( 'template' => 'blank', 'fromName' => '', 'fromAddress' => '', 'replyToAddress' => '', 'bccAddress' => '', 'useOwnerAsMailer' => '0', 'customHtml' => '<title> {subject} </title>

Hello World!

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid officia consequatur placeat reprehenderit excepturi, tempore, id quos quaerat ab fuga. 

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Inventore, voluptate. Hello World!

⁠⁠⁠⁠⁠⁠⁠This is a good change.

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dignissimos alias rerum nemo ducimus modi perspiciatis.

{unsubscribe_text} | {webview_text}
<style>#outlook a{padding:0;}.ExternalClass *{line-height:100%;}body{margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;font-family:"Open Sans", Helvetica, Arial, sans-serif !important;font-size:14px;line-height:1.6;text-align:left;color:#414141;}table, td{border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;}img{border:0;height:auto;line-height:100%;outline:none;text-decoration:none;-ms-interpolation-mode:bicubic;}p{display:block;margin:13px 0;}div[data-slot="text"]{font-size:14px !important;line-height:1.6 !important;text-align:left !important;color:#414141 !important;margin-bottom:10px !important;}div[style="clear:both"]{margin-bottom:20px !important;}h1, h2, h3, h4, h5, h6{margin:0 !important;margin-bottom:10px !important;}.outlook-group-fix{width:100% !important;}#ik0w{background-color:#ffffff;}#i04s{Margin:0px auto;border-radius:4px;max-width:600px;}#i3wc{width:100%;border-radius:4px;}#iw25{direction:ltr;padding:20px 0;text-align:center;vertical-align:top;}#ihvk{font-size:13px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;}#i4s0y{background-color:#FFFFFF;vertical-align:top;padding:20px 20px;}#iksmj{padding:0;word-break:break-word;}#irq88{font-family:'Open Sans', Helvetica, Arial, sans-serif;font-size:14px;line-height:1.6;text-align:left;color:#414141;}#irvk4{font-size:13px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;}#io1rt{vertical-align:top;}#id2mb{padding:20px 20px;word-break:break-word;}#iarci{font-family:'Open Sans', Helvetica, Arial, sans-serif !important;font-size:12px !important;line-height:1.4 !important;text-align:left !important;color:#999999 !important;}@media only screen and (min-width:480px){.mj-column-per-100{width:100% !important;}}</style>', 'plainText' => '', 'dynamicContent' => array( array( 'tokenName' => 'Dynamic Content 1', 'content' => 'Default Dynamic Content', 'filters' => array( array( 'content' => '' ) ) ) ), 'subject' => 'test', 'name' => 'test', 'category' => '', 'language' => 'en', 'segmentTranslationParent' => '', 'templateTranslationParent' => '', 'isPublished' => '1', 'publishUp' => '', 'publishDown' => '', 'unsubscribeForm' => '', 'preferenceCenter' => '', 'utmTags' => array( 'utmSource' => '', 'utmMedium' => '', 'utmCampaign' => '', 'utmContent' => '' ), 'variantParent' => '', 'translationParent' => '', 'sessionId' => '1', 'emailType' => 'template', 'unlockModel' => 'email.email', 'unlockId' => '1', 'buttons' => array( 'save' => '' ) ), array( 'content' => 'html', 'customHtml' => 'html', 'headers' => 'clean' ) )
/html/vendor/symfony/event-dispatcher/EventDispatcher.php:230 at Mautic\CoreBundle\Form\EventListener\CleanFormSubscriber -> preSubmitData ( object(PreSubmitEvent), 'form.pre_submit', object(EventDispatcher) )
/html/vendor/symfony/event-dispatcher/EventDispatcher.php:59 at Symfony\Component\EventDispatcher\EventDispatcher -> callListeners ( array( object(Closure), object(Closure), object(Closure), object(Closure), object(Closure) ), 'form.pre_submit', object(PreSubmitEvent) )
/html/vendor/symfony/event-dispatcher/ImmutableEventDispatcher.php:33 at Symfony\Component\EventDispatcher\EventDispatcher -> dispatch ( object(PreSubmitEvent), 'form.pre_submit' )
/html/vendor/symfony/form/Form.php:568 at Symfony\Component\EventDispatcher\ImmutableEventDispatcher -> dispatch ( object(PreSubmitEvent), 'form.pre_submit' )
/html/vendor/symfony/form/Extension/HttpFoundation/HttpFoundationRequestHandler.php:110 at Symfony\Component\Form\Form -> submit ( array( 'template' => 'blank', 'fromName' => '', 'fromAddress' => '', 'replyToAddress' => '', 'bccAddress' => '', 'useOwnerAsMailer' => '0', 'customHtml' => '<title> {subject} </title>

Hello World!

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid officia consequatur placeat reprehenderit excepturi, tempore, id quos quaerat ab fuga. 

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Inventore, voluptate. Hello World!

⁠⁠⁠⁠⁠⁠⁠This is a good change.

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dignissimos alias rerum nemo ducimus modi perspiciatis.

{unsubscribe_text} | {webview_text}
<style>#outlook a{padding:0;}.ExternalClass *{line-height:100%;}body{margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;font-family:"Open Sans", Helvetica, Arial, sans-serif !important;font-size:14px;line-height:1.6;text-align:left;color:#414141;}table, td{border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;}img{border:0;height:auto;line-height:100%;outline:none;text-decoration:none;-ms-interpolation-mode:bicubic;}p{display:block;margin:13px 0;}div[data-slot="text"]{font-size:14px !important;line-height:1.6 !important;text-align:left !important;color:#414141 !important;margin-bottom:10px !important;}div[style="clear:both"]{margin-bottom:20px !important;}h1, h2, h3, h4, h5, h6{margin:0 !important;margin-bottom:10px !important;}.outlook-group-fix{width:100% !important;}#ik0w{background-color:#ffffff;}#i04s{Margin:0px auto;border-radius:4px;max-width:600px;}#i3wc{width:100%;border-radius:4px;}#iw25{direction:ltr;padding:20px 0;text-align:center;vertical-align:top;}#ihvk{font-size:13px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;}#i4s0y{background-color:#FFFFFF;vertical-align:top;padding:20px 20px;}#iksmj{padding:0;word-break:break-word;}#irq88{font-family:'Open Sans', Helvetica, Arial, sans-serif;font-size:14px;line-height:1.6;text-align:left;color:#414141;}#irvk4{font-size:13px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;}#io1rt{vertical-align:top;}#id2mb{padding:20px 20px;word-break:break-word;}#iarci{font-family:'Open Sans', Helvetica, Arial, sans-serif !important;font-size:12px !important;line-height:1.4 !important;text-align:left !important;color:#999999 !important;}@media only screen and (min-width:480px){.mj-column-per-100{width:100% !important;}}</style>', 'plainText' => '', 'dynamicContent' => array( array( 'tokenName' => 'Dynamic Content 1', 'content' => 'Default Dynamic Content', 'filters' => array( array( 'content' => '' ) ) ) ), 'subject' => 'test', 'name' => 'test', 'category' => '', 'language' => 'en', 'segmentTranslationParent' => '', 'templateTranslationParent' => '', 'isPublished' => '1', 'publishUp' => '', 'publishDown' => '', 'unsubscribeForm' => '', 'preferenceCenter' => '', 'utmTags' => array( 'utmSource' => '', 'utmMedium' => '', 'utmCampaign' => '', 'utmContent' => '' ), 'variantParent' => '', 'translationParent' => '', 'sessionId' => '1', 'emailType' => 'template', 'unlockModel' => 'email.email', 'unlockId' => '1', '_token' => '2b5e241369e63012.b-6DzWj8BxLT8d0VACLpK-rKTg3pS4wjPNQN0a30V-E.Qqfl9RuFU3-KxLNvS1DbQIyCDEG5e-1PVaFamvKnE4UolOmGOJVqeaOTsg', 'buttons' => array( 'save' => '' ) ), true )
/html/vendor/symfony/form/Form.php:503 at Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationRequestHandler -> handleRequest ( object(Form), object(Request) )
/html/app/bundles/CoreBundle/Controller/AbstractFormController.php:164 at Symfony\Component\Form\Form -> handleRequest ( object(Request) )
/html/app/bundles/EmailBundle/Controller/EmailController.php:701 at Mautic\CoreBundle\Controller\AbstractFormController -> isFormValid ( object(Form) )
/html/vendor/symfony/http-kernel/HttpKernel.php:163 at Mautic\EmailBundle\Controller\EmailController -> editAction ( object(Request), object(AssetsHelper), object(Translator), object(Router), object(CoreParametersHelper), '1', false, false )
/html/vendor/symfony/http-kernel/HttpKernel.php:75 at Symfony\Component\HttpKernel\HttpKernel -> handleRaw ( object(Request), 2 )
/html/vendor/symfony/framework-bundle/Controller/AbstractController.php:156 at Symfony\Component\HttpKernel\HttpKernel -> handle ( object(Request), 2 )
/html/app/bundles/CoreBundle/Controller/CommonController.php:415 at Symfony\Bundle\FrameworkBundle\Controller\AbstractController -> forward ( 'Mautic\EmailBundle\Controller\EmailController::editAction', array( 'objectId' => '1', 'objectModel' => '', '_stopwatch_token' => 'f5a626', '_route' => 'mautic_email_action', '_controller' => 'Mautic\EmailBundle\Controller\EmailController::editAction', 'objectAction' => 'edit', '_route_params' => array( 'objectId' => '1', 'objectAction' => 'edit' ), '_firewall_context' => 'security.firewall.map.context.main', '_security_firewall_run' => '_security_mautic' ), array( 'mauticUserLastActive' => '25', 'mauticLastNotificationId' => '' ) )
/html/vendor/symfony/http-kernel/HttpKernel.php:163 at Mautic\CoreBundle\Controller\CommonController -> executeAction ( object(Request), 'edit', '1', 0, '' )
/html/vendor/symfony/http-kernel/HttpKernel.php:75 at Symfony\Component\HttpKernel\HttpKernel -> handleRaw ( object(Request), 1 )
/html/vendor/symfony/http-kernel/Kernel.php:202 at Symfony\Component\HttpKernel\HttpKernel -> handle ( object(Request), 1, true )
/html/app/AppKernel.php:109 at Symfony\Component\HttpKernel\Kernel -> handle ( object(Request), 1, true )
/html/app/middlewares/CORSMiddleware.php:76 at AppKernel -> handle ( object(Request), 1, true )
/html/app/middlewares/HSTSMiddleware.php:39 at Mautic\Middleware\CORSMiddleware -> handle ( object(Request), 1, true )
/html/app/middlewares/CatchExceptionMiddleware.php:28 at Mautic\Middleware\HSTSMiddleware -> handle ( object(Request), 1, true )
/html/app/middlewares/Dev/IpRestrictMiddleware.php:52 at Mautic\Middleware\CatchExceptionMiddleware -> handle ( object(Request), 1, true )
/html/app/middlewares/VersionCheckMiddleware.php:58 at Mautic\Middleware\Dev\IpRestrictMiddleware -> handle ( object(Request), 1, true )
/html/app/middlewares/TrustMiddleware.php:42 at Mautic\Middleware\VersionCheckMiddleware -> handle ( object(Request), 1, true )
/html/vendor/stack/builder/src/Stack/StackedHttpKernel.php:23 at Mautic\Middleware\TrustMiddleware -> handle ( object(Request), 1, true )
/html/index.php:19 at Stack\StackedHttpKernel -> handle ( object(Request) )

@jos0405
Copy link
Contributor

jos0405 commented Apr 16, 2024

image

@patrykgruszka
Copy link
Member Author

@jos0405 unfortunately this is another issue on the 5.x branch. Here is PR that fixes it, but it's still a draft: #13572.

@patrykgruszka
Copy link
Member Author

@jos0405 I've updated the source, as the fix is already on the 5.x branch. After enabling the grapesjs and clearing the cache it should work fine.

Copy link

@magdalenaleonow magdalenaleonow left a comment

Choose a reason for hiding this comment

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

Tested, everything works :)
image

Copy link

codecov bot commented Apr 19, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 61.39%. Comparing base (4039055) to head (9468796).
Report is 5 commits behind head on 5.x.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff            @@
##                5.x   #13610   +/-   ##
=========================================
  Coverage     61.38%   61.39%           
  Complexity    34024    34024           
=========================================
  Files          2238     2238           
  Lines        101685   101686    +1     
=========================================
+ Hits          62420    62430   +10     
+ Misses        39265    39256    -9     

see 1 file with indirect coverage changes

RCheesley
RCheesley previously approved these changes Apr 19, 2024
Copy link
Sponsor Member

@RCheesley RCheesley left a comment

Choose a reason for hiding this comment

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

Works beautifully and I am sure this will be a huge benefit for those moments where something crashes and you haven't hit save in a while!

Thanks so much @patrykgruszka - great work! 🚀

@RCheesley RCheesley removed the ready-to-test PR's that are ready to test label Apr 19, 2024
@RCheesley RCheesley added the user-testing-passed PRs which have been successfully tested by the required number of people. label Apr 19, 2024
@RCheesley RCheesley added this to the 5.1.0 milestone Apr 19, 2024
@RCheesley RCheesley added the feature A new feature for inclusion in minor or major releases label Apr 19, 2024
@LordRembo
Copy link
Contributor

LordRembo commented Apr 19, 2024

Now that the grapesjs upgrade got merged, you might want to rebase and rebuild the dist files. There's an updated command now: npm run rebuild which clears the file cache.
That should sort out merge conflicts with the dist files.

@patrykgruszka
Copy link
Member Author

Thanks for letting me know @LordRembo. I've updated the code it's now working with the new editor.

I've also included a fix that will clear the storage item when a new email or page is saved.
This is ready to test and review, the grapesjs initialization workaround is no longer needed.

@patrykgruszka
Copy link
Member Author

It is worth mentioning that currently, copies are saved in the browser's local storage and are not tied to a specific user's login session. We could improve this by linking the copies to a user's ID, so that only their copies are loaded. However, all user records will still be visible in the browser's local storage because it's tied to the website's domain.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
builder-grapesjs Anything related to the GrapesJS email or landing page builders code-review-needed PR's that require a code review before merging email Anything related to email feature A new feature for inclusion in minor or major releases landing-pages Anything related to landing pages user-testing-passed PRs which have been successfully tested by the required number of people.
Projects
Status: 🧑🏻‍💻 Needs a code review
Development

Successfully merging this pull request may close these issues.

None yet

5 participants