diff --git a/packages/CanvasIFrame/pack.php b/packages/CanvasIFrame/pack.php index 38ed61b..f295208 100644 --- a/packages/CanvasIFrame/pack.php +++ b/packages/CanvasIFrame/pack.php @@ -4,7 +4,7 @@ $packageID = "BuildingBlock_CanvasIFrame"; -$packageLabel = "Canvas IFrame"; +$packageLabel = "BuildingBlocks: Canvas IFrame"; $supportedVersionRegex = '(9|8|7)\\..*$'; /******************************/ diff --git a/packages/ContextualIFrameDashlet/pack.php b/packages/ContextualIFrameDashlet/pack.php index 43ee0a6..034cb5b 100755 --- a/packages/ContextualIFrameDashlet/pack.php +++ b/packages/ContextualIFrameDashlet/pack.php @@ -4,7 +4,7 @@ $packageID = "BuildingBlock_ContextualIFrameDashlet"; -$packageLabel = "Contextual iFrame Dashlet package"; +$packageLabel = "BuildingBlocks: Contextual iFrame Dashlet package"; $supportedVersionRegex = '(9|8|7)\\..*$'; $acceptableSugarFlavors = array('PRO','ENT','ULT'); $description = 'Package for a configurable contextual iFrame Sugar Dashlet'; diff --git a/packages/CssLoader/pack.php b/packages/CssLoader/pack.php index b65b71f..185cc77 100755 --- a/packages/CssLoader/pack.php +++ b/packages/CssLoader/pack.php @@ -4,7 +4,7 @@ $packageID = "BuildingBlock_CssLoader"; -$packageLabel = "CssLoader"; +$packageLabel = "BuildingBlocks: CssLoader"; $supportedVersionRegex = '(9|8|7)\\..*$'; $acceptableSugarFlavors = array('PRO','ENT','ULT'); $description = 'Loading custom CSS into Sugar made easy'; diff --git a/packages/CustomHighlightField/pack.php b/packages/CustomHighlightField/pack.php new file mode 100755 index 0000000..7f27fca --- /dev/null +++ b/packages/CustomHighlightField/pack.php @@ -0,0 +1,93 @@ +#!/usr/bin/env php + $packageID, + 'name' => $packageLabel, + 'description' => $description, + 'version' => $version, + 'author' => 'SugarCRM, Inc.', + 'is_uninstallable' => 'true', + 'published_date' => date("Y-m-d H:i:s"), + 'type' => 'module', + 'acceptable_sugar_versions' => array( + 'exact_matches' => array( + ), + 'regex_matches' => array( + $supportedVersionRegex, + ), + ), + 'acceptable_sugar_flavors' => $acceptableSugarFlavors, +); + +$installdefs = array( + 'beans' => array (), + 'id' => $packageID +); + +echo "Creating {$zipFile} ... \n"; +$zip = new ZipArchive(); +$zip->open($zipFile, ZipArchive::CREATE); +$basePath = realpath('src/'); +$files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($basePath, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::LEAVES_ONLY +); +foreach ($files as $name => $file) { + if ($file->isFile()) { + $fileReal = $file->getRealPath(); + $fileRelative = 'src' . str_replace($basePath, '', $fileReal); + echo " [*] $fileRelative \n"; + $zip->addFile($fileReal, $fileRelative); + $installdefs['copy'][] = array( + 'from' => '/' . $fileRelative, + 'to' => preg_replace('/^src[\/\\\](.*)/', '$1', $fileRelative), + ); + } +} +$manifestContent = sprintf( + "addFromString('manifest.php', $manifestContent); +$zip->close(); +echo "Done creating {$zipFile}\n\n"; +exit(0); diff --git a/packages/CustomHighlightField/src/custom/Extension/application/Ext/Language/en_us.Highlightfield.php b/packages/CustomHighlightField/src/custom/Extension/application/Ext/Language/en_us.Highlightfield.php new file mode 100644 index 0000000..94ee795 --- /dev/null +++ b/packages/CustomHighlightField/src/custom/Extension/application/Ext/Language/en_us.Highlightfield.php @@ -0,0 +1,25 @@ + 'Blue', + '#00ffff' => 'Aqua', + '#FF00FF' => 'Fuchsia', + '#808080' => 'Gray', + '#ffff00' => 'Olive', + '#000000' => 'Black', + '#800000' => 'Maroon', + '#ff0000' => 'Red', + '#ffA500' => 'Orange', + '#ffff00' => 'Yellow', + '#800080' => 'Purple', + '#ffffff' => 'White', + '#00ff00' => 'Lime', + '#008000' => 'Green', + '#008080' => 'Teal', + '#c0c0c0' => 'Silver', + '#000080' => 'Navy' +); diff --git a/packages/CustomHighlightField/src/custom/Extension/modules/DynamicFields/Ext/Language/en_us.Highlightfield.php b/packages/CustomHighlightField/src/custom/Extension/modules/DynamicFields/Ext/Language/en_us.Highlightfield.php new file mode 100644 index 0000000..d53111e --- /dev/null +++ b/packages/CustomHighlightField/src/custom/Extension/modules/DynamicFields/Ext/Language/en_us.Highlightfield.php @@ -0,0 +1,7 @@ + + {{value}} + +{{/if}} \ No newline at end of file diff --git a/packages/CustomHighlightField/src/custom/clients/base/fields/Highlightfield/edit.hbs b/packages/CustomHighlightField/src/custom/clients/base/fields/Highlightfield/edit.hbs new file mode 100644 index 0000000..92f71f5 --- /dev/null +++ b/packages/CustomHighlightField/src/custom/clients/base/fields/Highlightfield/edit.hbs @@ -0,0 +1,18 @@ +{{! + We have not made any edits to this file that differ from stock, however, + we could add styling here just as we did for the detail and list templates. +}} + + + {{#unless hideHelp}} + {{#if def.help}} +

{{str def.help module}}

+ {{/if}} + {{/unless}} \ No newline at end of file diff --git a/packages/CustomHighlightField/src/custom/clients/base/fields/Highlightfield/list.hbs b/packages/CustomHighlightField/src/custom/clients/base/fields/Highlightfield/list.hbs new file mode 100644 index 0000000..cb74fde --- /dev/null +++ b/packages/CustomHighlightField/src/custom/clients/base/fields/Highlightfield/list.hbs @@ -0,0 +1,14 @@ +{{! + The data for field colors are passed into the handlebars template through the def array. Our case + being the def.backcolor and def.textcolor properties. These indexes are defined in: + ./custom/modules/DynamicFields/templates/Fields/TemplateHighlightfield.php +}} + +{{#if ellipsis}} +
+{{/if}} +{{#if href}}{{value}}{{else}}{{value}}{{/if~}} +{{#if ellipsis}} +
+{{/if~}} \ No newline at end of file diff --git a/packages/CustomHighlightField/src/custom/clients/base/filters/operators/operators.php b/packages/CustomHighlightField/src/custom/clients/base/filters/operators/operators.php new file mode 100644 index 0000000..59b9c2c --- /dev/null +++ b/packages/CustomHighlightField/src/custom/clients/base/filters/operators/operators.php @@ -0,0 +1,9 @@ + 'LBL_HIGHLIGHTFIELD_OPERATOR_CONTAINS', + '$not_contains' => 'LBL_HIGHLIGHTFIELD_OPERATOR_NOT_CONTAINS', + '$starts' => 'LBL_HIGHLIGHTFIELD_OPERATOR_STARTS_WITH', +); diff --git a/packages/CustomHighlightField/src/custom/include/SugarFields/Fields/Highlightfield/SugarFieldHighlightfield.php b/packages/CustomHighlightField/src/custom/include/SugarFields/Fields/Highlightfield/SugarFieldHighlightfield.php new file mode 100644 index 0000000..6b75d36 --- /dev/null +++ b/packages/CustomHighlightField/src/custom/include/SugarFields/Fields/Highlightfield/SugarFieldHighlightfield.php @@ -0,0 +1,15 @@ +debug("SugarFieldHighlightfield::save() function called."); + parent::save($bean, $params, $field, $properties, $prefix); + } +} diff --git a/packages/CustomHighlightField/src/custom/include/generic/SugarWidgets/SugarWidgetFieldHighlightfield.php b/packages/CustomHighlightField/src/custom/include/generic/SugarWidgets/SugarWidgetFieldHighlightfield.php new file mode 100644 index 0000000..e26aeb7 --- /dev/null +++ b/packages/CustomHighlightField/src/custom/include/generic/SugarWidgets/SugarWidgetFieldHighlightfield.php @@ -0,0 +1,41 @@ +reporter->db->convert($this->_get_column_select($layout_def), "text2char") . + " = " . $this->reporter->db->quoted($layout_def['input_name0']); + } + + function queryFilterNot_Equals_Str($layout_def) + { + $column = $this->_get_column_select($layout_def); + return "($column IS NULL OR " . $this->reporter->db->convert($column, "text2char") . " != " . + $this->reporter->db->quoted($layout_def['input_name0']) . ")"; + } + + function queryFilterNot_Empty($layout_def) + { + $column = $this->_get_column_select($layout_def); + return "($column IS NOT NULL AND " . $this->reporter->db->convert($column, "length") . " > 0)"; + } + + function queryFilterEmpty($layout_def) + { + $column = $this->_get_column_select($layout_def); + return "($column IS NULL OR " . $this->reporter->db->convert($column, "length") . " = 0)"; + } + + function displayList($layout_def) + { + return nl2br(parent::displayListPlain($layout_def)); + } +} diff --git a/packages/CustomHighlightField/src/custom/modules/DynamicFields/templates/Fields/Forms/Highlightfield.php b/packages/CustomHighlightField/src/custom/modules/DynamicFields/templates/Fields/Forms/Highlightfield.php new file mode 100644 index 0000000..0f1b6dc --- /dev/null +++ b/packages/CustomHighlightField/src/custom/modules/DynamicFields/templates/Fields/Forms/Highlightfield.php @@ -0,0 +1,49 @@ +get_template_vars(); + $fields = $vars['module']->mbvardefs->vardefs['fields']; + $fieldOptions = array(); + foreach ($fields as $id => $def) { + $fieldOptions[$id] = $def['name']; + } + $ss->assign('fieldOpts', $fieldOptions); + + //If there are no colors defined, use black text on + // a white background + if (isset($vardef['backcolor'])) { + $backcolor = $vardef['backcolor']; + } else { + $backcolor = '#ffffff'; + } + if (isset($vardef['textcolor'])) { + $textcolor = $vardef['textcolor']; + } else { + $textcolor = '#000000'; + } + $ss->assign('BACKCOLOR', $backcolor); + $ss->assign('TEXTCOLOR', $textcolor); + + $colorArray = $app_list_strings['highlightColors']; + asort($colorArray); + + $ss->assign('highlightColors', $colorArray); + $ss->assign('textColors', $colorArray); + + $ss->assign('BACKCOLORNAME', $app_list_strings['highlightColors'][$backcolor]); + $ss->assign('TEXTCOLORNAME', $app_list_strings['highlightColors'][$textcolor]); + + return $ss->fetch('custom/modules/DynamicFields/templates/Fields/Forms/Highlightfield.tpl'); +} diff --git a/packages/CustomHighlightField/src/custom/modules/DynamicFields/templates/Fields/Forms/Highlightfield.tpl b/packages/CustomHighlightField/src/custom/modules/DynamicFields/templates/Fields/Forms/Highlightfield.tpl new file mode 100644 index 0000000..1e25764 --- /dev/null +++ b/packages/CustomHighlightField/src/custom/modules/DynamicFields/templates/Fields/Forms/Highlightfield.tpl @@ -0,0 +1,67 @@ +{include file="modules/DynamicFields/templates/Fields/Forms/coreTop.tpl"} + + {sugar_translate module="DynamicFields" label="COLUMN_TITLE_DEFAULT_VALUE"}: + + {if $hideLevel < 5} + + {else} + {$vardef.default} + {/if} + + + + {sugar_translate module="DynamicFields" label="COLUMN_TITLE_MAX_SIZE"}: + + {if $hideLevel < 5} + + + {if $action=="saveSugarField"} + + {/if} + {literal} + + {/literal} + {else} + {$vardef.len} + {/if} + + + + {sugar_translate module="DynamicFields" label="LBL_HIGHLIGHTFIELD_BACKCOLOR"}: + + {if $hideLevel < 5} + {html_options name="ext1" id="ext1" selected=$BACKCOLOR options=$highlightColors} + {else} + + {$BACKCOLORNAME} + {/if} + + + + {sugar_translate module="DynamicFields" label="LBL_HIGHLIGHTFIELD_TEXTCOLOR"}: + + {if $hideLevel < 5} + {html_options name="ext2" id="ext2" selected=$TEXTCOLOR options=$highlightColors} + {else} + + {$TEXTCOLORNAME} + {/if} + + + +{include file="modules/DynamicFields/templates/Fields/Forms/coreBottom.tpl"} diff --git a/packages/CustomHighlightField/src/custom/modules/DynamicFields/templates/Fields/TemplateHighlightfield.php b/packages/CustomHighlightField/src/custom/modules/DynamicFields/templates/Fields/TemplateHighlightfield.php new file mode 100644 index 0000000..0f084a1 --- /dev/null +++ b/packages/CustomHighlightField/src/custom/modules/DynamicFields/templates/Fields/TemplateHighlightfield.php @@ -0,0 +1,94 @@ +vardef_map['ext1'] = 'backcolor'; + $this->vardef_map['ext2'] = 'textcolor'; + $this->vardef_map['backcolor'] = 'ext1'; + $this->vardef_map['textcolor'] = 'ext2'; + } + + //BEGIN BACKWARD COMPATIBILITY + // AS 7.x does not have EditViews and DetailViews anymore these are here + // for any modules in backwards compatibility mode. + + function get_xtpl_edit() + { + $name = $this->name; + $returnXTPL = array(); + + if (!empty($this->help)) { + $returnXTPL[strtoupper($this->name . '_help')] = translate($this->help, $this->bean->module_dir); + } + + if (isset($this->bean->$name)) { + $returnXTPL[$this->name] = $this->bean->$name; + } else { + if (empty($this->bean->id)) { + $returnXTPL[$this->name] = $this->default_value; + } + } + return $returnXTPL; + } + + function get_xtpl_search() + { + if (!empty($_REQUEST[$this->name])) { + return $_REQUEST[$this->name]; + } + } + + function get_xtpl_detail() + { + $name = $this->name; + if (isset($this->bean->$name)) { + return $this->bean->$name; + } + return ''; + } + + //END BACKWARD COMPATIBILITY + + /** + * Function: get_field_def + * Description: Get the field definition attributes that are required for the Highlightfield Field + * the primary reason this function is here is to set the dbType to 'varchar', + * otherwise 'Highlightfield' would be used by default. + * References: __construct function above + * + * @return Field Definition + */ + function get_field_def() + { + $def = parent::get_field_def(); + + //set our fields database type + $def['dbType'] = 'varchar'; + + //set our fields to false to avoid issues with Report Wizard. + $def['reportable'] = false; + + //set our field as custom type + $def['custom_type'] = 'Highlightfield'; + + //map our extension fields for colorizing the field + $def['backcolor'] = !empty($this->backcolor) ? $this->backcolor : $this->ext1; + $def['textcolor'] = !empty($this->textcolor) ? $this->textcolor : $this->ext2; + + return $def; + } +} \ No newline at end of file diff --git a/packages/CustomRecordViewButton/pack.php b/packages/CustomRecordViewButton/pack.php index 9ff0545..fd5e80e 100755 --- a/packages/CustomRecordViewButton/pack.php +++ b/packages/CustomRecordViewButton/pack.php @@ -4,7 +4,7 @@ $packageID = "BuildingBlock_CustomRecordViewButton"; -$packageLabel = "Custom Button on the Record View"; +$packageLabel = "BuildingBlocks: Custom Button on the Record View"; $supportedVersionRegex = '(9|8|7)\\..*$'; $acceptableSugarFlavors = array('PRO','ENT','ULT'); $description = 'Package that shows how you can add a custom Button to a modules Record View using Extensions Framework.'; diff --git a/packages/CustomRecordViewPanel/pack.php b/packages/CustomRecordViewPanel/pack.php index 4b0b6c9..e722364 100755 --- a/packages/CustomRecordViewPanel/pack.php +++ b/packages/CustomRecordViewPanel/pack.php @@ -4,7 +4,7 @@ $packageID = "BuildingBlock_CustomRecordViewPanel"; -$packageLabel = "Custom Panel on the Record View"; +$packageLabel = "BuildingBlocks: Custom Panel on the Record View"; $supportedVersionRegex = '(9|8|7)\\..*$'; $acceptableSugarFlavors = array('PRO','ENT','ULT'); $description = 'Package that shows how you can add a custom Panel to a modules Record View using Extensions Framework.'; diff --git a/packages/FloatingDivAction-10.2+/src/custom/Extension/application/Ext/clients/base/layouts/footer/addClickToCallAction.php b/packages/FloatingDivAction-10.2+/src/custom/Extension/application/Ext/clients/base/layouts/footer/addClickToCallAction.php deleted file mode 100644 index 13caee2..0000000 --- a/packages/FloatingDivAction-10.2+/src/custom/Extension/application/Ext/clients/base/layouts/footer/addClickToCallAction.php +++ /dev/null @@ -1,7 +0,0 @@ - 'click-to-call', -); diff --git a/packages/FloatingDivAction-10.2+/src/custom/clients/base/views/click-to-call/click-to-call.hbs b/packages/FloatingDivAction-10.2+/src/custom/clients/base/views/click-to-call/click-to-call.hbs deleted file mode 100644 index a3b6ad3..0000000 --- a/packages/FloatingDivAction-10.2+/src/custom/clients/base/views/click-to-call/click-to-call.hbs +++ /dev/null @@ -1,9 +0,0 @@ -{{! Copyright 2016 SugarCRM Inc. Licensed by SugarCRM under the Apache 2.0 license. }} - -{{! - Define HTML for our new button. We will mimic the style of other buttons - in the footer so we remain consistent. -}} - diff --git a/packages/FloatingDivAction-10.2+/src/custom/clients/base/views/click-to-call/click-to-call.js b/packages/FloatingDivAction-10.2+/src/custom/clients/base/views/click-to-call/click-to-call.js deleted file mode 100644 index c2be001..0000000 --- a/packages/FloatingDivAction-10.2+/src/custom/clients/base/views/click-to-call/click-to-call.js +++ /dev/null @@ -1,64 +0,0 @@ -({ - // Copyright 2016 SugarCRM Inc. Licensed by SugarCRM under the Apache 2.0 license. - events: { - //On click of our "button" element - 'click [data-action=open_phone]': 'togglePopup', - }, - - // tagName attribute is inherited from Backbone.js. - // We set it to "span" instead of default "div" so that our "button" element is displayed inline. - tagName: "span", - // Used to keep track of Popup since it is not attached to this View's DOM - $popup: undefined, - /** - * Toggle the display of the popup. Called when the phone icon is pressed in the footer of the page. - */ - togglePopup: function () { - //Toggle active status on button in footer - var $button = this.$('[data-action="open_phone"]'); - $button.toggleClass('active'); - //Create popup if necessary, otherwise just toggle the hidden class to hide/show. - if (!this.$popup) { - this._createPopup(); - } else { - this.$popup.toggleClass('hidden'); - } - }, - /** - * Used to create Popup as needed. Avoid calling this directly, should only need to be called once. - * @private - */ - _createPopup: function () { - var popupCss = app.template.get("click-to-call.popup-css"); - // We need to load some custom CSS, this is an easy way to do it without having to edit custom.less - $('head').append(popupCss()); - var popup = app.template.get("click-to-call.popup")(this); - // Add to main content pane of screen - $('#sidecar').append(popup); - this.$popup = $('#sidecar').find('div.cti-popup'); - // Hide pop up on click of X (close button) - this.$popup.find('[data-action=close]').click(_.bind(this._closePopup, this)); - // Make pop up draggable using existing jQuery UI plug-in - this.$popup.draggable(); - }, - /** - * Called when close button is pressed on CTI popup. - * @private - */ - _closePopup: function () { - this.$popup.addClass('hidden'); - var $button = this.$('[data-action="open_phone"]'); - $button.removeClass('active'); - }, - /** - * Dispose of unattached popup when footer destroyed - * @private - */ - _dispose: function(){ - this._super('_dispose'); - this.$popup.remove(); - this.$popup = null; - } - -}) - diff --git a/packages/FloatingDivAction-10.2+/src/custom/clients/base/views/click-to-call/click-to-call.php b/packages/FloatingDivAction-10.2+/src/custom/clients/base/views/click-to-call/click-to-call.php deleted file mode 100644 index d2c6cfc..0000000 --- a/packages/FloatingDivAction-10.2+/src/custom/clients/base/views/click-to-call/click-to-call.php +++ /dev/null @@ -1,6 +0,0 @@ - '//httpbin.org/get', -); diff --git a/packages/FloatingDivAction-10.2+/src/custom/clients/base/views/click-to-call/popup-css.hbs b/packages/FloatingDivAction-10.2+/src/custom/clients/base/views/click-to-call/popup-css.hbs deleted file mode 100644 index a130de9..0000000 --- a/packages/FloatingDivAction-10.2+/src/custom/clients/base/views/click-to-call/popup-css.hbs +++ /dev/null @@ -1,37 +0,0 @@ -{{! Copyright 2016 SugarCRM Inc. Licensed by SugarCRM under the Apache 2.0 license. }} - - - diff --git a/packages/FloatingDivAction-10.2+/src/custom/clients/base/views/click-to-call/popup.hbs b/packages/FloatingDivAction-10.2+/src/custom/clients/base/views/click-to-call/popup.hbs deleted file mode 100644 index cb67bf6..0000000 --- a/packages/FloatingDivAction-10.2+/src/custom/clients/base/views/click-to-call/popup.hbs +++ /dev/null @@ -1,10 +0,0 @@ -{{! Copyright 2016 SugarCRM Inc. Licensed by SugarCRM under the Apache 2.0 license. }} -
-
- CTI Window - -
-
- -
-
diff --git a/packages/FloatingDivAction-10.2+/version b/packages/FloatingDivAction-10.2+/version deleted file mode 100644 index afaf360..0000000 --- a/packages/FloatingDivAction-10.2+/version +++ /dev/null @@ -1 +0,0 @@ -1.0.0 \ No newline at end of file diff --git a/packages/FloatingDivAction/pack.php b/packages/FloatingDivAction/pack.php index 69a412b..f871226 100755 --- a/packages/FloatingDivAction/pack.php +++ b/packages/FloatingDivAction/pack.php @@ -3,9 +3,11 @@ // Copyright 2016 SugarCRM Inc. Licensed by SugarCRM under the Apache 2.0 license. - $packageID = "BuildingBlock_FloatingDivExample"; - $packageLabel = "Floating Div Example"; - $supportedVersionRegex = '(9|8|7)\\..*$'; +$packageID = "BuildingBlock_FloatingDivExample"; +$packageLabel = "BuildingBlocks: Floating Div Example"; +$supportedVersionRegex = '(26|25|14)\\..*$'; +$acceptableSugarFlavors = array('ENT'); +$description = 'Floating Div Example.'; /******************************/ if (empty($argv[1])) { @@ -39,7 +41,7 @@ $manifest = array( 'id' => $packageID, 'name' => $packageLabel, - 'description' => $packageLabel, + 'description' => $description, 'version' => $version, 'author' => 'SugarCRM, Inc.', 'is_uninstallable' => 'true', @@ -52,25 +54,24 @@ $supportedVersionRegex, ), ), + 'acceptable_sugar_flavors' => $acceptableSugarFlavors, ); $installdefs = array( 'beans' => array (), - 'id' => $packageID, + 'id' => $packageID ); -echo "Creating {$zipFile} ... \n"; +echo "Creating {$zipFile} ... \n"; $zip = new ZipArchive(); $zip->open($zipFile, ZipArchive::CREATE); $basePath = realpath('src/'); - $files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($basePath, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::LEAVES_ONLY ); - foreach ($files as $name => $file) { - if ($file->isFile() and !empty(pathinfo($file)['filename'])) { + if ($file->isFile()) { $fileReal = $file->getRealPath(); $fileRelative = 'src' . str_replace($basePath, '', $fileReal); echo " [*] $fileRelative \n"; @@ -81,15 +82,12 @@ ); } } - $manifestContent = sprintf( "addFromString('manifest.php', $manifestContent); $zip->close(); - echo "Done creating {$zipFile}\n\n"; exit(0); diff --git a/packages/FloatingDivAction/src/custom/Extension/application/Ext/clients/base/layouts/footer/addClickToCallAction.php b/packages/FloatingDivAction/src/custom/Extension/application/Ext/clients/base/layouts/footer/addClickToCallAction.php deleted file mode 100644 index 113c894..0000000 --- a/packages/FloatingDivAction/src/custom/Extension/application/Ext/clients/base/layouts/footer/addClickToCallAction.php +++ /dev/null @@ -1,7 +0,0 @@ - 'click-to-call', -); diff --git a/packages/FloatingDivAction/src/custom/Extension/application/Ext/clients/base/layouts/sidebar-nav/addClickToCallAction.php b/packages/FloatingDivAction/src/custom/Extension/application/Ext/clients/base/layouts/sidebar-nav/addClickToCallAction.php new file mode 100644 index 0000000..7c8111c --- /dev/null +++ b/packages/FloatingDivAction/src/custom/Extension/application/Ext/clients/base/layouts/sidebar-nav/addClickToCallAction.php @@ -0,0 +1,13 @@ + [ + 'name' => 'click-to-call', + 'type' => 'click-to-call', + 'icon' => 'sicon-bell-lg', + 'label' => 'Click to Call', + 'template' => 'sidebar-nav-item', + ], +); diff --git a/packages/FloatingDivAction/src/custom/clients/base/views/click-to-call/click-to-call.hbs b/packages/FloatingDivAction/src/custom/clients/base/views/click-to-call/click-to-call.hbs deleted file mode 100644 index a3b6ad3..0000000 --- a/packages/FloatingDivAction/src/custom/clients/base/views/click-to-call/click-to-call.hbs +++ /dev/null @@ -1,9 +0,0 @@ -{{! Copyright 2016 SugarCRM Inc. Licensed by SugarCRM under the Apache 2.0 license. }} - -{{! - Define HTML for our new button. We will mimic the style of other buttons - in the footer so we remain consistent. -}} - diff --git a/packages/FloatingDivAction/src/custom/clients/base/views/click-to-call/click-to-call.js b/packages/FloatingDivAction/src/custom/clients/base/views/click-to-call/click-to-call.js index c2be001..adde758 100644 --- a/packages/FloatingDivAction/src/custom/clients/base/views/click-to-call/click-to-call.js +++ b/packages/FloatingDivAction/src/custom/clients/base/views/click-to-call/click-to-call.js @@ -1,22 +1,18 @@ ({ - // Copyright 2016 SugarCRM Inc. Licensed by SugarCRM under the Apache 2.0 license. - events: { - //On click of our "button" element - 'click [data-action=open_phone]': 'togglePopup', + extendsFrom: 'SidebarNavItemView', + + primaryActionOnClick: function() { + this.togglePopup(); }, - - // tagName attribute is inherited from Backbone.js. - // We set it to "span" instead of default "div" so that our "button" element is displayed inline. - tagName: "span", + // Used to keep track of Popup since it is not attached to this View's DOM $popup: undefined, /** * Toggle the display of the popup. Called when the phone icon is pressed in the footer of the page. */ togglePopup: function () { - //Toggle active status on button in footer - var $button = this.$('[data-action="open_phone"]'); - $button.toggleClass('active'); + // var $button = this.$('[data-action="open_phone"]'); + // $button.toggleClass('active'); //Create popup if necessary, otherwise just toggle the hidden class to hide/show. if (!this.$popup) { this._createPopup(); @@ -47,8 +43,8 @@ */ _closePopup: function () { this.$popup.addClass('hidden'); - var $button = this.$('[data-action="open_phone"]'); - $button.removeClass('active'); + // var $button = this.$('[data-action="open_phone"]'); + // $button.removeClass('active'); }, /** * Dispose of unattached popup when footer destroyed diff --git a/packages/HelloWorldDashlet/pack.php b/packages/HelloWorldDashlet/pack.php index 6412f65..4966f16 100755 --- a/packages/HelloWorldDashlet/pack.php +++ b/packages/HelloWorldDashlet/pack.php @@ -4,7 +4,7 @@ $packageID = "BuildingBlock_HelloWorldDashlet"; -$packageLabel = "Hello World Dashlet"; +$packageLabel = "BuildingBlocks: Hello World Dashlet"; $supportedVersionRegex = '(9|8|7)\\..*$'; $acceptableSugarFlavors = array('PRO','ENT','ULT'); $description = 'Package for a basic Sugar Dashlet'; diff --git a/packages/IFrameDrawerAction/pack.php b/packages/IFrameDrawerAction/pack.php index d70daf6..c1efa5e 100755 --- a/packages/IFrameDrawerAction/pack.php +++ b/packages/IFrameDrawerAction/pack.php @@ -4,7 +4,7 @@ $packageID = "BuildingBlocks_IFrameDrawerAction"; -$packageLabel = "IFrame Drawer Action"; +$packageLabel = "BuildingBlocks: IFrame Drawer Action"; $supportedVersionRegex = '(9|8|7)\\..*$'; $acceptableSugarFlavors = array('PRO','ENT','ULT'); $description = 'Opens a drawer that displays a custom IFrame'; diff --git a/packages/README.md b/packages/README.md index f595e11..82acd43 100644 --- a/packages/README.md +++ b/packages/README.md @@ -3,6 +3,18 @@ Each of these packages are ready to be built, zipped, uploaded and installed usi The `buildPackages.sh` script can be used to build all these packages easily on Unix based environments. +## [ProcessManagementDashlet](ProcessManagementDashlet/) + +Serves as the starting point for adding a custom dashlet to a record view. It triggers a Sidecar API call based on the dashlet’s configuration, which then invokes a backend endpoint to fetch and populate data specific to the current record. + +## [ProcessManagementPruner](ProcessManagementPruner/) + +Serves as the starting point for building a pruner for SugarBPM and its related tables. It retrieves `pmse*` records along with their relationships and prunes them in a cascaded manner. You can also leverage [Data Archiver](https://support.sugarcrm.com/SmartLinks/administration_guide/system/data_archiver/) to support your use case. + +## [HighlightField](HighlightField/) + +Based on the article [Creating Custom Field Types](https://support.sugarcrm.com/SmartLinks/developer_guide/cookbook/creating_custom_fields/), in this example, we create a custom field type called "Highlightfield", which will mimic the base text field type with the added feature that the displayed text for the field will be highlighted in a color chosen when the field is created in Studio. + ## [Canvas iFrame](CanvasIFrame) This package displays an IFrame inside Sugar. diff --git a/packages/RecentChangesAPI/pack.php b/packages/RecentChangesAPI/pack.php index 97553d9..6e8f267 100755 --- a/packages/RecentChangesAPI/pack.php +++ b/packages/RecentChangesAPI/pack.php @@ -4,7 +4,7 @@ $packageID = "BuildingBlock_RecentChangesAPI"; -$packageLabel = "RecentChangesAPI"; +$packageLabel = "BuildingBlocks: RecentChangesAPI"; $supportedVersionRegex = '(9|8\..*|7\.(9|10|11)\..*)'; $acceptableSugarFlavors = array('PRO','ENT','ULT'); $description = 'Adds a REST API endpoint that makes it easy to identify Users who have had recent changes to their assigned records.'; diff --git a/packages/ScriptLoader/pack.php b/packages/ScriptLoader/pack.php index f30ffd7..493a13d 100755 --- a/packages/ScriptLoader/pack.php +++ b/packages/ScriptLoader/pack.php @@ -4,7 +4,7 @@ $packageID = "BuildingBlock_ScriptLoader"; -$packageLabel = "ScriptLoader"; +$packageLabel = "BuildingBlocks: ScriptLoader"; $supportedVersionRegex = '(9|8|7)\\..*$'; $acceptableSugarFlavors = array('PRO','ENT','ULT'); $description = 'Loading custom JavaScript into Sugar made easy.'; diff --git a/packages/SugarBPM_CapitalizeRecordName_Action/pack.php b/packages/SugarBPM_CapitalizeRecordName_Action/pack.php new file mode 100755 index 0000000..017edf0 --- /dev/null +++ b/packages/SugarBPM_CapitalizeRecordName_Action/pack.php @@ -0,0 +1,93 @@ +#!/usr/bin/env php + $packageID, + 'name' => $packageLabel, + 'description' => $description, + 'version' => $version, + 'author' => 'SugarCRM, Inc.', + 'is_uninstallable' => 'true', + 'published_date' => date("Y-m-d H:i:s"), + 'type' => 'module', + 'acceptable_sugar_versions' => array( + 'exact_matches' => array( + ), + 'regex_matches' => array( + $supportedVersionRegex, + ), + ), + 'acceptable_sugar_flavors' => $acceptableSugarFlavors, +); + +$installdefs = array( + 'beans' => array (), + 'id' => $packageID +); + +echo "Creating {$zipFile} ... \n"; +$zip = new ZipArchive(); +$zip->open($zipFile, ZipArchive::CREATE); +$basePath = realpath('src/'); +$files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($basePath, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::LEAVES_ONLY +); +foreach ($files as $name => $file) { + if ($file->isFile()) { + $fileReal = $file->getRealPath(); + $fileRelative = 'src' . str_replace($basePath, '', $fileReal); + echo " [*] $fileRelative \n"; + $zip->addFile($fileReal, $fileRelative); + $installdefs['copy'][] = array( + 'from' => '/' . $fileRelative, + 'to' => preg_replace('/^src[\/\\\](.*)/', '$1', $fileRelative), + ); + } +} +$manifestContent = sprintf( + "addFromString('manifest.php', $manifestContent); +$zip->close(); +echo "Done creating {$zipFile}\n\n"; +exit(0); diff --git a/packages/SugarBPM_CapitalizeRecordName_Action/src/custom/Extension/application/Ext/JSGroupings/pmsegrouping.php b/packages/SugarBPM_CapitalizeRecordName_Action/src/custom/Extension/application/Ext/JSGroupings/pmsegrouping.php new file mode 100644 index 0000000..e3bfcf0 --- /dev/null +++ b/packages/SugarBPM_CapitalizeRecordName_Action/src/custom/Extension/application/Ext/JSGroupings/pmsegrouping.php @@ -0,0 +1,11 @@ + $groupings) { + foreach ($groupings as $target) { + if ($target == 'include/javascript/pmse.designer.min.js') { + // Map the name of your custom JS file to the index here + // It can be named whatever you want and can be stored wherever you want + $js_groupings[$key]['custom/include/javascript/pmse/capitalizename_activity.js'] = 'include/javascript/pmse.designer.min.js'; + } + break; + } +} \ No newline at end of file diff --git a/packages/SugarBPM_CapitalizeRecordName_Action/src/custom/include/javascript/pmse/capitalizename_activity.js b/packages/SugarBPM_CapitalizeRecordName_Action/src/custom/include/javascript/pmse/capitalizename_activity.js new file mode 100644 index 0000000..82601bb --- /dev/null +++ b/packages/SugarBPM_CapitalizeRecordName_Action/src/custom/include/javascript/pmse/capitalizename_activity.js @@ -0,0 +1,42 @@ +/** + * Gets custom action context menu addition + * @return {object} Action definition for a context menu + */ +AdamActivity.prototype.customContextMenuActions = function() { + return [{ + name: 'CAPITALIZE_NAME', + text: 'Capitalize Record Name' + }] +} + +/** + * Gets a custom action definition for rendering + * @param {string} type The action type + * @param {object} w Window definition + * @return {object} Definition for the custom action + */ +AdamActivity.prototype.customGetAction = function(type, w) { + switch (type) { + case 'CAPITALIZE_NAME': + // We should really use translate here for i18n + var actionText = 'Capitalize Record Name'; + var actionCSS = 'adam-menu-icon-configure'; + var disabled = true; + return {actionText: actionText, actionCSS: actionCSS, disabled: disabled}; + } +} + +/** + * Needed to define the modal that pops up with a form + * @param {string} type The action type + * @return {object} Definition needed for a modal + */ +AdamActivity.prototype.customGetWindowDef = function(type) { + switch(type) { + case 'CAPITALIZE_NAME': + var wWidth = 500; + var wHeight = 140; + var wTitle = 'Capitalize Record Name'; + return {wWidth: wWidth, wHeight: wHeight, wTitle: wTitle}; + } +} \ No newline at end of file diff --git a/packages/SugarBPM_CapitalizeRecordName_Action/src/custom/modules/pmse_Inbox/engine/PMSEElements/PMSECapitalizeName.php b/packages/SugarBPM_CapitalizeRecordName_Action/src/custom/modules/pmse_Inbox/engine/PMSEElements/PMSECapitalizeName.php new file mode 100644 index 0000000..d728593 --- /dev/null +++ b/packages/SugarBPM_CapitalizeRecordName_Action/src/custom/modules/pmse_Inbox/engine/PMSEElements/PMSECapitalizeName.php @@ -0,0 +1,76 @@ +capitalizeName($bean); + + $flowAction = $externalAction === 'RESUME_EXECUTION' ? 'UPDATE' : 'CREATE'; + return $this->prepareResponse($flowData, 'ROUTE', $flowAction); + } + + /** + * Handles the actual capitalization of the necessary fields + * @param SugarBean $bean + */ + protected function capitalizeName(SugarBean $bean) + { + if ($bean instanceof SugarBean) { + foreach ($bean->field_defs as $name => $defs) { + if ($this->isNameField($defs)) { + $fields = $this->getNameFields($defs); + foreach ($fields as $field) { + $bean->$field = strtoupper($bean->$field); + } + } + } + + $bean->save(); + } + } + + /** + * Gets the field or fields that are name type fields + * @param array $defs + * @return array + */ + protected function getNameFields($defs) + { + if (isset($defs['db_concat_fields'])) { + return $defs['db_concat_fields']; + } + + if (isset($defs['name'])) { + return array($defs['name']); + } + + return array(); + } + + /** + * Checks to see if a field def is a name type fields + * @param array $defs + * @return boolean + */ + protected function isNameField($defs) + { + if (isset($defs['type']) && $defs['type'] === 'name') { + return true; + } + + if (isset($defs['type']) && $defs['type'] === 'fullname') { + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/packages/SugarBPM_Dashlet/pack.php b/packages/SugarBPM_Dashlet/pack.php new file mode 100755 index 0000000..9396154 --- /dev/null +++ b/packages/SugarBPM_Dashlet/pack.php @@ -0,0 +1,93 @@ +#!/usr/bin/env php + $packageID, + 'name' => $packageLabel, + 'description' => $description, + 'version' => $version, + 'author' => 'SugarCRM, Inc.', + 'is_uninstallable' => 'true', + 'published_date' => date("Y-m-d H:i:s"), + 'type' => 'module', + 'acceptable_sugar_versions' => array( + 'exact_matches' => array( + ), + 'regex_matches' => array( + $supportedVersionRegex, + ), + ), + 'acceptable_sugar_flavors' => $acceptableSugarFlavors, +); + +$installdefs = array( + 'beans' => array (), + 'id' => $packageID +); + +echo "Creating {$zipFile} ... \n"; +$zip = new ZipArchive(); +$zip->open($zipFile, ZipArchive::CREATE); +$basePath = realpath('src/'); +$files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($basePath, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::LEAVES_ONLY +); +foreach ($files as $name => $file) { + if ($file->isFile()) { + $fileReal = $file->getRealPath(); + $fileRelative = 'src' . str_replace($basePath, '', $fileReal); + echo " [*] $fileRelative \n"; + $zip->addFile($fileReal, $fileRelative); + $installdefs['copy'][] = array( + 'from' => '/' . $fileRelative, + 'to' => preg_replace('/^src[\/\\\](.*)/', '$1', $fileRelative), + ); + } +} +$manifestContent = sprintf( + "addFromString('manifest.php', $manifestContent); +$zip->close(); +echo "Done creating {$zipFile}\n\n"; +exit(0); diff --git a/packages/SugarBPM_Dashlet/src/custom/Extension/application/Ext/Language/en_us.ProcessManagementDashlet.php b/packages/SugarBPM_Dashlet/src/custom/Extension/application/Ext/Language/en_us.ProcessManagementDashlet.php new file mode 100644 index 0000000..adec50b --- /dev/null +++ b/packages/SugarBPM_Dashlet/src/custom/Extension/application/Ext/Language/en_us.ProcessManagementDashlet.php @@ -0,0 +1,4 @@ + [ + 'reqType' => 'GET', + 'path' => ['pmse_Inbox', '?', '?'], + 'pathVars' => ['', 'module', 'id'], + 'method' => 'getModuleList', + 'acl' => 'adminOrDev', + 'shortHelp' => 'Returns the Current Processes for given module and id', + 'longHelp' => '', + ], + ]; + } + + public function getModuleList(ServiceBase $api, array $args) + { + + // cas_id + // pro_title + // cas_title + // cas_status + // prj_run_order + // cas_create_date + // assigned_user_full_name + // cas_user_id_full_name + // prj_user_id_full_name + + // Verify access + ProcessManager\AccessManager::getInstance()->verifyUserAccess($api, $args); + + // Set up the Sugar Query object + $q = new SugarQuery(); + + // And remove the order by stability since it was causing us problems + $q->setOrderByStability(false); + + // This is our primary select table + $inboxBean = BeanFactory::newBean('pmse_Inbox'); + + // Set the order by properly if we are expected a due date order + if ($args['order_by'] == 'cas_due_date:asc') { + $args['order_by'] = 'cas_create_date:asc'; + } + + $module = $args['module']; + $id = $args['id']; + + // Set up the necessary options for the query we will run + $options = $this->parseArguments($api, $args, $inboxBean); + + $fieldsArg = explode(",", $args['fields']); + + + // Replacement for using .* to get all columns + // Fields from inbox that are needed + // Removed the pro_title column because it contains old data and is never updated + $inboxFields = [ + 'id', 'name', 'date_entered', 'date_modified', + 'modified_user_id', 'deleted', + 'cas_id', 'cas_parent', 'cas_status', 'pro_id', + 'cas_title', 'cas_custom_status', 'cas_init_user', 'cas_create_date', + 'cas_update_date', 'cas_finish_date', 'cas_pin', 'cas_assigned_status', + 'cas_module', 'team_id', 'team_set_id', 'assigned_user_id', + ]; + + // Now put them into a format that SugarQuery likes + foreach ($inboxFields as $field) { + $fields[] = array("a.$field", $field); + } + + $q->from($inboxBean, array('alias' => 'a')); + + + //INNER USER TABLE + $fields[] = array("a.created_by", "created_by"); + $q->joinTable('users', array('alias' => 'u', 'joinType' => 'INNER', 'linkingTable' => true)) + ->on() + ->equalsField('u.id', 'a.created_by'); + $fields[] = ['u.last_name', 'assigned_user_name']; + + //INNER PROCESS TABLE + $q->joinTable('pmse_bpmn_process', array('alias' => 'pr', 'joinType' => 'INNER', 'linkingTable' => true)) + ->on() + ->equalsField('pr.id', 'a.pro_id'); + $fields[] = array('pr.prj_id', 'prj_id'); + $fields[] = array('pr.name', 'pro_title'); + + //INNER PROJECT TABLE + $q->joinTable('pmse_project', array('alias' => 'prj', 'joinType' => 'INNER', 'linkingTable' => true)) + ->on() + ->equalsField('prj.id', 'pr.prj_id'); + $fields[] = ['prj.assigned_user_id', 'prj_created_by']; + // $fields[] = ['prj.prj_module', 'prj_module']; + $fields[] = ['prj.prj_run_order', 'prj_run_order']; + + //INNER BPM FLOW + // This relationship is adding several duplicated rows to the query + // use of DISTINCT should be added + $q->joinTable('pmse_bpm_flow', array('alias' => 'pf', 'joinType' => 'INNER', 'linkingTable' => true)) + ->on() + ->equalsField('pf.cas_id', 'a.cas_id') + ->equals('pf.cas_index', 1); + $fields[] = ['pf.cas_sugar_module', 'cas_sugar_module']; + $fields[] = ['pf.cas_sugar_object_id', 'cas_sugar_object_id']; + + // Since we are retrieving deleted project's processes, we need to know + // which of them are from deleted projects. + $fields[] = array('pr.deleted', 'prj_deleted'); + + $q->select($fields); + + $q->where() + // Filtered for given module + ->equals('prj.prj_module', $module) + // Filtered for given module + ->equals('pf.cas_sugar_object_id', $id) + // Filtered for not deleted records + ->equals('u.deleted', 0); + + + if ($args['q'] != "") { + $regex = '/(?:(pnum):([\d]+))|(?:(pid):([a-f\d]{8}(-[a-f\d]{4}){4}[a-f\d]{8}+))|(?:(ptitle):(?:"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'|([^\s]+)))/'; + // If the string is only contains pid|ptitle: keyword + if (preg_match_all($regex, $args['q'], $matches, PREG_SET_ORDER, 0)){ + $queryAnd = $q->where->queryAnd(); + $queryOr = $queryAnd->queryOr(); + foreach ($matches as $match) { + foreach ($match as $matchKey => $matchValue) { + if($matchValue == ""){ + unset($match[$matchKey]); + } + } + $match = array_values($match); + switch($match[1]){ + case "pid":{ + $queryOr->equals('a.pro_id', $match[2]); + break; + } + case "pnum":{ + $queryOr->equals('a.cas_id', $match[2]); + break; + } + case "ptitle":{ + $queryOr->like('a.pro_title', '%'.$match[2].'%'); + break; + } + } + } + } + else{ + $qLike = $q->getDBManager()->quoted('%' . $args['q'] . '%'); + $q->where()->queryAnd() + ->addRaw(" + a.pro_title LIKE $qLike OR + a.cas_id LIKE $qLike OR + a.pro_id LIKE $qLike + "); + } + } + + if ($args['status'] != ""){ + $statusArgs = explode(",", $args['status']); + if(safeCount($statusArgs) > 0){ + $q->where()->queryAnd()->in("cas_status", array_values($statusArgs)); + } + } + + foreach ($options['order_by'] as $orderBy) { + $q->orderBy($orderBy[0], $orderBy[1]); + } + + // Add an extra record to the limit so we can detect if there are more records to be found + $q->limit($options['limit']); + $q->offset($options['offset']); + + $count = 0; + $list = $q->execute(); + + // Check if are passed in the field arguments + $additionFieldsCheck = safeCount(array_intersect(['date_entered, date_modified', 'cas_create_date', 'cas_user_id_full_name', 'prj_user_id_full_name', 'assigned_user_full_name'], $fieldsArg)) > 0; + if (!empty($list) && $additionFieldsCheck) { + foreach ($list as $key => $value) { + // Get the assigned bean early. This allows us to check for a bean + // id to determine if the bean has been deleted or not. This bean + // will also be used later to the assigned user of the record. + $assignedBean = BeanFactory::getBean($list[$key]['cas_sugar_module'], $list[$key]['cas_sugar_object_id'], ['erased_fields' => true]); + if (is_null($assignedBean)) { + continue; + } + + if(in_array('cas_title', $fieldsArg)){ + $list[$key] = PMSEEngineUtils::appendNameFields($assignedBean, $value); + } + if(in_array('cas_create_date', $fieldsArg)){ + $list[$key]['cas_create_date'] = PMSEEngineUtils::getDateToFE($value['cas_create_date'], 'datetime'); + } + // TODO: Add this to the field defs + if(in_array('date_entered', $fieldsArg)){ + $list[$key]['date_entered'] = PMSEEngineUtils::getDateToFE($value['date_entered'], 'datetime'); + } + // TODO: Add this to the field defs + if(in_array('date_modified', $fieldsArg)){ + $list[$key]['date_modified'] = PMSEEngineUtils::getDateToFE($value['date_modified'], 'datetime'); + } + if(in_array('assigned_user_full_name', $fieldsArg)){ + $assignedUsersBean = BeanFactory::getBean('Users', $assignedBean->assigned_user_id); + $list[$key]['assigned_user_full_name'] = $assignedUsersBean->full_name; + } + if(in_array('prj_user_id_full_name', $fieldsArg)){ + $prjUsersBean = BeanFactory::getBean('Users', $list[$key]['prj_created_by']); + $list[$key]['prj_user_id_full_name'] = $prjUsersBean->full_name; + } + + if(in_array('cas_user_id_full_name', $fieldsArg)){ + $qA = new SugarQuery(); + $flowBean = BeanFactory::newBean('pmse_BpmFlow'); + $qA->select->fieldRaw('*'); + $qA->from($flowBean); + $qA->where()->equals('cas_id', $list[$key]['cas_id']); + + $processUsers = $qA->execute(); + if (!empty($processUsers)) { + $processUsersNames = array(); + foreach ($processUsers as $k => $v) { + if ($processUsers[$k]['cas_flow_status'] != 'CLOSED') { + $casUsersBean = BeanFactory::getBean('Users', $processUsers[$k]['cas_user_id']); + $processUsersNames[] = (!empty($casUsersBean->full_name)) ? $casUsersBean->full_name : ''; + } + } + if (empty($processUsersNames)) { + $userNames = ''; + } else { + $processUsersNames = array_unique($processUsersNames); + $userNames = implode(', ', $processUsersNames); + } + $list[$key]['cas_user_id_full_name'] = $userNames; + } + } + + $count++; + } + } + else{ + $count = safeCount($list); + } + if ($count == $options['limit']) { + $offset = $options['offset'] + $options['limit']; + } else { + $offset = -1; + } + + $data = array(); + $data['next_offset'] = $offset; + $data['records'] = array_values($list); + return $data; + } +} diff --git a/packages/SugarBPM_Dashlet/src/custom/modules/pmse_Inbox/clients/base/views/process-management-dashlet/process-management-dashlet.hbs b/packages/SugarBPM_Dashlet/src/custom/modules/pmse_Inbox/clients/base/views/process-management-dashlet/process-management-dashlet.hbs new file mode 100644 index 0000000..1049ad4 --- /dev/null +++ b/packages/SugarBPM_Dashlet/src/custom/modules/pmse_Inbox/clients/base/views/process-management-dashlet/process-management-dashlet.hbs @@ -0,0 +1,88 @@ +{{!-- +/* + * Your installation or use of this SugarCRM file is subject to the applicable + * terms available at + * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. + * If you do not agree to all of the applicable terms or do not have the + * authority to bind the entity as an authorized representative, then do not + * install or use this SugarCRM file. + * + * Copyright (C) SugarCRM Inc. All rights reserved. + */ +--}} +{{#each filterFields}} +
+ {{#unless ignoreLabel}} +
+ {{str label ../../module}} +
+ {{/unless}} +
{{field ../this model=../filterFieldsModel}}
+
+{{/each}} +{{#if collection.dataFetched}} + {{#if collection.models.length}} + + + + + +
+ {{#each meta.panels}} + + + + + {{#each fields}} + + {{/each}} + + + + {{#each ../collection.models}} + + + {{#each ../fields}} + {{field ../../../this model=../this template=../../../viewName}} + {{/each}} + + {{/each}} + +
+ + + +
+
+ + {{#if icon}} + + {{/if}} + {{str label ../../module}} + +
+ + + + +
+
+ {{#with ../../rowActions}} + {{field ../../../this model=../this template=../../../viewName}} + {{/with}} +
+ {{/each}} +
+ {{else}} + + {{/if}} +{{else}} + +{{/if}} \ No newline at end of file diff --git a/packages/SugarBPM_Dashlet/src/custom/modules/pmse_Inbox/clients/base/views/process-management-dashlet/process-management-dashlet.js b/packages/SugarBPM_Dashlet/src/custom/modules/pmse_Inbox/clients/base/views/process-management-dashlet/process-management-dashlet.js new file mode 100644 index 0000000..18cef2f --- /dev/null +++ b/packages/SugarBPM_Dashlet/src/custom/modules/pmse_Inbox/clients/base/views/process-management-dashlet/process-management-dashlet.js @@ -0,0 +1,565 @@ +/** + * @class View.Views.Base.ProcessManagementDashletView + * @alias SUGAR.App.view.views.BaseProcessManagementDashletView + * @extends View.Views.Base.ListView + */ +({ + extendsFrom: 'ListView', + plugins:[ + 'Dashlet', + 'ResizableColumns', + 'Pagination', + 'ProcessActions' + ], + + _defaultSettings: { + label: 'LBL_PROCESS_MANAGEMENT', + limit: 5, + module: 'pmse_Inbox' + }, + + filterFields: [], + filterFieldsModel: new Backbone.Model({ + status_filter: [], + query_filter: "" + }), + isFocusDrawer: false, + + /** + * @inheritdoc + */ + initialize(options) { + this.logEnabled = app.user.lastState.get('pmse_Inbox:process-management-dashlet:logEnabled') === true; + this.cacheKiller = (new Date()).getTime(); + this.log({"this.isFocusDrawer": this.isFocusDrawer}); + this.log({"~this": this}); + this.isFocusDrawer = options.context.parent.get('layout') == 'focus'; + let lastStateId = options.type + (this.isFocusDrawer ? ":focus" : ""); + this.log({"lastStateId": lastStateId}); + // Append Last State for bunch of feature + options.meta['last_state'] = {'id': lastStateId }; + this._super('initialize', [options]); + + console.log("app.user.lastState.key('debug', this)", app.user.lastState.key('debug', this)); + console.log("app.user.lastState.get(app.user.lastState.key('debug', this))", app.user.lastState.get(app.user.lastState.key('debug', this))); + console.log("this.debugMode", this.debugMode); + this.orderByLastStateKey = app.user.lastState.key('order-by', this); + this.log({"this.orderByLastStateKey": this.orderByLastStateKey}); + + this.on('list:column:resize:save', (columns) => { + app.user.lastState.set(app.user.lastState.key('width-fields', this), columns); + }); + + this.process_module = this.context.parent.get("module"); + this.process_record_id = this.context.parent.get("model").get('id'); + this.log({"~this.process_module": this.process_module, "~this.process_record_id": this.process_record_id}); + + this.context.on('case:history', (model) => this.getHistory(model.get('cas_id'))); + this.context.on('case:notes', (model) => this.showNotes(model.get('cas_id'), 1)); + this.context.on('list:cancelCase:fire', (model) => this.cancelCases(model)); + this.context.on("list:preview:fire", (model) => app.events.trigger("preview:render", model, this.collection, true)); + this.context.on("case:preview:chart", (model) => window.open( + app.api.buildFileURL({ + module: 'pmse_Inbox', + id: model.get('cas_id'), + field: 'id' + }, {cleanCache: true})) + ); + this.events = _.extend(this.events, { + 'click [data-widths=reset]': 'resetColumnWidths', + 'click [track="click:rowactions"]': 'removeStyles' + }); + + this.rowActions = []; + this.addStatusFiltersField(); + this.addQueryFiltersField(); + }, + + removeStyles(e){ + this.$(e.currentTarget).next(".dropdown-menu").removeAttr('style'); + }, + + /** + * Adds status filter and necessary actions + */ + addStatusFiltersField(){ + const field = { + 'type':'enum', + 'name':'status_filter', + 'options':{ + 'COMPLETED': 'COMPLETED', + 'TERMINATED': 'TERMINATED', + 'IN PROGRESS': 'IN PROGRESS', + 'CANCELLED': 'CANCELLED', + 'ERROR': 'ERROR', + }, + 'label':'Status Filter: ', + 'placeholder': 'Status Filter', + 'tooltip':'Filters Processes by their statuses.', + 'isMultiSelect':true, + 'enabled':true, + 'tplName':'edit', + 'action':'edit', + 'view':'edit' + }; + if(this.filterFields.filter(f => f.name == "status_filter").length == 0){ + this.filterFields.push(field); + } + const statusFilterKey = app.user.lastState.key('status_filter', this); + const lastState = app.user.lastState.get(statusFilterKey); + if(lastState){ + this.filterFieldsModel.set("status_filter", lastState, {silent: true}); + } + this.filterFieldsModel.off("change:status_filter").on("change:status_filter", _.debounce(() => { + const statusFilter = this.filterFieldsModel.get("status_filter"); + app.user.lastState.set(statusFilterKey, statusFilter); + this.collection.fetch(); + }, 1000)); + }, + + /** + * Adds query filter and necessary actions + */ + addQueryFiltersField(){ + const field = { + 'type':'text', + 'name':'query_filter', + 'label':'Query Filter', + 'placeholder': 'Filter in Process Num(pnum:1), ID(pid:) and Title(ptitle:"text")', + 'tooltip':'Usage Num(pnum:1), ID(pid:), Title(ptitle:"search key") or any text', + 'enabled':true, + 'tplName':'edit', + 'action':'edit', + 'view':'edit', + 'ignoreLabel': true + }; + if(this.filterFields.filter(f => f.name == "query_filter").length == 0){ + this.filterFields.push(field); + } + const queryFilterKey = app.user.lastState.key('query_filter', this); + const lastState = app.user.lastState.get(queryFilterKey); + if(lastState){ + this.filterFieldsModel.set("query_filter", lastState, {silent: true}); + } + this.filterFieldsModel.off("change:query_filter").on("change:query_filter", _.debounce(() => { + const queryFilter = this.filterFieldsModel.get("query_filter"); + app.user.lastState.set(queryFilterKey, queryFilter); + this.collection.fetch(); + }, 750)); + }, + + /** + * @inheritdoc + */ + _initOrderBy() { + var lastStateOrderBy = app.user.lastState.get(this.orderByLastStateKey) || {}, + lastOrderedFieldMeta = this.getFieldMeta(lastStateOrderBy.field); + this.log({"~": "_initOrderBy", "~this.orderByLastStateKey": this.orderByLastStateKey, lastStateOrderBy, + lastOrderedFieldMeta}); + + if (_.isEmpty(lastOrderedFieldMeta) || !app.utils.isSortable(this.module, lastOrderedFieldMeta)) { + lastStateOrderBy = {}; + } + + // if no access to the field, don't use it + if (!_.isEmpty(lastStateOrderBy.field) && !app.acl.hasAccess('read', this.module, app.user.get('id'), lastStateOrderBy.field)) { + lastStateOrderBy = {}; + } + const dashletConfigOrderBy = (this.dashletConfig && 'orderBy' in this.dashletConfig) ? this.dashletConfig.orderBy : {}; + + return _.extend({ + field : '', + direction : 'desc' + }, + this.meta.orderBy, + dashletConfigOrderBy, + lastStateOrderBy + ); + }, + + /** + * Init dashlet settings + */ + initDashlet() { + this.log({"~": "initDashlet", "this": this}); + this.settings.on('change:module', () => { + this._updateDisplayColumns(); + this._hideUnselectedColumns(); + }); + this._initializeSettings(); + this.addRowActions(); + + this.log({"~this.meta.config": this.meta.config}); + if( !this.settings.get('display_columns') || this.settings.get('display_columns').length == 0 ){ + this._updateDisplayColumns(); + } + if (this.meta.config) { + this.log({"~": "Configuring Dashlet..", "this.settings.get('display_columns')": this.settings.get('display_columns')}); + this._configureDashlet(); + this.log({"~": "Configuring Dashlet.."}); + } + else { + this.log({"~": "Displaying Dashlet.."}); + this._displayDashlet(); + } + }, + + /** + * @inheritdoc + */ + getCacheWidths(){ + return app.user.lastState.get(app.user.lastState.key('width-fields', this)); + }, + + /** + * Certain dashlet settings can be defaulted. + * + * Builds the available module cache by way of the + * {@link BaseDashablelistView#_setDefaultModule} call. The module is set + * after "filter_id" because the value of "filter_id" could impact the value + * of "label" when the label is set in response to the module change while + * in configuration mode (see the "module:change" listener in + * {@link BaseDashablelistView#initDashlet}). + * + * @private + */ + _initializeSettings() { + for (const key in this._defaultSettings) { + if (!this.settings.get(key)) { + this.settings.set(key, this._defaultSettings[key]); + } + } + }, + + /** + * Perform any necessary setup before the user can configure the dashlet. + * + * Modifies the dashlet configuration panel metadata to allow it to be + * dynamically primed prior to rendering. + * + * @private + */ + _configureDashlet() { + var availableColumns = this._getAvailableColumns(); + this.log({"~_configureDashlet": availableColumns}); + _.each(this.getFieldMetaForView(this.meta), function(field) { + switch(field.name) { + case 'display_columns': + // load the list of available columns into the metadata + field.options = availableColumns; + break; + } + }); + }, + + /** + * Gets all of the fields from the list view metadata for the currently + * chosen module. + * + * This is used for the populating the list view columns field and + * displaying the list. + * + * @return {Object} {@link BaseDashablelistView#_availableColumns} + * @private + */ + _getAvailableColumns() { + var columns = {}, + module = this.settings.get('module'); + if (!module) { + return columns; + } + _.each(this.getDashletFields(), function(field) { + columns[field.name] = app.lang.get(field.label || field.name, module); + }); + return columns; + }, + + /** + * Gets the fields metadata from a particular view's metadata. + * + * @param {Object} meta The view's metadata. + * @return {Object[]} The fields metadata or an empty array. + */ + getFieldMetaForView(meta) { + meta = _.isObject(meta) ? meta : {}; + var fields = !_.isUndefined(meta.panels) ? _.flatten(_.pluck(meta.panels, 'fields')) : []; + return fields; + }, + + /** + * Returns the dashlet fields + * + * @returns {array} + */ + getDashletFields() { + let fields = []; + if('fields' in this.dashletConfig) { + fields = this.dashletConfig.fields; + } + else{ + fields = this.getFieldMetaForView(this._getListMeta(this.settings.get('module'))) + } + return fields; + }, + + /** + * Gets the correct list(casesList-list) view metadata. + * + * Returns the correct module list metadata + * + * @param {String} module + * @return {Object} + */ + _getListMeta(module) { + return app.metadata.getView(module, 'casesList-list'); + }, + + /** + * Update the display_columns attribute based on the current module defined + * in settings. + * + * This will mark, as selected, all fields in the module's list view + * definition. Any existing options will be replaced with the new options + * if the "display_columns" DOM field ({@link EnumField}) exists. + * + * @private + */ + _updateDisplayColumns() { + var availableColumns = this._getAvailableColumns(), + columnsFieldName = 'display_columns', + columnsField = this.getField(columnsFieldName); + if (columnsField) { + columnsField.items = availableColumns; + } + this.settings.set(columnsFieldName, _.keys(availableColumns)); + }, + + + /** + * Perform any necessary setup before displaying the dashlet. + * @private + */ + _displayDashlet() { + // Get the columns that are to be displayed and update the panel metadata. + const fields = this._getColumnsForDisplay(); + this.meta.panels = [{fields}]; + this.log({"~":"_displayDashlet", fields, "~this.meta.panels": this.meta.panels}); + this.context.set('skipFetch', false); + this.limit = this.settings.get('limit'); + this.context.set('limit', this.limit); + const contextFields = fields.reduce((p, c) => (p.push(c.name), p), []); + this.context.set('fields', contextFields); + + this.collection.setOption('endpoint', (method, model, options, callbacks) => { + this.log({method, model, options}); + const params = options.params || {}; + const status = this.filterFieldsModel.get("status_filter").join(","); + if(status !== ""){ + params['status'] = status; + } + const query = this.filterFieldsModel.get("query_filter"); + if(query !== ""){ + params['q'] = query; + } + const url = ['pmse_Inbox', this.process_module, this.process_record_id].join("/"); + this.log({url, params}); + return app.api.call('read',app.api.buildURL(url, 'read', '', params), null, callbacks); + }); + this.orderBy = this._initOrderBy(); + this.collection.orderBy = this.orderBy; + this.context.set('collection', this.collection); + + }, + + /** + * Gets the columns chosen for display for this dashlet list. + * + * The display_columns setting might not have been defined when the dashlet + * is being displayed from a metadata definition, like is the case for + * preview and the default dashablelist's that are defined. All columns for + * the selected module are shown in these cases. + * + * @return {Object[]} Array of objects defining the field metadata for + * each column. + * @private + */ + _getColumnsForDisplay() { + var columns = {}; + var fields = this.getDashletFields(); + this.log({"~":"_getColumnsForDisplay", fields, "this.settings.get('display_columns')": this.settings.get('display_columns')}); + if (!this.settings.get('display_columns')) { + this._updateDisplayColumns(); + this._hideUnselectedColumns(); + } + for (const field of fields) { + const displayColumns = this.settings.get('display_columns'); + const index = displayColumns.indexOf(field.name); + if(index !== -1){ + columns[index] = field; + } + } + columns = Object.values(columns); + this.log({columns}); + + return columns; + }, + + /** + * When creating a dashlet by default all columns available will be shown. + * By a flag set in metadata (selected) some column can be rendered hidden + * and optionally selectable. Display only columns that are not excluded from + * the initial list of columns. Changes made by the users should not be overwritten. + */ + _hideUnselectedColumns() { + var columns = this.settings.get('display_columns'); + _.each(this.getDashletFields(), function(fieldDef) { + if (_.contains(columns, fieldDef.name) && fieldDef.selected === false) { + columns = _.without(columns, fieldDef.name); + } + }); + this.settings.set('display_columns', columns); + }, + + /** + * Adds row actions as a meta to be used + */ + addRowActions() { + var _generateMeta = function(label, css_class, buttons) { + return { + 'type': 'rowactions', + 'label': label || '', + 'css_class': css_class, + 'buttons': buttons || [], + 'name': 'process-manager-dashlet-actions', + 'no_default_action': true, + 'value': false, + 'sortable': false + }; + }; + var def = this.dashletConfig.rowactions; + if(this.isFocusDrawer){ + def.actions = def.actions.filter(button => !button.hideOnFocusDrawer); + } + this.rowActions = _generateMeta(def.label, def.css_class, def.actions); + }, + + /** + * Resets the column widths to the default settings. + * + * If the stickiness is enabled, it also removes the entry from the cache. + */ + resetColumnWidths: function() { + const widthFieldsKey = app.user.lastState.key('width-fields', this); + if (widthFieldsKey) { + app.user.lastState.remove(widthFieldsKey); + } + if (!this.disposed) { + this.render(); + } + }, + + /** + * Cancel Project event handler + * + * @param {object} model + */ + cancelCases(model){ + let messages = app.lang.get('LBL_PMSE_CANCEL_MESSAGE', this.module) + .replace('[]', model.get('cas_title')) + .replace('{}', model.get('cas_id')); + + const Alert = app.alert; + Alert.show('cancelCase-id', { + level: 'confirmation', + messages, + autoClose: false, + onConfirm: () => { + Alert.show('cancelCase-in-progress', {level: 'process', title: 'LBL_LOADING', autoclose: false}); + const payload = model.toJSON(); + payload.cas_id = [model.get('cas_id')]; + app.api.call('update', app.api.buildURL(this.module + '/cancelCases'), payload, { + success: () => { + Alert.dismiss('cancelCase-in-progress'); + this.context.reloadData({ + recursive:false, + }); + } + }); + }, + onCancel: () => Alert.dismiss('cancelCase-id') + }); + }, + + // Utils + logEnabled: false, + log(argsObject, level){ + if( this.logEnabled !== true) return; + + let colors = ['sienna', 'darkcyan', 'cornflowerblue', 'chocolate', 'palevioletred']; + // let colors = [ 'darkkhaki', 'chocolate', 'sienna', 'saddlebrown', 'tomato', 'slategray', 'cadetblue' ]; + let niceColors = [ 'steelblue' ]; + let defaultValueColor = 'dimgray'; + let valueColor = defaultValueColor; + if(level == "success"){ + niceColors = ['seagreen'] + } + else if ( level == "warning" ){ + niceColors = ['peru'] + } + else if ( level == "fail" || level == "error" || level == "danger" ){ + niceColors = ['indianred'] + } + niceColors = niceColors.concat(colors); + + let styles = [], + log = "", + misc = [], + count = 0, + size = 0; + + let printable = {}; + for (const variableName in argsObject) { + if(!["object", "function"].includes(typeof argsObject[variableName])) { + size++; + printable[variableName] = argsObject[variableName]; + } + else{ + misc.push(variableName+":"); + misc.push(argsObject[variableName]); + } + } + for (const variableName in printable) { + const value = printable[variableName]; + let isIgnore = /\~.*/.test(variableName); + let currentLog = (isIgnore ? `` : `%c ${variableName} `) + `%c ${value} `; + let firstRadius = ""; + let secondRadius = ""; + let color = niceColors[count%niceColors.length]; + + if (size == 1 && isIgnore) { + secondRadius = "border-radius:3px;"; + valueColor = color; + } + else if (count == 0) { + if(!isIgnore){ + firstRadius = "border-radius:3px 0 0 3px;"; + } + else{ + secondRadius = "border-radius:3px 0 0 3px;"; + valueColor = color; + } + } + else if (count == size-1){ + secondRadius = "border-radius:0 3px 3px 0;"; + } + + if(!isIgnore) + styles.push(`background:${color}; padding: 2px 1px; color: #fff; font-weight:bold; font-style:italic; ${firstRadius}`); + styles.push(`background:${valueColor}; padding: 2px 1px; color: #fff; ${secondRadius}`); + log += currentLog; + count++; + valueColor = defaultValueColor; + } + console.log(log, ...styles, ...misc); + } +}) \ No newline at end of file diff --git a/packages/SugarBPM_Dashlet/src/custom/modules/pmse_Inbox/clients/base/views/process-management-dashlet/process-management-dashlet.php b/packages/SugarBPM_Dashlet/src/custom/modules/pmse_Inbox/clients/base/views/process-management-dashlet/process-management-dashlet.php new file mode 100644 index 0000000..8c98109 --- /dev/null +++ b/packages/SugarBPM_Dashlet/src/custom/modules/pmse_Inbox/clients/base/views/process-management-dashlet/process-management-dashlet.php @@ -0,0 +1,162 @@ + 'process-management-dashlet', + 'dashlets' => [ + [ + 'label' => 'LBL_PROCESS_MANAGEMENT_DASHLET_TITLE', + 'description' => 'LBL_PROCESS_MANAGEMENT_DASHLET_DESCRIPTION', + 'config' => [ + 'module' => 'pmse_Inbox', + 'label' => 'LBL_PROCESS_MANAGEMENT_DASHLET', + ], + 'preview' => [ + 'module' => 'pmse_Inbox', + 'label' => 'LBL_PROCESS_MANAGEMENT_DASHLET', + ], + 'filter' => [ + 'view' => 'record', + ], + ], + ], + 'panels' => [ + [ + 'name' => 'panel_body', + 'columns' => 2, + 'labelsOnTop' => true, + 'placeholders' => true, + 'fields' => [ + [ + 'name' => 'display_columns', + 'label' => 'LBL_COLUMNS', + 'type' => 'enum', + 'isMultiSelect' => true, + 'ordered' => true, + 'span' => 12, + 'hasBlank' => false, + ], [ + 'name' => 'limit', + 'label' => 'LBL_DASHLET_CONFIGURE_DISPLAY_ROWS', + 'type' => 'enum', + 'options' => 'dashlet_limit_options', + ] + ], + ], + ], + 'fields' => [ + [ + 'name' => 'cas_id', + 'label' => 'LBL_CAS_ID', + 'default' => true, + 'enabled' => true, + 'link' => false, + ], + [ + 'name' => 'pro_title', + 'label' => 'LBL_PROCESS_DEFINITION_NAME', + 'type' => 'pmse-link', + 'default' => true, + 'enabled' => true, + 'link' => true, + ], + [ + 'name' => 'cas_status', + 'label' => 'LBL_STATUS', + 'type' => 'event-status-pmse', + 'enabled' => true, + 'default' => true, + ], + [ + 'name' => 'prj_run_order', + 'label' => 'LBL_PROJECT_RUN_ORDER', + 'default' => true, + 'enabled' => true, + ], + [ + 'label' => 'LBL_DATE_ENTERED', + 'enabled' => true, + 'default' => true, + 'name' => 'cas_create_date', + 'readonly' => true, + ], + [ + 'label' => 'LBL_OWNER', + 'enabled' => true, + 'default' => true, + 'name' => 'assigned_user_full_name', + 'readonly' => true, + 'link' => false, + ], + [ + 'name' => 'cas_user_id_full_name', + 'label' => 'LBL_ACTIVITY_OWNER', + 'default' => true, + 'enabled' => true, + 'link' => false, + ], + [ + 'name' => 'prj_user_id_full_name', + 'label' => 'LBL_PROCESS_OWNER', + 'default' => true, + 'enabled' => true, + 'link' => false, + ], + [ + 'name' => 'date_entered', + 'label' => 'LBL_DATE_ENTERED' + ], + [ + 'name' => 'date_modified', + 'label' => 'LBL_DATE_MODIFIED' + ] + ], + 'orderBy' => [ + 'field' => 'cas_status', + 'direction' => 'desc', + ], + 'rowactions' => [ + 'actions' => [ + [ + 'type' => 'cancelcasebutton', + 'name' => 'cancelButton', + 'label' => 'Cancel Process', + 'icon' => 'sicon-remove', + 'event' => 'list:cancelCase:fire', + 'css_class'=>'overflow-visible', + 'tooltip'=> 'Cancel Process', + ], [ + 'type' => 'rowaction', + 'label' => 'Preview Chart', + 'icon' => 'sicon-nodes', + 'event' => 'case:preview:chart', + 'css_class'=>'overflow-visible', + 'tooltip'=> 'Preview Chart', + ], [ + 'type' => 'rowaction', + 'name' => 'History', + 'icon' => 'sicon-message', + 'label' => 'LBL_PMSE_LABEL_HISTORY', + 'event' => 'case:history', + 'css_class'=>'overflow-visible', + 'tooltip'=> 'History', + ], [ + 'type' => 'rowaction', + 'name' => 'viewNotes', + 'icon' => 'sicon-document', + 'label' => 'LBL_PMSE_LABEL_NOTES', + 'event' => 'case:notes', + 'css_class'=>'overflow-visible', + 'tooltip'=> 'Notes', + + ], [ + 'type' => 'rowaction', + 'label' => 'ListView Preview', + 'icon' => 'sicon-preview', + 'event' => 'list:preview:fire', + 'css_class'=>'overflow-visible', + 'tooltip'=> 'Preview', + 'hideOnFocusDrawer' => true + ], + ], + 'css_class'=>'overflow-visible actionmenu', + ], +]; \ No newline at end of file diff --git a/packages/FloatingDivAction-10.2+/pack.php b/packages/SugarBPM_Pruner/pack.php similarity index 83% rename from packages/FloatingDivAction-10.2+/pack.php rename to packages/SugarBPM_Pruner/pack.php index 9c06d63..cad28c1 100755 --- a/packages/FloatingDivAction-10.2+/pack.php +++ b/packages/SugarBPM_Pruner/pack.php @@ -1,11 +1,13 @@ #!/usr/bin/env php $packageID, 'name' => $packageLabel, - 'description' => $packageLabel, + 'description' => $description, 'version' => $version, 'author' => 'SugarCRM, Inc.', 'is_uninstallable' => 'true', @@ -52,25 +54,24 @@ $supportedVersionRegex, ), ), + 'acceptable_sugar_flavors' => $acceptableSugarFlavors, ); $installdefs = array( 'beans' => array (), - 'id' => $packageID, + 'id' => $packageID ); -echo "Creating {$zipFile} ... \n"; +echo "Creating {$zipFile} ... \n"; $zip = new ZipArchive(); $zip->open($zipFile, ZipArchive::CREATE); $basePath = realpath('src/'); - $files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($basePath, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::LEAVES_ONLY ); - foreach ($files as $name => $file) { - if ($file->isFile() and !empty(pathinfo($file)['filename'])) { + if ($file->isFile()) { $fileReal = $file->getRealPath(); $fileRelative = 'src' . str_replace($basePath, '', $fileReal); echo " [*] $fileRelative \n"; @@ -81,15 +82,12 @@ ); } } - $manifestContent = sprintf( "addFromString('manifest.php', $manifestContent); $zip->close(); - echo "Done creating {$zipFile}\n\n"; exit(0); diff --git a/packages/SugarBPM_Pruner/src/custom/Extension/modules/Schedulers/Ext/Language/en_us.SugarBPMPruneOldProcessesJob.php b/packages/SugarBPM_Pruner/src/custom/Extension/modules/Schedulers/Ext/Language/en_us.SugarBPMPruneOldProcessesJob.php new file mode 100644 index 0000000..107d619 --- /dev/null +++ b/packages/SugarBPM_Pruner/src/custom/Extension/modules/Schedulers/Ext/Language/en_us.SugarBPMPruneOldProcessesJob.php @@ -0,0 +1,2 @@ + 50, + 'days_before' => 30, +]; diff --git a/packages/SugarBPM_Pruner/src/custom/src/SugarBPM/PruneOldProcessesJob.php b/packages/SugarBPM_Pruner/src/custom/src/SugarBPM/PruneOldProcessesJob.php new file mode 100644 index 0000000..b17251b --- /dev/null +++ b/packages/SugarBPM_Pruner/src/custom/src/SugarBPM/PruneOldProcessesJob.php @@ -0,0 +1,318 @@ +setLogger(\Sugarcrm\Sugarcrm\Logger\Factory::getLogger('bpm_pruner')); + $this->limit = \SugarConfig::getInstance()->get('bpm_pruner.limit', $this->limit); + $this->deleteDaysBefore = \SugarConfig::getInstance()->get('bpm_pruner.days_before', $this->deleteDaysBefore); + } + + /** + * @param SchedulersJob $job + * + * @return void + */ + public function setJob(SchedulersJob $job): void + { + $this->job = $job; + } + + /** + * @param mixed $data + * + * @return bool + */ + public function run($data): bool + { + try { + $this->init($data); + + $status = $this->runWithDeadline($this->deadline); + + if ($status["shouldReschedule"] === true) { + $this->scheduleNextQuery(true, $status["processed"]); + } + + } catch (\Throwable $e) { + $this->job->name .= " [ERROR: " . $e->getMessage() . "]"; + $this->job->message = $e->getMessage() . "\n" . $e->getTraceAsString(); + } + + return true; + } + + /** + * + * @param mixed $data + * @return void + */ + protected function init($data): void + { + /** @var SugarCronJobs $jobq */ + global $jobq; + + $this->timeStart = \TimeDate::getInstance()->getNow()->getTimestamp(); + + if (\intval($jobq->max_runtime) <= $this->timeoutThreshold) { + $this->deadline = $this->timeStart + (\intval($jobq->max_runtime)); + } else { + $this->deadline = $this->timeStart + (\intval($jobq->max_runtime)) - $this->timeoutThreshold; + } + + $this->logger->info( + "Reschedule job started at {$this->time_start} with a deadline of {$this->deadline}." + ); + + //Set $this->shouldProcess = true in order to enter at the first time in the functionality coode. + $this->shouldProcess = true; + } + + /** + * + * @param int $deadline + * @return array + * @throws Exception + * @throws DBALException + */ + protected function runWithDeadline(int $deadline): array + { + + while ($this->shouldProcess === true) { + $recordsData = $this->getData($this->deleteDaysBefore); + $processed = 0; + + if (empty($recordsData) === true) { + return [ + "shouldReschedule" => false, + ]; + } + + foreach ($recordsData as $recordData) { + $recordCasId = $recordData["cas_id"]; + + $this->deleteOldRecords($recordCasId); + + $processed++; + + if (time() > $deadline) { + return [ + "shouldReschedule" => true, + "processed" => $processed, + ]; + } + } + + if (time() > $this->deadline) { + return [ + "shouldReschedule" => true, + "processed" => $processed, + ]; + } + + if ($processed < $this->limit) { + $this->shouldProcess = false; + } + } + return [ + "shouldReschedule" => false, + ]; + } + + /** + * + * @param string $firstLimitDateModified + * @param string $secondLimitDateModified + * @param array $statuses + * @param string $status + * @return array + * @throws Exception + * @throws DBALException + */ + protected function getData(int $daysAgo): array + { + $conn = \DBManagerFactory::getConnection(); + + $bpm_inbox_select_query = <<logger->info("getData: Running query to get cas_id list"); + $bpm_inbox_select_stmt = $conn->executeQuery($bpm_inbox_select_query, + [ + self::DONE_STATUSES, + $daysAgo, + $this->limit, + ], + [ + \Sugarcrm\Sugarcrm\Dbal\Connection::PARAM_STR_ARRAY, + \Doctrine\DBAL\ParameterType::INTEGER, + \Doctrine\DBAL\ParameterType::INTEGER, + ]); + + return $bpm_inbox_select_stmt->fetchAllAssociative(); + } + + + /** + * + * @param string $recordCasId + * @return void + * @throws Exception + * @throws DBALException + */ + public function deleteOldStartEvents(string $recordCasId): void + { + + $conn = \DBManagerFactory::getConnection(); + + //Check that cas_id linked to Process Definiton that is not a "First Update" BPM + $bpm_flow_start_event_select_query = <<logger->info("deleteOldStartEvents: Running query to confirm cas_id belongs to non-update Process Definition [cas_id : {$recordCasId}]"); + + $bpm_flow_start_event_select_stmt = $conn->executeQuery($bpm_flow_start_event_select_query,[$recordCasId], [\Doctrine\DBAL\ParameterType::STRING]); + $bpm_flow_start_event_select_result_count = $bpm_flow_start_event_select_stmt->rowCount(); + + if($bpm_flow_start_event_select_result_count > 0) { + $this->logger->info("deleteOldStartEvents: Deleting rows from pmse_bpm_flow [cas_id : {$recordCasId}]"); + $bpm_flow_start_event_delete_query = 'DELETE FROM pmse_bpm_flow WHERE cas_id = ?'; + $bpm_flow_start_event_delete_stmt = $conn->executeQuery($bpm_flow_start_event_delete_query,[$recordCasId], [\Doctrine\DBAL\ParameterType::STRING]); + } + } + + /** + * + * @param string $recordCasId + * @return void + * @throws Exception + */ + public function deleteOldRecords(string $recordCasId): void + { + $this->deleteOldStartEvents($recordCasId); + + $conn = \DBManagerFactory::getConnection(); + + $this->logger->info("deleteOldRecords: Deleting rows from pmse_bpm_flow [cas_id : {$recordCasId}]"); + $bpm_flow_delete_query = 'DELETE FROM pmse_bpm_flow WHERE cas_id = ? AND cas_index != 1'; + $bpm_flow_delete_stmt = $conn->executeQuery($bpm_flow_delete_query,[$recordCasId], [\Doctrine\DBAL\ParameterType::STRING]); + + $this->logger->info("deleteOldRecords: Deleting rows from pmse_inbox [cas_id : {$recordCasId}]"); + $bpm_inbox_delete_query = 'DELETE FROM pmse_inbox WHERE cas_id = ?'; + $bpm_inbox_delete_stmt = $conn->executeQuery($bpm_inbox_delete_query,[$recordCasId], [\Doctrine\DBAL\ParameterType::STRING]); + + $this->logger->info("deleteOldRecords: Deleting rows from pmse_bpm_thread [cas_id : {$recordCasId}]"); + $bpm_thread_delete_query = 'DELETE FROM pmse_bpm_thread WHERE cas_id = ?'; + $bpm_thread_delete_stmt = $conn->executeQuery($bpm_thread_delete_query,[$recordCasId], [\Doctrine\DBAL\ParameterType::STRING]); + } + + /** + * + * @param bool $timeLimitReached + * @param string $processed + * @return void + * @throws UnsatisfiedDependencyException + * @throws Exception + * @throws ExceptionInvalidArgumentException + */ + protected function scheduleNextQuery(bool $timeLimitReached, string $processed): void + { + global $current_user; + $processed = [ + "Processed" => $processed, + ]; + $job = new \SchedulersJob(); + $job->name = $this->buildJobName($processed); + $job->target = $this->job->target; + $job->assigned_user_id = $current_user->id; + $job->scheduler_id = $this->job->scheduler_id; + + /** + * === Start hack === + * + * Prevent the current job from being scheduled multiple times at the same time and date + */ + $timedate = \TimeDate::getInstance(); + $previousAllowCache = $timedate->allow_cache; + $timedate->allow_cache = false; + $job->execute_time = $timedate->getNow()->modify($timeLimitReached ? "+15 seconds" : "+120 seconds")->asDb(); + $timedate->allow_cache = $previousAllowCache; + /** + * === End hack === + */ + + $jobQueue = new \SugarJobQueue(); + $jobQueue->submitJob($job); + } + + /** + * @param array $params + * + * @return string + * + * @throws SugarQueryException + */ + protected function buildJobName(array $params = []): string + { + if (empty($this->job->scheduler_id)) { + return $this->job->name; + } + + $scheduler = \BeanFactory::retrieveBean("Schedulers", $this->job->scheduler_id); + + $jobName = $scheduler instanceof Scheduler ? $scheduler->name : ""; + + foreach ($params as $name => $value) { + $jobName = "[CHILD]{$jobName}[{$name}: {$value}]"; + } + + return $jobName; + } +} diff --git a/packages/SugarBPM_Webhook_Action/pack.php b/packages/SugarBPM_Webhook_Action/pack.php new file mode 100755 index 0000000..3a0c644 --- /dev/null +++ b/packages/SugarBPM_Webhook_Action/pack.php @@ -0,0 +1,93 @@ +#!/usr/bin/env php + $packageID, + 'name' => $packageLabel, + 'description' => $description, + 'version' => $version, + 'author' => 'SugarCRM, Inc.', + 'is_uninstallable' => 'true', + 'published_date' => date("Y-m-d H:i:s"), + 'type' => 'module', + 'acceptable_sugar_versions' => array( + 'exact_matches' => array( + ), + 'regex_matches' => array( + $supportedVersionRegex, + ), + ), + 'acceptable_sugar_flavors' => $acceptableSugarFlavors, +); + +$installdefs = array( + 'beans' => array (), + 'id' => $packageID +); + +echo "Creating {$zipFile} ... \n"; +$zip = new ZipArchive(); +$zip->open($zipFile, ZipArchive::CREATE); +$basePath = realpath('src/'); +$files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($basePath, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::LEAVES_ONLY +); +foreach ($files as $name => $file) { + if ($file->isFile()) { + $fileReal = $file->getRealPath(); + $fileRelative = 'src' . str_replace($basePath, '', $fileReal); + echo " [*] $fileRelative \n"; + $zip->addFile($fileReal, $fileRelative); + $installdefs['copy'][] = array( + 'from' => '/' . $fileRelative, + 'to' => preg_replace('/^src[\/\\\](.*)/', '$1', $fileRelative), + ); + } +} +$manifestContent = sprintf( + "addFromString('manifest.php', $manifestContent); +$zip->close(); +echo "Done creating {$zipFile}\n\n"; +exit(0); diff --git a/packages/SugarBPM_Webhook_Action/src/custom/Extension/application/Ext/JSGroupings/CustomPMSEWebhook.php b/packages/SugarBPM_Webhook_Action/src/custom/Extension/application/Ext/JSGroupings/CustomPMSEWebhook.php new file mode 100644 index 0000000..7bd0391 --- /dev/null +++ b/packages/SugarBPM_Webhook_Action/src/custom/Extension/application/Ext/JSGroupings/CustomPMSEWebhook.php @@ -0,0 +1,21 @@ + $groupings) { + foreach ($groupings as $file => $target) { + //if the target grouping is found + if ($target == 'include/javascript/pmse.designer.min.js') { + //append the custom JavaScript file to existing grouping + $js_groupings[$key]['custom/include/javascript/pmse/CustomWebhookAction.js'] = 'include/javascript/pmse.designer.min.js'; + } + break; + } +} diff --git a/packages/SugarBPM_Webhook_Action/src/custom/Extension/application/Ext/Language/en_us.CustomPMSEWebhook.php b/packages/SugarBPM_Webhook_Action/src/custom/Extension/application/Ext/Language/en_us.CustomPMSEWebhook.php new file mode 100644 index 0000000..820da95 --- /dev/null +++ b/packages/SugarBPM_Webhook_Action/src/custom/Extension/application/Ext/Language/en_us.CustomPMSEWebhook.php @@ -0,0 +1,24 @@ + { + e.preventDefault(); + e.stopPropagation(); + this.openPanelOnItem(); + }); + return this._configButton; + } + _onValueGenerationHandler(module) { + const that = this; + return function () { + const [panel, , selected] = arguments; + const variable = "{::" + module + "::" + selected.value + "::}"; + const control = that.controlObject; + const startPos = control.selectionStart; + const endPos = control.selectionEnd; + const textBefore = control.value.substring(0, startPos); + const textAfter = control.value.substring(endPos, control.value.length); + const newValue = textBefore + variable + textAfter; + that.setValue(newValue); + setTimeout(() => { + control.selectionStart = startPos + variable.length; + control.selectionEnd = startPos + variable.length; + }, 200); + panel.close(); + control.focus(); + }; + } + openPanelOnItem() { + const that = this; + if (!this._variablesList) { + this._variablesList = new FieldPanel({ + className: "updateritem-panel", + appendTo: that, + items: [ + { + type: "list", + bodyHeight: 100, + collapsed: false, + itemsContent: "{{text}}", + fieldToFilter: "type", + title: translate('LBL_PMSE_UPDATERFIELD_VARIABLES_LIST_TITLE').replace(/%MODULE%/g, app.lang.getModuleName(PROJECT_MODULE)) + } + ], + onItemValueAction: that._onValueGenerationHandler(PROJECT_MODULE), + onOpen: function () { + jQuery(that.currentField.html).addClass("opened"); + }, + onClose: function () { + jQuery(that.currentField.html).removeClass("opened"); + } + }); + } + const targetPanel = this._variablesList; + const list = this._variablesList.getItems()[0]; + //We check if the variables list has the same filter than the one we need right now, + //if it do then we don't need to apply the data filtering for a new criteria + if (list.getFilterMode() === 'inclusive') { + list.setFilterMode('exclusive') + .setDataItems(this._variables, "type", ["Checkbox", "DropDown"]); + } + this.currentField = this; + const currentOwner = targetPanel.getOwner(); + if (currentOwner !== this.controlObject) { + targetPanel.close(); + targetPanel.setOwner(this.controlObject); + targetPanel.open(); + } else { + if (targetPanel.isOpen()) { + targetPanel.close(); + } else { + targetPanel.open(); + } + } + return this; + } + createHTML() { + this.html = Base.prototype.createHTML.call(this); + this._createConfigButton(); + if (this._configButton) { + // append config button after + this.controlObject.after(this._configButton); + } + // return the containing html + return this.html; + } + } + } + + function WithKeyValueController(Base) { + return class extends Base { + constructor(...args) { + const [options] = args; + super(...args); + this.mode = options.mode || 'list'; + this.allowPlain = !!options.allowPlain; + // binding default validator + this.setValidators([{ + jtype: 'custom', + criteria: { + validationFunction: () => this.inputValidator(this.value), + }, + errorMessage: translate('LBL_PMSE_FORM_ERROR_JSON_FORMAT') + }]); + } + insertStyles() { + const style = this.createHTMLElement("style"); + style.innerHTML = ` + #inputContainerWrapper.sourceMode { + display: flex; + } + #inputContainerWrapper button.mode { + /* gray background */ + background-color: #94a3b8; + border-radius: 5px; + margin-left: 5px; + margin-right: 10px; + color: #fff; + position: absolute; + right: 6px; + top: 6px; + } + #inputContainer { + padding: 0 25px; + } + #inputContainer > .row { + display: flex; + flex-direction: row; + position: relative; + align-items: center; + gap: 3px; + } + #inputContainer .row:not(:first-child){ + margin-top: 5px; + } + #inputContainer > .row > input { + margin: 0px; + padding: 2px 3px; + font-size: 90%; + height: 14px; + width: auto; + flex-grow: 1; + } + #inputContainer > .row > button { + border-radius: 5px; + display: none; + width: 20px; + color: #fff; + margin-right: 5px; + margin-left: 5px; + } + #inputContainer .row button.addRow{ + background-color: #007bff; + } + #inputContainer .row button.removeRow{ + background-color: #dc3545; + } + + #inputContainerWrapper.sourceMode > textarea, + #inputContainerWrapper.sourceMode > input { + flex-grow: 1; + display: inline-block; + margin-right: 45px; + } + + #inputContainerWrapper > textarea, + #inputContainerWrapper > input, + #inputContainerWrapper.sourceMode #inputContainer, + #inputContainerWrapper:not(.sourceMode) .adam-itemupdater-cfg, + #inputContainerWrapper:not(.sourceMode) button.mode > span:not(.active), + #inputContainerWrapper.sourceMode button.mode > span.active { + display: none; + } + + #inputContainer .row:first-child button.addRow, + #inputContainer .row:not(:first-child) button.removeRow { + display: block; + } + `; + this.html.id = "inputContainerWrapper"; + + // append styles to begining of the this.html + this.html.insertBefore(style, this.html.firstChild); + } + isSourceMode() { + return this.html.classList.contains("sourceMode"); + } + switchMode(mode) { + if (mode === 'source') { + this.html?.classList?.add("sourceMode"); + } else { + this.html?.classList?.remove("sourceMode"); + } + } + getModeButton() { + const modeButton = this.createHTMLElement("button"); + modeButton.className = "mode"; + modeButton.innerHTML = '{}+/-'; + modeButton.addEventListener("click", (e) => { + e.preventDefault(); e.stopPropagation(); + // check if this.html have class sourceMode + const isCurrentModeSourceMode = this.isSourceMode(); + const isValidKeyValueJSON = this.validateKeyValueJSON(); + if (isCurrentModeSourceMode && isValidKeyValueJSON) { + this.switchMode('list'); + } else { + this.switchMode('source'); + } + }); + return modeButton; + } + initiateKeyValueInputsAndButtons() { + this.insertStyles(); + + const inputContainer = this.createHTMLElement("div"); + inputContainer.id = "inputContainer"; + inputContainer.appendChild(this.generateRow()); + + this.html.appendChild(inputContainer); + this.html.appendChild(this.getModeButton()); + + if (this.mode === 'source') { + this.html.classList.add("sourceMode"); + } else { + this.html.classList.remove("sourceMode"); + } + return inputContainer; + } + generateRow(valueForKey, valueForValue) { + const row = this.createHTMLElement("div"); + row.className = "row"; + + const key = this.createHTMLElement("input"); + key.type = "text"; + key.className = "key"; + key.value = valueForKey || ""; + key.placeholder = "key"; + key.onkeyup = this.onListItemKeyUp.bind(this); + + // add semicolon in between key and value + const semicolon = this.createHTMLElement("span"); + semicolon.innerHTML = ":"; + + const value = this.createHTMLElement("input"); + value.type = "text"; + value.className = "value"; + value.value = valueForValue || ""; + value.placeholder = "value"; + value.onkeyup = this.onListItemKeyUp.bind(this); + + const addRow = this.createHTMLElement("button"); + addRow.className = "addRow"; + addRow.innerHTML = "+"; + addRow.addEventListener("click", (e) => { + e.preventDefault(); e.stopPropagation(); + const newRow = this.generateRow() + row.parentElement.insertBefore(newRow, row.parentElement.firstChild); + }); + + const removeRow = this.createHTMLElement("button"); + removeRow.className = "removeRow"; + removeRow.innerHTML = "-"; + removeRow.addEventListener("click", (e) => { + e.preventDefault(); e.stopPropagation(); + const target = e.target; + const row = target.parentElement; + const container = row.parentElement; + if (container.children.length > 1) { + row.remove(); + this.onListItemKeyUp(); + } + }); + + row.appendChild(key); + row.appendChild(semicolon); + row.appendChild(value); + row.appendChild(addRow); + row.appendChild(removeRow); + return row; + } + isJsonString(value) { + try { + JSON.parse(value); + return true; + } catch (e) { + return false; + } + } + setValue(value, skipPopulate = false) { + const isValueJSON = this.isJsonString(value); + if (isValueJSON) { + // parse the value to JSON + value = JSON.parse(value); + if (!skipPopulate) { + // on set value we should sync the list view + this.populateRowsFromJSON(value); + } + // reassigning value to stringified JSON + value = JSON.stringify(value, null, 2); + } else { + this.switchMode('source'); // forcing to source mode if value is not JSON + // make sure the value is string + value = value?.toString() || ""; + } + super.setValue(value); + } + inputValidator(value) { + return this.allowPlain || this.validateKeyValueJSON(value); + } + // validate() { + // return this.inputValidator(this.value); + // } + // checks the value is json and only key value contains + isKeyValueJSON(value) { + if (!this.isJsonString(value)) { + return false; + } + const jsonData = JSON.parse(value); + return Object.keys(jsonData).every(key => typeof jsonData[key] === "string"); + } + getJsonFromInput() { + const inputContainer = this.html.querySelector("#inputContainer"); + const rows = inputContainer.querySelectorAll(".row"); + const data = {}; + rows.forEach(row => { + const key = row.querySelector(".key").value; + const value = row.querySelector(".value").value; + if (key.trim() !== '') { + data[key] = value || ""; + } + }); + return data; + } + onListItemKeyUpTimer = null; + onListItemKeyUp() { + clearTimeout(this.onListItemKeyUpTimer); + this.onListItemKeyUpTimer = setTimeout(() => { + const data = this.getJsonFromInput(); + this.setValue(JSON.stringify(data), true); + }, 100); + } + populateRowsFromJSON(jsonData) { + const inputContainer = this.html.querySelector("#inputContainer"); + inputContainer.innerHTML = ""; + const keys = Object.keys(jsonData); + inputContainer.appendChild(this.generateRow()); + if (keys.length > 0) { + keys.forEach((key, index) => { + const row = this.generateRow(key, jsonData[key]); + inputContainer.appendChild(row); + }); + } + } + validateKeyValueJSON() { + let ret = !this.value || this.isKeyValueJSON(this.value); + if (!ret) { + app.alert.show("invalid-json", { + level: "error", + messages: "The value must be a valid JSON object with only key value pairs", + autoClose: true + }); + } + return ret; + } + createHTML() { + this.html = Base.prototype.createHTML.call(this); + this.initiateKeyValueInputsAndButtons(); + return this.html; + } + } + } + + class TextFieldWithConfig extends WithConfig(TextField) { } + class TextareaFieldWithConfig extends WithConfig(TextareaField) { } + class TextareaFieldWithKeyValueControllerAndConfig extends WithKeyValueController(TextareaFieldWithConfig) { } + + const doMagic = (w) => { + const html = w.html; + const parent = html.parentElement; + const style = document.createElement("style"); + style.innerHTML = ` + .customDynamicWindow { + position: fixed; + left: calc(50vw - 330px + 56px + 25px); + top: 138px; + width: auto; + height: auto; + display: block; + } + .customDynamicWindow .adam-panel-body { + padding: 10px 10px 15px; + max-height: calc(80vh - 138px); + max-width: 80vw; + display: flex; + flex-direction: column; + } + .customDynamicWindow .adam-field { + display: flex; + flex-wrap: wrap; + gap: 3px; + } + .customDynamicWindow .adam-form-label { + flex: 1 1 100%; + margin:0px; + text-align: left; + } + .customDynamicWindow .adam-field > :not(.adam-form-label){ + display: flex; + white-space: nowrap; + } + .customDynamicWindow .adam-field > select { + margin: 0px; + } + .customDynamicWindow .adam-field > input, + .customDynamicWindow .adam-field > select, + .customDynamicWindow .adam-field > textarea{ + min-width: 370px; + flex-grow: 1; + }, + .customDynamicWindow .adam-field > #inputContainer > .row { + flex-grow: 1; + } + .customDynamicWindow .adam-field > #inputContainer { + flex: 1; + flex-direction: column; + } + .customDynamicWindow #inputContainer > .row > button, + .customDynamicWindow #inputContainerWrapper button.mode, + .customDynamicWindow #inputContainerWrapper.sourceMode > textarea { + margin-right: 0px !important; + } + .customDynamicWindow #inputContainerWrapper .adam-form-label{ + margin-bottom: 5px; + } + .customDynamicWindow #inputContainer { + padding: 0; + } + .customDynamicWindow .adam-panel-footer { + display: flex; + justify-content: flex-end; + align-items: center; + padding-top: 7px; + } + .customDynamicWindow .adam-panel-footer .adam-button { + margin:0px; + } + .customDynamicWindow .pmse-form-error { + flex-grow: 1; + padding: 0 15px; + } + .customDynamicWindow .pmse-form-error .sicon { + margin-right: 5px; + } + /* Hack for adding bigger click region wo/ changing the button size */ + .customDynamicWindow .adam-window-close { + padding: 5px; + margin: -5px; + } + .customDynamicWindow .adam-field > style { + display: none !important; + } + `; + //append beginnig of the window + parent.insertBefore(style, html); + html.classList.add("customDynamicWindow"); + html.removeAttribute("style"); + html.querySelectorAll(".adam-panel-body,.adam-window-body,.adam-form-label,.adam-field,.adam-panel-footer").forEach(e => e.removeAttribute('style')); + html.querySelector('.adam-panel').style.overflow = 'visible'; + } + app.events.on("app:sync:complete", function () { + const ACTION_IDENDTIFIER = "CUSTOM_WEBHOOK"; + + const customContextMenuActions = AdamActivity.prototype.customContextMenuActions; + AdamActivity.prototype.customContextMenuActions = function () { + var baseActions = customContextMenuActions?.apply(this) || []; + const webhookActionDef = { + text: translate('LBL_PA_FORM_ACTIVITY_CUSTOM_WEBHOOK'), + cssStyle: 'adam-menu-script-assign_team', + name: ACTION_IDENDTIFIER, + }; + const webhookAction = { + ...webhookActionDef, + handler: this._getScriptTypeActionHandler(webhookActionDef.name) + }; + baseActions.push(webhookAction); + return baseActions; + }; + + const customGetAction = AdamActivity.prototype.customGetAction; + AdamActivity.prototype.customGetAction = function (type, w) { + const self = this; + let action; + if (type == ACTION_IDENDTIFIER) { + const hidden_module = new HiddenField({ + name: 'act_field_module', + initialValue: PROJECT_MODULE + }); + + const request_method = new ComboboxField({ + jtype: 'combobox', + name: 'act_request_method', + label: translate('LBL_PMSE_FORM_LABEL_REQUEST_METHOD'), + options: [ + { text: 'GET', value: 'GET' }, + { text: 'POST', value: 'POST' }, + { text: 'PUT', value: 'PUT' }, + { text: 'DELETE', value: 'DELETE' }, + { text: 'PATCH', value: 'PATCH' }, + { text: 'HEAD', value: 'HEAD' }, + ], + initialValue: 'GET', + readOnly: false, + }); + + const request_timeout = new TextFieldWithConfig({ + jtype: 'text', + name: 'act_request_timeout', + label: translate('LBL_PMSE_FORM_LABEL_REQUEST_TIMEOUT'), + value: '30', + required: true, + validators: [{ + jtype: 'custom', + criteria: { + validationFunction: () => { + return request_timeout.value.trim() !== '' + && parseInt(request_timeout.value) == request_timeout.value; + }, + }, + errorMessage: translate('LBL_PMSE_FORM_ERROR_INVALID_TIMEOUT') + }], + }); + + const request_url = new TextFieldWithConfig({ + jtype: 'required', + name: 'act_request_url', + label: translate('LBL_PA_FORM_LABEL_REQUEST_URL'), + required: true, + validators: [{ + jtype: 'custom', + criteria: { + validationFunction: () => { + return request_url.value.trim() !== '' + }, + }, + errorMessage: translate('LBL_PMSE_FORM_ERROR_MISSING_REQ_URL') + }], + value: '', + }); + const request_headers = new TextareaFieldWithKeyValueControllerAndConfig({ + jtype: 'text', + name: 'act_request_headers', + label: translate('LBL_PA_FORM_LABEL_REQUEST_HEADERS'), + value: '', + }); + const request_payload = new TextareaFieldWithKeyValueControllerAndConfig({ + jtype: 'text', + name: 'act_request_payload', + label: translate('LBL_PA_FORM_LABEL_REQUEST_PAYLOAD'), + value: '', + allowPlain: true, + mode: 'source' + }); + + const proxy = new SugarProxy({ + url: 'pmse_Project/ActivityDefinition/' + this.id, + uid: this.id, + callback: null + }); + + const items = [request_method, request_url, request_payload, request_headers, request_timeout, hidden_module]; + const labelWidth = '40%'; + const actionText = translate('LBL_PMSE_CONTEXT_MENU_SETTINGS'); + const actionCSS = 'adam-menu-icon-configure'; + const callback = { + 'loaded': function (data) { + self.canvas.emptyCurrentSelection(); + app.alert.dismiss("upload"); + doMagic(w); + // w.html.style.display = "inline"; // shows the window 🤯 + + if (data.act_fields) { + let fieldDatas = {}; + try { + fieldDatas = JSON.parse(data.act_fields); + } catch (e) { } + items.forEach(item => { + if (item.name in fieldDatas) { + item.setValue(fieldDatas[item.name]); + } + }); + } + project.addMetadata('projectModuleFields', { + dataURL: 'pmse_Project/CrmData/fields/' + PROJECT_MODULE, + dataRoot: 'result', + success: function (data) { + request_url.setVariables(data); + request_headers.setVariables(data); + request_payload.setVariables(data); + request_timeout.setVariables(data); + } + }); + const dataCollector = () => { + const data = {}; + items.forEach(item => (item.name.startsWith('act_request')) && (data[item.name] = item.value)); + return data; + } + this.getData = function () { + const act_fields = JSON.stringify(dataCollector()); + return { act_fields }; + } + } + }; + action = { + proxy, + items, + labelWidth, + actionText, + actionCSS, + callback + }; + } else { + action = customGetAction?.apply(this, [type, w]) || {}; + } + return action; + }; + + + const customGetWindowDef = AdamActivity.prototype.customGetWindowDef; + AdamActivity.prototype.customGetWindowDef = function (type) { + const wWidth = 550; + const wHeight = 302; + const wTitle = translate('LBL_PMSE_FORM_TITLE_WEBHOOK'); + const windowDef = { wWidth, wHeight, wTitle }; + return (type == ACTION_IDENDTIFIER) ? windowDef : (customGetWindowDef?.apply(this, [type]) || {}); + }; + }); +})(SUGAR.App); \ No newline at end of file diff --git a/packages/SugarBPM_Webhook_Action/src/custom/modules/pmse_Inbox/engine/PMSEElements/CustomPMSECustomWebhook.php b/packages/SugarBPM_Webhook_Action/src/custom/modules/pmse_Inbox/engine/PMSEElements/CustomPMSECustomWebhook.php new file mode 100644 index 0000000..275d751 --- /dev/null +++ b/packages/SugarBPM_Webhook_Action/src/custom/modules/pmse_Inbox/engine/PMSEElements/CustomPMSECustomWebhook.php @@ -0,0 +1,149 @@ +noteLines[] = $msg; + } + + private function getNoteLines(bool $asString = false): array|string + { + $noteLines = $this->noteLines; + if ($asString) { + $noteLines = implode("
● ", $noteLines); + } + return $noteLines; + } + + private function saveBPMNote(array $flowData): void + { + $notes = \BeanFactory::newBean('pmse_BpmNotes'); + $notes->cas_id = $flowData['cas_id']; + $notes->cas_index = $flowData['cas_index']; + $notes->not_user_id = $flowData['cas_user_id']; + $notes->not_user_recipient_id = null; + $notes->not_type = 'LOG'; //'GENERAL' + $notes->not_date = date("Y-m-d H:i:s"); + $notes->not_status = 'ACTIVE'; + $notes->not_availability = ''; + $notes->not_content = $this->getNoteLines(true); + $notes->not_recipients = ''; + $notes->save(); + } + + protected function isJsonString(string $string): bool + { + $isJson = false; + try { + json_decode($string); + $isJson = json_last_error() === JSON_ERROR_NONE; + } catch (\Exception $e) { + } + return $isJson; + } + + public function run($flowData, $bean = null, $externalAction = '', $arguments = []) + { + $bpmnElement = $this->retrieveDefinitionData($flowData['bpmn_id']); + + if ($bpmnElement) { + $act_fields = json_decode($bpmnElement['act_fields'], true); + $variableFields = [ + 'act_request_url', + 'act_request_headers', + 'act_request_payload', + 'act_request_timeout', + ]; + $jsonFields = [ + 'act_request_headers', + 'act_request_payload', + ]; + foreach ($variableFields as $variableField) { + $value = $act_fields[$variableField]; + if (in_array($variableField, $jsonFields) && $this->isJsonString($value)) { + $jsonValue = json_decode($value, true); + if (is_array($jsonValue)) { + foreach ($jsonValue as $k => $jv) { + $jsonValue[$k] = $this->beanHandler->mergeBeanInTemplate($bean, $jv); + } + $value = $jsonValue; + } else { + $value = $this->beanHandler->mergeBeanInTemplate($bean, $value); + } + } else { + $value = $this->beanHandler->mergeBeanInTemplate($bean, $value); + } + $act_fields[$variableField] = $value; + } + + if (isset($act_fields['act_request_timeout']) && is_numeric($act_fields['act_request_timeout'])) { + $this->timeout = (int)$act_fields['act_request_timeout']; + $this->addNoteLine("Request timeout set to {$this->timeout} seconds."); + } + + $headers = $act_fields['act_request_headers']; + if (empty($headers)) { + $headers = []; + } + $payload = $act_fields['act_request_payload']; + //If payload is an array, that means it was valid JSON and needs to be encoded back to string for request + if (is_array($payload)) { + $payload = json_encode($payload); + } + $this->processWebhook( + $act_fields['act_request_method'], + $act_fields['act_request_url'], + $headers, + $payload + ); + } else { + $this->addNoteLine("[ERROR] BPM Element not found."); + } + // save the note before returning + $this->saveBPMNote($flowData); + return parent::run($flowData, $bean, $externalAction, $arguments); + } + + protected function processWebhook( + string $method, + string $url, + array $headers = [], + string|array|null $payload = null + ): void + { + $this->addNoteLine("Processing Webhook: [$method] $url"); + try { + $client = new ExternalResourceClient($this->timeout); + //ExternalResourceClient does this automatically, so we add it here to properly log it + $headers = array_merge(['Content-type' => 'application/x-www-form-urlencoded'], $headers); + $this->addNoteLine("Headers: " . json_encode($headers, JSON_PRETTY_PRINT)); + if ($payload) { + $this->addNoteLine("Payload RAW: " . json_encode($payload, JSON_PRETTY_PRINT)); + $payload = is_string($payload) ? $payload : http_build_query($payload); + $this->addNoteLine("Payload: " . json_encode($payload, JSON_PRETTY_PRINT)); + } + $response = $client->request($method, $url, $payload, $headers); + if (!empty($response)) { + $this->addNoteLine("Response Status Code: " . $response->getStatusCode()); + $bodyContent = $response->getBody()->getContents(); + // TODO: Better to write to somewhere else ? + $this->addNoteLine("Response Body: " . $bodyContent); + } + } catch (Exception $e) { + $this->addNoteLine("[ERROR] Request failed: " . $e->getMessage()); + } + } +} \ No newline at end of file diff --git a/packages/buildPackages.sh b/packages/buildPackages.sh index 3a3712a..74eadfc 100755 --- a/packages/buildPackages.sh +++ b/packages/buildPackages.sh @@ -1,17 +1,24 @@ #!/bin/bash # Copyright 2015 SugarCRM Inc. Licensed by SugarCRM under the Apache 2.0 license. +VERSION="$1" + +if [ -z "$VERSION" ]; then + echo "Usage: $0 " + exit 1 +fi + for i in * ; do if [ -d "$i" ]; then echo "building $i" cd "$i" if [ -f "manifest.php" ] then - zip -r --filesync ../$i.zip * -x "*.DS_Store" -x ".git*" -x "__MAC*" + zip -r --filesync ../$i.zip * -x "*.DS_Store" -x ".git*" -x "__MAC*" else if [ -f "pack.php" ] then - php pack.php "1.0.0" + php pack.php "$VERSION" fi fi cd ..