Initial commit: CloudOps infrastructure platform
This commit is contained in:
101
docker-compose/mautic-setup/mautic-backup-files/docroot/plugins/GrapesJsBuilderBundle/.github/CODE_OF_CONDUCT.md
vendored
Executable file
101
docker-compose/mautic-setup/mautic-backup-files/docroot/plugins/GrapesJsBuilderBundle/.github/CODE_OF_CONDUCT.md
vendored
Executable file
@@ -0,0 +1,101 @@
|
||||
The primary source of the Code of Conduct is at [mautic.org](https://www.mautic.org/code-of-conduct/) - it is reproduced here for reference.
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
A primary goal of the Mautic community is to support you and your business in the development, use and implementation of Mautic. It’s to be inclusive and add value to the largest number of participants, with the most varied and diverse backgrounds possible. As such, we are committed to providing a friendly, safe and welcoming environment for all.
|
||||
|
||||
This code of conduct outlines our expectations for all those who participate in our community, whether in-person or online, as well as the consequences for unacceptable behavior.
|
||||
|
||||
Your participation is contingent upon following these guidelines in all Mautic activities, including but not limited to:
|
||||
|
||||
* Using Mautic community resources.
|
||||
* Working with other Mauticians and other Mautic community participants whether virtually or co-located.
|
||||
* Representing Mautic at public events.
|
||||
* Representing Mautic in social media (official accounts, personal accounts, Facebook pages and groups).
|
||||
* Participating in Mautic sprints and training events.
|
||||
* Participating in Mautic-related forums, mailing lists, wikis, websites, chat channels, bugs, group or person-to-person meetings, and Mautic-related correspondence.
|
||||
|
||||
We invite all those who participate in Mautic activities online to help us create safe and positive experiences for everyone, everywhere.
|
||||
|
||||
|
||||
## 2. Open Source & Culture Citizenship
|
||||
|
||||
A supplemental goal of this Code of Conduct is to increase open source and culture citizenship by encouraging participants to recognize and strengthen the relationships between our actions and their effects on our community.
|
||||
|
||||
Communities mirror the societies in which they exist and positive action is essential to counteract the many forms of inequality and abuses of power that exist in society.
|
||||
|
||||
If you see someone who is making an extra effort to ensure our community is welcoming, friendly, and encourages all participants to contribute to the fullest extent, please recognize their efforts.
|
||||
|
||||
## 3. Welcoming to all
|
||||
We are committed to providing a friendly, safe and welcoming environment for all, regardless of level of experience or job role, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, national origin, citizenship and immigration status, neurodiversity, mental health or socio-economic status.
|
||||
|
||||
|
||||
## 4. Expected Behavior
|
||||
|
||||
The following behaviors are expected and requested of all community members:
|
||||
|
||||
* Participate in an authentic and active way. In doing so, you contribute to the health and longevity of this community.
|
||||
* Exercise consideration and respect in your speech and actions.
|
||||
* Attempt collaboration before conflict.
|
||||
* Guide conversations toward issue resolution.
|
||||
* Refrain from demeaning, discriminatory, or harassing behavior and speech.
|
||||
|
||||
Alert Mautic team members if you notice a dangerous situation, someone in distress, or violations of this Code of Conduct, even if they seem inconsequential.
|
||||
|
||||
## 5. Unacceptable Behavior
|
||||
|
||||
The following behaviors are considered harassment and are unacceptable within our community:
|
||||
|
||||
* **Violence and Threats of Violence** are not acceptable - online or offline. This includes incitement of violence toward any individual, including encouraging a person to commit self-harm. This also includes posting or threatening to post other people’s personally identifying information (“doxxing”) online.
|
||||
* **Public or private harassment** is never acceptable in any form.
|
||||
* **Personal Attacks** Conflicts will inevitably arise, but frustration should never turn into a personal attack. It is not okay to insult, demean or belittle others. Attacking someone for their opinions, beliefs and ideas is not acceptable. It is important to speak directly when we disagree and when we think we need to improve, but such discussions must be conducted respectfully and professionally, remaining focused on the issue at hand.
|
||||
* **Derogatory Language** Hurtful or harmful language is never acceptable in any context related to: background, family status, gender, gender identity or expression, marital status, sex, sexual orientation, personal appearance, body size, native language, age, ability, neurodiversity, mental health, race and/or ethnicity, national origin, citizenship and immigration status, socioeconomic status, religion, geographic location.
|
||||
* **Unwelcome Sexual Attention or Physical Contact** Unwelcome sexual attention or unwelcome physical contact is not acceptable. This includes sexualized comments, jokes or imagery in interactions, communications or presentation materials, as well as inappropriate touching, groping, or sexual advances. This includes touching a person without permission, including sensitive areas such as their hair, pregnant stomach, mobility device (wheelchair, scooter, etc) or tattoos. This also includes physically blocking or intimidating another person. Physical contact or simulated physical contact (such as emojis like “kiss”) without affirmative consent is not acceptable. This includes sharing or distribution of sexualized images or text.
|
||||
* **Disruptive Behavior** Sustained disruption of events, forums, or meetings, including talks and presentations, will not be tolerated. This includes spamming community discussions with the solicitation of unwanted products or services.
|
||||
* **Influencing Disruptive Behavior** We will treat influencing or leading such activities the same way we treat the activities themselves, and thus the same consequences apply.
|
||||
|
||||
## 6. Consequences of Unacceptable Behavior
|
||||
|
||||
Unacceptable behavior from any community member, including sponsors and those with decision-making authority, will not be tolerated.
|
||||
|
||||
Anyone asked to stop unacceptable behavior is expected to comply immediately.
|
||||
|
||||
If a community member engages in unacceptable behavior, we may take any action deemed appropriate, up to and including a temporary ban or permanent expulsion from the community without warning. Examples of sanctions which may be applied include but is not limited to:
|
||||
* Verbal warnings.
|
||||
* Written warnings.
|
||||
* Temporary absence from participation.
|
||||
* Long-term absence from participation.
|
||||
* Being required to follow a conduct agreement that dictates the process of returning to the community.
|
||||
|
||||
|
||||
## 7. Reporting Guidelines
|
||||
If you are subject to or witness unacceptable behavior, or have any other concerns, please notify us as soon as possible by emailing info@mautic.org, or contacting a Mautic team member on the specific platform.
|
||||
|
||||
Processes for dealing with breaches of the Code of Conduct can be found [here][coc-breaches].
|
||||
|
||||
## 8. Addressing Grievances
|
||||
Only permanent resolutions (such as bans) may be appealed. To appeal a decision, contact the Mautic team at info@mautic.org with your appeal and the team will review the situation.
|
||||
|
||||
## 9. Scope
|
||||
We expect all community participants (contributors, moderators and other guests) to abide by this Code of Conduct in all community venues–online and in-person–as well as in all one-on-one communications pertaining to community affairs.
|
||||
|
||||
While this code of conduct is specifically aimed at Mautic’s official resources and community, we recognize that it is possible for actions taken outside of Mautic’s official online or in person spaces to have a deep impact on community health.
|
||||
|
||||
Resources or incidents which break this code of conduct for any reason in a non-Mautic community location will be considered in the same way as resources or incidents from owned channels, and subject to the same sanctions.
|
||||
|
||||
## 10. Contact info
|
||||
For more information, please contact info@mautic.org.
|
||||
|
||||
## 11. License and attribution
|
||||
This Code of Conduct is directly adapted from the Stumptown Syndicate and distributed under a [Creative Commons Attribution-ShareAlike license][cc-by-sa].
|
||||
|
||||
Additional text from [Mozilla Community Participation Guidelines][mozilla-guidelines] distributed under a [Creative Commons Attribution-ShareAlike license][cc-by-sa].
|
||||
|
||||
Reviewed and updated using the [Mozilla Code of Conduct Assessment Tool][mozilla-tool].
|
||||
|
||||
[coc-breaches]: </policies/code-of-conduct-breaches>
|
||||
[mozilla-guidelines]: <https://www.mozilla.org/en-US/about/governance/policies/participation/>
|
||||
[cc-by-sa]: <https://creativecommons.org/licenses/by-sa/3.0/>
|
||||
[mozilla-tool]:<https://mozilla.github.io/diversity-coc-review.io>
|
||||
|
||||
(Code of Conduct is subject to change without notice).
|
||||
66
docker-compose/mautic-setup/mautic-backup-files/docroot/plugins/GrapesJsBuilderBundle/.github/CONTRIBUTING.md
vendored
Executable file
66
docker-compose/mautic-setup/mautic-backup-files/docroot/plugins/GrapesJsBuilderBundle/.github/CONTRIBUTING.md
vendored
Executable file
@@ -0,0 +1,66 @@
|
||||
## Reporting Security Vulnerabilities
|
||||
|
||||
If you think that you have found a security vulnerability, please email security@mautic.com with as much detail as possible. The core team will review the vulnerability and if found applicable, will create the patch in a private repository. The vulnerability will be disclosed once the patch has been included into a release.
|
||||
|
||||
## Contributing Code
|
||||
|
||||
Development is open and available to any member of the Mautic community. All fixes and improvements are done through pull requests to the code. This code is open source and publicly available.
|
||||
|
||||
### Developer Documentation
|
||||
|
||||
Developer documentation is available at [https://devdocs.mautic.org](https://devdocs.mautic.org). To add additions or corrects to the documentation, submit Issues or Pull Requests against [https://github.com/mautic/developer-documentation-new](https://github.com/mautic/developer-documentation-new).
|
||||
|
||||
### Core Feature Development Procedures
|
||||
|
||||
Pull Requests with additional features should be created with the Mautic Core goals in consideration. Any features that are created for core that don’t follow the overall goals may not be included.
|
||||
|
||||
In addition to following the general direction of the development goals, the pull request code must be well-formed following coding standards and guidelines. If you wish to target a specific release version number for the feature, its best to make the pull request early so any feedback from the core team can be implemented and adequate testing can be performed.
|
||||
|
||||
Features that are determined not to fit within the direction of the Mautic Core goals are more than welcome to be created as plugins instead.
|
||||
|
||||
### Code Contribution Requirements
|
||||
|
||||
#### Code Standards
|
||||
|
||||
Mautic follows [Symfony's coding standards](http://symfony.com/doc/current/contributing/code/standards.html) by implementing pre-commit git hook running [php-cs-fixer](https://github.com/friendsofphp/php-cs-fixer), which is installed and updated with `composer install`/`composer update`.
|
||||
|
||||
All code styling is handled automatically by the aforementioned git hook. In case if you setup git hook correctly (which is true if you ever run `composer install`/`composer update` before creating a pull request), you can format your code as you like - it will be converted to Mautic code style automatically.
|
||||
|
||||
#### Automated Tests
|
||||
|
||||
All code contributions should include adequate and appropriate unit tests using [PHPUnit](https://phpunit.de/manual/5.7/en/index.html) and/or Symfony functional tests ([https://symfony.com/doc/2.8/testing.html](https://symfony.com/doc/2.8/testing.html)). Pull Requests without these tests will not be merged.
|
||||
|
||||
#### Pull Request Description
|
||||
|
||||
When creating a new Pull Request, the description template should be filled appropriately in detail. Any Pull Request that does not have an appropriate description will not be considered for merge.
|
||||
|
||||
#### Documentation
|
||||
|
||||
Each new feature should include a reference to a pull request in our [End User Documentation](https://github.com/mautic/documentation) repository or [Developer Documentation](https://github.com/mautic/developer-documentation) repository if applicable.
|
||||
|
||||
## Core Development Rules
|
||||
|
||||
Pull requests and code submissions are decided upon by the release leader and the core team. When a decision is not clearly evident then the following voting process will be implemented.
|
||||
|
||||
### Voting Policy
|
||||
|
||||
Votes are cast by all members of the core team. Votes can be changed at any time during the discussion. Positive votes require no explanation. A negative vote must be justified by technical or objective logic. A core team member cannot vote on any code they submit.
|
||||
|
||||
### Merging Policy
|
||||
|
||||
The voting process on any particular pull request must allow for enough time for review by the community and the core team. This involves a minimum of 2 days for minor modifications and minimum of 5 days for significant code changes. Minor changes involve typographical errors, documentation, code standards, minor CSS, javascript, and HTML modifications. Minor modifications do not require a voting process. All other submissions require a vote after the minimum code review period and must be approved by two or more core members (with no core members voting against).
|
||||
|
||||
### Core Membership Application
|
||||
|
||||
Core team members are based on a form of meritocracy. We actively seek to empower our active community members and those demonstrating increased involvement will be given everything needed for their continued success.
|
||||
|
||||
### Core Membership Revocation
|
||||
|
||||
A Mautic Core membership can be revoked for any of the following reasons:
|
||||
|
||||
- Refusal to follow the rules and policies listed herein
|
||||
- Lack of activity for the previous 6 months
|
||||
- Willful negligence or intent to harm the Mautic project
|
||||
- Upon decision of the project leader
|
||||
|
||||
Revoked members may re-apply for core membership following a 12 month period.
|
||||
2
docker-compose/mautic-setup/mautic-backup-files/docroot/plugins/GrapesJsBuilderBundle/.github/FUNDING.yml
vendored
Executable file
2
docker-compose/mautic-setup/mautic-backup-files/docroot/plugins/GrapesJsBuilderBundle/.github/FUNDING.yml
vendored
Executable file
@@ -0,0 +1,2 @@
|
||||
github: mautic
|
||||
open_collective: mautic
|
||||
@@ -0,0 +1,26 @@
|
||||
# Workflow name:
|
||||
name: Close Pull Requests
|
||||
|
||||
# Workflow triggers:
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
# Workflow jobs:
|
||||
jobs:
|
||||
run:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: superbrothers/close-pull-request@v3
|
||||
with:
|
||||
comment: |
|
||||
Thank you for submitting a pull request. :raised_hands:
|
||||
|
||||
We greatly appreciate your willingness to submit a contribution. However, we are not accepting pull requests against this repository, as all development happens on the [main project repository](https://github.com/mautic/mautic).
|
||||
|
||||
We kindly request that you submit this pull request against the [respective directory](https://github.com/mautic/mautic/blob/head/plugins/GrapesJsBuilderBundle) of the main repository where we'll review and provide feedback. If this is your first Mautic contribution, be sure to read the [contributing guide](https://github.com/mautic/mautic/blob/4.x/.github/CONTRIBUTING.md) which provides guidelines and instructions for submitting contributions.
|
||||
|
||||
Thank you again, and we look forward to receiving your contribution! :smiley:
|
||||
|
||||
Best,
|
||||
The Mautic team
|
||||
3
docker-compose/mautic-setup/mautic-backup-files/docroot/plugins/GrapesJsBuilderBundle/.gitignore
vendored
Executable file
3
docker-compose/mautic-setup/mautic-backup-files/docroot/plugins/GrapesJsBuilderBundle/.gitignore
vendored
Executable file
@@ -0,0 +1,3 @@
|
||||
vendor/*
|
||||
node_modules/*
|
||||
!.npmrc
|
||||
@@ -0,0 +1,2 @@
|
||||
# .npmrc
|
||||
engine-strict=true
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 7.0 KiB |
@@ -0,0 +1 @@
|
||||
.code-panel{text-align:left;font-size:1rem;height:100%;display:flex;flex-direction:column}.code-panel section{flex:1}.code-panel section .codepanel-separator{display:flex;justify-content:space-between;padding-left:.6rem;padding-right:.6rem}.code-panel section .codepanel-label{line-height:20px;font-size:13px;color:#aaa;user-select:none;text-transform:uppercase}.code-panel section button{background-color:#d6d6d6}.gutter{cursor:ns-resize;position:relative;background-color:rgba(0,0,0,0.2)}.gutter:after{content:'';display:block;height:8px;width:100%;position:absolute;top:-3px;z-index:150}.code-panel .CodeMirror{height:calc(100% - 20px)}.gjs-pn-views{border-left:1px solid rgba(0,0,0,0.2);border-bottom:0}.gjs-pn-views-container{box-shadow:initial;top:40px;padding-top:0;height:calc(100% - 40px)}.gjs-pn-views-container,.gjs-cv-canvas{transition:width .3s ease-in-out}
|
||||
@@ -0,0 +1,85 @@
|
||||
export default class AssetService {
|
||||
constructor() {
|
||||
this.uploadPath = '/s/grapesjsbuilder/upload';
|
||||
this.deletePath = '/s/grapesjsbuilder/delete';
|
||||
this.assetsPath = '/s/grapesjsbuilder/media';
|
||||
this.assetsPage = 1;
|
||||
this.totalPages = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path for uploading assets
|
||||
* @returns {string} The upload path
|
||||
*/
|
||||
getUploadPath() {
|
||||
return this.uploadPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path for deleting assets
|
||||
* @returns {string} The delete path
|
||||
*/
|
||||
getDeletePath() {
|
||||
return this.deletePath;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get assets path with current page
|
||||
* @returns {string}
|
||||
*/
|
||||
getAssetsPath() {
|
||||
return `${this.assetsPath}?page=${this.assetsPage}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch assets from the server
|
||||
* @param {string} assetsPath
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async fetchAssets(assetsPath) {
|
||||
try {
|
||||
const response = await fetch(assetsPath);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
const result = await response.json();
|
||||
if (result.totalPages !== undefined) {
|
||||
this.totalPages = result.totalPages;
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error fetching assets:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get assets for the first page
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async getAssetsXhr() {
|
||||
this.assetsPage = 1;
|
||||
return this.fetchAssets(this.getAssetsPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get assets for the next page
|
||||
* @returns {Promise<Object>|null}
|
||||
*/
|
||||
async getAssetsNextPageXhr() {
|
||||
if (this.hasLoadedAllAssets()) {
|
||||
return null;
|
||||
}
|
||||
this.assetsPage++;
|
||||
return this.fetchAssets(this.getAssetsPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if all assets have been loaded
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasLoadedAllAssets() {
|
||||
return this.totalPages !== null && this.assetsPage >= this.totalPages;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
import AssetService from './asset.service';
|
||||
import BuilderService from './builder.service';
|
||||
|
||||
// all css get combined into one builder.css and automatically loaded via js/parcel
|
||||
import 'grapesjs/dist/css/grapes.min.css';
|
||||
import './grapesjs-custom.css';
|
||||
|
||||
/**
|
||||
* Launch builder
|
||||
*
|
||||
* @param formName
|
||||
*/
|
||||
function launchBuilderGrapesjs(formName) {
|
||||
if (useBuilderForCodeMode() === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
Mautic.showChangeThemeWarning = true;
|
||||
|
||||
// Prepare HTML
|
||||
mQuery('html').css('font-size', '100%');
|
||||
mQuery('body').css('overflow-y', 'hidden');
|
||||
mQuery('.builder-panel').css('padding', 0);
|
||||
mQuery('.builder-panel').css('display', 'block');
|
||||
const $builder = mQuery('.builder');
|
||||
$builder.addClass('builder-active').removeClass('hide');
|
||||
|
||||
const assetService = new AssetService();
|
||||
const builder = new BuilderService(assetService);
|
||||
// Initialize GrapesJS
|
||||
builder.initGrapesJS(formName);
|
||||
|
||||
// trigger show event on DOM element
|
||||
$builder.trigger('builder:show', [builder.editor])
|
||||
// trigger show event on editor instance
|
||||
builder.editor.trigger('show');
|
||||
|
||||
// Load and add assets
|
||||
(async () => {
|
||||
try {
|
||||
const result = await assetService.getAssetsXhr();
|
||||
builder.editor.AssetManager.add(result.data);
|
||||
} catch (error) {
|
||||
console.error('Error loading initial assets:', error);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* The user acknowledges the risk before editing an email or landing page created in Code Mode in the Builder
|
||||
*/
|
||||
function useBuilderForCodeMode() {
|
||||
const theme = mQuery('.theme-selected').find('[data-theme]').attr('data-theme');
|
||||
const isCodeMode = theme === 'mautic_code_mode';
|
||||
|
||||
if (isCodeMode) {
|
||||
if (confirm(Mautic.translate('grapesjsbuilder.builder.warning.code_mode')) === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set theme's HTML
|
||||
*
|
||||
* @param theme
|
||||
*/
|
||||
function setThemeHtml(theme) {
|
||||
BuilderService.setupButtonLoadingIndicator(true);
|
||||
// Load template and fill field
|
||||
mQuery.ajax({
|
||||
url: mQuery('#builder_url').val(),
|
||||
data: `template=${theme}`,
|
||||
dataType: 'json',
|
||||
success(response) {
|
||||
const textareaHtml = mQuery('textarea.builder-html');
|
||||
const textareaMjml = mQuery('textarea.builder-mjml');
|
||||
|
||||
textareaHtml.val(response.templateHtml);
|
||||
|
||||
if (typeof textareaMjml !== 'undefined') {
|
||||
textareaMjml.val(response.templateMjml);
|
||||
}
|
||||
|
||||
// If MJML template, generate HTML before save
|
||||
if (!textareaHtml.val().length && textareaMjml.val().length) {
|
||||
const assetService = new AssetService();
|
||||
const builder = new BuilderService(assetService);
|
||||
|
||||
textareaHtml.val(builder.mjmlToHtml(response.templateMjml));
|
||||
}
|
||||
},
|
||||
error(request, textStatus) {
|
||||
console.log(`setThemeHtml - Request failed: ${textStatus}`);
|
||||
},
|
||||
complete() {
|
||||
BuilderService.setupButtonLoadingIndicator(false);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The builder button to launch GrapesJS will be disabled when the code mode theme is selected
|
||||
*
|
||||
* @param theme
|
||||
*/
|
||||
function switchBuilderButton(theme) {
|
||||
const builderButton = mQuery('.btn-builder');
|
||||
const mEmailBuilderButton = mQuery('#emailform_buttons_builder_toolbar_mobile');
|
||||
const mPageBuilderButton = mQuery('#page_buttons_builder_toolbar_mobile');
|
||||
const isCodeMode = theme === 'mautic_code_mode';
|
||||
|
||||
builderButton.attr('disabled', isCodeMode);
|
||||
|
||||
if (isCodeMode) {
|
||||
mPageBuilderButton.addClass('link-is-disabled');
|
||||
mEmailBuilderButton.addClass('link-is-disabled');
|
||||
|
||||
mPageBuilderButton.parent().addClass('is-not-allowed');
|
||||
mEmailBuilderButton.parent().addClass('is-not-allowed');
|
||||
} else {
|
||||
mPageBuilderButton.removeClass('link-is-disabled');
|
||||
mEmailBuilderButton.removeClass('link-is-disabled');
|
||||
|
||||
mPageBuilderButton.parent().removeClass('is-not-allowed');
|
||||
mEmailBuilderButton.parent().removeClass('is-not-allowed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The textarea with the HTML source will be displayed if the code mode theme is selected
|
||||
*
|
||||
* @param theme
|
||||
*/
|
||||
function switchCustomHtml(theme) {
|
||||
const customHtmlRow = mQuery('#custom-html-row');
|
||||
const isPageMode = mQuery('[name="page"]').length !== 0;
|
||||
const isCodeMode = theme === 'mautic_code_mode';
|
||||
const advancedTab = isPageMode ? mQuery('#advanced-tab') : null;
|
||||
|
||||
if (isCodeMode === true) {
|
||||
customHtmlRow.removeClass('hidden');
|
||||
isPageMode && advancedTab.removeClass('hidden');
|
||||
} else {
|
||||
customHtmlRow.addClass('hidden');
|
||||
isPageMode && advancedTab.addClass('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize original Mautic theme selection with grapejs specific modifications
|
||||
*/
|
||||
function initSelectThemeGrapesjs(parentInitSelectTheme) {
|
||||
function childInitSelectTheme(themeField) {
|
||||
const builderUrl = mQuery('#builder_url');
|
||||
let url;
|
||||
|
||||
switchBuilderButton(themeField.val());
|
||||
switchCustomHtml(themeField.val());
|
||||
|
||||
// Replace Mautic URL by plugin URL
|
||||
if (builderUrl.length) {
|
||||
if (builderUrl.val().indexOf('pages') !== -1) {
|
||||
url = builderUrl.val().replace('s/pages/builder', 's/grapesjsbuilder/page');
|
||||
} else {
|
||||
url = builderUrl.val().replace('s/emails/builder', 's/grapesjsbuilder/email');
|
||||
}
|
||||
|
||||
builderUrl.val(url);
|
||||
}
|
||||
|
||||
// Launch original Mautic.initSelectTheme function
|
||||
parentInitSelectTheme(themeField);
|
||||
|
||||
mQuery('[data-theme]').click((event) => {
|
||||
const target = mQuery(event.target);
|
||||
const theme = target.closest('[data-theme]').attr('data-theme');
|
||||
|
||||
switchBuilderButton(theme);
|
||||
switchCustomHtml(theme);
|
||||
});
|
||||
}
|
||||
return childInitSelectTheme;
|
||||
}
|
||||
|
||||
Mautic.launchBuilder = launchBuilderGrapesjs;
|
||||
Mautic.initSelectTheme = initSelectThemeGrapesjs(Mautic.initSelectTheme);
|
||||
Mautic.setThemeHtml = setThemeHtml;
|
||||
@@ -0,0 +1,550 @@
|
||||
import grapesjs from 'grapesjs';
|
||||
import grapesjsmjml from 'grapesjs-mjml';
|
||||
import grapesjsnewsletter from 'grapesjs-preset-newsletter';
|
||||
import grapesjswebpage from 'grapesjs-preset-webpage';
|
||||
import grapesjsblocksbasic from 'grapesjs-blocks-basic';
|
||||
import grapesjscomponentcountdown from 'grapesjs-component-countdown';
|
||||
import grapesjsnavbar from 'grapesjs-navbar';
|
||||
import grapesjscustomcode from 'grapesjs-custom-code';
|
||||
import grapesjstouch from 'grapesjs-touch';
|
||||
import grapesjstuiimageeditor from 'grapesjs-tui-image-editor';
|
||||
import grapesjsstylebg from 'grapesjs-style-bg';
|
||||
import grapesjspostcss from 'grapesjs-parser-postcss';
|
||||
import grapesjsckeditor from './plugins/grapesjs.ckeditor';
|
||||
import contentService from 'grapesjs-preset-mautic/dist/content.service';
|
||||
import grapesjsmautic from 'grapesjs-preset-mautic';
|
||||
import editorFontsService from 'grapesjs-preset-mautic/dist/editorFonts/editorFonts.service';
|
||||
import StorageService from './storage.service';
|
||||
|
||||
// for local dev
|
||||
// import contentService from '../../../../../../grapesjs-preset-mautic/src/content.service';
|
||||
// import grapesjsmautic from '../../../../../../grapesjs-preset-mautic/src';
|
||||
|
||||
import CodeModeButton from './codeMode/codeMode.button';
|
||||
import CompCopyPaste from './commands/compCopyPaste';
|
||||
import MjmlService from 'grapesjs-preset-mautic/dist/mjml/mjml.service';
|
||||
|
||||
export default class BuilderService {
|
||||
editor;
|
||||
|
||||
storageService;
|
||||
|
||||
assetService;
|
||||
|
||||
/**
|
||||
* @param {AssetService} assetService
|
||||
*/
|
||||
constructor(assetService) {
|
||||
this.assetService = assetService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize GrapesJsBuilder
|
||||
*
|
||||
* @param object
|
||||
*/
|
||||
setListeners() {
|
||||
if (!this.editor) {
|
||||
throw Error('No editor found');
|
||||
}
|
||||
|
||||
// Why would we not want to keep the history?
|
||||
//
|
||||
// this.editor.on('load', () => {
|
||||
// const um = this.editor.UndoManager;
|
||||
// // Clear stack of undo/redo
|
||||
// um.clear();
|
||||
// });
|
||||
|
||||
const keymaps = this.editor.Keymaps;
|
||||
let allKeymaps;
|
||||
|
||||
if (mauticEditorFonts) {
|
||||
this.editor.on('load', () => editorFontsService.loadEditorFonts(this.editor));
|
||||
}
|
||||
|
||||
this.editor.on('modal:open', () => {
|
||||
// Save all keyboard shortcuts
|
||||
allKeymaps = { ...keymaps.getAll() };
|
||||
|
||||
// Remove keyboard shortcuts to prevent launch behind popup
|
||||
keymaps.removeAll();
|
||||
});
|
||||
|
||||
this.editor.on('modal:close', () => {
|
||||
// ReMap keyboard shortcuts on modal close
|
||||
Object.keys(allKeymaps).map((objectKey) => {
|
||||
const shortcut = allKeymaps[objectKey];
|
||||
|
||||
keymaps.add(shortcut.id, shortcut.keys, shortcut.handler);
|
||||
return keymaps;
|
||||
});
|
||||
});
|
||||
|
||||
this.editor.on('asset:remove', (response) => {
|
||||
// Delete file on server
|
||||
mQuery.ajax({
|
||||
url: this.assetService.getDeletePath(),
|
||||
data: { filename: response.getFilename() },
|
||||
});
|
||||
});
|
||||
|
||||
this.editor.on('asset:upload:error', (error) => {
|
||||
Mautic.setFlashes(Mautic.addErrorFlashMessage(error));
|
||||
});
|
||||
|
||||
this.editor.on('asset:open', () => {
|
||||
const editor = this.editor;
|
||||
const assetsService = this.assetService;
|
||||
const assetsContainer = document.querySelector('.gjs-am-assets');
|
||||
const $assetsSpinner = document.createElement('div');
|
||||
$assetsSpinner.className = 'gjs-assets-spinner';
|
||||
$assetsSpinner.innerHTML = '<i class="ri-loader-3-line ri-spin"></i>';
|
||||
|
||||
if (assetsContainer) {
|
||||
let isLoading = false;
|
||||
|
||||
const loadNextPage = async () => {
|
||||
if (isLoading) return;
|
||||
isLoading = true;
|
||||
assetsContainer.appendChild($assetsSpinner);
|
||||
|
||||
try {
|
||||
const result = await assetsService.getAssetsNextPageXhr();
|
||||
if (result) {
|
||||
const assetManager = editor.AssetManager;
|
||||
const currentAssets = assetManager.getAll().models;
|
||||
const newAssets = result.data;
|
||||
|
||||
// Combine current assets with new assets
|
||||
const combinedAssets = [...currentAssets, ...newAssets];
|
||||
|
||||
// Reset the entire collection with combined assets
|
||||
assetManager.getAll().reset(combinedAssets);
|
||||
assetManager.render();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading next page of assets:', error);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
};
|
||||
|
||||
assetsContainer.addEventListener('scroll', function() {
|
||||
const hasScrolledToBottom = this.scrollTop + this.clientHeight >= this.scrollHeight - 5;
|
||||
if (hasScrolledToBottom && !assetsService.hasLoadedAllAssets()) {
|
||||
loadNextPage();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.warn('Element with class "gjs-am-assets" not found');
|
||||
}
|
||||
});
|
||||
|
||||
const triggerBuilderHide = () => {
|
||||
// trigger hide event on DOM element
|
||||
mQuery('.builder').trigger('builder:hide', [this.editor]);
|
||||
// trigger hide event on editor instance
|
||||
this.editor.trigger('hide');
|
||||
};
|
||||
this.editor.on('run:mautic-editor-page-html-close', triggerBuilderHide);
|
||||
this.editor.on('run:mautic-editor-email-html-close', triggerBuilderHide);
|
||||
this.editor.on('run:mautic-editor-email-mjml-close', triggerBuilderHide);
|
||||
|
||||
// add offset to flashes container for better UI visibility when builder is on
|
||||
this.editor.on('show', () => mQuery('#flashes').addClass('alert-offset'));
|
||||
this.editor.on('hide', () => mQuery('#flashes').removeClass('alert-offset'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the grapesjs build in the
|
||||
* correct mode
|
||||
*/
|
||||
initGrapesJS(object) {
|
||||
// grapesjs-custom-plugins: add globally defined mautic-grapesjs-plugins using name as pluginId for the plugin-function
|
||||
if (window.MauticGrapesJsPlugins) {
|
||||
window.MauticGrapesJsPlugins.forEach((item) => {
|
||||
if (!item.name) {
|
||||
console.warn('A name is required for Mautic-GrapesJs plugins in window.MauticGrapesJsPlugins. Registration skipped!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof item.plugin !== 'function') {
|
||||
console.warn('The Mautic-GrapesJs plugin must be a function in window.MauticGrapesJsPlugins. Registration skipped!');
|
||||
return;
|
||||
}
|
||||
|
||||
grapesjs.plugins.add(item.name, item.plugin);
|
||||
});
|
||||
}
|
||||
|
||||
// disable mautic global shortcuts
|
||||
Mousetrap.reset();
|
||||
if (object === 'page') {
|
||||
this.editor = this.initPage();
|
||||
} else if (object === 'emailform') {
|
||||
if (MjmlService.getOriginalContentMjml()) {
|
||||
this.editor = this.initEmailMjml();
|
||||
} else {
|
||||
this.editor = this.initEmailHtml();
|
||||
}
|
||||
} else {
|
||||
throw Error(`Not supported builder type: ${object}`);
|
||||
}
|
||||
|
||||
// add code mode button
|
||||
// @todo: only show button if configured: sourceEdit: 1,
|
||||
const codeModeButton = new CodeModeButton(this.editor);
|
||||
codeModeButton.addCommand();
|
||||
codeModeButton.addButton();
|
||||
|
||||
/**
|
||||
* Add command that will allow users
|
||||
* to copy paste component across tabs
|
||||
*/
|
||||
new CompCopyPaste(this.editor).addCommand();
|
||||
|
||||
this.storageService = new StorageService(this.editor, object);
|
||||
this.setListeners();
|
||||
}
|
||||
|
||||
static getMauticConf(mode) {
|
||||
return {
|
||||
mode,
|
||||
};
|
||||
}
|
||||
|
||||
static getCkeConf(tokenCallback) {
|
||||
const ckEditorToolbarOptions = ['undo', 'redo', '|', 'bold','italic', 'underline','strikethrough', '|', 'fontSize','fontFamily','fontColor','fontBackgroundColor', '|' ,'alignment','outdent', 'indent', '|', 'blockQuote', 'insertTable', '|', 'bulletedList','numberedList', '|', 'link', '|', 'TokenPlugin'];
|
||||
return Mautic.GetCkEditorConfigOptions(ckEditorToolbarOptions, tokenCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the builder in the landingapge mode
|
||||
*/
|
||||
initPage() {
|
||||
// Launch GrapesJS with body part
|
||||
this.editor = grapesjs.init({
|
||||
clearOnRender: true,
|
||||
container: '.builder-panel',
|
||||
components: contentService.getOriginalContentHtml().body.innerHTML,
|
||||
height: '100%',
|
||||
canvas: {
|
||||
styles: contentService.getStyles(),
|
||||
},
|
||||
storageManager: false, // https://grapesjs.com/docs/modules/Storage.html#basic-configuration
|
||||
assetManager: this.getAssetManagerConf(),
|
||||
styleManager: {
|
||||
clearProperties: true, // Temp fix https://github.com/artf/grapesjs-preset-webpage/issues/27
|
||||
},
|
||||
plugins: [
|
||||
// partially copied from: https://github.com/GrapesJS/grapesjs/blob/gh-pages/demo.html
|
||||
grapesjswebpage,
|
||||
grapesjspostcss,
|
||||
grapesjsmautic,
|
||||
grapesjsckeditor,
|
||||
grapesjsblocksbasic,
|
||||
grapesjscomponentcountdown,
|
||||
grapesjsnavbar,
|
||||
grapesjscustomcode,
|
||||
grapesjstouch,
|
||||
grapesjspostcss,
|
||||
grapesjstuiimageeditor,
|
||||
grapesjsstylebg,
|
||||
...BuilderService.getPluginNames('page'), // grapesjs-custom-plugins: load custom plugins by their name
|
||||
],
|
||||
pluginsOpts: {
|
||||
[grapesjswebpage]: {
|
||||
formsOpts: false,
|
||||
useCustomTheme: false,
|
||||
},
|
||||
grapesjsmautic: BuilderService.getMauticConf('page-html'),
|
||||
[grapesjsckeditor]: BuilderService.getCkeConf('page:getBuilderTokens'),
|
||||
...BuilderService.getPluginOptions('page'), // grapesjs-custom-plugins: add the plugin-options
|
||||
},
|
||||
});
|
||||
|
||||
this.moveBlocksPage();
|
||||
return this.editor;
|
||||
}
|
||||
|
||||
mjmlToHtml(mjml) {
|
||||
const converted = MjmlService.mjmlToHtml(mjml);
|
||||
|
||||
if (0 === converted.errors.length) {
|
||||
return converted.html;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
initEmailMjml() {
|
||||
const components = MjmlService.getOriginalContentMjml();
|
||||
// validate
|
||||
MjmlService.mjmlToHtml(components);
|
||||
|
||||
const styles = [
|
||||
`${mauticBaseUrl}plugins/GrapesJsBuilderBundle/Assets/library/js/grapesjs-editor.css`
|
||||
];
|
||||
|
||||
this.editor = grapesjs.init({
|
||||
selectorManager: {
|
||||
componentFirst: true,
|
||||
},
|
||||
avoidInlineStyle: false, // TEMP: fixes issue with disappearing inline styles
|
||||
forceClass: false, // create new styles if there are some already on the element: https://github.com/GrapesJS/grapesjs/issues/1531
|
||||
clearOnRender: true,
|
||||
container: '.builder-panel',
|
||||
height: '100%',
|
||||
canvas: {
|
||||
styles,
|
||||
},
|
||||
domComponents: {
|
||||
// disable all except link components
|
||||
disableTextInnerChilds: (child) => !child.is('link'), // https://github.com/GrapesJS/grapesjs/releases/tag/v0.21.2
|
||||
},
|
||||
storageManager: false,
|
||||
assetManager: this.getAssetManagerConf(),
|
||||
plugins: [grapesjsmjml, grapesjspostcss, grapesjsmautic, grapesjsckeditor, ...BuilderService.getPluginNames('email-mjml')],
|
||||
pluginsOpts: {
|
||||
[grapesjsmjml]: {
|
||||
hideSelector: false,
|
||||
custom: false,
|
||||
useCustomTheme: false,
|
||||
},
|
||||
grapesjsmautic: BuilderService.getMauticConf('email-mjml'),
|
||||
[grapesjsckeditor]: BuilderService.getCkeConf('email:getBuilderTokens'),
|
||||
...BuilderService.getPluginOptions('email-mjml'),
|
||||
},
|
||||
});
|
||||
|
||||
this.unsetComponentVoidTypes(this.editor);
|
||||
this.editor.setComponents(components);
|
||||
|
||||
// Reinitialize the content after parsing MJML.
|
||||
// This can be removed once the issue with self-closing tags is resolved in grapesjs-mjml.
|
||||
// See: https://github.com/GrapesJS/mjml/issues/149
|
||||
const parsedContent = MjmlService.getEditorMjmlContent(this.editor);
|
||||
this.editor.setComponents(parsedContent);
|
||||
|
||||
this.editor.BlockManager.get('mj-button').set({
|
||||
content: '<mj-button href="https://">Button</mj-button>',
|
||||
});
|
||||
|
||||
this.removeSelectedElementsEmailMjml();
|
||||
|
||||
return this.editor;
|
||||
}
|
||||
|
||||
unsetComponentVoidTypes(editor) {
|
||||
// Support for self-closing components is temporarily disabled due to parsing issues with mjml tags.
|
||||
// Browsers only recognize explicit self-closing tags like <img /> and <br />, leading to rendering problems.
|
||||
// This can be reverted once the issue with self-closing tags is resolved in grapesjs-mjml.
|
||||
// See: https://github.com/GrapesJS/mjml/issues/149
|
||||
const voidTypes = ['mj-image', 'mj-divider', 'mj-font', 'mj-spacer'];
|
||||
voidTypes.forEach(function(component) {
|
||||
editor.DomComponents.addType(component, {
|
||||
model: {
|
||||
defaults: {
|
||||
void: false
|
||||
},
|
||||
toHTML() {
|
||||
const tag = this.get('tagName');
|
||||
const attr = this.getAttrToHTML();
|
||||
const content = this.get('content');
|
||||
let strAttr = '';
|
||||
|
||||
for (let prop in attr) {
|
||||
const val = attr[prop];
|
||||
const hasValue = typeof val !== 'undefined' && val !== '';
|
||||
strAttr += hasValue ? ` ${prop}="${val}"` : '';
|
||||
}
|
||||
|
||||
let html = `<${tag}${strAttr}>${content}</${tag}>`;
|
||||
|
||||
// Add the components after the closing tag
|
||||
const componentsHtml = this.get('components')
|
||||
.map(model => model.toHTML())
|
||||
.join('');
|
||||
return html + componentsHtml;
|
||||
},
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initEmailHtml() {
|
||||
const components = contentService.getOriginalContentHtml().body.innerHTML;
|
||||
if (!components) {
|
||||
throw new Error('no components');
|
||||
}
|
||||
|
||||
const styles = [
|
||||
`${mauticBaseUrl}plugins/GrapesJsBuilderBundle/Assets/library/js/grapesjs-editor.css`
|
||||
];
|
||||
|
||||
// Launch GrapesJS with body part
|
||||
this.editor = grapesjs.init({
|
||||
clearOnRender: true,
|
||||
container: '.builder-panel',
|
||||
components,
|
||||
height: '100%',
|
||||
canvas: {
|
||||
styles,
|
||||
},
|
||||
storageManager: false,
|
||||
assetManager: this.getAssetManagerConf(),
|
||||
plugins: [grapesjsnewsletter, grapesjspostcss, grapesjsmautic, grapesjsckeditor, ...BuilderService.getPluginNames('email-html')],
|
||||
pluginsOpts: {
|
||||
grapesjsnewsletter: {
|
||||
useCustomTheme: false,
|
||||
},
|
||||
grapesjsmautic: BuilderService.getMauticConf('email-html'),
|
||||
[grapesjsckeditor]: BuilderService.getCkeConf('email:getBuilderTokens'),
|
||||
...BuilderService.getPluginOptions('email-html'),
|
||||
},
|
||||
});
|
||||
|
||||
// add a Mautic custom block Button
|
||||
this.editor.BlockManager.get('button').set({
|
||||
content:
|
||||
'<a href="#" target="_blank" style="display:inline-block;text-decoration:none;border-color:#4e5d9d;border-width: 10px 20px;border-style:solid; text-decoration: none; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; background-color: #4e5d9d; display: inline-block;font-size: 16px; color: #ffffff; ">\n' +
|
||||
'Button\n' +
|
||||
'</a>',
|
||||
});
|
||||
|
||||
return this.editor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the names of dynamically added plugins
|
||||
* @param context
|
||||
* @returns string[]
|
||||
*/
|
||||
static getPluginNames(context) {
|
||||
let plugins = [];
|
||||
|
||||
if (window.MauticGrapesJsPlugins) {
|
||||
window.MauticGrapesJsPlugins.forEach((item) => {
|
||||
if (item.name) {
|
||||
if (!item.context || !Array.isArray(item.context) || item.context.length === 0) {
|
||||
// if no context is given, the plugin is always added
|
||||
plugins.push(item.name);
|
||||
} else {
|
||||
// check if the plugin should be added for the current editor context
|
||||
item.context.forEach((pluginContext) => {
|
||||
if (pluginContext === context) {
|
||||
plugins.push(item.name);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return plugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the options of dynamically added plugins
|
||||
* @param context
|
||||
* @returns object[]
|
||||
*/
|
||||
static getPluginOptions(context) {
|
||||
let pluginOptions = {};
|
||||
|
||||
if (window.MauticGrapesJsPlugins) {
|
||||
window.MauticGrapesJsPlugins.forEach((item) => {
|
||||
if (!item.context || !Array.isArray(item.context) || item.context.length === 0) {
|
||||
// if no context is given, the plugin is always added
|
||||
pluginOptions[item.name] = item.pluginOptions ?? {};
|
||||
} else {
|
||||
// check if the plugin should be added for the current editor context
|
||||
item.context.forEach((pluginContext) => {
|
||||
if (pluginContext === context) {
|
||||
pluginOptions[item.name] = item.pluginOptions ?? {};
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return pluginOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manage button loading indicator
|
||||
*
|
||||
* @param activate - true or false
|
||||
*/
|
||||
static setupButtonLoadingIndicator(activate) {
|
||||
const builderButton = mQuery('.btn-builder');
|
||||
const saveButton = mQuery('.btn-save');
|
||||
const applyButton = mQuery('.btn-apply');
|
||||
|
||||
if (activate) {
|
||||
Mautic.activateButtonLoadingIndicator(builderButton);
|
||||
Mautic.activateButtonLoadingIndicator(saveButton);
|
||||
Mautic.activateButtonLoadingIndicator(applyButton);
|
||||
} else {
|
||||
Mautic.removeButtonLoadingIndicator(builderButton);
|
||||
Mautic.removeButtonLoadingIndicator(saveButton);
|
||||
Mautic.removeButtonLoadingIndicator(applyButton);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the Asset Manager for all modes
|
||||
* @link https://grapesjs.com/docs/modules/Assets.html#configuration
|
||||
*/
|
||||
getAssetManagerConf() {
|
||||
return {
|
||||
assets: [],
|
||||
noAssets: Mautic.translate('grapesjsbuilder.assetManager.noAssets'),
|
||||
upload: this.assetService.getUploadPath(),
|
||||
uploadName: 'files',
|
||||
multiUpload: 1,
|
||||
embedAsBase64: false,
|
||||
openAssetsOnDrop: 1,
|
||||
autoAdd: 1,
|
||||
headers: { 'X-CSRF-Token': mauticAjaxCsrf }, // global variable
|
||||
};
|
||||
}
|
||||
|
||||
getEditor() {
|
||||
return this.editor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the blocks and categories in the sidebar
|
||||
*/
|
||||
moveBlocksPage() {
|
||||
const blocks = this.editor.BlockManager.getAll();
|
||||
blocks.map(block => {
|
||||
// columns go into a new category, at the top
|
||||
if(block.attributes.id.indexOf('column') !== -1) {
|
||||
this.editor.BlockManager.get(block.attributes.id).set('category', {
|
||||
label:"Sections",
|
||||
order: -1
|
||||
});
|
||||
}
|
||||
// 'Blocks' category goes after 'Basic'
|
||||
if(block.attributes.category === 'Basic') {
|
||||
this.editor.BlockManager.get(block.attributes.id).set('category', {
|
||||
label:"Basic",
|
||||
order: -1
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeSelectedElementsEmailMjml() {
|
||||
|
||||
// Remove the RAW block (it's just not usable)
|
||||
const rawblock = this.editor.BlockManager.get('mj-raw');
|
||||
|
||||
if (rawblock !== null) {
|
||||
this.editor.BlockManager.remove(rawblock);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// import ContentService from '../../../../../../../grapesjs-preset-mautic/src/content.service';
|
||||
import MjmlService from 'grapesjs-preset-mautic/dist/mjml/mjml.service';
|
||||
import ContentService from 'grapesjs-preset-mautic/dist/content.service';
|
||||
|
||||
class CodeEditor {
|
||||
editor;
|
||||
|
||||
opts;
|
||||
|
||||
codeEditor;
|
||||
|
||||
codePopup;
|
||||
|
||||
constructor(editor, opts = {}) {
|
||||
this.editor = editor;
|
||||
this.opts = opts;
|
||||
|
||||
this.codeEditor = this.buildCodeEditor();
|
||||
this.codePopup = this.buildCodePopup();
|
||||
}
|
||||
|
||||
// Build codeEditor (CodeMirror instance)
|
||||
buildCodeEditor() {
|
||||
const codeEditor = this.editor.CodeManager.getViewer('CodeMirror').clone();
|
||||
|
||||
codeEditor.set({
|
||||
codeName: 'htmlmixed',
|
||||
readOnly: false,
|
||||
theme: 'hopscotch',
|
||||
autoBeautify: true,
|
||||
autoCloseTags: true,
|
||||
autoCloseBrackets: true,
|
||||
lineWrapping: true,
|
||||
styleActiveLine: true,
|
||||
smartIndent: true,
|
||||
indentWithTabs: true,
|
||||
});
|
||||
|
||||
return codeEditor;
|
||||
}
|
||||
|
||||
// Build popup content, codeEditor area and buttons
|
||||
buildCodePopup() {
|
||||
const cfg = this.editor.getConfig();
|
||||
|
||||
const codePopup = document.createElement('div');
|
||||
const btnEdit = document.createElement('button');
|
||||
const btnCancel = document.createElement('button');
|
||||
const textarea = document.createElement('textarea');
|
||||
|
||||
btnEdit.innerHTML = Mautic.translate('grapesjsbuilder.sourceEditBtnLabel');
|
||||
btnEdit.className = `${cfg.stylePrefix}btn-prim ${cfg.stylePrefix}btn-code-edit`;
|
||||
btnEdit.onclick = this.updateCode.bind(this);
|
||||
|
||||
btnCancel.innerHTML = Mautic.translate('grapesjsbuilder.sourceCancelBtnLabel');
|
||||
btnCancel.className = `${cfg.stylePrefix}btn-prim ${cfg.stylePrefix}btn-code-cancel`;
|
||||
btnCancel.onclick = this.cancelCode.bind(this);
|
||||
|
||||
codePopup.appendChild(textarea);
|
||||
codePopup.appendChild(btnEdit);
|
||||
codePopup.appendChild(btnCancel);
|
||||
|
||||
this.codeEditor.init(textarea);
|
||||
|
||||
return codePopup;
|
||||
}
|
||||
|
||||
// Load content and show popup
|
||||
showCodePopup(editor) {
|
||||
this.updateEditorContents();
|
||||
// this.codeEditor.editor.refresh();
|
||||
// editor.Modal.setContent('');
|
||||
editor.Modal.setContent(this.codePopup);
|
||||
editor.Modal.setTitle(Mautic.translate('grapesjsbuilder.sourceEditModalTitle'));
|
||||
editor.Modal.open();
|
||||
|
||||
editor.Modal.onceClose(() => editor.stopCommand('preset-mautic:code-edit'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the main editors canvas content with the
|
||||
* content from modals editor.
|
||||
* @todo show validation results in UI
|
||||
*/
|
||||
updateCode() {
|
||||
const code = this.codeEditor.editor.getValue();
|
||||
// validate MJML code
|
||||
if (ContentService.isMjmlMode(this.editor)) {
|
||||
MjmlService.mjmlToHtml(code);
|
||||
}
|
||||
|
||||
try {
|
||||
// delete canvas and set new content
|
||||
this.editor.DomComponents.getWrapper().set('content', '');
|
||||
this.editor.setComponents(code.trim())
|
||||
|
||||
// Reinitialize the content after parsing MJML.
|
||||
// This can be removed once the issue with self-closing tags is resolved in grapesjs-mjml.
|
||||
// See: https://github.com/GrapesJS/mjml/issues/149
|
||||
const parsedContent = MjmlService.getEditorMjmlContent(this.editor);
|
||||
this.editor.setComponents(parsedContent);
|
||||
|
||||
this.editor.Modal.close();
|
||||
} catch (e) {
|
||||
window.alert(`${Mautic.translate('grapesjsbuilder.sourceSyntaxError')} \n${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Close popup
|
||||
cancelCode() {
|
||||
this.editor.Modal.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the content to be edited in the popup editor
|
||||
*/
|
||||
updateEditorContents() {
|
||||
// Check if MJML plugin is on
|
||||
let content;
|
||||
if (ContentService.isMjmlMode(this.editor)) {
|
||||
content = MjmlService.getEditorMjmlContent(this.editor);
|
||||
} else {
|
||||
content = ContentService.getEditorHtmlContent(this.editor);
|
||||
}
|
||||
this.codeEditor.setContent(content);
|
||||
}
|
||||
}
|
||||
|
||||
export default CodeEditor;
|
||||
@@ -0,0 +1,35 @@
|
||||
import CodeModeCommand from './codeMode.command';
|
||||
|
||||
export default class CodeModeButton {
|
||||
editor;
|
||||
|
||||
/**
|
||||
* Add close button with save for Mautic
|
||||
*/
|
||||
constructor(editor) {
|
||||
if (!editor) {
|
||||
throw new Error('no editor');
|
||||
}
|
||||
this.editor = editor;
|
||||
}
|
||||
|
||||
addButton() {
|
||||
this.editor.Panels.addButton('options', [
|
||||
{
|
||||
id: 'code-edit',
|
||||
className: 'ri-edit-line',
|
||||
attributes: {
|
||||
title: Mautic.translate('grapesjsbuilder.sourceEditModalTitle'),
|
||||
},
|
||||
command: CodeModeCommand.name,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
addCommand() {
|
||||
this.editor.Commands.add(CodeModeCommand.name, {
|
||||
run: CodeModeCommand.launchCodeEditorModal,
|
||||
stop: CodeModeCommand.stopCodeEditorModal,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import CodeEditor from './codeEditor';
|
||||
|
||||
export default class CodeModeCommand {
|
||||
/**
|
||||
* The command to run on button click
|
||||
*/
|
||||
static name = 'preset-mautic:code-edit';
|
||||
|
||||
static codeEditor;
|
||||
|
||||
static launchCodeEditorModal(editor, sender, opts) {
|
||||
if (!editor) {
|
||||
throw new Error('no editor');
|
||||
}
|
||||
|
||||
CodeModeCommand.codeEditor = new CodeEditor(editor, opts);
|
||||
|
||||
if (sender) {
|
||||
sender.set('active', 0);
|
||||
}
|
||||
|
||||
CodeModeCommand.codeEditor.showCodePopup(editor);
|
||||
|
||||
// Transform DC Component to token
|
||||
editor.runCommand('preset-mautic:dynamic-content-components-to-tokens');
|
||||
}
|
||||
|
||||
static stopCodeEditorModal(editor) {
|
||||
if (!editor) {
|
||||
throw new Error('no editor');
|
||||
}
|
||||
// Transform Token to Components
|
||||
editor.runCommand('preset-mautic:update-dc-components-from-dc-store');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// copied from: https://github.com/GrapesJS/grapesjs/issues/1855
|
||||
|
||||
export default class CompCopyPaste {
|
||||
static storage_key = 'preset-mautic:grapesjs-clipboard';
|
||||
editor;
|
||||
|
||||
// COPY PASTE COMPONENTS/STYLE BETWEEN PAGES
|
||||
getStyles(components) {
|
||||
// recurse down through components and store styles in temp attribute
|
||||
components.forEach((component) => {
|
||||
const recurse = (comp) => {
|
||||
// if component has any styling
|
||||
if (Object.keys(comp.getStyle()).length !== 0) comp.attributes.savedStyle = comp.getStyle();
|
||||
if (comp.get('components').length) {
|
||||
comp.get('components').forEach((child) => {
|
||||
recurse(child);
|
||||
});
|
||||
}
|
||||
};
|
||||
recurse(component);
|
||||
});
|
||||
return components;
|
||||
}
|
||||
|
||||
setStyles(component) {
|
||||
// recurse down and re-apply style back to components
|
||||
const recurse = (comp) => {
|
||||
if ('savedStyle' in comp.attributes) {
|
||||
comp.setStyle(comp.attributes.savedStyle);
|
||||
delete comp.attributes.savedStyle;
|
||||
}
|
||||
if (comp.attributes.components.length) {
|
||||
comp.attributes.components.forEach((child) => {
|
||||
recurse(child);
|
||||
});
|
||||
}
|
||||
};
|
||||
recurse(component);
|
||||
}
|
||||
|
||||
newCopy(selected) {
|
||||
window.localStorage.setItem(this.storage_key, JSON.stringify(selected));
|
||||
}
|
||||
|
||||
newPaste(selected) {
|
||||
let components = JSON.parse(window.localStorage.getItem(this.storage_key));
|
||||
if (components) {
|
||||
if (selected && selected.attributes.type !== 'wrapper') {
|
||||
const index = selected.index();
|
||||
// Invert the order so last item gets added first and gets pushed down as others get added.
|
||||
components.reverse();
|
||||
const currentSelection = selected.collection;
|
||||
components.forEach((comp) => {
|
||||
if (currentSelection) {
|
||||
const added = currentSelection.add(comp, { at: index + 1 });
|
||||
editor.trigger('component:paste', added);
|
||||
this.setStyles(added);
|
||||
}
|
||||
});
|
||||
selected.emitUpdate();
|
||||
} else {
|
||||
components = editor.addComponents(components);
|
||||
components.forEach((comp) => {
|
||||
this.setStyles(comp);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constructor(editor) {
|
||||
if (!editor) {
|
||||
throw new Error('no editor');
|
||||
}
|
||||
this.editor = editor;
|
||||
}
|
||||
|
||||
addCommand() {
|
||||
this.editor.Commands.add('core:copy', (ed) => {
|
||||
const selected = this.getStyles([...ed.getSelectedAll()]);
|
||||
let filteredSelected = selected.filter((item) => item.attributes.copyable == true);
|
||||
if (filteredSelected.length) {
|
||||
this.newCopy(filteredSelected);
|
||||
}
|
||||
});
|
||||
|
||||
this.editor.Commands.add('core:paste', (ed) => {
|
||||
const selected = ed.getSelected();
|
||||
this.newPaste(selected);
|
||||
});
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,375 @@
|
||||
body,
|
||||
html {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* GrapesJS temporary fix #2490 */
|
||||
.gjs-clm-tag-status,
|
||||
.gjs-clm-tag-close {
|
||||
}
|
||||
.gjs-clm-tags-btn {
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
/* Fix hidden scroll */
|
||||
.gjs-pn-views-container {
|
||||
height: auto;
|
||||
padding: 0 0 0;
|
||||
top: 40px;
|
||||
bottom: 0;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.gjs-btn-code-edit {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* GrapesJS FileManager custom CSS */
|
||||
.gjs-btn-code-cancel {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.gjs-am-file-uploader {
|
||||
width: 100%;
|
||||
float: none;
|
||||
}
|
||||
|
||||
.gjs-am-assets-cont {
|
||||
width: 100%;
|
||||
float: none;
|
||||
}
|
||||
|
||||
.gjs-am-assets {
|
||||
height: 280px;
|
||||
}
|
||||
|
||||
.gjs-am-asset {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.gjs-am-file-uploader > form #gjs-am-uploadFile {
|
||||
padding: 20px 10px;
|
||||
}
|
||||
|
||||
.gjs-am-file-uploader #gjs-am-title {
|
||||
padding: 20px 10px;
|
||||
}
|
||||
|
||||
.gjs-am-preview {
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.gjs-am-preview-cont {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.gjs-am-meta {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.gjs-pn-commands .gjs-pn-buttons {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* GrapesJS Colors / Theme */
|
||||
.gjs-clm-tags .gjs-sm-title,
|
||||
.gjs-sm-sector .gjs-sm-title {
|
||||
}
|
||||
|
||||
.gjs-block-category .gjs-title,
|
||||
.gjs-category-title,
|
||||
.gjs-clm-tags .gjs-sm-title,
|
||||
.gjs-layer-title,
|
||||
.gjs-sm-sector .gjs-sm-title {
|
||||
font-weight: lighter;
|
||||
background-color: rgba(0,0,0,.1);
|
||||
letter-spacing: 1px;
|
||||
padding: 9px 10px 9px 20px;
|
||||
border-bottom: 1px solid rgba(0,0,0,.25);
|
||||
text-align: left;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.gjs-sm-properties {
|
||||
font-size: .75rem;
|
||||
padding: 10px 5px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.gjs-clm-tags .gjs-clm-tag {
|
||||
border: 1px solid #707070;
|
||||
box-shadow: none;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.gjs-field {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.gjs-btnt.gjs-pn-active,
|
||||
.gjs-pn-btn.gjs-pn-active {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.gjs-import-label,
|
||||
.gjs-export-label {
|
||||
margin-bottom: 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.gjs-mdl-dialog .gjs-btn-import {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
border-radius: 3px;
|
||||
height: 450px;
|
||||
font-family: sans-serif, monospace;
|
||||
letter-spacing: 0.3px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* GrapesJS Extra */
|
||||
#gjs-pn-views-container.gjs-pn-panel {
|
||||
padding: 39px 0 0;
|
||||
}
|
||||
|
||||
#gjs-pn-views.gjs-pn-panel {
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#gjs-pn-views .gjs-pn-btn {
|
||||
margin: 0;
|
||||
height: 40px;
|
||||
padding: 10px;
|
||||
width: 25%;
|
||||
border-bottom: 2px solid rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
#gjs-pn-views .gjs-pn-active {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#gjs-pn-devices-c {
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
#gjs-pn-options {
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.gjs-sm-composite .gjs-sm-properties {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#gjs-sm-border-top-left-radius,
|
||||
#gjs-sm-border-top-right-radius,
|
||||
#gjs-sm-border-bottom-left-radius,
|
||||
#gjs-sm-border-bottom-right-radius,
|
||||
#gjs-sm-margin-top,
|
||||
#gjs-sm-margin-bottom,
|
||||
#gjs-sm-margin-right,
|
||||
#gjs-sm-margin-left,
|
||||
#gjs-sm-padding-top,
|
||||
#gjs-sm-padding-bottom,
|
||||
#gjs-sm-padding-right,
|
||||
#gjs-sm-padding-left {
|
||||
}
|
||||
|
||||
#gjs-sm-border-width,
|
||||
#gjs-sm-border-style,
|
||||
#gjs-sm-border-color {
|
||||
flex: 999 1 80px;
|
||||
}
|
||||
|
||||
#gjs-sm-margin-left,
|
||||
#gjs-sm-padding-left {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
#gjs-sm-margin-right,
|
||||
#gjs-sm-padding-right {
|
||||
order: 3;
|
||||
}
|
||||
|
||||
#gjs-sm-margin-bottom,
|
||||
#gjs-sm-padding-bottom {
|
||||
order: 4;
|
||||
}
|
||||
|
||||
.gjs-field-radio {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.gjs-field-radio #gjs-sm-input-holder {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.gjs-radio-item {
|
||||
flex: 1 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.gjs-sm-sector .gjs-sm-property.gjs-sm-list {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.gjs-mdl-content {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.gjs-sm-sector .gjs-sm-property .gjs-sm-layer.gjs-sm-active {
|
||||
background-color: rgba(255, 255, 255, 0.09);
|
||||
}
|
||||
|
||||
.gjs-f-divider::before {
|
||||
content: 'D';
|
||||
}
|
||||
|
||||
.gjs-mdl-dialog-sm {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.gjs-mdl-dialog form .gjs-sm-property {
|
||||
font-size: 12px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.gjs-mdl-dialog form .gjs-sm-label {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
#gjs-clm-status-c {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.anim-spin {
|
||||
animation: 0.5s linear 0s normal none infinite running spin;
|
||||
}
|
||||
|
||||
.form-status {
|
||||
float: right;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #f92929;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.is-not-allowed {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.link-is-disabled {
|
||||
pointer-events: none;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* overrides for the icons now that they are SVGs */
|
||||
|
||||
.gjs-pn-panel .gjs-block__media svg {
|
||||
max-height: 48px;
|
||||
}
|
||||
|
||||
/* Newsletter preset theme */
|
||||
|
||||
.gjs-one-bg {
|
||||
background-color: #373d49;
|
||||
}
|
||||
|
||||
.gjs-one-color {
|
||||
color: #373d49;
|
||||
}
|
||||
|
||||
.gjs-one-color-h:hover {
|
||||
color: #373d49;
|
||||
}
|
||||
|
||||
.gjs-two-bg{
|
||||
color: #dae5e6;
|
||||
}
|
||||
|
||||
.gjs-two-color {
|
||||
color: #dae5e6;
|
||||
}
|
||||
|
||||
.gjs-two-color-h:hover {
|
||||
color: #dae5e6;
|
||||
}
|
||||
|
||||
.gjs-three-bg {
|
||||
background-color: #4c9790;
|
||||
}
|
||||
|
||||
.gjs-three-color {
|
||||
color: #4c9790;
|
||||
}
|
||||
|
||||
.gjs-three-color-h:hover {
|
||||
color: #4c9790;
|
||||
}
|
||||
|
||||
.gjs-four-bg {
|
||||
background-color: #35d7bb;
|
||||
}
|
||||
|
||||
.gjs-four-color {
|
||||
color: #35d7bb;
|
||||
}
|
||||
|
||||
.gjs-four-color-h:hover {
|
||||
color: #35d7bb;
|
||||
}
|
||||
|
||||
/* revert colors for nested panels */
|
||||
.gjs-mdl-dialog .panel {
|
||||
color: var(--gjs-main-color);
|
||||
}
|
||||
|
||||
.gjs-assets-spinner {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.anim-spin {
|
||||
animation: 0.5s linear 0s normal none infinite running spin; }
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg); }
|
||||
100% {
|
||||
transform: rotate(360deg); } }
|
||||
|
||||
|
||||
/* CKEditor styles */
|
||||
.cke-modal .ck-editor .ck-content {
|
||||
min-height: 400px;
|
||||
background: var(--background);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.cke-modal .gjs-btn-prim {
|
||||
margin: 10px 5px 5px 0;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
html,
|
||||
body,
|
||||
[data-gjs-type='wrapper'] {
|
||||
height: auto !important;
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
export default (editor, opts = {}) => {
|
||||
let ckEditorInstance = null;
|
||||
|
||||
const SIMPLE_EDITING_TYPES = ['mj-button', 'link'];
|
||||
|
||||
editor.on('rte:enable', (view, gjsRte) => {
|
||||
if (!isSimpleEditingEl(view.el)) {
|
||||
openCKEditorModal(gjsRte.el, view);
|
||||
editor.RichTextEditor.hideToolbar();
|
||||
}
|
||||
});
|
||||
|
||||
function isSimpleEditingEl(el) {
|
||||
return el && el.getAttribute && SIMPLE_EDITING_TYPES.includes(el.getAttribute('data-gjs-type'));
|
||||
}
|
||||
|
||||
function openCKEditorModal(el, view) {
|
||||
const ckEditorElementId = `ckeditor-${Date.now()}`;
|
||||
const modal = editor.Modal;
|
||||
|
||||
modal.onceOpen(() => initCKEditor(ckEditorElementId));
|
||||
modal.onceClose(() => destroyCKEditor());
|
||||
modal.config.backdrop = false;
|
||||
modal.open({
|
||||
title: 'Edit',
|
||||
content: `
|
||||
<div id="${ckEditorElementId}">${el.innerHTML}</div>
|
||||
<button type="button" class="gjs-btn-prim" id="gjs-cke-save-btn">Save</button>
|
||||
<button type="button" class="gjs-btn-prim" id="gjs-cke-close-btn">Cancel</button>
|
||||
`,
|
||||
attributes: {
|
||||
class: 'cke-modal'
|
||||
}
|
||||
});
|
||||
|
||||
const { backgroundColor, color } = getRealColors(el);
|
||||
setEditorStyle(color, backgroundColor);
|
||||
|
||||
document.getElementById('gjs-cke-save-btn').onclick = () => saveContent(view, modal);
|
||||
document.getElementById('gjs-cke-close-btn').onclick = () => modal.close();
|
||||
}
|
||||
|
||||
function initCKEditor(elementId) {
|
||||
if (typeof ClassicEditor === 'undefined') {
|
||||
throw new Error('CKEDITOR instance not found');
|
||||
}
|
||||
ClassicEditor.create(document.getElementById(elementId), opts)
|
||||
.then(instance => {
|
||||
ckEditorInstance = instance;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error initializing CKEditor:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function destroyCKEditor() {
|
||||
if (ckEditorInstance) {
|
||||
ckEditorInstance.destroy()
|
||||
.catch(error => {
|
||||
console.error('Error destroying CKEditor instance:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function saveContent(view, modal) {
|
||||
if (ckEditorInstance) {
|
||||
const content = ckEditorInstance.getData();
|
||||
const selectedElement = view.model;
|
||||
const currentContent = selectedElement.get('content');
|
||||
if (currentContent !== content) {
|
||||
// Clear existing components to avoid conflicts
|
||||
selectedElement.components('');
|
||||
// Set the new content
|
||||
selectedElement.set('content', content);
|
||||
}
|
||||
}
|
||||
modal.close();
|
||||
}
|
||||
|
||||
function setEditorStyle(color, backgroundColor) {
|
||||
const STYLE_ID = 'gjs-ckeditor-styles';
|
||||
let styleElement = document.getElementById(STYLE_ID);
|
||||
|
||||
if (!styleElement) {
|
||||
styleElement = document.createElement('style');
|
||||
styleElement.id = STYLE_ID;
|
||||
document.head.appendChild(styleElement);
|
||||
}
|
||||
|
||||
styleElement.innerHTML = `
|
||||
.cke-modal .ck-editor .ck-content {
|
||||
background: ${backgroundColor};
|
||||
color: ${color};
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function getRealColors(elem, maxDepth = 100) {
|
||||
const transparent = ['rgba(0, 0, 0, 0)', 'transparent'];
|
||||
const defaults = { backgroundColor: 'rgba(0, 0, 0, 0)', color: 'rgb(0, 0, 0)' };
|
||||
|
||||
function getColors(el, depth) {
|
||||
if (!el || depth <= 0) return defaults;
|
||||
try {
|
||||
const style = getComputedStyle(el);
|
||||
const bg = style.backgroundColor;
|
||||
const color = style.color;
|
||||
const result = {};
|
||||
|
||||
if (!transparent.includes(bg)) result.backgroundColor = bg;
|
||||
if (color && !transparent.includes(color)) result.color = color;
|
||||
|
||||
if (result.backgroundColor && result.color) return result;
|
||||
|
||||
const parentColors = getColors(el.parentElement, depth - 1);
|
||||
return {
|
||||
backgroundColor: result.backgroundColor || parentColors.backgroundColor,
|
||||
color: result.color || parentColors.color
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('Error computing colors:', error);
|
||||
return defaults;
|
||||
}
|
||||
}
|
||||
|
||||
return getColors(elem, maxDepth);
|
||||
}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,182 @@
|
||||
import MjmlService from "grapesjs-preset-mautic/dist/mjml/mjml.service";
|
||||
import ContentService from 'grapesjs-preset-mautic/dist/content.service';
|
||||
|
||||
export default class StorageService {
|
||||
constructor(editor, mode) {
|
||||
this.editor = editor;
|
||||
this.mode = mode;
|
||||
this.maxStorageItems = 10;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.storageKey = 'gjs-storage';
|
||||
this.restoreMessage = null;
|
||||
const stackItemId = this.getStackItemId();
|
||||
const storageItem = this.getStorageItemById(stackItemId);
|
||||
const editorContent = this.getEditorContent();
|
||||
if (storageItem && editorContent !== storageItem.content) {
|
||||
this.displayRestoreMessage(storageItem);
|
||||
}
|
||||
this.editor.on("update", () => this.handleUpdate());
|
||||
this.addFormSubmitListeners();
|
||||
}
|
||||
|
||||
displayRestoreMessage(storedContent) {
|
||||
const buttonContainer = document.createElement('div');
|
||||
buttonContainer.className = 'alert-growl-buttons';
|
||||
|
||||
const restoreButton = document.createElement('button');
|
||||
restoreButton.innerHTML = '<i class="ri-arrow-go-back-line"></i> ' + Mautic.translate('mautic.core.builder.storage.restore.button')
|
||||
restoreButton.className = 'btn btn-primary btn-sm ml-md';
|
||||
|
||||
const dismissButton = document.createElement('button');
|
||||
dismissButton.innerHTML = Mautic.translate('mautic.core.builder.storage.dismiss.button')
|
||||
dismissButton.className = 'btn btn-ghost btn-sm';
|
||||
buttonContainer.append(restoreButton, dismissButton);
|
||||
|
||||
const formattedDateTime = this.formatDateTime(storedContent.date);
|
||||
const message = Mautic.translate('mautic.core.builder.storage.restore.message', {
|
||||
date: formattedDateTime
|
||||
});
|
||||
const flashMessage = Mautic.addInfoFlashMessage(message);
|
||||
flashMessage.append(buttonContainer);
|
||||
|
||||
const closeButton = flashMessage.querySelector('button.close')
|
||||
|
||||
this.addMessageEventListeners(restoreButton, dismissButton, closeButton);
|
||||
Mautic.setFlashes(flashMessage, false);
|
||||
this.restoreMessage = flashMessage;
|
||||
}
|
||||
|
||||
dismissRestoreMessage() {
|
||||
if (this.restoreMessage instanceof Element) {
|
||||
this.restoreMessage.remove();
|
||||
}
|
||||
this.restoreMessage = null;
|
||||
}
|
||||
|
||||
addMessageEventListeners(restoreButton, dismissButtom, closeButton) {
|
||||
restoreButton.addEventListener('click', (event) => {
|
||||
this.load();
|
||||
this.dismissRestoreMessage();
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
dismissButtom.addEventListener('click', (event) => {
|
||||
this.handleUpdate();
|
||||
this.dismissRestoreMessage();
|
||||
this.removeStorageItemById(this.getStackItemId());
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
closeButton.addEventListener('click', () => {
|
||||
this.handleUpdate();
|
||||
});
|
||||
|
||||
this.editor.on('hide', () => this.dismissRestoreMessage());
|
||||
}
|
||||
|
||||
addFormSubmitListeners() {
|
||||
mQuery(this.getForm()).on('submit:success', (e, requestUrl, response) => {
|
||||
const lastRequestUrlPart = requestUrl.split('/').pop();
|
||||
const lastResponseUrlPart = response.route.split('/').pop();
|
||||
|
||||
// Check if the form was submitted for a new entity and the response contains the entity id
|
||||
// The success response code alone does not guarantee that the form was saved,
|
||||
// so we need to validate the URL changes and the presence of the entity id
|
||||
if (lastRequestUrlPart === 'new' && !isNaN(lastResponseUrlPart)){
|
||||
// Remove the local storage item for the newly created entity after successful form submission
|
||||
this.removeStorageItemById(`gjs-${this.mode}-${Mautic.builderTheme}-new`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleUpdate() {
|
||||
// update the storage content only when the restore prompt is not available
|
||||
if (!this.restoreMessage) {
|
||||
const editorContent = this.getEditorContent();
|
||||
const dateTime = new Date().toISOString();
|
||||
const stackItemId = this.getStackItemId();
|
||||
const contentWithDateTime = { id: stackItemId, content: editorContent, date: dateTime };
|
||||
this.saveStorageItem(contentWithDateTime);
|
||||
}
|
||||
}
|
||||
|
||||
load() {
|
||||
const stackItemId = this.getStackItemId();
|
||||
const storageItem = this.getStorageItemById(stackItemId);
|
||||
if (storageItem) {
|
||||
this.editor.setComponents(storageItem.content);
|
||||
}
|
||||
}
|
||||
|
||||
getEditorContent() {
|
||||
let content;
|
||||
if (ContentService.isMjmlMode(this.editor)) {
|
||||
content = MjmlService.getEditorMjmlContent(this.editor);
|
||||
} else {
|
||||
content = ContentService.getEditorHtmlContent(this.editor);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
getStackItemId() {
|
||||
const entityId = this.getFormEntityId(this.mode === 'page' ? 'page' : 'emailform')
|
||||
return `gjs-${this.mode}-${Mautic.builderTheme}-${entityId}`;
|
||||
}
|
||||
|
||||
saveStorageItem(item) {
|
||||
const stack = JSON.parse(localStorage.getItem(this.storageKey)) || [];
|
||||
const index = stack.findIndex(existingItem => existingItem.id === item.id);
|
||||
if (index !== -1) {
|
||||
// If the item already exists, update it
|
||||
stack[index] = item;
|
||||
} else {
|
||||
// If the item doesn't exist, push it to the stack
|
||||
if (stack.length >= this.maxStorageItems) {
|
||||
// Ensure that the stack does not exceed the maximum allowed number of items
|
||||
// to prevent web storage from exceeding its 10MiB per domain limit
|
||||
stack.pop(); // Remove the oldest item
|
||||
}
|
||||
stack.push(item);
|
||||
}
|
||||
|
||||
stack.sort((a, b) => new Date(b.date) - new Date(a.date));
|
||||
localStorage.setItem(this.storageKey, JSON.stringify(stack));
|
||||
}
|
||||
|
||||
getStorageItemById(id) {
|
||||
const stack = JSON.parse(localStorage.getItem(this.storageKey)) || [];
|
||||
return stack.find(item => item.id === id);
|
||||
}
|
||||
|
||||
removeStorageItemById(id) {
|
||||
const stack = JSON.parse(localStorage.getItem(this.storageKey)) || [];
|
||||
const index = stack.findIndex(item => item.id === id);
|
||||
if (index !== -1) {
|
||||
stack.splice(index, 1);
|
||||
localStorage.setItem(this.storageKey, JSON.stringify(stack));
|
||||
}
|
||||
}
|
||||
|
||||
formatDateTime(dateTime) {
|
||||
return new Date(dateTime).toISOString().slice(0, 16).replace('T', ' ');
|
||||
}
|
||||
|
||||
getFormEntityId(name) {
|
||||
const form = document.querySelector(`form[name="${name}"]`);
|
||||
const actionUrl = form.getAttribute('action');
|
||||
const urlParts = actionUrl.split('/');
|
||||
const lastPart = urlParts.pop();
|
||||
if (isNaN(lastPart)) {
|
||||
return 'new';
|
||||
} else {
|
||||
return lastPart;
|
||||
}
|
||||
}
|
||||
|
||||
getForm() {
|
||||
return document.querySelector(this.mode === 'page' ? 'form[name="page"]' : 'form[name="emailform"]')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
'name' => 'GrapesJS Builder',
|
||||
'description' => 'GrapesJS Builder with MJML support for Mautic',
|
||||
'version' => '1.0.0',
|
||||
'author' => 'Mautic Community',
|
||||
'routes' => [
|
||||
'main' => [
|
||||
'grapesjsbuilder_upload' => [
|
||||
'path' => '/grapesjsbuilder/upload',
|
||||
'controller' => 'MauticPlugin\GrapesJsBuilderBundle\Controller\FileManagerController::uploadAction',
|
||||
],
|
||||
'grapesjsbuilder_delete' => [
|
||||
'path' => '/grapesjsbuilder/delete',
|
||||
'controller' => 'MauticPlugin\GrapesJsBuilderBundle\Controller\FileManagerController::deleteAction',
|
||||
],
|
||||
/** @depreacated since Mautic 5.2, to be removed in 6.0. Use grapesjsbuilder_media instead */
|
||||
'grapesjsbuilder_assets' => [
|
||||
'path' => '/grapesjsbuilder/assets',
|
||||
'controller' => 'MauticPlugin\GrapesJsBuilderBundle\Controller\FileManagerController::assetsAction',
|
||||
],
|
||||
'grapesjsbuilder_media' => [
|
||||
'path' => '/grapesjsbuilder/media',
|
||||
'controller' => 'MauticPlugin\GrapesJsBuilderBundle\Controller\FileManagerController::getMediaAction',
|
||||
],
|
||||
'grapesjsbuilder_builder' => [
|
||||
'path' => '/grapesjsbuilder/{objectType}/{objectId}',
|
||||
'controller' => 'MauticPlugin\GrapesJsBuilderBundle\Controller\GrapesJsController::builderAction',
|
||||
],
|
||||
],
|
||||
'public' => [],
|
||||
'api' => [],
|
||||
],
|
||||
'menu' => [],
|
||||
'services' => [
|
||||
'other' => [
|
||||
// Provides access to configured API keys, settings, field mapping, etc
|
||||
'grapesjsbuilder.config' => [
|
||||
'class' => MauticPlugin\GrapesJsBuilderBundle\Integration\Config::class,
|
||||
'arguments' => [
|
||||
'mautic.integrations.helper',
|
||||
],
|
||||
],
|
||||
],
|
||||
'sync' => [],
|
||||
'helpers' => [
|
||||
'grapesjsbuilder.helper.filemanager' => [
|
||||
'class' => MauticPlugin\GrapesJsBuilderBundle\Helper\FileManager::class,
|
||||
'arguments' => [
|
||||
'mautic.helper.file_uploader',
|
||||
'mautic.helper.core_parameters',
|
||||
'mautic.helper.paths',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'parameters' => [
|
||||
'image_path_exclude' => ['flags', 'mejs'], // exclude certain folders from showing in the image browser
|
||||
'static_url' => '', // optional base url for images
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Mautic\CoreBundle\DependencyInjection\MauticCoreExtension;
|
||||
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
||||
|
||||
return function (ContainerConfigurator $configurator): void {
|
||||
$services = $configurator->services()
|
||||
->defaults()
|
||||
->autowire()
|
||||
->autoconfigure()
|
||||
->public();
|
||||
|
||||
$excludes = [
|
||||
'node_modules',
|
||||
];
|
||||
|
||||
$services->load('MauticPlugin\\GrapesJsBuilderBundle\\', '../')
|
||||
->exclude('../{'.implode(',', array_merge(MauticCoreExtension::DEFAULT_EXCLUDES, $excludes)).'}');
|
||||
|
||||
$services->load('MauticPlugin\\GrapesJsBuilderBundle\\Entity\\', '../Entity/*Repository.php');
|
||||
|
||||
$services->alias('grapesjsbuilder.model', MauticPlugin\GrapesJsBuilderBundle\Model\GrapesJsBuilderModel::class);
|
||||
// Basic definitions with name, display name and icon
|
||||
$services->alias('mautic.integration.grapesjsbuilder', MauticPlugin\GrapesJsBuilderBundle\Integration\GrapesJsBuilderIntegration::class);
|
||||
// Provides the form types to use for the configuration UI
|
||||
$services->alias('grapesjsbuilder.integration.configuration', MauticPlugin\GrapesJsBuilderBundle\Integration\Support\ConfigSupport::class);
|
||||
// Tells Mautic what themes it should support when enabled
|
||||
$services->alias('grapesjsbuilder.integration.builder', MauticPlugin\GrapesJsBuilderBundle\Integration\Support\BuilderSupport::class);
|
||||
|
||||
$services->get(MauticPlugin\GrapesJsBuilderBundle\InstallFixtures\ORM\GrapesJsData::class)
|
||||
->tag(Doctrine\Bundle\FixturesBundle\DependencyInjection\CompilerPass\FixturesCompilerPass::FIXTURE_TAG);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MauticPlugin\GrapesJsBuilderBundle\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Controller\AjaxController;
|
||||
use Mautic\CoreBundle\Exception\FileUploadException;
|
||||
use MauticPlugin\GrapesJsBuilderBundle\Helper\FileManager;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class FileManagerController extends AjaxController
|
||||
{
|
||||
private const DEFAULT_PAGE = 1;
|
||||
private const DEFAULT_LIMIT = 20;
|
||||
|
||||
public function uploadAction(Request $request, FileManager $fileManager): Response
|
||||
{
|
||||
try {
|
||||
$response = $this->sendJsonResponse(['data'=> $fileManager->uploadFiles($request)]);
|
||||
} catch (FileUploadException $error) {
|
||||
return new Response($error->getMessage(), Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function deleteAction(Request $request, FileManager $fileManager): JsonResponse
|
||||
{
|
||||
$fileName = basename($request->get('filename'));
|
||||
$filePath = $fileManager->getCompleteFilePath($fileName);
|
||||
|
||||
if (!file_exists($filePath) || !exif_imagetype($filePath)) {
|
||||
return $this->sendJsonResponse(['success'=> false]);
|
||||
}
|
||||
|
||||
$fileManager->deleteFile($fileName);
|
||||
|
||||
return $this->sendJsonResponse(['success'=> true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since Mautic 5.2, to be removed in 6.0. Use FileManagerController::getMediaAction instead
|
||||
*/
|
||||
public function assetsAction(FileManager $fileManager): JsonResponse
|
||||
{
|
||||
return $this->sendJsonResponse([
|
||||
'data' => $fileManager->getImages(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getMediaAction(Request $request, FileManager $fileManager): JsonResponse
|
||||
{
|
||||
$page = $request->query->getInt('page', self::DEFAULT_PAGE);
|
||||
$limit = $request->query->getInt('limit', self::DEFAULT_LIMIT);
|
||||
|
||||
return $this->sendJsonResponse($fileManager->getMediaFiles($page, $limit));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MauticPlugin\GrapesJsBuilderBundle\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Controller\CommonController;
|
||||
use Mautic\CoreBundle\Helper\EmojiHelper;
|
||||
use Mautic\CoreBundle\Helper\InputHelper;
|
||||
use Mautic\CoreBundle\Helper\ThemeHelper;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\PageBundle\Entity\Page;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class GrapesJsController extends CommonController
|
||||
{
|
||||
public const OBJECT_TYPE = ['email', 'page'];
|
||||
|
||||
/**
|
||||
* Activate the custom builder.
|
||||
*
|
||||
* @param string $objectType
|
||||
* @param int $objectId
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function builderAction(
|
||||
Request $request,
|
||||
LoggerInterface $mauticLogger,
|
||||
ThemeHelper $themeHelper,
|
||||
$objectType,
|
||||
$objectId,
|
||||
) {
|
||||
if (!in_array($objectType, self::OBJECT_TYPE)) {
|
||||
throw new \Exception('Object not authorized to load custom builder', Response::HTTP_CONFLICT);
|
||||
}
|
||||
|
||||
/** @var \Mautic\EmailBundle\Model\EmailModel|\Mautic\PageBundle\Model\PageModel $model */
|
||||
$model = $this->getModel($objectType);
|
||||
$aclToCheck = 'email:emails:';
|
||||
|
||||
if ('page' === $objectType) {
|
||||
$aclToCheck = 'page:pages:';
|
||||
}
|
||||
|
||||
// permission check
|
||||
if (str_contains((string) $objectId, 'new')) {
|
||||
$isNew = true;
|
||||
|
||||
if (!$this->security->isGranted($aclToCheck.'create')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
/** @var Email|Page $entity */
|
||||
$entity = $model->getEntity();
|
||||
$entity->setSessionId($objectId);
|
||||
} else {
|
||||
/** @var Email|Page $entity */
|
||||
$entity = $model->getEntity($objectId);
|
||||
$isNew = false;
|
||||
|
||||
if (null == $entity
|
||||
|| !$this->security->hasEntityAccess(
|
||||
$aclToCheck.'viewown',
|
||||
$aclToCheck.'viewother',
|
||||
$entity->getCreatedBy()
|
||||
)
|
||||
) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
}
|
||||
|
||||
$type = 'html';
|
||||
$template = InputHelper::clean($request->query->get('template'));
|
||||
if (!$template) {
|
||||
$mauticLogger->warning('Grapesjs: no template in query');
|
||||
|
||||
return $this->json(false);
|
||||
}
|
||||
$templateName = '@themes/'.$template.'/html/'.$objectType;
|
||||
$content = $entity->getContent();
|
||||
|
||||
// Check for MJML template
|
||||
// @deprecated - use mjml directly in email.html.twig
|
||||
if ($logicalName = $this->checkForMjmlTemplate($templateName.'.mjml.twig')) {
|
||||
$type = 'mjml';
|
||||
} else {
|
||||
$logicalName = $themeHelper->checkForTwigTemplate($templateName.'.html.twig');
|
||||
}
|
||||
|
||||
// Replace short codes to emoji
|
||||
$content = array_map(fn ($text) => EmojiHelper::toEmoji($text, 'short'), $content);
|
||||
|
||||
$renderedTemplate = $this->renderView(
|
||||
$logicalName,
|
||||
[
|
||||
'isNew' => $isNew,
|
||||
'content' => $content,
|
||||
$objectType => $entity,
|
||||
'template' => $template,
|
||||
'basePath' => $request->getBasePath(),
|
||||
]
|
||||
);
|
||||
|
||||
if (str_contains($renderedTemplate, '<mjml>')) {
|
||||
$type = 'mjml';
|
||||
}
|
||||
|
||||
$renderedTemplateHtml = ('html' === $type) ? $renderedTemplate : '';
|
||||
$renderedTemplateMjml = ('mjml' === $type) ? $renderedTemplate : '';
|
||||
|
||||
return $this->render(
|
||||
'@GrapesJsBuilder/Builder/template.html.twig',
|
||||
[
|
||||
'templateHtml' => $renderedTemplateHtml,
|
||||
'templateMjml' => $renderedTemplateMjml,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated deprecated since version 5.0 - use mjml directly in email.html.twig
|
||||
*/
|
||||
private function checkForMjmlTemplate($template)
|
||||
{
|
||||
$twig = $this->container->get('twig');
|
||||
|
||||
if ($twig->getLoader()->exists($template)) {
|
||||
return $template;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"templateHtml": "",
|
||||
"templateMjml": "<mjml>\n <mj-body>\n <mj-section>\n <mj-column>\n <mj-text font-size=\"20px\">Hello there!</mj-text>\n <mj-text>This is MJML version of blank template for Mautic.</mj-text>\n <mj-text>{unsubscribe_text} | {webview_text}</mj-text>\n </mj-column>\n </mj-section>\n </mj-body>\n</mjml>"
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,36 @@
|
||||
import 'grapesjs/dist/css/grapes.min.css';
|
||||
import grapesJS from 'grapesjs';
|
||||
import grapesJSMJML from 'grapesjs-mjml';
|
||||
|
||||
const editor = grapesJS.init({
|
||||
fromElement: 1,
|
||||
container: '#gjs',
|
||||
avoidInlineStyle: false,
|
||||
plugins: [grapesJSMJML],
|
||||
pluginsOpts: {
|
||||
[grapesJSMJML]: {
|
||||
// The font imports are included on HTML <head/> when fonts are used on the template
|
||||
fonts: {
|
||||
Montserrat: 'https://fonts.googleapis.com/css?family=Montserrat',
|
||||
'Open Sans': 'https://fonts.googleapis.com/css?family=Open+Sans',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// add custom fonts options on editor's font list
|
||||
editor.on('load', () => {
|
||||
const styleManager = editor.StyleManager;
|
||||
const fontProperty = styleManager.getProperty('typography', 'font-family');
|
||||
|
||||
const list = [];
|
||||
// empty list
|
||||
fontProperty.set('list', list);
|
||||
|
||||
// custom list
|
||||
list.push(fontProperty.addOption({ value: 'Montserrat, sans-serif', name: 'Montserrat' }));
|
||||
list.push(fontProperty.addOption({ value: 'Open Sans, sans-serif', name: 'Open Sans' }));
|
||||
fontProperty.set('list', list);
|
||||
|
||||
styleManager.render();
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Hello World</title>
|
||||
<script src="https://cdn.ckeditor.com/4.16.0/standard-all/ckeditor.js"></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="../../node_modules/grapesjs/dist/css/grapes.min.css"
|
||||
data-source="mautic"
|
||||
/>
|
||||
<link href="style.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="gjs">
|
||||
<mjml>
|
||||
<mj-body>
|
||||
<mj-raw>
|
||||
<!-- Company Header -->
|
||||
</mj-raw>
|
||||
<mj-section background-color="#f0f0f0">
|
||||
<mj-column>
|
||||
<mj-text font-style="bold" font-size="24px" color="#6f6f6f">My Company </mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section background-color="#fafafa">
|
||||
<mj-column width="400px">
|
||||
<mj-image
|
||||
src="https://via.placeholder.com/172x215/%7B%7Bgray600%7D%7D/ffffff?text=172+x+215+x2"
|
||||
/>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-body>
|
||||
</mjml>
|
||||
</div>
|
||||
<script src="helloWorld.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,11 @@
|
||||
/* Let's highlight canvas boundaries */
|
||||
#gjs {
|
||||
border: 3px solid #444;
|
||||
}
|
||||
|
||||
/* Reset some default styling */
|
||||
.gjs-cv-canvas {
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MauticPlugin\GrapesJsBuilderBundle\DependencyInjection;
|
||||
|
||||
use Symfony\Component\Config\FileLocator;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Extension\Extension;
|
||||
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
|
||||
|
||||
class GrapesJsBuilderExtension extends Extension
|
||||
{
|
||||
/**
|
||||
* @param mixed[] $configs
|
||||
*/
|
||||
public function load(array $configs, ContainerBuilder $container): void
|
||||
{
|
||||
$loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../Config'));
|
||||
$loader->load('services.php');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MauticPlugin\GrapesJsBuilderBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
|
||||
class GrapesJsBuilder
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* @var Email|null
|
||||
*/
|
||||
protected $email;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $customMjml;
|
||||
|
||||
private ?string $draftCustomMjml = null;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
$builder->setTable('bundle_grapesjsbuilder')
|
||||
->setCustomRepositoryClass(GrapesJsBuilderRepository::class)
|
||||
->addNamedField('customMjml', Types::TEXT, 'custom_mjml', true)
|
||||
->addNamedField('draftCustomMjml', Types::TEXT, 'draft_custom_mjml', true)
|
||||
->addId();
|
||||
|
||||
$builder->createManyToOne(
|
||||
'email',
|
||||
Email::class
|
||||
)->addJoinColumn('email_id', 'id', true, false, 'CASCADE')->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get id.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Email
|
||||
*/
|
||||
public function getEmail()
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return GrapesJsBuilder
|
||||
*/
|
||||
public function setEmail(Email $email)
|
||||
{
|
||||
$this->email = $email;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getCustomMjml()
|
||||
{
|
||||
return $this->customMjml;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $customMjml
|
||||
*
|
||||
* @return GrapesJsBuilder
|
||||
*/
|
||||
public function setCustomMjml($customMjml)
|
||||
{
|
||||
$this->customMjml = $customMjml;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDraftCustomMjml(): ?string
|
||||
{
|
||||
return $this->draftCustomMjml;
|
||||
}
|
||||
|
||||
public function setDraftCustomMjml(?string $draftCustomMjml): void
|
||||
{
|
||||
$this->draftCustomMjml = $draftCustomMjml;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MauticPlugin\GrapesJsBuilderBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<GrapesJsBuilder>
|
||||
*/
|
||||
class GrapesJsBuilderRepository extends CommonRepository
|
||||
{
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 'gjb';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MauticPlugin\GrapesJsBuilderBundle\EventSubscriber;
|
||||
|
||||
use Mautic\CoreBundle\CoreEvents;
|
||||
use Mautic\CoreBundle\Event\CustomAssetsEvent;
|
||||
use Mautic\InstallBundle\Install\InstallService;
|
||||
use MauticPlugin\GrapesJsBuilderBundle\Integration\Config;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
class AssetsSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private Config $config,
|
||||
private InstallService $installer,
|
||||
private RequestStack $requestStack,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
CoreEvents::VIEW_INJECT_CUSTOM_ASSETS => ['injectAssets', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function injectAssets(CustomAssetsEvent $assetsEvent): void
|
||||
{
|
||||
if (!$this->installer->checkIfInstalled() || !$this->isMauticAdministrationPage()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->config->isPublished()) {
|
||||
$assetsEvent->addScript('plugins/GrapesJsBuilderBundle/Assets/library/js/dist/builder.js');
|
||||
$assetsEvent->addStylesheet('plugins/GrapesJsBuilderBundle/Assets/library/js/dist/builder.css');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true for routes that starts with /s/.
|
||||
*/
|
||||
private function isMauticAdministrationPage(): bool
|
||||
{
|
||||
return preg_match('/^\/s\//', $this->requestStack->getCurrentRequest()->getPathInfo()) >= 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MauticPlugin\GrapesJsBuilderBundle\EventSubscriber;
|
||||
|
||||
use Mautic\EmailBundle\EmailEvents;
|
||||
use Mautic\EmailBundle\Event as Events;
|
||||
use Mautic\EmailBundle\Helper\EmailConfigInterface;
|
||||
use Mautic\EmailBundle\Model\EmailModel;
|
||||
use MauticPlugin\GrapesJsBuilderBundle\Integration\Config;
|
||||
use MauticPlugin\GrapesJsBuilderBundle\Model\GrapesJsBuilderModel;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class EmailSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
private string $existingMjml = '';
|
||||
private string $existingHtml = '';
|
||||
|
||||
public function __construct(
|
||||
private Config $config,
|
||||
private GrapesJsBuilderModel $grapesJsBuilderModel,
|
||||
private EmailModel $emailModel,
|
||||
private EmailConfigInterface $emailConfig,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
EmailEvents::EMAIL_PRE_SAVE => ['onEmailPreSave', 0],
|
||||
EmailEvents::EMAIL_POST_SAVE => ['onEmailPostSave', 0],
|
||||
EmailEvents::EMAIL_POST_DELETE => ['onEmailDelete', 0],
|
||||
EmailEvents::ON_EMAIL_EDIT_SUBMIT => ['manageEmailDraft'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the current MJML for use when managing drafts.
|
||||
*/
|
||||
public function onEmailPreSave(Events\EmailEvent $event): void
|
||||
{
|
||||
if (!$this->config->isPublished() || !$this->emailConfig->isDraftEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$email = $event->getEmail();
|
||||
|
||||
$this->existingHtml = $email->getCustomHtml() ?? '';
|
||||
|
||||
if ($grapesJsBuilder = $this->grapesJsBuilderModel->getRepository()->findOneBy(['email' => $email])) {
|
||||
$this->existingMjml = $grapesJsBuilder->getCustomMjml();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an entry.
|
||||
*/
|
||||
public function onEmailPostSave(Events\EmailEvent $event): void
|
||||
{
|
||||
if (!$this->config->isPublished()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->grapesJsBuilderModel->addOrEditEntity($event->getEmail());
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an entry.
|
||||
*/
|
||||
public function onEmailDelete(Events\EmailEvent $event): void
|
||||
{
|
||||
if (!$this->config->isPublished()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$email = $event->getEmail();
|
||||
$grapesJsBuilder = $this->grapesJsBuilderModel->getRepository()->findOneBy(['email' => $email]);
|
||||
|
||||
if ($grapesJsBuilder) {
|
||||
$this->grapesJsBuilderModel->getRepository()->deleteEntity($grapesJsBuilder);
|
||||
}
|
||||
}
|
||||
|
||||
public function manageEmailDraft(Events\EmailEditSubmitEvent $event): void
|
||||
{
|
||||
if (!$this->config->isPublished()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$email = $event->getCurrentEmail();
|
||||
$grapesJsBuilder = $this->grapesJsBuilderModel->getRepository()->findOneBy(['email' => $email]);
|
||||
|
||||
if ($event->isSaveAsDraft()) {
|
||||
// Set draft MJML and restore previous version when saving a draft
|
||||
$grapesJsBuilder->setDraftCustomMjml($grapesJsBuilder->getCustomMjml());
|
||||
$grapesJsBuilder->setCustomMjml($this->existingMjml);
|
||||
|
||||
// reset the html of the parent email as well
|
||||
$email->setCustomHtml($this->existingHtml);
|
||||
}
|
||||
|
||||
if ($event->isApplyDraft()) {
|
||||
// Remove the draft version when applying - the customMjml is already up to date
|
||||
$grapesJsBuilder->setDraftCustomMjml(null);
|
||||
}
|
||||
|
||||
if ($event->isDiscardDraft() && $email->hasDraft()) {
|
||||
$grapesJsBuilder->setDraftCustomMjml(null);
|
||||
}
|
||||
|
||||
$this->grapesJsBuilderModel->getRepository()->saveEntity($grapesJsBuilder);
|
||||
$this->emailModel->getRepository()->saveEntity($email);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MauticPlugin\GrapesJsBuilderBundle\EventSubscriber;
|
||||
|
||||
use Mautic\CoreBundle\CoreEvents;
|
||||
use Mautic\CoreBundle\Event\CustomContentEvent;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use MauticPlugin\GrapesJsBuilderBundle\Entity\GrapesJsBuilder;
|
||||
use MauticPlugin\GrapesJsBuilderBundle\Integration\Config;
|
||||
use MauticPlugin\GrapesJsBuilderBundle\Model\GrapesJsBuilderModel;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
use Twig\Environment;
|
||||
|
||||
class InjectCustomContentSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private Config $config,
|
||||
private GrapesJsBuilderModel $grapesJsBuilderModel,
|
||||
private Environment $twig,
|
||||
private RequestStack $requestStack,
|
||||
private RouterInterface $router,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
CoreEvents::VIEW_INJECT_CUSTOM_CONTENT => ['injectViewCustomContent', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function injectViewCustomContent(CustomContentEvent $customContentEvent): void
|
||||
{
|
||||
if (!$this->config->isPublished()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$passParams = [];
|
||||
$parameters = $customContentEvent->getVars();
|
||||
|
||||
if ('email.settings.advanced' === $customContentEvent->getContext()) {
|
||||
// Inject MJML form within mail page
|
||||
if (empty($parameters['email']) || !$parameters['email'] instanceof Email) {
|
||||
return;
|
||||
}
|
||||
|
||||
$passParams = ['customMjml' => ''];
|
||||
if ($this->requestStack->getCurrentRequest()->request->has('grapesjsbuilder')) {
|
||||
$data = $this->requestStack->getCurrentRequest()->get('grapesjsbuilder', '');
|
||||
|
||||
if (isset($data['customMjml'])) {
|
||||
$passParams['customMjml'] = $data['customMjml'];
|
||||
}
|
||||
}
|
||||
|
||||
$grapesJsBuilder = $this->grapesJsBuilderModel->getRepository()->findOneBy(['email' => $parameters['email']]);
|
||||
if ('POST' !== $this->requestStack->getCurrentRequest()->getMethod()) {
|
||||
if (!$grapesJsBuilder instanceof GrapesJsBuilder && $parameters['email']->getIsClone()) {
|
||||
$grapesJsBuilder = $this->grapesJsBuilderModel->getGrapesJsFromEmailId(
|
||||
$parameters['email']->getClonedId()
|
||||
);
|
||||
}
|
||||
|
||||
if ($grapesJsBuilder instanceof GrapesJsBuilder) {
|
||||
$passParams['customMjml'] = $grapesJsBuilder->getCustomMjml();
|
||||
}
|
||||
}
|
||||
$content = $this->twig->render(
|
||||
'@GrapesJsBuilder/Setting/fields.html.twig',
|
||||
$passParams
|
||||
);
|
||||
|
||||
$customContentEvent->addContent($content);
|
||||
} elseif ('page.header.left' === $customContentEvent->getContext()) {
|
||||
// Inject fileManager URL
|
||||
$passParams['dataAssets'] = $this->router->generate('grapesjsbuilder_assets', [], \Symfony\Component\Routing\Generator\UrlGeneratorInterface::ABSOLUTE_URL);
|
||||
$passParams['dataUpload'] = $this->router->generate('grapesjsbuilder_upload', [], \Symfony\Component\Routing\Generator\UrlGeneratorInterface::ABSOLUTE_URL);
|
||||
$passParams['dataDelete'] = $this->router->generate('grapesjsbuilder_delete', [], \Symfony\Component\Routing\Generator\UrlGeneratorInterface::ABSOLUTE_URL);
|
||||
|
||||
$content = $this->twig->render(
|
||||
'@GrapesJsBuilder/Setting/vars.html.twig',
|
||||
$passParams
|
||||
);
|
||||
|
||||
$customContentEvent->addContent($content);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MauticPlugin\GrapesJsBuilderBundle\EventSubscriber;
|
||||
|
||||
use JMS\Serializer\EventDispatcher\Events;
|
||||
use JMS\Serializer\EventDispatcher\EventSubscriberInterface;
|
||||
use JMS\Serializer\EventDispatcher\ObjectEvent;
|
||||
use JMS\Serializer\JsonSerializationVisitor;
|
||||
use JMS\Serializer\Metadata\StaticPropertyMetadata;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use MauticPlugin\GrapesJsBuilderBundle\Integration\Config;
|
||||
use MauticPlugin\GrapesJsBuilderBundle\Model\GrapesJsBuilderModel;
|
||||
|
||||
class SerializerSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private GrapesJsBuilderModel $grapesJsBuilderModel,
|
||||
private Config $config,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, string>>
|
||||
*/
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'event' => Events::POST_SERIALIZE,
|
||||
'method' => 'addCustomMJML',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function addCustomMJML(ObjectEvent $event): void
|
||||
{
|
||||
if (!$this->config->isPublished()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$object = $event->getObject();
|
||||
if (!$object instanceof Email) {
|
||||
return;
|
||||
}
|
||||
|
||||
$grapesJsBuilder = $this->grapesJsBuilderModel->getRepository()->findOneBy(['email' => $object]);
|
||||
if (is_null($grapesJsBuilder)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add it to the serialized data.
|
||||
$visitor = $event->getContext()->getVisitor();
|
||||
if ($visitor instanceof JsonSerializationVisitor && !empty($grapesJsBuilder->getCustomMjml())) {
|
||||
$visitor->visitProperty(
|
||||
new StaticPropertyMetadata(
|
||||
'', 'grapesjsbuilder', ['customMjml' => $grapesJsBuilder->getCustomMjml()]
|
||||
), ['customMjml' => $grapesJsBuilder->getCustomMjml()]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MauticPlugin\GrapesJsBuilderBundle;
|
||||
|
||||
use Mautic\IntegrationsBundle\Bundle\AbstractPluginBundle;
|
||||
|
||||
class GrapesJsBuilderBundle extends AbstractPluginBundle
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MauticPlugin\GrapesJsBuilderBundle\Helper;
|
||||
|
||||
use Mautic\CoreBundle\Exception\FileUploadException;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\FileUploader;
|
||||
use Mautic\CoreBundle\Helper\PathsHelper;
|
||||
use Symfony\Component\Filesystem\Exception\IOException;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
use Symfony\Component\Finder\SplFileInfo;
|
||||
|
||||
class FileManager
|
||||
{
|
||||
public const GRAPESJS_IMAGES_DIRECTORY = '';
|
||||
|
||||
public function __construct(
|
||||
private FileUploader $fileUploader,
|
||||
private CoreParametersHelper $coreParametersHelper,
|
||||
private PathsHelper $pathsHelper,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*
|
||||
* @throws FileUploadException
|
||||
*/
|
||||
public function uploadFiles($request)
|
||||
{
|
||||
if (isset($request->files->all()['files'])) {
|
||||
$files = $request->files->all()['files'];
|
||||
$uploadDir = $this->getUploadDir();
|
||||
$uploadedFiles = [];
|
||||
|
||||
foreach ($files as $file) {
|
||||
$this->fileUploader->validateImage($file);
|
||||
}
|
||||
|
||||
foreach ($files as $file) {
|
||||
$uploadedFiles[] = $this->getFullUrl($this->fileUploader->upload($uploadDir, $file));
|
||||
}
|
||||
}
|
||||
|
||||
return $uploadedFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $fileName
|
||||
*/
|
||||
public function deleteFile($fileName): void
|
||||
{
|
||||
$this->fileUploader->delete($this->getCompleteFilePath($fileName));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $fileName
|
||||
*/
|
||||
public function getCompleteFilePath($fileName): string
|
||||
{
|
||||
$uploadDir = $this->getUploadDir();
|
||||
|
||||
return $uploadDir.$fileName;
|
||||
}
|
||||
|
||||
private function getUploadDir(): string
|
||||
{
|
||||
return $this->getGrapesJsImagesPath(true);
|
||||
}
|
||||
|
||||
public function getFullUrl($fileName, $separator = '/'): string
|
||||
{
|
||||
// if a static_url (CDN) is configured use that, otherwise use the site url
|
||||
$url = $this->coreParametersHelper->get('static_url') ?? $this->coreParametersHelper->get('site_url');
|
||||
|
||||
return $url
|
||||
.$separator
|
||||
.$this->getGrapesJsImagesPath(false, $separator)
|
||||
.$fileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $fullPath
|
||||
* @param string $separator
|
||||
*/
|
||||
private function getGrapesJsImagesPath($fullPath = false, $separator = '/'): string
|
||||
{
|
||||
return $this->pathsHelper->getSystemPath('images', $fullPath)
|
||||
.$separator
|
||||
.self::GRAPESJS_IMAGES_DIRECTORY;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since Mautic 5.2, to be removed in 6.0. Use FileManager::getMediaFiles instead
|
||||
*/
|
||||
public function getImages(): array
|
||||
{
|
||||
$files = [];
|
||||
$uploadDir = $this->getUploadDir();
|
||||
|
||||
$fileSystem = new Filesystem();
|
||||
|
||||
if (!$fileSystem->exists($uploadDir)) {
|
||||
try {
|
||||
$fileSystem->mkdir($uploadDir);
|
||||
} catch (IOException) {
|
||||
return $files;
|
||||
}
|
||||
}
|
||||
|
||||
$finder = new Finder();
|
||||
$finder->files()->in($uploadDir);
|
||||
|
||||
foreach ($finder as $file) {
|
||||
// exclude certain folders from grapesjs file manager
|
||||
if (in_array($file->getRelativePath(), $this->coreParametersHelper->get('image_path_exclude'))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($size = @getimagesize($this->getCompleteFilePath($file->getRelativePathname()))) {
|
||||
$files[] = [
|
||||
'src' => $this->getFullUrl($file->getRelativePathname()),
|
||||
'width' => $size[0],
|
||||
'type' => 'image',
|
||||
'height' => $size[1],
|
||||
];
|
||||
} else {
|
||||
$files[] = $this->getFullUrl($file->getRelativePathname());
|
||||
}
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getMediaFiles(int $page, int $limit): array
|
||||
{
|
||||
$files = [];
|
||||
$uploadDir = $this->getUploadDir();
|
||||
$fileSystem = new Filesystem();
|
||||
|
||||
if (!$fileSystem->exists($uploadDir)) {
|
||||
try {
|
||||
$fileSystem->mkdir($uploadDir);
|
||||
} catch (IOException) {
|
||||
return [
|
||||
'data' => [],
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
'totalItems' => 0,
|
||||
'totalPages' => 0,
|
||||
'hasNextPage' => false,
|
||||
'hasPreviousPage' => false,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$finder = new Finder();
|
||||
$finder->files()->in($uploadDir)->sortByModifiedTime()->reverseSorting();
|
||||
|
||||
$totalFiles = iterator_count($finder);
|
||||
$totalPages = (int) ceil($totalFiles / $limit);
|
||||
|
||||
// Check if the requested page is out of range
|
||||
if ($page < 1 || $page > $totalPages) {
|
||||
return [
|
||||
'data' => [],
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
'totalItems' => $totalFiles,
|
||||
'totalPages' => $totalPages,
|
||||
'hasNextPage' => $page < $totalPages,
|
||||
'hasPreviousPage' => $page > 1,
|
||||
];
|
||||
}
|
||||
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
$filesIterator = new \LimitIterator($finder->getIterator(), $offset, $limit);
|
||||
|
||||
foreach ($filesIterator as $file) {
|
||||
if (in_array($file->getRelativePath(), $this->coreParametersHelper->get('image_path_exclude'))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fileInfo = $this->getFileInfo($file);
|
||||
if ($fileInfo) {
|
||||
$files[] = $fileInfo;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'data' => $files,
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
'totalItems' => $totalFiles,
|
||||
'totalPages' => $totalPages,
|
||||
'hasNextPage' => $page < $totalPages,
|
||||
'hasPreviousPage' => $page > 1,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
private function getFileInfo(SplFileInfo $file): ?array
|
||||
{
|
||||
$filePath = $this->getCompleteFilePath($file->getRelativePathname());
|
||||
$size = @getimagesize($filePath);
|
||||
|
||||
if ($size) {
|
||||
return [
|
||||
'src' => $this->getFullUrl($file->getRelativePathname()),
|
||||
'width' => $size[0],
|
||||
'height' => $size[1],
|
||||
'type' => 'image',
|
||||
];
|
||||
} elseif (in_array($file->getExtension(), ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'])) {
|
||||
return [
|
||||
'src' => $this->getFullUrl($file->getRelativePathname()),
|
||||
'type' => 'document',
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MauticPlugin\GrapesJsBuilderBundle\InstallFixtures\ORM;
|
||||
|
||||
use Doctrine\Bundle\FixturesBundle\FixtureGroupInterface;
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\PluginBundle\Entity\Integration;
|
||||
use Mautic\PluginBundle\Entity\Plugin;
|
||||
|
||||
class GrapesJsData extends AbstractFixture implements OrderedFixtureInterface, FixtureGroupInterface
|
||||
{
|
||||
public function __construct(private CoreParametersHelper $coreParametersHelper)
|
||||
{
|
||||
}
|
||||
|
||||
public static function getGroups(): array
|
||||
{
|
||||
return ['group_install', 'group_mautic_install_data'];
|
||||
}
|
||||
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
$applicationDir = $this->coreParametersHelper->get('mautic.application_dir');
|
||||
$grapeJsBuilderConfigPath = $applicationDir.'/plugins/GrapesJsBuilderBundle/Config/config.php';
|
||||
|
||||
if (!file_exists($grapeJsBuilderConfigPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$parameters = include $grapeJsBuilderConfigPath;
|
||||
|
||||
if (!is_array($parameters)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$plugin = new Plugin();
|
||||
$plugin->setName($parameters['name']);
|
||||
$plugin->setDescription($parameters['description']);
|
||||
$plugin->setVersion($parameters['version']);
|
||||
$plugin->setAuthor($parameters['author']);
|
||||
$plugin->setBundle('GrapesJsBuilderBundle');
|
||||
$manager->persist($plugin);
|
||||
|
||||
$integration = new Integration();
|
||||
$integration->setIsPublished(true);
|
||||
$integration->setName('GrapesJsBuilder');
|
||||
$integration->setPlugin($plugin);
|
||||
$manager->persist($integration);
|
||||
|
||||
$manager->flush();
|
||||
}
|
||||
|
||||
public function getOrder(): int
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MauticPlugin\GrapesJsBuilderBundle\Integration;
|
||||
|
||||
use Mautic\IntegrationsBundle\Exception\IntegrationNotFoundException;
|
||||
use Mautic\IntegrationsBundle\Helper\IntegrationsHelper;
|
||||
use Mautic\PluginBundle\Entity\Integration;
|
||||
|
||||
class Config
|
||||
{
|
||||
public function __construct(
|
||||
private IntegrationsHelper $integrationsHelper,
|
||||
) {
|
||||
}
|
||||
|
||||
public function isPublished(): bool
|
||||
{
|
||||
try {
|
||||
$integration = $this->getIntegrationEntity();
|
||||
|
||||
return (bool) $integration->getIsPublished() ?: false;
|
||||
} catch (IntegrationNotFoundException) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getFeatureSettings(): array
|
||||
{
|
||||
try {
|
||||
$integration = $this->getIntegrationEntity();
|
||||
|
||||
return $integration->getFeatureSettings() ?: [];
|
||||
} catch (IntegrationNotFoundException) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws IntegrationNotFoundException
|
||||
*/
|
||||
public function getIntegrationEntity(): Integration
|
||||
{
|
||||
$integrationObject = $this->integrationsHelper->getIntegration(GrapesJsBuilderIntegration::NAME);
|
||||
|
||||
return $integrationObject->getIntegrationConfiguration();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MauticPlugin\GrapesJsBuilderBundle\Integration;
|
||||
|
||||
use Mautic\IntegrationsBundle\Integration\BasicIntegration;
|
||||
use Mautic\IntegrationsBundle\Integration\ConfigurationTrait;
|
||||
use Mautic\IntegrationsBundle\Integration\Interfaces\BasicInterface;
|
||||
|
||||
class GrapesJsBuilderIntegration extends BasicIntegration implements BasicInterface
|
||||
{
|
||||
use ConfigurationTrait;
|
||||
|
||||
public const NAME = 'grapesjsbuilder';
|
||||
|
||||
public const DISPLAY_NAME = 'GrapesJS';
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return self::NAME;
|
||||
}
|
||||
|
||||
public function getDisplayName(): string
|
||||
{
|
||||
return self::DISPLAY_NAME;
|
||||
}
|
||||
|
||||
public function getIcon(): string
|
||||
{
|
||||
return 'plugins/GrapesJsBuilderBundle/Assets/img/grapesjsbuilder.png';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MauticPlugin\GrapesJsBuilderBundle\Integration\Support;
|
||||
|
||||
use Mautic\IntegrationsBundle\Integration\Interfaces\BuilderInterface;
|
||||
use MauticPlugin\GrapesJsBuilderBundle\Integration\GrapesJsBuilderIntegration;
|
||||
|
||||
class BuilderSupport extends GrapesJsBuilderIntegration implements BuilderInterface
|
||||
{
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private array $featuresSupported = ['email', 'page'];
|
||||
|
||||
public function isSupported(string $featureName): bool
|
||||
{
|
||||
return in_array($featureName, $this->featuresSupported);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MauticPlugin\GrapesJsBuilderBundle\Integration\Support;
|
||||
|
||||
use Mautic\IntegrationsBundle\Integration\DefaultConfigFormTrait;
|
||||
use Mautic\IntegrationsBundle\Integration\Interfaces\ConfigFormInterface;
|
||||
use MauticPlugin\GrapesJsBuilderBundle\Integration\GrapesJsBuilderIntegration;
|
||||
|
||||
class ConfigSupport extends GrapesJsBuilderIntegration implements ConfigFormInterface
|
||||
{
|
||||
use DefaultConfigFormTrait;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MauticPlugin\GrapesJsBuilderBundle\Model;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Model\AbstractCommonModel;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\EmailBundle\Model\EmailModel;
|
||||
use MauticPlugin\GrapesJsBuilderBundle\Entity\GrapesJsBuilder;
|
||||
use MauticPlugin\GrapesJsBuilderBundle\Entity\GrapesJsBuilderRepository;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
/**
|
||||
* @extends AbstractCommonModel<GrapesJsBuilder>
|
||||
*/
|
||||
class GrapesJsBuilderModel extends AbstractCommonModel
|
||||
{
|
||||
public function __construct(
|
||||
private RequestStack $requestStack,
|
||||
private EmailModel $emailModel,
|
||||
EntityManager $em,
|
||||
CorePermissions $security,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
UrlGeneratorInterface $router,
|
||||
Translator $translator,
|
||||
UserHelper $userHelper,
|
||||
LoggerInterface $mauticLogger,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
) {
|
||||
parent::__construct($em, $security, $dispatcher, $router, $translator, $userHelper, $mauticLogger, $coreParametersHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return GrapesJsBuilderRepository
|
||||
*/
|
||||
public function getRepository()
|
||||
{
|
||||
/** @var GrapesJsBuilderRepository $repository */
|
||||
$repository = $this->em->getRepository(GrapesJsBuilder::class);
|
||||
|
||||
$repository->setTranslator($this->translator);
|
||||
|
||||
return $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or edit email settings entity based on request.
|
||||
*/
|
||||
public function addOrEditEntity(Email $email): void
|
||||
{
|
||||
if ($this->emailModel->isUpdatingTranslationChildren()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$grapesJsBuilder = $this->getRepository()->findOneBy(['email' => $email]);
|
||||
|
||||
if (!$grapesJsBuilder) {
|
||||
$grapesJsBuilder = new GrapesJsBuilder();
|
||||
$grapesJsBuilder->setEmail($email);
|
||||
}
|
||||
|
||||
$currentRequest = $this->requestStack->getCurrentRequest();
|
||||
if ($currentRequest && $currentRequest->request->has('grapesjsbuilder')) {
|
||||
$data = $this->requestStack->getCurrentRequest()->get('grapesjsbuilder', '');
|
||||
|
||||
if (isset($data['customMjml'])) {
|
||||
$grapesJsBuilder->setCustomMjml($data['customMjml']);
|
||||
}
|
||||
|
||||
$this->getRepository()->saveEntity($grapesJsBuilder);
|
||||
|
||||
$customHtml = $this->requestStack->getCurrentRequest()->get('emailform')['customHtml'] ?? null;
|
||||
if (is_null($customHtml)) {
|
||||
$customHtml = $this->requestStack->getCurrentRequest()->get('customHtml') ?? null;
|
||||
}
|
||||
$email->setCustomHtml($customHtml);
|
||||
$this->emailModel->getRepository()->saveEntity($email);
|
||||
}
|
||||
}
|
||||
|
||||
public function getGrapesJsFromEmailId(?int $emailId)
|
||||
{
|
||||
if ($email = $this->emailModel->getEntity($emailId)) {
|
||||
return $this->getRepository()->findOneBy(['email' => $email]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# GrapesJS Builder with MJML support for Mautic
|
||||
|
||||
## This plugin is managed centrally in https://github.com/mautic/mautic/blob/head/plugins/GrapesJsBuilderBundle and this is a read-only mirror repository.
|
||||
|
||||
**📣 Please make PRs and issues against Mautic Core, not here!**
|
||||
@@ -0,0 +1,5 @@
|
||||
{% set templateObject = {
|
||||
'templateHtml': templateHtml,
|
||||
'templateMjml': templateMjml,
|
||||
} %}
|
||||
{{ templateObject | json_encode() | raw }}
|
||||
@@ -0,0 +1,7 @@
|
||||
<div class="row hide">
|
||||
<div class="form-group col-xs-12 ">
|
||||
<div class="input-group">
|
||||
<textarea id="grapesjsbuilder_customMjml" class="form-control builder-mjml" name="grapesjsbuilder[customMjml]">{{ customMjml }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
<div id="grapesjsbuilder_assets" class="hide" data-assets="{{ dataAssets }}" data-upload="{{ dataUpload }}" data-delete="{{ dataDelete }}"></div>
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MauticPlugin\GrapesJsBuilderBundle\Tests\Functional\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\PluginBundle\Entity\Integration;
|
||||
use Mautic\PluginBundle\Entity\Plugin;
|
||||
use MauticPlugin\GrapesJsBuilderBundle\Entity\GrapesJsBuilder;
|
||||
use MauticPlugin\GrapesJsBuilderBundle\Entity\GrapesJsBuilderRepository;
|
||||
|
||||
class AssertCustomMjmlTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->createIntegration();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
* @throws \Doctrine\ORM\Exception\ORMException
|
||||
*/
|
||||
public function testAssertCustomMjml(): void
|
||||
{
|
||||
// Create email & add GrapesJs to it.
|
||||
$email = $this->createEmail();
|
||||
$this->addToGrapesJsBuilder($email);
|
||||
$emailId = $email->getId();
|
||||
|
||||
// Get the Email via API and assert customMjml.
|
||||
$this->client->request('GET', '/api/emails/'.$emailId);
|
||||
$this->assertResponseStatusCodeSame(200);
|
||||
$content = json_decode($this->client->getResponse()->getContent(), true);
|
||||
$this->assertNotEmpty($content['email']['grapesjsbuilder']['customMjml']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Doctrine\ORM\Exception\NotSupported
|
||||
*/
|
||||
public function testAssertCustomHtmlAndCustomMjml(): void
|
||||
{
|
||||
// Create email using an API call and add GrapesJS into it.
|
||||
$responseData = $this->createEmailViaApi();
|
||||
$emailId = $responseData['email']['id'];
|
||||
$email = $this->em->getRepository(Email::class)->find($emailId);
|
||||
$this->addToGrapesJsBuilder($email);
|
||||
|
||||
// Get email & check for both customHtml & customMjml in the response.
|
||||
$this->client->request('GET', '/api/emails/'.$emailId);
|
||||
$this->assertResponseStatusCodeSame(200);
|
||||
$content = json_decode($this->client->getResponse()->getContent(), true);
|
||||
$this->assertNotEmpty($content['email']['customHtml']);
|
||||
$this->assertNotEmpty($content['email']['grapesjsbuilder']['customMjml']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Doctrine\ORM\Exception\NotSupported
|
||||
*/
|
||||
private function getRepository(): GrapesJsBuilderRepository
|
||||
{
|
||||
/** @var GrapesJsBuilderRepository $repository */
|
||||
$repository = $this->em->getRepository(GrapesJsBuilder::class);
|
||||
|
||||
$repository->setTranslator($this->getTranslatorMock());
|
||||
|
||||
return $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Doctrine\ORM\Exception\ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
private function createEmail(): Email
|
||||
{
|
||||
$email = new Email();
|
||||
$email->setName('Test email');
|
||||
$email->setSubject('Test email subject');
|
||||
$email->setEmailType('template');
|
||||
$email->setCustomHtml('<html></html>');
|
||||
$email->setIsPublished(true);
|
||||
$this->em->persist($email);
|
||||
$this->em->flush();
|
||||
|
||||
return $email;
|
||||
}
|
||||
|
||||
private function getTranslatorMock(): Translator
|
||||
{
|
||||
$translator = $this->createMock(Translator::class);
|
||||
$translator->method('hasId')
|
||||
->willReturn(false);
|
||||
|
||||
return $translator;
|
||||
}
|
||||
|
||||
private function addToGrapesJsBuilder(Email $email): void
|
||||
{
|
||||
$grapesJsBuilder = new GrapesJsBuilder();
|
||||
$grapesJsBuilder->setEmail($email);
|
||||
$grapesJsBuilder->setCustomMjml('<mjml>></mjml>');
|
||||
$this->getRepository()->saveEntity($grapesJsBuilder);
|
||||
}
|
||||
|
||||
private function createEmailViaApi(): mixed
|
||||
{
|
||||
$emailData = [
|
||||
'name' => 'Test email',
|
||||
'subject' => 'Test email subject',
|
||||
'emailType' => 'template',
|
||||
'customHtml' => '<html></html>',
|
||||
'isPublished' => true,
|
||||
];
|
||||
|
||||
$this->client->request('POST', '/api/emails/new', [], [], ['CONTENT_TYPE' => 'application/json'], json_encode($emailData));
|
||||
$this->assertResponseStatusCodeSame(201);
|
||||
|
||||
return json_decode($this->client->getResponse()->getContent(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Doctrine\ORM\Exception\ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
private function createIntegration(): void
|
||||
{
|
||||
$plugin = new Plugin();
|
||||
$plugin->setName('GrapesJS Builder');
|
||||
$plugin->setBundle('GrapesJsBuilderBundle');
|
||||
$this->em->persist($plugin);
|
||||
|
||||
$integration = new Integration();
|
||||
$integration->setPlugin($plugin);
|
||||
$integration->setIsPublished(true);
|
||||
$integration->setName('grapesjsbuilder');
|
||||
$this->em->persist($integration);
|
||||
$this->em->flush();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MauticPlugin\GrapesJsBuilderBundle\Tests\Functional\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
final class FileManagerControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
private const ASSETS_ENDPOINT = '/s/grapesjsbuilder/media';
|
||||
private const UPLOAD_ENDPOINT = '/s/grapesjsbuilder/upload';
|
||||
private const DELETE_ENDPOINT = '/s/grapesjsbuilder/delete';
|
||||
private const IMAGE_COUNT = 3;
|
||||
|
||||
/** @var array<string> */
|
||||
private array $tempFilePaths = [];
|
||||
|
||||
protected function beforeTearDown(): void
|
||||
{
|
||||
$this->cleanupTempFiles();
|
||||
}
|
||||
|
||||
public function testAssetsManagerWorkflow(): void
|
||||
{
|
||||
$initialAssetCount = $this->getAssetCount();
|
||||
|
||||
$uploadedFiles = $this->uploadImages();
|
||||
$this->assertUploadSuccessful($uploadedFiles);
|
||||
|
||||
$newAssetCount = $this->getAssetCount();
|
||||
$this->assertEquals($initialAssetCount + self::IMAGE_COUNT, $newAssetCount);
|
||||
|
||||
$this->testPagination($newAssetCount);
|
||||
$this->testRecentlyAddedFilesAppearFirst($uploadedFiles);
|
||||
|
||||
$this->deleteUploadedFiles($uploadedFiles);
|
||||
|
||||
$finalAssetCount = $this->getAssetCount();
|
||||
$this->assertEquals($initialAssetCount, $finalAssetCount);
|
||||
}
|
||||
|
||||
private function getAssetCount(): int
|
||||
{
|
||||
$response = $this->makeRequest('GET', self::ASSETS_ENDPOINT);
|
||||
$content = $this->getJsonResponse($response);
|
||||
|
||||
$this->assertArrayHasKey('data', $content);
|
||||
$this->assertArrayHasKey('page', $content);
|
||||
$this->assertArrayHasKey('limit', $content);
|
||||
$this->assertArrayHasKey('totalItems', $content);
|
||||
$this->assertArrayHasKey('totalPages', $content);
|
||||
$this->assertArrayHasKey('hasNextPage', $content);
|
||||
$this->assertArrayHasKey('hasPreviousPage', $content);
|
||||
|
||||
return $content['totalItems'];
|
||||
}
|
||||
|
||||
private function testPagination(int $totalAssets): void
|
||||
{
|
||||
$limit = 2;
|
||||
$totalPages = ceil($totalAssets / $limit);
|
||||
|
||||
for ($page = 1; $page <= $totalPages; ++$page) {
|
||||
$response = $this->makeRequest('GET', self::ASSETS_ENDPOINT."?limit={$limit}&page={$page}");
|
||||
$content = $this->getJsonResponse($response);
|
||||
|
||||
$this->assertArrayHasKey('data', $content);
|
||||
$this->assertArrayHasKey('page', $content);
|
||||
$this->assertArrayHasKey('limit', $content);
|
||||
$this->assertArrayHasKey('totalItems', $content);
|
||||
$this->assertArrayHasKey('totalPages', $content);
|
||||
$this->assertArrayHasKey('hasNextPage', $content);
|
||||
$this->assertArrayHasKey('hasPreviousPage', $content);
|
||||
|
||||
$this->assertEquals($page, $content['page']);
|
||||
$this->assertEquals($limit, $content['limit']);
|
||||
$this->assertEquals($totalAssets, $content['totalItems']);
|
||||
$this->assertEquals($totalPages, $content['totalPages']);
|
||||
|
||||
$this->assertEquals($page < $totalPages, $content['hasNextPage']);
|
||||
$this->assertEquals($page > 1, $content['hasPreviousPage']);
|
||||
|
||||
$expectedItemCount = ($page < $totalPages) ? $limit : (($totalAssets % $limit) ?: $limit);
|
||||
$this->assertCount($expectedItemCount, $content['data']);
|
||||
|
||||
foreach ($content['data'] as $item) {
|
||||
$this->assertArrayHasKey('src', $item);
|
||||
$this->assertArrayHasKey('width', $item);
|
||||
$this->assertArrayHasKey('height', $item);
|
||||
$this->assertArrayHasKey('type', $item);
|
||||
}
|
||||
}
|
||||
|
||||
// Test invalid page
|
||||
$response = $this->makeRequest('GET', self::ASSETS_ENDPOINT."?limit={$limit}&page=".($totalPages + 1));
|
||||
$content = $this->getJsonResponse($response);
|
||||
$this->assertEmpty($content['data']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $uploadedFiles
|
||||
*/
|
||||
private function testRecentlyAddedFilesAppearFirst(array $uploadedFiles): void
|
||||
{
|
||||
$response = $this->makeRequest('GET', self::ASSETS_ENDPOINT);
|
||||
$content = $this->getJsonResponse($response);
|
||||
|
||||
$this->assertArrayHasKey('data', $content);
|
||||
$this->assertNotEmpty($content['data']);
|
||||
|
||||
$assetList = $content['data'];
|
||||
$uploadedFileNames = array_map([$this, 'getFileNameFromUrl'], $uploadedFiles);
|
||||
|
||||
// Check if the first 'IMAGE_COUNT' assets in the list are the recently uploaded files
|
||||
for ($i = 0; $i < self::IMAGE_COUNT; ++$i) {
|
||||
$this->assertArrayHasKey($i, $assetList);
|
||||
$this->assertArrayHasKey('src', $assetList[$i]);
|
||||
$assetFileName = $this->getFileNameFromUrl($assetList[$i]['src']);
|
||||
$this->assertContains($assetFileName, $uploadedFileNames, 'Recently uploaded file not found in the first {self::IMAGE_COUNT} assets');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>
|
||||
*/
|
||||
private function uploadImages(): array
|
||||
{
|
||||
$imageFiles = $this->createTempImageFiles();
|
||||
$response = $this->makeRequest('POST', self::UPLOAD_ENDPOINT, [], ['files' => $imageFiles]);
|
||||
|
||||
return $this->getJsonResponse($response)['data'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<UploadedFile>
|
||||
*/
|
||||
private function createTempImageFiles(): array
|
||||
{
|
||||
$imageFiles = [];
|
||||
for ($i = 1; $i <= self::IMAGE_COUNT; ++$i) {
|
||||
$imagePath = sys_get_temp_dir()."/test-image-{$i}.png";
|
||||
$this->createImage($imagePath);
|
||||
$this->tempFilePaths[] = $imagePath;
|
||||
$imageFiles[] = new UploadedFile($imagePath, "test-image-{$i}.png", 'image/png', null, true);
|
||||
}
|
||||
|
||||
return $imageFiles;
|
||||
}
|
||||
|
||||
private function createImage(string $path): void
|
||||
{
|
||||
$image = imagecreatetruecolor(100, 100);
|
||||
imagepng($image, $path);
|
||||
imagedestroy($image);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $uploadedFiles
|
||||
*/
|
||||
private function assertUploadSuccessful(array $uploadedFiles): void
|
||||
{
|
||||
$this->assertCount(self::IMAGE_COUNT, $uploadedFiles);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $uploadedFiles
|
||||
*/
|
||||
private function deleteUploadedFiles(array $uploadedFiles): void
|
||||
{
|
||||
foreach ($uploadedFiles as $uploadedFile) {
|
||||
$fileName = $this->getFileNameFromUrl($uploadedFile);
|
||||
$this->makeRequest('GET', self::DELETE_ENDPOINT."?filename={$fileName}");
|
||||
}
|
||||
}
|
||||
|
||||
private function getFileNameFromUrl(string $url): string
|
||||
{
|
||||
$fileUrlParts = explode('/', $url);
|
||||
|
||||
return end($fileUrlParts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $parameters
|
||||
* @param array<string, mixed> $files
|
||||
*/
|
||||
private function makeRequest(string $method, string $endpoint, array $parameters = [], array $files = []): Response
|
||||
{
|
||||
$this->client->request($method, $endpoint, $parameters, $files);
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertSame(Response::HTTP_OK, $response->getStatusCode());
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function getJsonResponse(Response $response): array
|
||||
{
|
||||
return json_decode($response->getContent(), true);
|
||||
}
|
||||
|
||||
private function cleanupTempFiles(): void
|
||||
{
|
||||
foreach ($this->tempFilePaths as $path) {
|
||||
if (file_exists($path)) {
|
||||
unlink($path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MauticPlugin\GrapesJsBuilderBundle\Tests\InstallFixtures\ORM;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\PluginBundle\Entity\Integration;
|
||||
use Mautic\PluginBundle\Entity\Plugin;
|
||||
use MauticPlugin\GrapesJsBuilderBundle\InstallFixtures\ORM\GrapesJsData;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
class GrapeJsDataTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected $useCleanupRollback = false;
|
||||
|
||||
public function testGetGroups(): void
|
||||
{
|
||||
Assert::assertSame(['group_install', 'group_mautic_install_data'], GrapesJsData::getGroups());
|
||||
}
|
||||
|
||||
public function testLoad(): void
|
||||
{
|
||||
$findOneByCriteria = [
|
||||
'name' => 'GrapesJS Builder',
|
||||
'description' => 'GrapesJS Builder with MJML support for Mautic',
|
||||
'version' => '1.0.0',
|
||||
'author' => 'Mautic Community',
|
||||
'bundle' => 'GrapesJsBuilderBundle',
|
||||
];
|
||||
$plugin = $this->em->getRepository(Plugin::class)->findOneBy($findOneByCriteria);
|
||||
self::assertNull($plugin);
|
||||
|
||||
$this->loadFixtures([GrapesJsData::class]);
|
||||
|
||||
$plugin = $this->em->getRepository(Plugin::class)->findOneBy($findOneByCriteria);
|
||||
self::assertInstanceOf(Plugin::class, $plugin);
|
||||
|
||||
$integration = $this->em->getRepository(Integration::class)->findOneBy(
|
||||
[
|
||||
'isPublished' => true,
|
||||
'name' => 'GrapesJsBuilder',
|
||||
'plugin' => $plugin,
|
||||
]
|
||||
);
|
||||
self::assertInstanceOf(Integration::class, $integration);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MauticPlugin\GrapesJsBuilderBundle\Tests\Unit\EventSubscriber;
|
||||
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\EmailBundle\Entity\EmailRepository;
|
||||
use Mautic\EmailBundle\Event\EmailEditSubmitEvent;
|
||||
use Mautic\EmailBundle\Helper\EmailConfigInterface;
|
||||
use Mautic\EmailBundle\Model\EmailModel;
|
||||
use MauticPlugin\GrapesJsBuilderBundle\Entity\GrapesJsBuilder;
|
||||
use MauticPlugin\GrapesJsBuilderBundle\Entity\GrapesJsBuilderRepository;
|
||||
use MauticPlugin\GrapesJsBuilderBundle\EventSubscriber\EmailSubscriber;
|
||||
use MauticPlugin\GrapesJsBuilderBundle\Integration\Config;
|
||||
use MauticPlugin\GrapesJsBuilderBundle\Model\GrapesJsBuilderModel;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class EmailSubscriberTest extends TestCase
|
||||
{
|
||||
/** @var MockObject&Config */
|
||||
private MockObject $config;
|
||||
/** @var MockObject&GrapesJsBuilderModel */
|
||||
private MockObject $grapesJsBuilderModel;
|
||||
/** @var MockObject&GrapesJsBuilderRepository */
|
||||
private MockObject $grapesJsBuilderRepo;
|
||||
private EmailModel|MockObject $emailModel;
|
||||
private EmailConfigInterface|MockObject $emailConfig;
|
||||
private EmailSubscriber $subscriber;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->config = $this->createMock(Config::class);
|
||||
$this->grapesJsBuilderModel = $this->createMock(GrapesJsBuilderModel::class);
|
||||
$this->emailModel = $this->createMock(EmailModel::class);
|
||||
$this->emailConfig = $this->createMock(EmailConfigInterface::class);
|
||||
$this->grapesJsBuilderRepo = $this->createMock(GrapesJsBuilderRepository::class);
|
||||
$this->subscriber = new EmailSubscriber($this->config, $this->grapesJsBuilderModel, $this->emailModel, $this->emailConfig);
|
||||
|
||||
$this->emailModel->method('getRepository')
|
||||
->willReturn($this->createMock(EmailRepository::class));
|
||||
|
||||
$this->grapesJsBuilderModel->method('getRepository')
|
||||
->willReturn($this->grapesJsBuilderRepo);
|
||||
}
|
||||
|
||||
public function testManageEmailDraftExitsWhenPluginNotPublished(): void
|
||||
{
|
||||
$event = $this->createMock(EmailEditSubmitEvent::class);
|
||||
|
||||
$event->expects($this->never())
|
||||
->method('getCurrentEmail');
|
||||
|
||||
$this->config->expects($this->once())
|
||||
->method('isPublished')
|
||||
->willReturn(false);
|
||||
|
||||
$this->subscriber->manageEmailDraft($event);
|
||||
}
|
||||
|
||||
public function testManageEmailDraftHandlesSaveAsDraft(): void
|
||||
{
|
||||
$event = $this->createMock(EmailEditSubmitEvent::class);
|
||||
|
||||
$event->expects($this->once())
|
||||
->method('getCurrentEmail')
|
||||
->willReturn($this->createMock(Email::class));
|
||||
|
||||
$event->expects($this->once())
|
||||
->method('isSaveAsDraft')
|
||||
->willReturn(true);
|
||||
|
||||
$this->grapesJsBuilderRepo->method('findOneBy')
|
||||
->willReturn($grapesJsBuilder = $this->createMock(GrapesJsBuilder::class));
|
||||
|
||||
$this->config->expects($this->once())
|
||||
->method('isPublished')
|
||||
->willReturn(true);
|
||||
|
||||
$grapesJsBuilder->expects($this->once())->method('setDraftCustomMjml');
|
||||
$grapesJsBuilder->expects($this->once())->method('setCustomMjml');
|
||||
|
||||
$this->subscriber->manageEmailDraft($event);
|
||||
}
|
||||
|
||||
public function testManageEmailDraftHandlesApply(): void
|
||||
{
|
||||
$event = $this->createMock(EmailEditSubmitEvent::class);
|
||||
|
||||
$event->expects($this->once())
|
||||
->method('getCurrentEmail')
|
||||
->willReturn($this->createMock(Email::class));
|
||||
|
||||
$event->expects($this->once())
|
||||
->method('isApplyDraft')
|
||||
->willReturn(true);
|
||||
|
||||
$this->grapesJsBuilderRepo->method('findOneBy')
|
||||
->willReturn($grapesJsBuilder = $this->createMock(GrapesJsBuilder::class));
|
||||
|
||||
$this->config->expects($this->once())
|
||||
->method('isPublished')
|
||||
->willReturn(true);
|
||||
|
||||
$grapesJsBuilder->expects($this->once())->method('setDraftCustomMjml');
|
||||
$grapesJsBuilder->expects($this->never())->method('setCustomMjml');
|
||||
|
||||
$this->subscriber->manageEmailDraft($event);
|
||||
}
|
||||
|
||||
public function testManageEmailDraftHandlesDiscardDraft(): void
|
||||
{
|
||||
$event = $this->createMock(EmailEditSubmitEvent::class);
|
||||
|
||||
$event->expects($this->once())
|
||||
->method('getCurrentEmail')
|
||||
->willReturn($mockEmail = $this->createMock(Email::class));
|
||||
|
||||
$event->expects($this->once())
|
||||
->method('isDiscardDraft')
|
||||
->willReturn(true);
|
||||
|
||||
$mockEmail->expects($this->once())
|
||||
->method('hasDraft')
|
||||
->willReturn(true);
|
||||
|
||||
$this->grapesJsBuilderRepo->method('findOneBy')
|
||||
->willReturn($grapesJsBuilder = $this->createMock(GrapesJsBuilder::class));
|
||||
|
||||
$this->config->expects($this->once())
|
||||
->method('isPublished')
|
||||
->willReturn(true);
|
||||
|
||||
$grapesJsBuilder->expects($this->once())
|
||||
->method('setDraftCustomMjml')
|
||||
->with(null);
|
||||
|
||||
$this->subscriber->manageEmailDraft($event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MauticPlugin\GrapesJsBuilderBundle\Tests\Unit\Model;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\EmailBundle\Entity\EmailRepository;
|
||||
use Mautic\EmailBundle\Model\EmailModel;
|
||||
use MauticPlugin\GrapesJsBuilderBundle\Entity\GrapesJsBuilder;
|
||||
use MauticPlugin\GrapesJsBuilderBundle\Entity\GrapesJsBuilderRepository;
|
||||
use MauticPlugin\GrapesJsBuilderBundle\Model\GrapesJsBuilderModel;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Routing\Router;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
class GrapesJsBuilderModelTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testAddOrEditEntityWithoutMatchingEntityAndNoRequestQuery(): void
|
||||
{
|
||||
$requestStack = new class extends RequestStack {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function getCurrentRequest(): Request
|
||||
{
|
||||
return new Request();
|
||||
}
|
||||
};
|
||||
|
||||
$emailRepository = new class extends EmailRepository {
|
||||
public int $saveEntityCallCount = 0;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function saveEntity($entity, $flush = true): void
|
||||
{
|
||||
++$this->saveEntityCallCount;
|
||||
}
|
||||
};
|
||||
|
||||
$emailModel = $this->getEmailModel($emailRepository);
|
||||
|
||||
$grapesJsBuilderRepository = new class extends GrapesJsBuilderRepository {
|
||||
public int $saveEntityCallCount = 0;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function findOneBy(array $criteria, ?array $orderBy = null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function saveEntity($entity, $flush = true): void
|
||||
{
|
||||
++$this->saveEntityCallCount;
|
||||
}
|
||||
};
|
||||
|
||||
$entityManager = new class($grapesJsBuilderRepository) extends EntityManager {
|
||||
public function __construct(
|
||||
private GrapesJsBuilderRepository $grapesJsBuilderRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getRepository($entityName)
|
||||
{
|
||||
Assert::assertSame(GrapesJsBuilder::class, $entityName);
|
||||
|
||||
return $this->grapesJsBuilderRepository; // @phpstan-ignore-line
|
||||
}
|
||||
};
|
||||
|
||||
$email = new Email();
|
||||
|
||||
$grapeJsBuilderModel = new GrapesJsBuilderModel(
|
||||
$requestStack,
|
||||
$emailModel,
|
||||
$entityManager,
|
||||
$this->createMock(CorePermissions::class),
|
||||
$this->createMock(EventDispatcherInterface::class),
|
||||
$this->createMock(Router::class),
|
||||
$this->getTranslator(),
|
||||
$this->createMock(UserHelper::class),
|
||||
$this->createMock(LoggerInterface::class),
|
||||
$this->createMock(CoreParametersHelper::class)
|
||||
);
|
||||
|
||||
$grapeJsBuilderModel->addOrEditEntity($email);
|
||||
|
||||
// Not a GrapeJs email, so we are not saving anything.
|
||||
Assert::assertSame(0, $grapesJsBuilderRepository->saveEntityCallCount);
|
||||
Assert::assertSame(0, $emailRepository->saveEntityCallCount);
|
||||
}
|
||||
|
||||
public function testAddOrEditEntityWithoutMatchingEntityAndGrapeRequestQuery(): void
|
||||
{
|
||||
$requestStack = new class extends RequestStack {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function getCurrentRequest(): Request
|
||||
{
|
||||
return new Request(
|
||||
[],
|
||||
[
|
||||
'grapesjsbuilder' => [
|
||||
'customMjml' => '</mjml>',
|
||||
],
|
||||
'emailform' => [
|
||||
'customHtml' => '</html>',
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
$emailRepository = new class extends EmailRepository {
|
||||
public int $saveEntityCallCount = 0;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Email $entity
|
||||
*/
|
||||
public function saveEntity($entity, $flush = true): void
|
||||
{
|
||||
++$this->saveEntityCallCount;
|
||||
|
||||
Assert::assertSame('</html>', $entity->getCustomHtml());
|
||||
}
|
||||
};
|
||||
|
||||
$emailModel = $this->getEmailModel($emailRepository);
|
||||
|
||||
$grapesJsBuilderRepository = new class extends GrapesJsBuilderRepository {
|
||||
public int $saveEntityCallCount = 0;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function findOneBy(array $criteria, ?array $orderBy = null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param GrapesJsBuilder $entity
|
||||
*/
|
||||
public function saveEntity($entity, $flush = true): void
|
||||
{
|
||||
++$this->saveEntityCallCount;
|
||||
|
||||
Assert::assertSame('</mjml>', $entity->getCustomMjml());
|
||||
}
|
||||
};
|
||||
|
||||
$entityManager = new class($grapesJsBuilderRepository) extends EntityManager {
|
||||
public function __construct(
|
||||
private GrapesJsBuilderRepository $grapesJsBuilderRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getRepository($entityName)
|
||||
{
|
||||
Assert::assertSame(GrapesJsBuilder::class, $entityName);
|
||||
|
||||
return $this->grapesJsBuilderRepository; // @phpstan-ignore-line
|
||||
}
|
||||
};
|
||||
|
||||
$email = new Email();
|
||||
|
||||
$grapeJsBuilderModel = new GrapesJsBuilderModel(
|
||||
$requestStack,
|
||||
$emailModel,
|
||||
$entityManager,
|
||||
$this->createMock(CorePermissions::class),
|
||||
$this->createMock(EventDispatcherInterface::class),
|
||||
$this->createMock(Router::class),
|
||||
$this->getTranslator(),
|
||||
$this->createMock(UserHelper::class),
|
||||
$this->createMock(LoggerInterface::class),
|
||||
$this->createMock(CoreParametersHelper::class)
|
||||
);
|
||||
|
||||
$grapeJsBuilderModel->addOrEditEntity($email);
|
||||
|
||||
// Saving the entities now.
|
||||
Assert::assertSame(1, $grapesJsBuilderRepository->saveEntityCallCount);
|
||||
Assert::assertSame(1, $emailRepository->saveEntityCallCount);
|
||||
}
|
||||
|
||||
private function getEmailModel(EmailRepository $emailRepository): EmailModel
|
||||
{
|
||||
return new class($emailRepository) extends EmailModel {
|
||||
public function __construct(
|
||||
private EmailRepository $emailRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getRepository(): EmailRepository
|
||||
{
|
||||
return $this->emailRepository;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private function getTranslator(): Translator
|
||||
{
|
||||
return new class extends Translator {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
grapesjsbuilder.sourceEditBtnLabel="Upravit"
|
||||
grapesjsbuilder.sourceCancelBtnLabel="Zrušit"
|
||||
grapesjsbuilder.sourceEditModalTitle="Upravit kód"
|
||||
grapesjsbuilder.deleteAssetConfirmText="Opravdu chete smazat tento soubor?"
|
||||
grapesjsbuilder.categorySectionLabel="Sekce"
|
||||
grapesjsbuilder.categoryBlockLabel="Bloky"
|
||||
grapesjsbuilder.dynamicContentBlockLabel="Dynamický obsah"
|
||||
grapesjsbuilder.dynamicContentBtnLabel="Uložit"
|
||||
grapesjsbuilder.dynamicContentModalTitle="Upravit dynamický obsah"
|
||||
grapesjsbuilder.assetManager.noAssets="Nejsou zde žádné <b>assety</b>, přetáhněte sem soubory pro nahrání."
|
||||
grapesjsbuilder.buttonBlockLabel="Tlačítko"
|
||||
grapesjsbuilder.components.names.twoColumnThirdSevens="2 Sloupce 3/7"
|
||||
grapesjsbuilder.components.names.textSectionBlkLabel="Textová část"
|
||||
grapesjsbuilder.components.names.gridItemsBlkLabel="Položky mřížky"
|
||||
grapesjsbuilder.components.names.listItemsBlkLabel="Položky seznamu"
|
||||
@@ -0,0 +1,16 @@
|
||||
grapesjsbuilder.sourceEditBtnLabel="Speichern"
|
||||
grapesjsbuilder.sourceCancelBtnLabel="Abbrechen"
|
||||
grapesjsbuilder.sourceEditModalTitle="Code bearbeiten"
|
||||
grapesjsbuilder.sourceSyntaxError="Bitte korrigieren Sie den folgenden Fehler:"
|
||||
grapesjsbuilder.deleteAssetConfirmText="Möchten Sie diese Datei wirklich löschen?"
|
||||
grapesjsbuilder.categorySectionLabel="Abschnitte"
|
||||
grapesjsbuilder.categoryBlockLabel="Blöcke"
|
||||
grapesjsbuilder.dynamicContentBlockLabel="Dynamischer Inhalt"
|
||||
grapesjsbuilder.dynamicContentBtnLabel="Speichern"
|
||||
grapesjsbuilder.dynamicContentModalTitle="Dynamischen Inhalt bearbeiten"
|
||||
grapesjsbuilder.assetManager.noAssets="Keine <b>Assets</b> hier, zum Hochladen ziehen"
|
||||
grapesjsbuilder.buttonBlockLabel="Button"
|
||||
grapesjsbuilder.components.names.twoColumnThirdSevens="2 Säulen 3/7"
|
||||
grapesjsbuilder.components.names.textSectionBlkLabel="Text Abschnitt"
|
||||
grapesjsbuilder.components.names.gridItemsBlkLabel="Raster Elemente"
|
||||
grapesjsbuilder.components.names.listItemsBlkLabel="Elemente auflisten"
|
||||
@@ -0,0 +1,23 @@
|
||||
grapesjsbuilder.sourceEditBtnLabel="Save"
|
||||
grapesjsbuilder.sourceCancelBtnLabel="Cancel"
|
||||
grapesjsbuilder.sourceEditModalTitle="Edit code"
|
||||
grapesjsbuilder.sourceSyntaxError="Please fix the following error:"
|
||||
grapesjsbuilder.deleteAssetConfirmText="Are you sure you wish to delete this file?"
|
||||
grapesjsbuilder.categorySectionLabel="Sections"
|
||||
grapesjsbuilder.categoryBlockLabel="Blocks"
|
||||
grapesjsbuilder.dynamicContentBlockLabel="Dynamic Content"
|
||||
grapesjsbuilder.dynamicContentBtnLabel="Save"
|
||||
grapesjsbuilder.dynamicContentModalTitle="Edit Dynamic Content"
|
||||
grapesjsbuilder.buttonBlockLabel="Button"
|
||||
grapesjsbuilder.builder.warning.code_mode="By opening the builder you are leaving Code Mode. The builder might change your HTML code in unexpected ways. Only proceed if you know what you are doing."
|
||||
grapesjsbuilder.components.names.oneColumn ="1 Column"
|
||||
grapesjsbuilder.components.names.twoColumn ="2 Columns"
|
||||
grapesjsbuilder.components.names.twoColumnThirdSevens="2 Columns 3/7"
|
||||
grapesjsbuilder.components.names.threeColumn = "3 Columns"
|
||||
grapesjsbuilder.components.names.textSectionBlkLabel="Text Section"
|
||||
grapesjsbuilder.components.names.gridItemsBlkLabel="Grid Items"
|
||||
grapesjsbuilder.components.names.listItemsBlkLabel="List Items"
|
||||
grapesjsbuilder.panelsViewsCommandModalTitleError="Could not request your action"
|
||||
grapesjsbuilder.panelsViewsButtonsApplyTitle="Apply changes"
|
||||
grapesjsbuilder.buttons.buttonPreview.title="Preview"
|
||||
grapesjsbuilder.buttons.buttonPreview.titleDisabled="Apply your changes to preview"
|
||||
@@ -0,0 +1,15 @@
|
||||
grapesjsbuilder.sourceEditBtnLabel="Enregistrer"
|
||||
grapesjsbuilder.sourceCancelBtnLabel="Annuler"
|
||||
grapesjsbuilder.sourceEditModalTitle="Éditer le code"
|
||||
grapesjsbuilder.deleteAssetConfirmText="Êtes-vous sûr de vouloir supprimer ce fichier ?"
|
||||
grapesjsbuilder.categorySectionLabel="Sections"
|
||||
grapesjsbuilder.categoryBlockLabel="Blocs"
|
||||
grapesjsbuilder.dynamicContentBlockLabel="Contenu dynamique"
|
||||
grapesjsbuilder.dynamicContentBtnLabel="Valider"
|
||||
grapesjsbuilder.dynamicContentModalTitle="Éditer le contenu dynamique"
|
||||
grapesjsbuilder.assetManager.noAssets="Pas <b>d'images</b>, déposez des fichiers ici pour en ajouter"
|
||||
grapesjsbuilder.buttonBlockLabel="Bouton"
|
||||
grapesjsbuilder.components.names.twoColumnThirdSevens="2 Colonnes 3/7"
|
||||
grapesjsbuilder.components.names.textSectionBlkLabel="Section de texte"
|
||||
grapesjsbuilder.components.names.gridItemsBlkLabel="Éléments de la grille"
|
||||
grapesjsbuilder.components.names.listItemsBlkLabel="Éléments de liste"
|
||||
@@ -0,0 +1,15 @@
|
||||
grapesjsbuilder.sourceEditBtnLabel="Editar"
|
||||
grapesjsbuilder.sourceCancelBtnLabel="Cancelar"
|
||||
grapesjsbuilder.sourceEditModalTitle="Editar HTML"
|
||||
grapesjsbuilder.deleteAssetConfirmText="Tem certeza que deseja remover este arquivo?"
|
||||
grapesjsbuilder.categorySectionLabel="Seções"
|
||||
grapesjsbuilder.categoryBlockLabel="Blocos"
|
||||
grapesjsbuilder.dynamicContentBlockLabel="Conteúdo Dinâmico"
|
||||
grapesjsbuilder.dynamicContentBtnLabel="Salvar"
|
||||
grapesjsbuilder.dynamicContentModalTitle="Alterar Conteúdo Dinâmico"
|
||||
grapesjsbuilder.assetManager.noAssets="Nenhum <b>assets</b> aqui, arraste para fazer Upload"
|
||||
grapesjsbuilder.buttonBlockLabel="Botón"
|
||||
grapesjsbuilder.components.names.twoColumnThirdSevens="2 Colunas 3/7"
|
||||
grapesjsbuilder.components.names.textSectionBlkLabel="Seção de Texto"
|
||||
grapesjsbuilder.components.names.gridItemsBlkLabel="Itens da grade"
|
||||
grapesjsbuilder.components.names.listItemsBlkLabel="Itens da lista"
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "mautic/grapes-js-builder-bundle",
|
||||
"description": "GrapesJS Builder with MJML support for Mautic",
|
||||
"type": "mautic-plugin",
|
||||
"keywords": [
|
||||
"mautic",
|
||||
"plugin",
|
||||
"integration"
|
||||
],
|
||||
"extra": {
|
||||
"install-directory-name": "GrapesJsBuilderBundle"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^0.11.12",
|
||||
"symplify/easy-coding-standard": "^6.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": [
|
||||
"@phpunit",
|
||||
"@fixcs",
|
||||
"@phpstan"
|
||||
],
|
||||
"quicktest": [
|
||||
"@unit"
|
||||
],
|
||||
"phpunit": "../../bin/phpunit -d memory_limit=2048M --bootstrap ../../vendor/autoload.php --configuration phpunit.xml --fail-on-warning --testsuite=all",
|
||||
"unit": "../../bin/phpunit -d memory_limit=2048M --bootstrap ../../vendor/autoload.php --configuration phpunit.xml --fail-on-warning --testsuite=unit",
|
||||
"coverage": "../../bin/phpunit -d memory_limit=2048M --bootstrap ../../vendor/autoload.php --configuration phpunit.xml --fail-on-warning --testsuite=all --coverage-text --coverage-html=Tests/Coverage",
|
||||
"phpstan": "vendor/bin/phpstan analyse --autoload-file=../../vendor/autoload.php --level=max Config Connection Entity Form Integration Migrations Sync Tests",
|
||||
"csfixer": "vendor/bin/ecs check .",
|
||||
"fixcs": "vendor/bin/ecs check . --fix"
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"require": {
|
||||
"mautic/core-lib": "^7.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
imports:
|
||||
- { resource: 'vendor/symplify/easy-coding-standard/config/set/clean-code.yaml' }
|
||||
- { resource: 'vendor/symplify/easy-coding-standard/config/set/php71.yaml' }
|
||||
- { resource: 'vendor/symplify/easy-coding-standard/config/set/symfony.yaml' }
|
||||
|
||||
services:
|
||||
PhpCsFixer\Fixer\Operator\BinaryOperatorSpacesFixer:
|
||||
align_equals: true
|
||||
align_double_arrow: true
|
||||
|
||||
parameters:
|
||||
exclude_checkers:
|
||||
- 'PhpCsFixer\Fixer\Operator\NotOperatorWithSuccessorSpaceFixer'
|
||||
- 'Symplify\CodingStandard\Fixer\Commenting\RemoveUselessDocBlockFixer'
|
||||
- 'PhpCsFixer\Fixer\Import\OrderedImportsFixer'
|
||||
exclude_files:
|
||||
- 'vendor/*'
|
||||
skip:
|
||||
PhpCsFixer\Fixer\ArraNotation\TrailingCommaInMultilineArrayFixer:
|
||||
- 'Config/config.php' # Forces us to do not use full path class names
|
||||
SlevomatCodingStandard\Sniffs\TypeHints\TypeHintDeclarationSniff.MissingParameterTypeHint:
|
||||
- 'Tests/*'
|
||||
SlevomatCodingStandard\Sniffs\TypeHints\TypeHintDeclarationSniff.MissingPropertyTypeHint:
|
||||
- 'Tests/*'
|
||||
SlevomatCodingStandard\Sniffs\Classes\UnusedPrivateElementsSniff.UnusedMethod:
|
||||
- 'Connection/Client.php'
|
||||
SlevomatCodingStandard\Sniffs\Classes\UnusedPrivateElementsSniff.WriteOnlyProperty:
|
||||
- 'Connection/Client.php'
|
||||
17030
docker-compose/mautic-setup/mautic-backup-files/docroot/plugins/GrapesJsBuilderBundle/package-lock.json
generated
Executable file
17030
docker-compose/mautic-setup/mautic-backup-files/docroot/plugins/GrapesJsBuilderBundle/package-lock.json
generated
Executable file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"name": "grapesjsbuilderbundle",
|
||||
"version": "1.0.0",
|
||||
"description": "⚠️ This Plugin is still Beta! It works great already and we're developing it actively! Please use it and report everything inside the \"Issues\" here in Github. ⚠️",
|
||||
"test": "Assets/library/js/dist/builder.js",
|
||||
"targets": {
|
||||
"test": {
|
||||
"outputFormat": "global",
|
||||
"distDir": "Assets/library/js/dist",
|
||||
"publicUrl": "./",
|
||||
"includeNodeModules": true
|
||||
}
|
||||
},
|
||||
"@parcel/transformer-css": {
|
||||
"errorRecovery": true
|
||||
},
|
||||
"scripts": {
|
||||
"build": "parcel build Assets/library/js/builder.js --dist-dir Assets/library/js/dist --public-url ./ --no-source-maps",
|
||||
"build-dev": "NODE_ENV=development parcel build Assets/library/js/builder.js --dist-dir Assets/library/js/dist --public-url ./ --no-optimize",
|
||||
"remove-build": "rm -rf ./Assets/library/js/dist/* && rm -rf .parcel-cache/*",
|
||||
"rebuild": "npm run remove-build && npm run build",
|
||||
"rebuild-dev": "npm run remove-build && npm run build-dev",
|
||||
"dev": "NODE_ENV=development parcel watch Assets/library/js/* --dist-dir Assets/library/js/dist --public-url /plugins/GrapesJsBuilderBundle/Assets/library/js/dist --hmr-hostname localhost",
|
||||
"lint": "eslint Assets/library/js/",
|
||||
"prettier": "node_modules/.bin/prettier -w Assets/library/js/",
|
||||
"prettier-check": "node_modules/.bin/prettier -c Assets/library/js/",
|
||||
"start-helloWorld": "parcel Demo/helloWorld/index.html",
|
||||
"start-mautic-full": "cp -r Demo/data dist && parcel Demo/mautic/full.html",
|
||||
"start-mautic": "cp -r Demo/data dist && parcel Demo/mautic/index.html",
|
||||
"update-mautic-preset": "rm -r node_modules/grapesjs-preset-mautic && npm install mautic/grapesjs-preset-mautic#main"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/mautic/plugin-grapesjs-builder.git"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/mautic/plugin-grapesjs-builder/issues"
|
||||
},
|
||||
"homepage": "https://github.com/mautic/plugin-grapesjs-builder#readme",
|
||||
"engines": {
|
||||
"npm": ">=8.0.0",
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"grapesjs": "^0.22.4",
|
||||
"grapesjs-blocks-basic": "^1.0.2",
|
||||
"grapesjs-component-countdown": "^1.0.2",
|
||||
"grapesjs-custom-code": "^1.0.2",
|
||||
"grapesjs-mjml": "^1.0.6",
|
||||
"grapesjs-navbar": "^1.0.2",
|
||||
"grapesjs-parser-postcss": "^1.0.3",
|
||||
"grapesjs-plugin-export": "^1.0.12",
|
||||
"grapesjs-plugin-forms": "^2.0.6",
|
||||
"grapesjs-preset-newsletter": "^1.0.2",
|
||||
"grapesjs-preset-webpage": "^1.0.3",
|
||||
"grapesjs-style-bg": "^2.0.2",
|
||||
"grapesjs-tabs": "^1.0.6",
|
||||
"grapesjs-tooltip": "^0.1.8",
|
||||
"grapesjs-touch": "^0.1.1",
|
||||
"grapesjs-tui-image-editor": "^1.0.2",
|
||||
"grapesjs-typed": "^2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.16.8",
|
||||
"@babel/core": "^7.16.7",
|
||||
"@babel/plugin-proposal-class-properties": "^7.16.7",
|
||||
"@babel/plugin-transform-runtime": "^7.16.8",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"buffer": "^6.0.3",
|
||||
"eslint": "^8.7.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-react": "^7.28.0",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"grapesjs-preset-mautic": "github:mautic/grapesjs-preset-mautic#5.2",
|
||||
"parcel": "^2.11.0",
|
||||
"prettier": "^2.5.1",
|
||||
"process": "^0.11.10",
|
||||
"sass": "^1.48.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
parameters:
|
||||
ignoreErrors:
|
||||
- '#.*|PHPUnit_Framework_MockObject_MockObject given$#'
|
||||
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<!-- http://www.phpunit.de/manual/current/en/appendixes.configuration.html -->
|
||||
<phpunit
|
||||
backupGlobals = "false"
|
||||
backupStaticAttributes = "false"
|
||||
colors = "true"
|
||||
convertErrorsToExceptions = "true"
|
||||
convertNoticesToExceptions = "true"
|
||||
convertWarningsToExceptions = "true"
|
||||
processIsolation = "false"
|
||||
stopOnFailure = "false"
|
||||
syntaxCheck = "false"
|
||||
bootstrap = "autoload.php" >
|
||||
|
||||
<testsuites>
|
||||
<testsuite name="unit">
|
||||
<directory>Tests/Unit</directory>
|
||||
</testsuite>
|
||||
<testsuite name="functional">
|
||||
<directory>Tests/Functional</directory>
|
||||
</testsuite>
|
||||
<testsuite name="all">
|
||||
<directory>Tests/Unit</directory>
|
||||
<directory>Tests/Functional</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<php>
|
||||
<server name="KERNEL_DIR" value="../../app" />
|
||||
</php>
|
||||
|
||||
<filter>
|
||||
<whitelist>
|
||||
<directory>*</directory>
|
||||
<exclude>
|
||||
<directory>Assets</directory>
|
||||
<directory>Config</directory>
|
||||
<directory>Tests</directory>
|
||||
<directory>Translations</directory>
|
||||
<directory>Views</directory>
|
||||
<directory>vendor</directory>
|
||||
</exclude>
|
||||
</whitelist>
|
||||
</filter>
|
||||
|
||||
</phpunit>
|
||||
@@ -0,0 +1,26 @@
|
||||
# Workflow name:
|
||||
name: Close Pull Requests
|
||||
|
||||
# Workflow triggers:
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
# Workflow jobs:
|
||||
jobs:
|
||||
run:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: superbrothers/close-pull-request@v3
|
||||
with:
|
||||
comment: |
|
||||
Thank you for submitting a pull request. :raised_hands:
|
||||
|
||||
We greatly appreciate your willingness to submit a contribution. However, we are not accepting pull requests against this repository, as all development happens on the [main project repository](https://github.com/mautic/mautic).
|
||||
|
||||
We kindly request that you submit this pull request against the [respective directory](https://github.com/mautic/mautic/blob/head/plugins/MauticClearbitBundle) of the main repository where we'll review and provide feedback. If this is your first Mautic contribution, be sure to read the [contributing guide](https://github.com/mautic/mautic/blob/4.x/.github/CONTRIBUTING.md) which provides guidelines and instructions for submitting contributions.
|
||||
|
||||
Thank you again, and we look forward to receiving your contribution! :smiley:
|
||||
|
||||
Best,
|
||||
The Mautic team
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 8.1 KiB |
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'name' => 'Clearbit',
|
||||
'description' => 'Enables integration with Clearbit for contact and company lookup',
|
||||
'version' => '1.0',
|
||||
'author' => 'Werner Garcia',
|
||||
|
||||
'routes' => [
|
||||
'public' => [
|
||||
'mautic_plugin_clearbit_index' => [
|
||||
'path' => '/clearbit/callback',
|
||||
'controller' => 'MauticPlugin\MauticClearbitBundle\Controller\PublicController::callbackAction',
|
||||
],
|
||||
],
|
||||
'main' => [
|
||||
'mautic_plugin_clearbit_action' => [
|
||||
'path' => '/clearbit/{objectAction}/{objectId}',
|
||||
'controller' => 'MauticPlugin\MauticClearbitBundle\Controller\ClearbitController::executeAction',
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'services' => [
|
||||
'others' => [
|
||||
'mautic.plugin.clearbit.lookup_helper' => [
|
||||
'class' => MauticPlugin\MauticClearbitBundle\Helper\LookupHelper::class,
|
||||
'arguments' => [
|
||||
'mautic.helper.integration',
|
||||
'mautic.helper.user',
|
||||
'monolog.logger.mautic',
|
||||
'mautic.lead.model.lead',
|
||||
'mautic.lead.model.company',
|
||||
],
|
||||
],
|
||||
],
|
||||
'integrations' => [
|
||||
'mautic.integration.clearbit' => [
|
||||
'class' => MauticPlugin\MauticClearbitBundle\Integration\ClearbitIntegration::class,
|
||||
'arguments' => [
|
||||
'event_dispatcher',
|
||||
'mautic.helper.cache_storage',
|
||||
'doctrine.orm.entity_manager',
|
||||
'request_stack',
|
||||
'router',
|
||||
'translator',
|
||||
'monolog.logger.mautic',
|
||||
'mautic.helper.encryption',
|
||||
'mautic.lead.model.lead',
|
||||
'mautic.lead.model.company',
|
||||
'mautic.helper.paths',
|
||||
'mautic.core.model.notification',
|
||||
'mautic.lead.model.field',
|
||||
'mautic.plugin.model.integration_entity',
|
||||
'mautic.lead.model.dnc',
|
||||
'mautic.lead.field.fields_with_unique_identifier',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Mautic\CoreBundle\DependencyInjection\MauticCoreExtension;
|
||||
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
||||
|
||||
return function (ContainerConfigurator $configurator): void {
|
||||
$services = $configurator->services()
|
||||
->defaults()
|
||||
->autowire()
|
||||
->autoconfigure()
|
||||
->public();
|
||||
|
||||
$excludes = [
|
||||
'Services',
|
||||
];
|
||||
|
||||
$services->load('MauticPlugin\\MauticClearbitBundle\\', '../')
|
||||
->exclude('../{'.implode(',', array_merge(MauticCoreExtension::DEFAULT_EXCLUDES, $excludes)).'}');
|
||||
};
|
||||
@@ -0,0 +1,520 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticClearbitBundle\Controller;
|
||||
|
||||
use Mautic\FormBundle\Controller\FormController;
|
||||
use Mautic\LeadBundle\Entity\Company;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use MauticPlugin\MauticClearbitBundle\Form\Type\BatchLookupType;
|
||||
use MauticPlugin\MauticClearbitBundle\Form\Type\LookupType;
|
||||
use MauticPlugin\MauticClearbitBundle\Helper\LookupHelper;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ClearbitController extends FormController
|
||||
{
|
||||
/**
|
||||
* @param string $objectId
|
||||
*
|
||||
* @return JsonResponse
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function lookupPersonAction(Request $request, LookupHelper $lookupHelper, $objectId = '')
|
||||
{
|
||||
if ('POST' === $request->getMethod()) {
|
||||
$data = $request->request->all()['clearbit_lookup'] ?? [];
|
||||
$objectId = $data['objectId'];
|
||||
}
|
||||
/** @var \Mautic\LeadBundle\Model\LeadModel $model */
|
||||
$model = $this->getModel('lead');
|
||||
$lead = $model->getEntity($objectId);
|
||||
|
||||
if (!$this->security->hasEntityAccess(
|
||||
'lead:leads:editown',
|
||||
'lead:leads:editother',
|
||||
$lead->getPermissionUser()
|
||||
)
|
||||
) {
|
||||
$this->addFlashMessage(
|
||||
$this->translator->trans('mautic.plugin.clearbit.forbidden'),
|
||||
[],
|
||||
'error'
|
||||
);
|
||||
|
||||
return new JsonResponse(
|
||||
[
|
||||
'closeModal' => true,
|
||||
'flashes' => $this->getFlashContent(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
if ('GET' === $request->getMethod()) {
|
||||
$route = $this->generateUrl(
|
||||
'mautic_plugin_clearbit_action',
|
||||
[
|
||||
'objectAction' => 'lookupPerson',
|
||||
]
|
||||
);
|
||||
|
||||
return $this->delegateView(
|
||||
[
|
||||
'viewParameters' => [
|
||||
'form' => $this->createForm(
|
||||
LookupType::class,
|
||||
[
|
||||
'objectId' => $objectId,
|
||||
],
|
||||
[
|
||||
'action' => $route,
|
||||
]
|
||||
)->createView(),
|
||||
'lookupItem' => $lead->getEmail(),
|
||||
],
|
||||
'contentTemplate' => '@MauticClearbit/Clearbit/lookup.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_contact_index',
|
||||
'mauticContent' => 'lead',
|
||||
'route' => $route,
|
||||
],
|
||||
]
|
||||
);
|
||||
} else {
|
||||
if ('POST' === $request->getMethod()) {
|
||||
try {
|
||||
$lookupHelper->lookupContact($lead, array_key_exists('notify', $data));
|
||||
$this->addFlashMessage(
|
||||
'mautic.lead.batch_leads_affected',
|
||||
[
|
||||
'%count%' => 1,
|
||||
]
|
||||
);
|
||||
} catch (\Exception $ex) {
|
||||
$this->addFlashMessage(
|
||||
$ex->getMessage(),
|
||||
[],
|
||||
'error'
|
||||
);
|
||||
}
|
||||
|
||||
return new JsonResponse(
|
||||
[
|
||||
'closeModal' => true,
|
||||
'flashes' => $this->getFlashContent(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return new Response('Bad Request', 400);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return JsonResponse
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function batchLookupPersonAction(Request $request, LookupHelper $lookupHelper)
|
||||
{
|
||||
/** @var \Mautic\LeadBundle\Model\LeadModel $model */
|
||||
$model = $this->getModel('lead');
|
||||
if ('GET' === $request->getMethod()) {
|
||||
$data = $request->query->all()['clearbit_batch_lookup'] ?? [];
|
||||
} else {
|
||||
$data = $request->request->all()['clearbit_batch_lookup'] ?? [];
|
||||
}
|
||||
|
||||
$entities = [];
|
||||
if (array_key_exists('ids', $data)) {
|
||||
$ids = $data['ids'];
|
||||
|
||||
if (!is_array($ids)) {
|
||||
$ids = json_decode($ids, true);
|
||||
}
|
||||
|
||||
if (is_array($ids) && count($ids)) {
|
||||
$entities = $model->getEntities(
|
||||
[
|
||||
'filter' => [
|
||||
'force' => [
|
||||
[
|
||||
'column' => 'l.id',
|
||||
'expr' => 'in',
|
||||
'value' => $ids,
|
||||
],
|
||||
],
|
||||
],
|
||||
'ignore_paginator' => true,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$lookupEmails = [];
|
||||
if ($count = count($entities)) {
|
||||
/** @var Lead $lead */
|
||||
foreach ($entities as $lead) {
|
||||
if ($this->security->hasEntityAccess(
|
||||
'lead:leads:editown',
|
||||
'lead:leads:editother',
|
||||
$lead->getPermissionUser()
|
||||
)
|
||||
&& $lead->getEmail()
|
||||
) {
|
||||
$lookupEmails[$lead->getId()] = $lead->getEmail();
|
||||
}
|
||||
}
|
||||
|
||||
$count = count($lookupEmails);
|
||||
}
|
||||
|
||||
if (0 === $count) {
|
||||
$this->addFlashMessage(
|
||||
$this->translator->trans('mautic.plugin.clearbit.empty'),
|
||||
[],
|
||||
'error'
|
||||
);
|
||||
|
||||
return new JsonResponse(
|
||||
[
|
||||
'closeModal' => true,
|
||||
'flashes' => $this->getFlashContent(),
|
||||
]
|
||||
);
|
||||
} else {
|
||||
if ($count > 20) {
|
||||
$this->addFlashMessage(
|
||||
$this->translator->trans('mautic.plugin.clearbit.toomany'),
|
||||
[],
|
||||
'error'
|
||||
);
|
||||
|
||||
return new JsonResponse(
|
||||
[
|
||||
'closeModal' => true,
|
||||
'flashes' => $this->getFlashContent(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
if ('GET' === $request->getMethod()) {
|
||||
$route = $this->generateUrl(
|
||||
'mautic_plugin_clearbit_action',
|
||||
[
|
||||
'objectAction' => 'batchLookupPerson',
|
||||
]
|
||||
);
|
||||
|
||||
return $this->delegateView(
|
||||
[
|
||||
'viewParameters' => [
|
||||
'form' => $this->createForm(
|
||||
BatchLookupType::class,
|
||||
[],
|
||||
[
|
||||
'action' => $route,
|
||||
]
|
||||
)->createView(),
|
||||
'lookupItems' => array_values($lookupEmails),
|
||||
],
|
||||
'contentTemplate' => '@MauticClearbit/Clearbit/batchLookup.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_contact_index',
|
||||
'mauticContent' => 'leadBatch',
|
||||
'route' => $route,
|
||||
],
|
||||
]
|
||||
);
|
||||
} else {
|
||||
if ('POST' === $request->getMethod()) {
|
||||
$notify = array_key_exists('notify', $data);
|
||||
foreach ($lookupEmails as $id => $lookupEmail) {
|
||||
if ($lead = $model->getEntity($id)) {
|
||||
try {
|
||||
$lookupHelper->lookupContact($lead, $notify);
|
||||
} catch (\Exception $ex) {
|
||||
$this->addFlashMessage(
|
||||
$ex->getMessage(),
|
||||
[],
|
||||
'error'
|
||||
);
|
||||
--$count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($count) {
|
||||
$this->addFlashMessage(
|
||||
'mautic.lead.batch_leads_affected',
|
||||
[
|
||||
'%count%' => $count,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return new JsonResponse(
|
||||
[
|
||||
'closeModal' => true,
|
||||
'flashes' => $this->getFlashContent(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return new Response('Bad Request', 400);
|
||||
}
|
||||
|
||||
/***************** COMPANY ***********************/
|
||||
|
||||
/**
|
||||
* @param string $objectId
|
||||
*
|
||||
* @return JsonResponse
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function lookupCompanyAction(Request $request, LookupHelper $lookupHelper, $objectId = '')
|
||||
{
|
||||
if ('POST' === $request->getMethod()) {
|
||||
$data = $request->request->all()['clearbit_lookup'] ?? [];
|
||||
$objectId = $data['objectId'];
|
||||
}
|
||||
/** @var \Mautic\LeadBundle\Model\CompanyModel $model */
|
||||
$model = $this->getModel('lead.company');
|
||||
/** @var Company $company */
|
||||
$company = $model->getEntity($objectId);
|
||||
|
||||
if ('GET' === $request->getMethod()) {
|
||||
$route = $this->generateUrl(
|
||||
'mautic_plugin_clearbit_action',
|
||||
[
|
||||
'objectAction' => 'lookupCompany',
|
||||
]
|
||||
);
|
||||
|
||||
$website = $company->getFieldValue('companywebsite');
|
||||
|
||||
if (!$website) {
|
||||
$this->addFlashMessage(
|
||||
$this->translator->trans('mautic.plugin.clearbit.compempty'),
|
||||
[],
|
||||
'error'
|
||||
);
|
||||
|
||||
return new JsonResponse(
|
||||
[
|
||||
'closeModal' => true,
|
||||
'flashes' => $this->getFlashContent(),
|
||||
]
|
||||
);
|
||||
}
|
||||
$parse = parse_url($website);
|
||||
|
||||
return $this->delegateView(
|
||||
[
|
||||
'viewParameters' => [
|
||||
'form' => $this->createForm(
|
||||
LookupType::class,
|
||||
[
|
||||
'objectId' => $objectId,
|
||||
],
|
||||
[
|
||||
'action' => $route,
|
||||
]
|
||||
)->createView(),
|
||||
'lookupItem' => $parse['host'],
|
||||
],
|
||||
'contentTemplate' => '@MauticClearbit/Clearbit/lookup.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_company_index',
|
||||
'mauticContent' => 'company',
|
||||
'route' => $route,
|
||||
],
|
||||
]
|
||||
);
|
||||
} else {
|
||||
if ('POST' === $request->getMethod()) {
|
||||
try {
|
||||
$lookupHelper->lookupCompany($company, array_key_exists('notify', $data));
|
||||
$this->addFlashMessage(
|
||||
'mautic.company.batch_companies_affected',
|
||||
[
|
||||
'%count%' => 1,
|
||||
]
|
||||
);
|
||||
} catch (\Exception $ex) {
|
||||
$this->addFlashMessage(
|
||||
$ex->getMessage(),
|
||||
[],
|
||||
'error'
|
||||
);
|
||||
}
|
||||
|
||||
return new JsonResponse(
|
||||
[
|
||||
'closeModal' => true,
|
||||
'flashes' => $this->getFlashContent(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return new Response('Bad Request', 400);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return JsonResponse
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function batchLookupCompanyAction(Request $request, LookupHelper $lookupHelper)
|
||||
{
|
||||
/** @var \Mautic\LeadBundle\Model\CompanyModel $model */
|
||||
$model = $this->getModel('lead.company');
|
||||
if ('GET' === $request->getMethod()) {
|
||||
$data = $request->query->all()['clearbit_batch_lookup'] ?? [];
|
||||
} else {
|
||||
$data = $request->request->all()['clearbit_batch_lookup'] ?? [];
|
||||
}
|
||||
|
||||
$entities = [];
|
||||
if (array_key_exists('ids', $data)) {
|
||||
$ids = $data['ids'];
|
||||
|
||||
if (!is_array($ids)) {
|
||||
$ids = json_decode($ids, true);
|
||||
}
|
||||
|
||||
if (is_array($ids) && count($ids)) {
|
||||
$entities = $model->getEntities(
|
||||
[
|
||||
'filter' => [
|
||||
'force' => [
|
||||
[
|
||||
'column' => 'comp.id',
|
||||
'expr' => 'in',
|
||||
'value' => $ids,
|
||||
],
|
||||
],
|
||||
],
|
||||
'ignore_paginator' => true,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$lookupWebsites = [];
|
||||
if ($count = count($entities)) {
|
||||
/** @var Company $company */
|
||||
foreach ($entities as $company) {
|
||||
if ($company->getFieldValue('companywebsite')) {
|
||||
$website = $company->getFieldValue('companywebsite');
|
||||
$parse = parse_url($website);
|
||||
if (!isset($parse['host'])) {
|
||||
continue;
|
||||
}
|
||||
$lookupWebsites[$company->getId()] = $parse['host'];
|
||||
}
|
||||
}
|
||||
|
||||
$count = count($lookupWebsites);
|
||||
}
|
||||
|
||||
if (0 === $count) {
|
||||
$this->addFlashMessage(
|
||||
$this->translator->trans('mautic.plugin.clearbit.compempty'),
|
||||
[],
|
||||
'error'
|
||||
);
|
||||
|
||||
return new JsonResponse(
|
||||
[
|
||||
'closeModal' => true,
|
||||
'flashes' => $this->getFlashContent(),
|
||||
]
|
||||
);
|
||||
} else {
|
||||
if ($count > 20) {
|
||||
$this->addFlashMessage(
|
||||
$this->translator->trans('mautic.plugin.clearbit.comptoomany'),
|
||||
[],
|
||||
'error'
|
||||
);
|
||||
|
||||
return new JsonResponse(
|
||||
[
|
||||
'closeModal' => true,
|
||||
'flashes' => $this->getFlashContent(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
if ('GET' === $request->getMethod()) {
|
||||
$route = $this->generateUrl(
|
||||
'mautic_plugin_clearbit_action',
|
||||
[
|
||||
'objectAction' => 'batchLookupCompany',
|
||||
]
|
||||
);
|
||||
|
||||
return $this->delegateView(
|
||||
[
|
||||
'viewParameters' => [
|
||||
'form' => $this->createForm(
|
||||
BatchLookupType::class,
|
||||
[],
|
||||
[
|
||||
'action' => $route,
|
||||
]
|
||||
)->createView(),
|
||||
'lookupItems' => array_values($lookupWebsites),
|
||||
],
|
||||
'contentTemplate' => '@MauticClearbit/Clearbit/batchLookup.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_company_index',
|
||||
'mauticContent' => 'companyBatch',
|
||||
'route' => $route,
|
||||
],
|
||||
]
|
||||
);
|
||||
} else {
|
||||
if ('POST' === $request->getMethod()) {
|
||||
$notify = array_key_exists('notify', $data);
|
||||
foreach ($lookupWebsites as $id => $lookupWebsite) {
|
||||
if ($company = $model->getEntity($id)) {
|
||||
try {
|
||||
$lookupHelper->lookupCompany($company, $notify);
|
||||
} catch (\Exception $ex) {
|
||||
$this->addFlashMessage(
|
||||
$ex->getMessage(),
|
||||
[],
|
||||
'error'
|
||||
);
|
||||
--$count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($count) {
|
||||
$this->addFlashMessage(
|
||||
'mautic.company.batch_companies_affected',
|
||||
[
|
||||
'%count%' => $count,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return new JsonResponse(
|
||||
[
|
||||
'closeModal' => true,
|
||||
'flashes' => $this->getFlashContent(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return new Response('Bad Request', 400);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticClearbitBundle\Controller;
|
||||
|
||||
use Mautic\FormBundle\Controller\FormController;
|
||||
use Mautic\LeadBundle\Entity\Company;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Mautic\UserBundle\Model\UserModel;
|
||||
use MauticPlugin\MauticClearbitBundle\Helper\LookupHelper;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class PublicController extends FormController
|
||||
{
|
||||
/**
|
||||
* Write a notification.
|
||||
*
|
||||
* @param string $message Message of the notification
|
||||
* @param string $header Header for message
|
||||
* @param string $iconClass CSS class for the icon (e.g. ri-eye-line)
|
||||
* @param User|null $user User object; defaults to current user
|
||||
*/
|
||||
public function addNewNotification($message, $header, $iconClass, User $user): void
|
||||
{
|
||||
/** @var \Mautic\CoreBundle\Model\NotificationModel $notificationModel */
|
||||
$notificationModel = $this->getModel('core.notification');
|
||||
$notificationModel->addNotification($message, 'FullContact', false, $header, $iconClass, null, $user);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function callbackAction(Request $request, LoggerInterface $mauticLogger, LookupHelper $lookupHelper): Response
|
||||
{
|
||||
if (!$request->request->has('body') || !$request->request->has('id')
|
||||
|| !$request->request->has('type')
|
||||
|| !$request->request->has('status')
|
||||
|| 200 !== $request->request->get('status')
|
||||
) {
|
||||
$mauticLogger->log('error', 'ERROR on Clearbit callback: Malformed request variables: '.json_encode($request->request->all(), JSON_PRETTY_PRINT));
|
||||
|
||||
return new Response('ERROR');
|
||||
}
|
||||
|
||||
/** @var array $result */
|
||||
$result = $request->request->all()['body'] ?? [];
|
||||
$oid = $request->request->get('id');
|
||||
$validatedRequest = $lookupHelper->validateRequest($oid, $request->request->get('type'));
|
||||
|
||||
if (!$validatedRequest || !is_array($result)) {
|
||||
$mauticLogger->log('error', 'ERROR on Clearbit callback: Wrong body or id in request: id='.$oid.' body='.json_encode($result, JSON_PRETTY_PRINT));
|
||||
|
||||
return new Response('ERROR');
|
||||
}
|
||||
|
||||
$notify = $validatedRequest['notify'];
|
||||
|
||||
try {
|
||||
if ('person' === $request->request->get('type')) {
|
||||
/** @var \Mautic\LeadBundle\Model\LeadModel $model */
|
||||
$model = $this->getModel('lead');
|
||||
/** @var Lead $lead */
|
||||
$lead = $validatedRequest['entity'];
|
||||
$currFields = $lead->getFields(true);
|
||||
$mauticLogger->log('debug', 'CURRFIELDS: '.var_export($currFields, true));
|
||||
|
||||
$loc = [];
|
||||
if (array_key_exists('geo', $result)) {
|
||||
$loc = $result['geo'];
|
||||
}
|
||||
|
||||
$data = [];
|
||||
|
||||
foreach ([
|
||||
'facebook' => 'http://www.facebook.com/',
|
||||
'linkedin' => 'http://www.linkedin.com/',
|
||||
'twitter' => 'http://www.twitter.com/',
|
||||
] as $p => $u) {
|
||||
foreach ($result as $type => $socialProfile) {
|
||||
if ($type === $p && empty($currFields[$p]['value'])) {
|
||||
$data[$p] = (array_key_exists('handle', $socialProfile) && $socialProfile['handle']) ? $u.$socialProfile['handle'] : '';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists('name', $result)
|
||||
&& array_key_exists(
|
||||
'familyName',
|
||||
$result['name']
|
||||
)
|
||||
&& empty($currFields['lastname']['value'])
|
||||
) {
|
||||
$data['lastname'] = $result['name']['familyName'];
|
||||
}
|
||||
|
||||
if (array_key_exists('name', $result)
|
||||
&& array_key_exists(
|
||||
'givenName',
|
||||
$result['name']
|
||||
)
|
||||
&& empty($currFields['firstname']['value'])
|
||||
) {
|
||||
$data['firstname'] = $result['name']['givenName'];
|
||||
}
|
||||
|
||||
if (array_key_exists('site', $result) && empty($currFields['website']['value'])) {
|
||||
$data['website'] = $result['site'];
|
||||
}
|
||||
|
||||
if (array_key_exists('employment', $result)
|
||||
&& array_key_exists(
|
||||
'name',
|
||||
$result['employment']
|
||||
)
|
||||
&& empty($currFields['company']['value'])
|
||||
) {
|
||||
$data['company'] = $result['employment']['name'];
|
||||
}
|
||||
|
||||
if (array_key_exists('employment', $result)
|
||||
&& array_key_exists(
|
||||
'title',
|
||||
$result['employment']
|
||||
)
|
||||
&& empty($currFields['position']['value'])
|
||||
) {
|
||||
$data['position'] = $result['employment']['title'];
|
||||
}
|
||||
|
||||
if (array_key_exists('city', $loc) && empty($currFields['city']['value'])) {
|
||||
$data['city'] = $loc['city'];
|
||||
}
|
||||
if (array_key_exists('state', $loc) && empty($currFields['state']['value'])) {
|
||||
$data['state'] = $loc['state'];
|
||||
}
|
||||
|
||||
if (array_key_exists('country', $loc) && empty($currFields['country']['value'])) {
|
||||
$data['country'] = $loc['country'];
|
||||
}
|
||||
|
||||
$mauticLogger->log('debug', 'SETTING FIELDS: '.print_r($data, true));
|
||||
|
||||
// Unset the nonce so that it's not used again
|
||||
$socialCache = $lead->getSocialCache();
|
||||
unset($socialCache['clearbit']['nonce']);
|
||||
$lead->setSocialCache($socialCache);
|
||||
|
||||
$model->setFieldValues($lead, $data);
|
||||
$model->saveEntity($lead);
|
||||
|
||||
if ($notify && (!isset($lead->imported) || !$lead->imported)) {
|
||||
/** @var UserModel $userModel */
|
||||
$userModel = $this->getModel('user');
|
||||
if ($user = $userModel->getEntity($notify)) {
|
||||
$this->addNewNotification(
|
||||
sprintf($this->translator->trans('mautic.plugin.clearbit.contact_retrieved'), $lead->getEmail()),
|
||||
'Clearbit Plugin',
|
||||
'ri-search-line',
|
||||
$user
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/****************** COMPANY STUFF *********************/
|
||||
|
||||
if ('company' === $request->request->get('type')) {
|
||||
/** @var \Mautic\LeadBundle\Model\CompanyModel $model */
|
||||
$model = $this->getModel('lead.company');
|
||||
/** @var Company $company */
|
||||
$company = $validatedRequest['entity'];
|
||||
$currFields = $company->getFields(true);
|
||||
|
||||
$loc = [];
|
||||
if (array_key_exists('geo', $result)) {
|
||||
$loc = $result['geo'];
|
||||
}
|
||||
|
||||
$data = [];
|
||||
|
||||
if (array_key_exists('streetNumber', $loc)
|
||||
&& array_key_exists(
|
||||
'streetName',
|
||||
$loc
|
||||
)
|
||||
&& empty($currFields['companyaddress1']['value'])
|
||||
) {
|
||||
$data['companyaddress1'] = $loc['streetNumber'].' '.$loc['streetName'];
|
||||
}
|
||||
|
||||
if (array_key_exists('city', $loc) && empty($currFields['companycity']['value'])) {
|
||||
$data['companycity'] = $loc['city'];
|
||||
}
|
||||
|
||||
if (array_key_exists('metrics', $result)
|
||||
&& array_key_exists(
|
||||
'employees',
|
||||
$result['metrics']
|
||||
)
|
||||
&& empty($currFields['companynumber_of_employees']['value'])
|
||||
) {
|
||||
$data['companynumber_of_employees'] = $result['metrics']['employees'];
|
||||
}
|
||||
|
||||
if (array_key_exists('description', $result) && empty($currFields['companydescription']['value'])) {
|
||||
$data['companydescription'] = $result['description'];
|
||||
}
|
||||
|
||||
if (array_key_exists('phone', $result) && empty($currFields['companyphone']['value'])) {
|
||||
$data['companyphone'] = $result['phone'];
|
||||
}
|
||||
|
||||
if (array_key_exists('site', $result)
|
||||
&& array_key_exists(
|
||||
'emailAddresses',
|
||||
$result['site']
|
||||
)
|
||||
&& count($result['site']['emailAddresses'])
|
||||
&& empty($currFields['companyemail']['value'])
|
||||
) {
|
||||
$data['companyemail'] = $result['site']['emailAddresses'][0];
|
||||
}
|
||||
|
||||
if (array_key_exists('country', $loc) && empty($currFields['companycountry']['value'])) {
|
||||
$data['companycountry'] = $loc['country'];
|
||||
}
|
||||
|
||||
if (array_key_exists('state', $loc) && empty($currFields['companystate']['value'])) {
|
||||
$data['companystate'] = $loc['state'];
|
||||
}
|
||||
|
||||
$mauticLogger->log('debug', 'SETTING FIELDS: '.print_r($data, true));
|
||||
|
||||
// Unset the nonce so that it's not used again
|
||||
$socialCache = $company->getSocialCache();
|
||||
unset($socialCache['clearbit']['nonce']);
|
||||
$company->setSocialCache($socialCache);
|
||||
|
||||
$model->setFieldValues($company, $data);
|
||||
$model->saveEntity($company);
|
||||
|
||||
if ($notify) {
|
||||
/** @var UserModel $userModel */
|
||||
$userModel = $this->getModel('user');
|
||||
if ($user = $userModel->getEntity($notify)) {
|
||||
$this->addNewNotification(
|
||||
sprintf($this->translator->trans('mautic.plugin.clearbit.company_retrieved'), $company->getName()),
|
||||
'Clearbit Plugin',
|
||||
'ri-search-line',
|
||||
$user
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Exception $ex) {
|
||||
$mauticLogger->log('error', 'ERROR on Clearbit callback: '.$ex->getMessage());
|
||||
try {
|
||||
if ($notify) {
|
||||
/** @var UserModel $userModel */
|
||||
$userModel = $this->getModel('user');
|
||||
if ($user = $userModel->getEntity($notify)) {
|
||||
$this->addNewNotification(
|
||||
sprintf(
|
||||
$this->translator->trans('mautic.plugin.clearbit.unable'),
|
||||
$ex->getMessage()
|
||||
),
|
||||
'Clearbit Plugin',
|
||||
'ri-error-warning-line',
|
||||
$user
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (\Exception $ex2) {
|
||||
$mauticLogger->log('error', 'Clearbit: '.$ex2->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return new Response('OK');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MauticPlugin\MauticClearbitBundle\DependencyInjection;
|
||||
|
||||
use Symfony\Component\Config\FileLocator;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Extension\Extension;
|
||||
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
|
||||
|
||||
class MauticClearbitExtension extends Extension
|
||||
{
|
||||
/**
|
||||
* @param mixed[] $configs
|
||||
*/
|
||||
public function load(array $configs, ContainerBuilder $container): void
|
||||
{
|
||||
$loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../Config'));
|
||||
$loader->load('services.php');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticClearbitBundle\EventListener;
|
||||
|
||||
use Mautic\CoreBundle\CoreEvents;
|
||||
use Mautic\CoreBundle\Event\CustomButtonEvent;
|
||||
use Mautic\CoreBundle\Twig\Helper\ButtonHelper;
|
||||
use Mautic\PluginBundle\Helper\IntegrationHelper;
|
||||
use MauticPlugin\MauticClearbitBundle\Integration\ClearbitIntegration;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class ButtonSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private IntegrationHelper $helper,
|
||||
private TranslatorInterface $translator,
|
||||
private RouterInterface $router,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
CoreEvents::VIEW_INJECT_CUSTOM_BUTTONS => ['injectViewButtons', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function injectViewButtons(CustomButtonEvent $event): void
|
||||
{
|
||||
/** @var ClearbitIntegration $myIntegration */
|
||||
$myIntegration = $this->helper->getIntegrationObject('Clearbit');
|
||||
|
||||
if (false === $myIntegration || !$myIntegration->getIntegrationSettings()->getIsPublished()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (str_starts_with($event->getRoute(), 'mautic_contact_')) {
|
||||
$event->addButton(
|
||||
[
|
||||
'attr' => [
|
||||
'class' => 'btn btn-ghost btn-sm btn-nospin',
|
||||
'data-toggle' => 'ajaxmodal',
|
||||
'data-target' => '#MauticSharedModal',
|
||||
'onclick' => 'this.href=\''.
|
||||
$this->router->generate(
|
||||
'mautic_plugin_clearbit_action',
|
||||
['objectAction' => 'batchLookupPerson']
|
||||
).
|
||||
'?\' + mQuery.param({\'clearbit_batch_lookup\':{\'ids\':JSON.parse(Mautic.getCheckedListIds(false, true))}});return true;',
|
||||
'data-header' => $this->translator->trans('mautic.plugin.clearbit.button.caption'),
|
||||
],
|
||||
'btnText' => $this->translator->trans('mautic.plugin.clearbit.button.caption'),
|
||||
'iconClass' => 'ri-search-line',
|
||||
],
|
||||
ButtonHelper::LOCATION_BULK_ACTIONS
|
||||
);
|
||||
|
||||
if ($event->getItem()) {
|
||||
$lookupContactButton = [
|
||||
'attr' => [
|
||||
'data-toggle' => 'ajaxmodal',
|
||||
'data-target' => '#MauticSharedModal',
|
||||
'data-header' => $this->translator->trans(
|
||||
'mautic.plugin.clearbit.lookup.header',
|
||||
['%item%' => $event->getItem()->getEmail()]
|
||||
),
|
||||
'href' => $this->router->generate(
|
||||
'mautic_plugin_clearbit_action',
|
||||
['objectId' => $event->getItem()->getId(), 'objectAction' => 'lookupPerson']
|
||||
),
|
||||
],
|
||||
'btnText' => $this->translator->trans('mautic.plugin.clearbit.button.caption'),
|
||||
'iconClass' => 'ri-search-line',
|
||||
];
|
||||
|
||||
$event->addButton(
|
||||
$lookupContactButton,
|
||||
ButtonHelper::LOCATION_PAGE_ACTIONS,
|
||||
['mautic_contact_action', ['objectAction' => 'view']]
|
||||
);
|
||||
|
||||
$event->addButton(
|
||||
$lookupContactButton,
|
||||
ButtonHelper::LOCATION_LIST_ACTIONS,
|
||||
'mautic_contact_index'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (str_starts_with($event->getRoute(), 'mautic_company_')) {
|
||||
$event->addButton(
|
||||
[
|
||||
'attr' => [
|
||||
'class' => 'btn btn-ghost btn-sm btn-nospin',
|
||||
'data-toggle' => 'ajaxmodal',
|
||||
'data-target' => '#MauticSharedModal',
|
||||
'onclick' => 'this.href=\''.
|
||||
$this->router->generate(
|
||||
'mautic_plugin_clearbit_action',
|
||||
['objectAction' => 'batchLookupCompany']
|
||||
).
|
||||
'?\' + mQuery.param({\'clearbit_batch_lookup\':{\'ids\':JSON.parse(Mautic.getCheckedListIds(false, true))}});return true;',
|
||||
'data-header' => $this->translator->trans(
|
||||
'mautic.plugin.clearbit.button.caption'
|
||||
),
|
||||
],
|
||||
'btnText' => $this->translator->trans('mautic.plugin.clearbit.button.caption'),
|
||||
'iconClass' => 'ri-search-line',
|
||||
],
|
||||
ButtonHelper::LOCATION_BULK_ACTIONS
|
||||
);
|
||||
|
||||
if ($event->getItem()) {
|
||||
$lookupCompanyButton = [
|
||||
'attr' => [
|
||||
'data-toggle' => 'ajaxmodal',
|
||||
'data-target' => '#MauticSharedModal',
|
||||
'data-header' => $this->translator->trans(
|
||||
'mautic.plugin.clearbit.lookup.header',
|
||||
['%item%' => $event->getItem()->getName()]
|
||||
),
|
||||
'href' => $this->router->generate(
|
||||
'mautic_plugin_clearbit_action',
|
||||
['objectId' => $event->getItem()->getId(), 'objectAction' => 'lookupCompany']
|
||||
),
|
||||
],
|
||||
'btnText' => $this->translator->trans('mautic.plugin.clearbit.button.caption'),
|
||||
'iconClass' => 'ri-search-line',
|
||||
];
|
||||
|
||||
$event->addButton(
|
||||
$lookupCompanyButton,
|
||||
ButtonHelper::LOCATION_LIST_ACTIONS,
|
||||
'mautic_company_index'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticClearbitBundle\EventListener;
|
||||
|
||||
use Mautic\LeadBundle\Event\CompanyEvent;
|
||||
use Mautic\LeadBundle\Event\LeadEvent;
|
||||
use Mautic\LeadBundle\LeadEvents;
|
||||
use MauticPlugin\MauticClearbitBundle\Helper\LookupHelper;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class LeadSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private LookupHelper $lookupHelper,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
LeadEvents::LEAD_POST_SAVE => ['leadPostSave', 0],
|
||||
LeadEvents::COMPANY_POST_SAVE => ['companyPostSave', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function leadPostSave(LeadEvent $event): void
|
||||
{
|
||||
$this->lookupHelper->lookupContact($event->getLead(), true, true);
|
||||
}
|
||||
|
||||
public function companyPostSave(CompanyEvent $event): void
|
||||
{
|
||||
$this->lookupHelper->lookupCompany($event->getCompany(), true, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticClearbitBundle\Form\Type;
|
||||
|
||||
use Mautic\CoreBundle\Form\Type\FormButtonsType;
|
||||
use Mautic\CoreBundle\Form\Type\YesNoButtonGroupType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<array<mixed>>
|
||||
*/
|
||||
class BatchLookupType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add('ids', HiddenType::class);
|
||||
|
||||
$builder->add(
|
||||
'buttons',
|
||||
FormButtonsType::class,
|
||||
[
|
||||
'apply_text' => false,
|
||||
'save_text' => 'mautic.core.form.submit',
|
||||
'cancel_onclick' => 'javascript:void(0);',
|
||||
'cancel_attr' => [
|
||||
'data-dismiss' => 'modal',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'notify',
|
||||
YesNoButtonGroupType::class,
|
||||
[
|
||||
'label' => 'mautic.plugin.clearbit.notify',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
],
|
||||
'data' => true,
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
if (!empty($options['action'])) {
|
||||
$builder->setAction($options['action']);
|
||||
}
|
||||
}
|
||||
|
||||
public function getBlockPrefix(): string
|
||||
{
|
||||
return 'clearbit_batch_lookup';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticClearbitBundle\Form\Type;
|
||||
|
||||
use Mautic\CoreBundle\Form\Type\FormButtonsType;
|
||||
use Mautic\CoreBundle\Form\Type\YesNoButtonGroupType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<array<mixed>>
|
||||
*/
|
||||
class LookupType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add(
|
||||
'objectId',
|
||||
HiddenType::class,
|
||||
[
|
||||
'attr' => [
|
||||
'value' => $options['data']['objectId'],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'buttons',
|
||||
FormButtonsType::class,
|
||||
[
|
||||
'apply_text' => false,
|
||||
'save_text' => 'mautic.core.form.submit',
|
||||
'cancel_onclick' => 'javascript:void(0);',
|
||||
'cancel_attr' => [
|
||||
'data-dismiss' => 'modal',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'notify',
|
||||
YesNoButtonGroupType::class,
|
||||
[
|
||||
'label' => 'mautic.plugin.clearbit.notify',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
],
|
||||
'data' => true,
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
if (!empty($options['action'])) {
|
||||
$builder->setAction($options['action']);
|
||||
}
|
||||
}
|
||||
|
||||
public function getBlockPrefix(): string
|
||||
{
|
||||
return 'clearbit_lookup';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticClearbitBundle\Helper;
|
||||
|
||||
use Mautic\CoreBundle\Helper\EncryptionHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\LeadBundle\Entity\Company;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Model\CompanyModel;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Mautic\PluginBundle\Helper\IntegrationHelper;
|
||||
use MauticPlugin\MauticClearbitBundle\Integration\ClearbitIntegration;
|
||||
use MauticPlugin\MauticClearbitBundle\Services\Clearbit_Company;
|
||||
use MauticPlugin\MauticClearbitBundle\Services\Clearbit_Person;
|
||||
use Monolog\Logger;
|
||||
|
||||
class LookupHelper
|
||||
{
|
||||
/**
|
||||
* @var bool|ClearbitIntegration
|
||||
*/
|
||||
protected $integration;
|
||||
|
||||
public function __construct(
|
||||
IntegrationHelper $integrationHelper,
|
||||
protected UserHelper $userHelper,
|
||||
protected Logger $logger,
|
||||
protected LeadModel $leadModel,
|
||||
protected CompanyModel $companyModel,
|
||||
) {
|
||||
$this->integration = $integrationHelper->getIntegrationObject('Clearbit');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $notify
|
||||
* @param bool $checkAuto
|
||||
*/
|
||||
public function lookupContact(Lead $lead, $notify = false, $checkAuto = false): void
|
||||
{
|
||||
if (!$lead->getEmail()) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* @var Clearbit_Person $clearbit */
|
||||
if ($clearbit = $this->getClearbit()) {
|
||||
if (!$checkAuto || ($checkAuto && $this->integration->shouldAutoUpdate())) {
|
||||
try {
|
||||
[$cacheId, $webhookId, $cache] = $this->getCache($lead, $notify);
|
||||
|
||||
if (!array_key_exists($cacheId, $cache['clearbit'])) {
|
||||
$clearbit->setWebhookId($webhookId);
|
||||
$res = $clearbit->lookupByEmail($lead->getEmail());
|
||||
// Prevent from filling up the cache
|
||||
$cache['clearbit'] = [
|
||||
$cacheId => serialize($res),
|
||||
'nonce' => $cache['clearbit']['nonce'],
|
||||
];
|
||||
$lead->setSocialCache($cache);
|
||||
|
||||
if ($checkAuto) {
|
||||
$this->leadModel->getRepository()->saveEntity($lead);
|
||||
} else {
|
||||
$this->leadModel->saveEntity($lead);
|
||||
}
|
||||
}
|
||||
} catch (\Exception $ex) {
|
||||
$this->logger->log('error', 'Error while using Clearbit to lookup '.$lead->getEmail().': '.$ex->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $notify
|
||||
* @param bool $checkAuto
|
||||
*/
|
||||
public function lookupCompany(Company $company, $notify = false, $checkAuto = false): void
|
||||
{
|
||||
if (!$website = $company->getFieldValue('companywebsite')) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* @var Clearbit_Company $clearbit */
|
||||
if ($clearbit = $this->getClearbit(false)) {
|
||||
if (!$checkAuto || ($checkAuto && $this->integration->shouldAutoUpdate())) {
|
||||
try {
|
||||
$parse = parse_url($company->getFieldValue('companywebsite'));
|
||||
[$cacheId, $webhookId, $cache] = $this->getCache($company, $notify);
|
||||
|
||||
if (isset($parse['host']) && !array_key_exists($cacheId, $cache['clearbit'])) {
|
||||
/* @var Router $router */
|
||||
$clearbit->setWebhookId($webhookId);
|
||||
$res = $clearbit->lookupByDomain($parse['host']);
|
||||
// Prevent from filling up the cache
|
||||
$cache['clearbit'] = [
|
||||
$cacheId => serialize($res),
|
||||
'nonce' => $cache['clearbit']['nonce'],
|
||||
];
|
||||
$company->setSocialCache($cache);
|
||||
if ($checkAuto) {
|
||||
$this->companyModel->getRepository()->saveEntity($company);
|
||||
} else {
|
||||
$this->companyModel->saveEntity($company);
|
||||
}
|
||||
}
|
||||
} catch (\Exception $ex) {
|
||||
$this->logger->log('error', 'Error while using Clearbit to lookup '.$parse['host'].': '.$ex->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function validateRequest($oid, $type)
|
||||
{
|
||||
// prefix#entityId#hour#userId#nonce
|
||||
[$w, $id, $hour, $uid, $nonce] = explode('#', $oid, 5);
|
||||
$notify = (str_contains($w, '_notify') && $uid) ? $uid : false;
|
||||
|
||||
switch ($type) {
|
||||
case 'person':
|
||||
$entity = $this->leadModel->getEntity($id);
|
||||
break;
|
||||
case 'company':
|
||||
$entity = $this->companyModel->getEntity($id);
|
||||
break;
|
||||
}
|
||||
|
||||
if ($entity) {
|
||||
$socialCache = $entity->getSocialCache();
|
||||
$cacheId = $w.'#'.$id.'#'.$hour;
|
||||
|
||||
if (isset($socialCache['clearbit'][$cacheId]) && !empty($socialCache['clearbit']['nonce']) && !empty($nonce)
|
||||
&& $socialCache['clearbit']['nonce'] === $nonce
|
||||
) {
|
||||
return [
|
||||
'notify' => $notify,
|
||||
'entity' => $entity,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $person
|
||||
*
|
||||
* @return bool|Clearbit_Company|Clearbit_Person
|
||||
*/
|
||||
protected function getClearbit($person = true)
|
||||
{
|
||||
if (!$this->integration || !$this->integration->getIntegrationSettings()->getIsPublished()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// get api_key from plugin settings
|
||||
$keys = $this->integration->getDecryptedApiKeys();
|
||||
|
||||
return ($person) ? new Clearbit_Person($keys['apikey']) : new Clearbit_Company($keys['apikey']);
|
||||
}
|
||||
|
||||
protected function getCache($entity, $notify): array
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->userHelper->getUser();
|
||||
$nonce = substr(EncryptionHelper::generateKey(), 0, 16);
|
||||
$cacheId = sprintf('clearbit%s#', $notify ? '_notify' : '').$entity->getId().'#'.gmdate('YmdH');
|
||||
$webhookId = $cacheId.'#'.$user->getId().'#'.$nonce;
|
||||
|
||||
$cache = $entity->getSocialCache();
|
||||
if (!isset($cache['clearbit'])) {
|
||||
$cache['clearbit'] = [];
|
||||
}
|
||||
|
||||
$cache['clearbit']['nonce'] = $nonce;
|
||||
|
||||
return [$cacheId, $webhookId, $cache];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticClearbitBundle\Integration;
|
||||
|
||||
use Mautic\CoreBundle\Form\Type\YesNoButtonGroupType;
|
||||
use Mautic\PluginBundle\Integration\AbstractIntegration;
|
||||
use Symfony\Component\Form\Form;
|
||||
use Symfony\Component\Form\FormBuilder;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
class ClearbitIntegration extends AbstractIntegration
|
||||
{
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Clearbit';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return's authentication method such as oauth2, oauth1a, key, etc.
|
||||
*/
|
||||
public function getAuthenticationType(): string
|
||||
{
|
||||
return 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return array of key => label elements that will be converted to inputs to
|
||||
* obtain from the user.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getRequiredKeyFields(): array
|
||||
{
|
||||
// Do not rename field. clearbit.js depends on it
|
||||
return [
|
||||
'apikey' => 'mautic.integration.clearbit.apikey',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilder|Form $builder
|
||||
* @param array $data
|
||||
* @param string $formArea
|
||||
*/
|
||||
public function appendToForm(&$builder, $data, $formArea): void
|
||||
{
|
||||
if ('keys' === $formArea) {
|
||||
$builder->add(
|
||||
'auto_update',
|
||||
YesNoButtonGroupType::class,
|
||||
[
|
||||
'label' => 'mautic.plugin.clearbit.auto_update',
|
||||
'data' => isset($data['auto_update']) && (bool) $data['auto_update'],
|
||||
'attr' => [
|
||||
'tooltip' => 'mautic.plugin.clearbit.auto_update.tooltip',
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function shouldAutoUpdate(): bool
|
||||
{
|
||||
$featureSettings = $this->getKeys();
|
||||
|
||||
return isset($featureSettings['auto_update']) && (bool) $featureSettings['auto_update'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|array
|
||||
*/
|
||||
public function getFormNotes($section)
|
||||
{
|
||||
if ('custom' === $section) {
|
||||
return [
|
||||
'template' => '@MauticClearbit/Integration/form.html.twig',
|
||||
'parameters' => [
|
||||
'mauticUrl' => $this->router->generate(
|
||||
'mautic_plugin_clearbit_index', [], UrlGeneratorInterface::ABSOLUTE_URL
|
||||
),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return parent::getFormNotes($section);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticClearbitBundle;
|
||||
|
||||
use Mautic\PluginBundle\Bundle\PluginBundleBase;
|
||||
|
||||
class MauticClearbitBundle extends PluginBundleBase
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# Mautic bundle for Clearbit plugin
|
||||
|
||||
## This plugin is managed centrally in https://github.com/mautic/mautic/blob/head/plugins/MauticClearbitBundle and this is a read-only mirror repository.
|
||||
|
||||
**📣 Please make PRs and issues against Mautic Core, not here!**
|
||||
@@ -0,0 +1,18 @@
|
||||
<div class="alert alert-info">{{ 'mautic.plugin.clearbit.submit_items'|trans }}</div>
|
||||
<div style="margin-top: 10px">
|
||||
<ul class="list-group" style="max-height: 400px;overflow-y: auto">
|
||||
{% for item in lookupItems %}
|
||||
<li class="list-group-item">{{ item }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var ids = Mautic.getCheckedListIds(false, true);
|
||||
if (mQuery('#clearbit_batch_lookup_ids').length) {
|
||||
mQuery('#clearbit_batch_lookup_ids').val(ids);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{{ form(form) }}
|
||||
@@ -0,0 +1,7 @@
|
||||
<div class="alert alert-info">{{ 'mautic.plugin.clearbit.submit'|trans }}</div>
|
||||
<div style="margin-top: 10px">
|
||||
<ul class="list-group" style="max-height: 400px;overflow-y: auto">
|
||||
<li class="list-group-item">{{ lookupItem }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
{{ form(form) }}
|
||||
@@ -0,0 +1,9 @@
|
||||
<div class="well well-sm" style="margin-bottom:0 !important;">
|
||||
<p>
|
||||
{{ 'mautic.plugin.clearbit.webhook_info'|trans|purify }}
|
||||
</p>
|
||||
<div class="alert alert-warning">
|
||||
{{ 'mautic.plugin.clearbit.public_info'|trans|purify }}
|
||||
</div>
|
||||
<input type="text" readonly="" onclick="this.setSelectionRange(0, this.value.length);" value="{{ mauticUrl }}" class="form-control">
|
||||
</div>
|
||||
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticClearbitBundle\Services;
|
||||
|
||||
/**
|
||||
* This class handles the actually HTTP request to the Clearbit endpoint.
|
||||
*/
|
||||
class Clearbit_Base
|
||||
{
|
||||
public const REQUEST_LATENCY = 0.2;
|
||||
|
||||
public const USER_AGENT = 'mautic/clearbit-php-0.1.0';
|
||||
|
||||
private \DateTime $_next_req_time;
|
||||
|
||||
protected $_baseUri = '';
|
||||
|
||||
protected $_resourceUri = '';
|
||||
|
||||
protected $_version = 'v2';
|
||||
|
||||
protected $_webhookId;
|
||||
|
||||
public $response_obj;
|
||||
|
||||
public $response_code;
|
||||
|
||||
public $response_json;
|
||||
|
||||
/**
|
||||
* Slow down calls to the Clearbit API if needed.
|
||||
*/
|
||||
private function _wait_for_rate_limit(): void
|
||||
{
|
||||
$now = new \DateTime();
|
||||
if ($this->_next_req_time->getTimestamp() > $now->getTimestamp()) {
|
||||
$t = $this->_next_req_time->getTimestamp() - $now->getTimestamp();
|
||||
sleep($t);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $hdr
|
||||
*/
|
||||
private function _update_rate_limit($hdr): void
|
||||
{
|
||||
$remaining = (float) $hdr['X-RateLimit-Remaining'];
|
||||
$reset = (float) $hdr['X-RateLimit-Reset'];
|
||||
$spacing = $reset / (1.0 + $remaining);
|
||||
$delay = $spacing - self::REQUEST_LATENCY;
|
||||
$this->_next_req_time = new \DateTime('now + '.$delay.' seconds');
|
||||
}
|
||||
|
||||
/**
|
||||
* The base constructor Sets the API key available from here:
|
||||
* https://dashboard.clearbit.com/keys.
|
||||
*
|
||||
* @param string $api_key
|
||||
*/
|
||||
public function __construct(
|
||||
protected $api_key,
|
||||
) {
|
||||
$this->_next_req_time = new \DateTime('@0');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $id
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function setWebhookId($id = null)
|
||||
{
|
||||
$this->_webhookId = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $params
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
protected function _execute($params = [])
|
||||
{
|
||||
$this->_wait_for_rate_limit();
|
||||
|
||||
if ($this->_webhookId) {
|
||||
$params['webhook_id'] = $this->_webhookId;
|
||||
}
|
||||
|
||||
$fullUrl = $this->_baseUri.$this->_version.$this->_resourceUri.
|
||||
'?'.http_build_query($params);
|
||||
|
||||
// open connection
|
||||
$connection = curl_init($fullUrl);
|
||||
curl_setopt($connection, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($connection, CURLOPT_USERAGENT, self::USER_AGENT);
|
||||
curl_setopt($connection, CURLOPT_HEADER, true); // return HTTP headers with response
|
||||
curl_setopt($connection, CURLOPT_HTTPHEADER, ['Authorization: Bearer '.$this->api_key]);
|
||||
|
||||
// execute request
|
||||
$resp = curl_exec($connection);
|
||||
|
||||
[$response_headers, $this->response_json] = explode("\r\n\r\n", $resp, 2);
|
||||
// $response_headers now has a string of the HTTP headers
|
||||
// $response_json is the body of the HTTP response
|
||||
|
||||
$headers = [];
|
||||
|
||||
foreach (explode("\r\n", $response_headers) as $i => $line) {
|
||||
if (0 === $i) {
|
||||
$headers['http_code'] = $line;
|
||||
} else {
|
||||
[$key, $value] = explode(': ', $line);
|
||||
$headers[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$this->response_code = curl_getinfo($connection, CURLINFO_HTTP_CODE);
|
||||
$this->response_obj = json_decode($this->response_json);
|
||||
|
||||
if (!in_array($this->response_code, [200, 201, 202], true)) {
|
||||
throw new \Exception($this->response_obj->error->message);
|
||||
} else {
|
||||
if ('200' === $this->response_code) {
|
||||
$this->_update_rate_limit($headers);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->response_obj;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticClearbitBundle\Services;
|
||||
|
||||
/**
|
||||
* This class handles everything related to the Company lookup API.
|
||||
*/
|
||||
class Clearbit_Company extends Clearbit_Base
|
||||
{
|
||||
public function __construct($api_key)
|
||||
{
|
||||
parent::__construct($api_key);
|
||||
$this->_baseUri = 'https://company.clearbit.com/';
|
||||
$this->_resourceUri = '/companies/find';
|
||||
}
|
||||
|
||||
public function lookupByDomain($search)
|
||||
{
|
||||
$this->_execute(['domain' => $search]);
|
||||
|
||||
return $this->response_obj;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticClearbitBundle\Services;
|
||||
|
||||
/**
|
||||
* This class handles everything related to the Person lookup API.
|
||||
*/
|
||||
class Clearbit_Person extends Clearbit_Base
|
||||
{
|
||||
protected $_resourceUri = '/people/find';
|
||||
|
||||
protected $_baseUri = 'https://person.clearbit.com/';
|
||||
|
||||
public function lookupByEmail($search)
|
||||
{
|
||||
$this->_execute(['email' => $search]);
|
||||
|
||||
return $this->response_obj;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
mautic.integration.clearbit.apikey="Clearbit API Key"
|
||||
mautic.plugin.clearbit.button.caption="Lookup using Clearbit"
|
||||
mautic.plugin.clearbit.lookup.header="Clearbit - Lookup information for %item%"
|
||||
mautic.plugin.clearbit.test_api="Test API and get Stats"
|
||||
mautic.plugin.clearbit.stats="Test Results"
|
||||
mautic.plugin.clearbit.toomany="You can only lookup 20 contacts at once!"
|
||||
mautic.plugin.clearbit.comptoomany="You can only lookup 20 companies at once!"
|
||||
mautic.plugin.clearbit.empty="There are no contacts to lookup!"
|
||||
mautic.plugin.clearbit.compempty="There are no company domains to lookup!<br/>(Company website is empty?)"
|
||||
mautic.plugin.clearbit.forbidden="You don't have permissions to update this contact"
|
||||
mautic.plugin.clearbit.compforbidden="You don't have permissions to update this company"
|
||||
mautic.plugin.clearbit.auto_update="Automatically update on save?"
|
||||
mautic.plugin.clearbit.auto_update.tooltip="WARNING: This could easily exhaust your quota of API calls per month."
|
||||
mautic.plugin.clearbit.notify="Show a notification when the information has been received."
|
||||
mautic.plugin.clearbit.contact_retrieved="The contact information for %s has been retrieved"
|
||||
mautic.plugin.clearbit.company_retrieved="The company information for %s has been retrieved"
|
||||
mautic.plugin.clearbit.unable="Unable to save the information: %s"
|
||||
mautic.plugin.clearbit.webhook_info="For the plugin to work, you must use the following as the Webhook URL in your account settings on the <a href=\"https://dashboard.clearbit.com/account\" target=\"_blank\">Clearbit Dashboard</a>:"
|
||||
mautic.plugin.clearbit.public_info="<strong>Warning!</strong> This must be a public accessible URL for the Webhook to work."
|
||||
mautic.plugin.clearbit.submit="Click submit to lookup the information for:"
|
||||
mautic.plugin.clearbit.submit_items="Click submit to lookup the information for the selected item(s)."
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "mautic/plugin-clearbit",
|
||||
"description": "Clearbit Plugin",
|
||||
"type": "mautic-plugin",
|
||||
"keywords": [
|
||||
"mautic",
|
||||
"plugin",
|
||||
"integration"
|
||||
],
|
||||
"extra": {
|
||||
"install-directory-name": "MauticClearbitBundle"
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"require": {
|
||||
"mautic/core-lib": "^7.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
# Workflow name:
|
||||
name: Close Pull Requests
|
||||
|
||||
# Workflow triggers:
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
# Workflow jobs:
|
||||
jobs:
|
||||
run:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: superbrothers/close-pull-request@v3
|
||||
with:
|
||||
comment: |
|
||||
Thank you for submitting a pull request. :raised_hands:
|
||||
|
||||
We greatly appreciate your willingness to submit a contribution. However, we are not accepting pull requests against this repository, as all development happens on the [main project repository](https://github.com/mautic/mautic).
|
||||
|
||||
We kindly request that you submit this pull request against the [respective directory](https://github.com/mautic/mautic/blob/head/plugins/MauticCloudStorageBundle) of the main repository where we'll review and provide feedback. If this is your first Mautic contribution, be sure to read the [contributing guide](https://github.com/mautic/mautic/blob/4.x/.github/CONTRIBUTING.md) which provides guidelines and instructions for submitting contributions.
|
||||
|
||||
Thank you again, and we look forward to receiving your contribution! :smiley:
|
||||
|
||||
Best,
|
||||
The Mautic team
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 9.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'name' => 'Cloud Storage',
|
||||
'description' => 'Enables integrations with Mautic supported cloud storage services.',
|
||||
'version' => '1.0',
|
||||
'author' => 'Mautic',
|
||||
|
||||
'services' => [
|
||||
'integrations' => [
|
||||
'mautic.integration.amazons3' => [
|
||||
'class' => MauticPlugin\MauticCloudStorageBundle\Integration\AmazonS3Integration::class,
|
||||
'arguments' => [
|
||||
'event_dispatcher',
|
||||
'mautic.helper.cache_storage',
|
||||
'doctrine.orm.entity_manager',
|
||||
'request_stack',
|
||||
'router',
|
||||
'translator',
|
||||
'monolog.logger.mautic',
|
||||
'mautic.helper.encryption',
|
||||
'mautic.lead.model.lead',
|
||||
'mautic.lead.model.company',
|
||||
'mautic.helper.paths',
|
||||
'mautic.core.model.notification',
|
||||
'mautic.lead.model.field',
|
||||
'mautic.plugin.model.integration_entity',
|
||||
'mautic.lead.model.dnc',
|
||||
'mautic.lead.field.fields_with_unique_identifier',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Mautic\CoreBundle\DependencyInjection\MauticCoreExtension;
|
||||
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
||||
|
||||
return function (ContainerConfigurator $configurator): void {
|
||||
$services = $configurator->services()
|
||||
->defaults()
|
||||
->autowire()
|
||||
->autoconfigure()
|
||||
->public();
|
||||
|
||||
$excludes = [
|
||||
];
|
||||
|
||||
$services->load('MauticPlugin\\MauticCloudStorageBundle\\', '../')
|
||||
->exclude('../{'.implode(',', array_merge(MauticCoreExtension::DEFAULT_EXCLUDES, $excludes)).'}');
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MauticPlugin\MauticCloudStorageBundle\DependencyInjection;
|
||||
|
||||
use Symfony\Component\Config\FileLocator;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
|
||||
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
|
||||
|
||||
class MauticCloudStorageExtension extends Extension
|
||||
{
|
||||
/**
|
||||
* @param mixed[] $configs
|
||||
*/
|
||||
public function load(array $configs, ContainerBuilder $container): void
|
||||
{
|
||||
$loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../Config'));
|
||||
$loader->load('services.php');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCloudStorageBundle\EventListener;
|
||||
|
||||
use Mautic\AssetBundle\AssetEvents;
|
||||
use Mautic\AssetBundle\Event as Events;
|
||||
use MauticPlugin\MauticCloudStorageBundle\Exception\InvalidCredentialConfigurationException;
|
||||
use MauticPlugin\MauticCloudStorageBundle\Integration\CloudStorageIntegration;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class RemoteAssetBrowseSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
AssetEvents::ASSET_ON_REMOTE_BROWSE => ['onAssetRemoteBrowse', 0],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the connector for an event's integration.
|
||||
*/
|
||||
public function onAssetRemoteBrowse(Events\RemoteAssetBrowseEvent $event): void
|
||||
{
|
||||
/** @var CloudStorageIntegration $integration */
|
||||
$integration = $event->getIntegration();
|
||||
|
||||
try {
|
||||
$event->setAdapter($integration->getAdapter());
|
||||
} catch (InvalidCredentialConfigurationException $e) {
|
||||
$event->setFailed($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCloudStorageBundle\Exception;
|
||||
|
||||
class InvalidCredentialConfigurationException extends \RuntimeException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace MauticPlugin\MauticCloudStorageBundle\Exception;
|
||||
|
||||
class NoFormNeededException extends \Exception
|
||||
{
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user