diff --git a/core/Updates/5.7.0-b1.php b/core/Updates/5.7.0-b1.php new file mode 100644 index 00000000000..50027715640 --- /dev/null +++ b/core/Updates/5.7.0-b1.php @@ -0,0 +1,42 @@ +migration = $factory; + } + + public function getMigrations(Updater $updater) + { + return [ + $this->migration->db->addColumns('segment', [ + 'starred' => 'TINYINT(1) NOT NULL DEFAULT 0', + 'starred_by' => 'VARCHAR(100) NULL DEFAULT NULL', + ]), + ]; + } + + public function doUpdate(Updater $updater) + { + $updater->executeMigrations(__FILE__, $this->getMigrations($updater)); + } +} diff --git a/core/Version.php b/core/Version.php index bff370eca44..4d6958c8be7 100644 --- a/core/Version.php +++ b/core/Version.php @@ -22,7 +22,7 @@ final class Version * The current Matomo version. * @var string */ - public const VERSION = '5.7.0-alpha'; + public const VERSION = '5.7.0-b1'; public const MAJOR_VERSION = 5; diff --git a/lang/en.json b/lang/en.json index ad60dd48cea..2f406ce111a 100644 --- a/lang/en.json +++ b/lang/en.json @@ -28,6 +28,18 @@ "BrokenDownReportDocumentation": "It is broken down into various reports, which are displayed in sparklines at the bottom of the page. You can enlarge the graphs by clicking on the report you'd like to see.", "Cancel": "Cancel", "CannotUnzipFile": "Cannot unzip file %1$s: %2$s", + "CanNotEditGlobalSegment": "This is a global segment. Only super users can edit global segments.", + "CanNotStarGlobalSegment": "This is a global segment. Only super users can star global segments.", + "CanNotUnstarGlobalSegment": "This is a global segment. Only super users can unstar global segments.", + "CanEditGlobalSegment": "This is a global segment. Any changes will apply across all websites.", + "CanStarGlobalSegment": "This is a global segment. Adding to Starred will apply across all websites.", + "CanUnstarGlobalSegment": "This is a global segment. Removing from Starred will apply across all websites.", + "CanNotEditSiteSegment": "You can only edit the segments you created yourself.", + "CanNotStarSiteSegment": "You can only add to Starred the segments you created yourself.", + "CanNotUnstarSiteSegment": "You can only remove from Starred the segments you created yourself.", + "CanEditSiteSegment": "Edit the segment for this website.", + "CanStarSiteSegment": "Add to Starred segments for this website.", + "CanUnstarSiteSegment": "Remove from Starred segments for this website.", "ChangeInX": "Change in %1$s", "ChangePassword": "Change password", "ChangeTagCloudView": "Please note, that you can view the report in other ways than as a tag cloud. Use the controls at the bottom of the report to do so.", @@ -463,6 +475,9 @@ "SmtpServerAddress": "SMTP server address", "SmtpUsername": "SMTP username", "Source": "Source", + "Star": "Star", + "StarredBy": "Starred by", + "StarredByYou": "Starred by you", "StatisticsAreNotRecorded": "Matomo Visitor Tracking is currently disabled! Re-enable tracking by setting record_statistics = 1 in your config/config.ini.php file.", "Subtotal": "Subtotal", "Summary": "Summary", diff --git a/plugins/SegmentEditor/API.php b/plugins/SegmentEditor/API.php index deaa0e51daa..6dd31337ac5 100644 --- a/plugins/SegmentEditor/API.php +++ b/plugins/SegmentEditor/API.php @@ -215,7 +215,7 @@ public function delete(int $idSegment): void * * @param int $idSegment The ID of the segment being deleted. */ - Piwik::postEvent('SegmentEditor.deactivate', array($idSegment)); + Piwik::postEvent('SegmentEditor.deactivate', [$idSegment]); $this->getModel()->deleteSegment($idSegment); @@ -263,14 +263,14 @@ public function update( $autoArchive = $this->checkAutoArchive($autoArchive, $idSite); - $bind = array( + $bind = [ 'name' => $name, 'definition' => $definition, 'enable_all_users' => (int) $enabledAllUsers, 'enable_only_idsite' => (int) $idSite, 'auto_archive' => (int) $autoArchive, 'ts_last_edit' => Date::now()->getDatetime(), - ); + ]; /** * Triggered before a segment is modified. @@ -280,7 +280,7 @@ public function update( * * @param int $idSegment The ID of the segment which visibility is reduced. */ - Piwik::postEvent('SegmentEditor.update', array($idSegment, $bind)); + Piwik::postEvent('SegmentEditor.update', [$idSegment, $bind]); $this->getModel()->updateSegment($idSegment, $bind); @@ -321,7 +321,7 @@ public function add( $enabledAllUsers = $this->checkEnabledAllUsers($enabledAllUsers); $autoArchive = $this->checkAutoArchive($autoArchive, $idSite); - $bind = array( + $bind = [ 'name' => $name, 'definition' => $definition, 'login' => Piwik::getCurrentUserLogin(), @@ -329,8 +329,10 @@ public function add( 'enable_only_idsite' => (int) $idSite, 'auto_archive' => (int) $autoArchive, 'ts_created' => Date::now()->getDatetime(), + 'starred' => 0, + 'starred_by' => null, 'deleted' => 0, - ); + ]; $id = $this->getModel()->createSegment($bind); @@ -348,6 +350,56 @@ public function add( return $id; } + /** + * Stars a stored segment. + * + * @param int $idSegment + * @return array{result: boolean, starred_by: string} + * @throws Exception if the user is not logged in or does not have the required permissions. + */ + public function star(int $idSegment): array + { + Piwik::checkUserHasSomeViewAccess(); + $segment = $this->getSegmentOrFail($idSegment); + $this->checkUserCanEditOrDeleteSegment($segment); + $login = Piwik::getCurrentUserLogin(); + $bind = [ + 'starred' => 1, + 'starred_by' => $login, + ]; + + $result = $this->getModel()->updateSegment($idSegment, $bind); + + return [ + 'result' => $result, + 'starred_by' => $login, + ]; + } + + /** + * Unstars a stored segment. + * + * @param int $idSegment + * @return array{result: boolean} + * @throws Exception if the user is not logged in or does not have the required permissions. + */ + public function unstar(int $idSegment): array + { + Piwik::checkUserHasSomeViewAccess(); + $segment = $this->getSegmentOrFail($idSegment); + $this->checkUserCanEditOrDeleteSegment($segment); + $bind = [ + 'starred' => 0, + 'starred_by' => null, + ]; + + $result = $this->getModel()->updateSegment($idSegment, $bind); + + return [ + 'result' => $result, + ]; + } + /** * Returns a stored segment by ID * @@ -444,7 +496,7 @@ private function filterSegmentsWithDisabledElements(array $segments, $idSite = n */ private function sortSegmentsCreatedByUserFirst(array $segments): array { - $orderedSegments = array(); + $orderedSegments = []; foreach ($segments as $id => &$segment) { if ($segment['login'] == Piwik::getCurrentUserLogin()) { $orderedSegments[] = $segment; diff --git a/plugins/SegmentEditor/Model.php b/plugins/SegmentEditor/Model.php index 10f9332ba3c..18e38429f76 100644 --- a/plugins/SegmentEditor/Model.php +++ b/plugins/SegmentEditor/Model.php @@ -282,6 +282,8 @@ public static function install() `auto_archive` tinyint(4) NOT NULL default 0, `ts_created` TIMESTAMP NULL, `ts_last_edit` TIMESTAMP NULL, + `starred` tinyint(4) NOT NULL default 0, + `starred_by` VARCHAR(100) NULL default NULL, `deleted` tinyint(4) NOT NULL default 0, PRIMARY KEY (`idsegment`)"; diff --git a/plugins/SegmentEditor/SegmentSelectorControl.php b/plugins/SegmentEditor/SegmentSelectorControl.php index 02857f84e31..f3a3c2cee22 100644 --- a/plugins/SegmentEditor/SegmentSelectorControl.php +++ b/plugins/SegmentEditor/SegmentSelectorControl.php @@ -109,6 +109,18 @@ private function wouldApplySegment($savedSegment) private function getTranslations() { $translationKeys = array( + 'General_CanNotEditGlobalSegment', + 'General_CanNotStarGlobalSegment', + 'General_CanNotUnstarGlobalSegment', + 'General_CanEditGlobalSegment', + 'General_CanStarGlobalSegment', + 'General_CanUnstarGlobalSegment', + 'General_CanNotEditSiteSegment', + 'General_CanNotStarSiteSegment', + 'General_CanNotUnstarSiteSegment', + 'General_CanEditSiteSegment', + 'General_CanStarSiteSegment', + 'General_CanUnstarSiteSegment', 'General_OperationEquals', 'General_OperationNotEquals', 'General_OperationAtMost', @@ -127,6 +139,9 @@ private function getTranslations() 'General_DefaultAppended', 'SegmentEditor_AddNewSegment', 'General_Edit', + 'General_StarredBy', + 'General_StarredByYou', + 'General_Edit', 'General_Search', 'General_SearchNoResults', ); diff --git a/plugins/SegmentEditor/images/edit_segment.png b/plugins/SegmentEditor/images/edit_segment.png deleted file mode 100644 index 6eb039254da..00000000000 Binary files a/plugins/SegmentEditor/images/edit_segment.png and /dev/null differ diff --git a/plugins/SegmentEditor/images/edit_segment.svg b/plugins/SegmentEditor/images/edit_segment.svg new file mode 100644 index 00000000000..4141d942238 --- /dev/null +++ b/plugins/SegmentEditor/images/edit_segment.svg @@ -0,0 +1,3 @@ + + + diff --git a/plugins/SegmentEditor/javascripts/Segmentation.js b/plugins/SegmentEditor/javascripts/Segmentation.js index 5a08f8f8834..a0237294e09 100644 --- a/plugins/SegmentEditor/javascripts/Segmentation.js +++ b/plugins/SegmentEditor/javascripts/Segmentation.js @@ -39,10 +39,7 @@ Segmentation = (function($) { self.editorTemplate = self.editorTemplate.detach(); - self.timer = ""; // variable for further use in timing events - self.searchAllowed = true; self.filterTimer = ""; - self.filterAllowed = true; self.availableMatches = []; self.availableMatches["metric"] = []; @@ -75,16 +72,9 @@ Segmentation = (function($) { }; segmentation.prototype.setTooltip = function (segmentDescription) { - - var title = _pk_translate('SegmentEditor_ChooseASegment') + '.'; - title += ' '+ _pk_translate('SegmentEditor_CurrentlySelectedSegment', [segmentDescription]); - - $('a.title', this.content).attr('title', title).tooltip({ - track: true, - show: {delay: 700, duration: 200}, // default from Tooltips.js - hide: false, - content: title, - }); + var title = _pk_translate('SegmentEditor_ChooseASegment') + '.'; + title += ' '+ _pk_translate('SegmentEditor_CurrentlySelectedSegment', [segmentDescription]); + addTooltip($('a.title', this.content), title); }; // We will listen to changes in the Segment Comparison Store // so we can mark compared segments properly. This will now include deletion of compared segments. @@ -93,15 +83,19 @@ Segmentation = (function($) { }); segmentation.prototype.markComparedSegments = function() { - var comparisonService = window.CoreHome.ComparisonsStoreInstance; - var comparedSegments = comparisonService.getSegmentComparisons().map(function (comparison) { + const comparisonService = window.CoreHome.ComparisonsStoreInstance; + const comparedSegments = comparisonService.getSegmentComparisons().map(function (comparison) { return comparison.params.segment; }); - $('div.segmentList ul li[data-definition]', this.target).removeClass('comparedSegment').filter(function () { - var definition = $(this).attr('data-definition'); - return comparedSegments.indexOf(definition) !== -1 || comparedSegments.indexOf(decodeURIComponent(definition)) !== -1; - }).each(function () { - $(this).addClass('comparedSegment'); + $('div.segmentList ul li[data-definition]', this.target).each(function () { + const $segment = $(this); + const definition = $segment.attr('data-definition'); + const isCompared = ( + comparedSegments.indexOf(definition) !== -1 || + comparedSegments.indexOf(decodeURIComponent(definition)) !== -1 + ); + $segment.toggleClass('comparedSegment', isCompared); + $segment.find('.compareSegment').attr('data-state', isCompared ? 'active' : ''); }); self.checkIfComparedSegmentsHasReachedLimit(); }; @@ -109,15 +103,19 @@ Segmentation = (function($) { const limit = piwik.config.data_comparison_segment_limit + 1; const comparisonService = window.CoreHome.ComparisonsStoreInstance; const comparedSegmentsLength = comparisonService.getSegmentComparisons().length; - $('div.segmentList ul li[data-definition] span.compareSegment').each(function() { + $('div.segmentList ul li[data-definition] .compareSegment').each(function() { + const $compareButton = $(this); + const $segment = $compareButton.parent(); + const currentState = $compareButton.attr('data-state'); + if (currentState === 'active') { + return; + } if (comparedSegmentsLength >= limit) { - $(this).addClass('no-click'); - $(this).parent().attr('title', _pk_translate('General_MaximumNumberOfSegmentsComparedIs', [limit])); + $compareButton.attr('data-state','disabled'); + addTooltip($compareButton, _pk_translate('General_MaximumNumberOfSegmentsComparedIs', [limit])); } else { - $(this).removeClass('no-click'); - var idSegment = $(this).parent().attr('data-idsegment'); - const title = getSegmentName(getSegmentFromId(idSegment)); - $(this).parent().attr('title', title); + $compareButton.attr('data-state',''); + addTooltip($compareButton, _pk_translate('SegmentEditor_CompareThisSegment')); } }); return false; @@ -195,85 +193,91 @@ Segmentation = (function($) { + ' ' + self.translations['General_DefaultAppended'] + ''; var comparisonService = window.CoreHome.ComparisonsStoreInstance; - if (comparisonService.isComparisonEnabled() - || comparisonService.isComparisonEnabled() === null // may not be initialized since this code is outside of Vue + if ( + comparisonService.isComparisonEnabled() || + comparisonService.isComparisonEnabled() === null // may not be initialized since this code is outside of Vue ) { - listHtml += ''; + const className = 'compareSegment allVisitsCompareSegment ' + (self.segmentAccess === 'write' ? 'allVisitsCompareSegment--write' : ''); + const title = _pk_translate('SegmentEditor_CompareThisSegment'); + listHtml += ''; } listHtml += ''; - var isVisibleToSuperUserNoticeAlreadyDisplayedOnce = false; - var isVisibleToSuperUserNoticeShouldBeClosed = false; + let isVisibleToSuperUserNoticeAlreadyDisplayedOnce = false; + let isSharedWithMeBySuperUserNoticeAlreadyDisplayedOnce = false; - var isSharedWithMeBySuperUserNoticeAlreadyDisplayedOnce = false; - var isSharedWithMeBySuperUserNoticeShouldBeClosed = false; - - if(self.availableSegments.length > 0) { + if (self.availableSegments.length > 0) { for(var i = 0; i < self.availableSegments.length; i++) { segment = self.availableSegments[i]; - if(isSegmentSharedWithMeBySuperUser(segment) && !isSharedWithMeBySuperUserNoticeAlreadyDisplayedOnce) { + // starred should be an int, but it could be converted as string + // and !"0" would then be false instead of true + segment.starred = Boolean(parseInt(segment.starred, 10)); + + if (isSegmentSharedWithMeBySuperUser(segment) && !isSharedWithMeBySuperUserNoticeAlreadyDisplayedOnce) { isSharedWithMeBySuperUserNoticeAlreadyDisplayedOnce = true; - isSharedWithMeBySuperUserNoticeShouldBeClosed = true; - listHtml += '
' + _pk_translate('SegmentEditor_SharedWithYou') + ':

'; + listHtml += '
' + _pk_translate('SegmentEditor_SharedWithYou') + ':
'; } - if(isSegmentVisibleToSuperUserOnly(segment) && !isVisibleToSuperUserNoticeAlreadyDisplayedOnce) { - // close - if(isSharedWithMeBySuperUserNoticeShouldBeClosed) { - isSharedWithMeBySuperUserNoticeShouldBeClosed = false; - listHtml += ''; - } - + if (isSegmentVisibleToSuperUserOnly(segment) && !isVisibleToSuperUserNoticeAlreadyDisplayedOnce) { isVisibleToSuperUserNoticeAlreadyDisplayedOnce = true; - isVisibleToSuperUserNoticeShouldBeClosed = true; - listHtml += '
' + _pk_translate('SegmentEditor_VisibleToSuperUser') + ':

'; + listHtml += '
' + _pk_translate('SegmentEditor_VisibleToSuperUser') + ':
'; } - injClass = ""; + injClass = []; var checkSelected = segment.definition; + var escapedSegmentName = (segment.definition).replace(/"/g, '"'); - if( checkSelected == self.currentSegmentStr || - checkSelected == decodeURIComponent(self.currentSegmentStr) - ) { - injClass = 'class="segmentSelected"'; + if (checkSelected === self.currentSegmentStr || checkSelected === decodeURIComponent(self.currentSegmentStr)) { + injClass.push('segmentSelected'); } - listHtml += '
  • '+getSegmentName(segment)+''; - if(self.segmentAccess == "write") { - listHtml += ''; + if (segment.starred) { + injClass.push('segmentStarred'); } - if (comparisonService.isComparisonEnabled() - || comparisonService.isComparisonEnabled() === null // may not be initialized since this code is outside of Vue + listHtml += '' + + '
  • ' + + '' + getSegmentName(segment) + ''; + + const canEdit = getIsUserCanEditSegment(segment); + // We do not use "disabled" attribute here because it remove pointer events and we want to show tooltips + const disabledAttribute = canEdit ? '' : 'data-state="disabled"'; + const starTitleAttribute = 'title="' + getStarSegmentTitle(segment, canEdit) + '"'; + listHtml += '' + + ''; + if (self.segmentAccess === 'write') { + const editTitleAttribute = 'title="' + getEditSegmentTitle(segment, canEdit) + '"'; + listHtml += ''; + } + + if ( + comparisonService.isComparisonEnabled() || + comparisonService.isComparisonEnabled() === null // may not be initialized since this code is outside of Vue ) { - listHtml += ''; + listHtml += ''; } listHtml += '
  • '; } - if(isVisibleToSuperUserNoticeShouldBeClosed) { - listHtml += '
    '; - } - - if(isSharedWithMeBySuperUserNoticeShouldBeClosed) { - listHtml += '
    '; - } - $(html).find(".segmentList > ul").append(listHtml); - if(self.segmentAccess === "write"){ + if (self.segmentAccess === "write"){ $(html).find(".add_new_segment").html(self.translations['SegmentEditor_AddNewSegment']); - } - else { + } else { $(html).find(".add_new_segment").hide(); } - } - else - { + } else { $(html).find(".segmentList > ul").append(listHtml); } + return html; }; @@ -366,25 +370,25 @@ Segmentation = (function($) { var filterSegmentList = function (keyword) { var curTitle; clearFilterSegmentList(); - $(self.target).find(" .filterNoResults").remove(); + $(self.target).find(".filterNoResults").remove(); $(self.target).find(".segmentList li").each(function () { - curTitle = $(this).prop('title'); + curTitle = $(this).find('.segname').prop('title'); $(this).hide(); if (curTitle.toLowerCase().indexOf(keyword.toLowerCase()) !== -1) { $(this).show(); } }); - if ($(self.target).find(".segmentList li:visible").length == 0) { + if ($(self.target).find(".segmentList li:visible").length === 0) { $(self.target).find(".segmentList li:first") .before("
  • " + self.translations['General_SearchNoResults'] + "
  • "); } - if ($(self.target).find(".segmentList .segmentsVisibleToSuperUser li:visible").length == 0) { + if ($(self.target).find(".segmentList .segmentsVisibleToSuperUser li:visible").length === 0) { $(self.target).find(".segmentList .segmentsVisibleToSuperUser").hide(); } - if ($(self.target).find(".segmentList .segmentsSharedWithMeBySuperUser li:visible").length == 0) { + if ($(self.target).find(".segmentList .segmentsSharedWithMeBySuperUser li:visible").length === 0) { $(self.target).find(".segmentList .segmentsSharedWithMeBySuperUser").hide(); } }; @@ -419,25 +423,66 @@ Segmentation = (function($) { }); self.target.on('click', '.editSegment', function(e) { - $(this).closest(".segmentationContainer").trigger("click"); - var target = $(this).parent("li"); + const $button = $(this); + if ($button.attr('data-state') === 'disabled') { + return false; + } + const $segment = $button.parent("li"); + $segment.closest(".segmentationContainer").trigger("click"); + + openEditFormGivenSegment($segment); + e.stopPropagation(); + e.preventDefault(); + }); - openEditFormGivenSegment(target); + self.target.on('click', '[data-star]', function (e) { e.stopPropagation(); e.preventDefault(); + const $button = $(this); + if ($button.attr('data-state') === 'disabled') { + return false; + } + const $root = $button.closest('li'); + const idSegment = $root.data('idsegment'); + const segment = getSegmentFromId(idSegment); + segment.starred = !segment.starred; + const method = segment.starred ? 'star' : 'unstar'; + updateStarredSegment($root, segment); + + var ajaxHandler = new ajaxHelper(); + ajaxHandler.addParams({ + "module": 'API', + "format": 'json', + "method": 'SegmentEditor.' + method, + "userLogin": piwik.userLogin, + "idSegment": idSegment, + }, 'POST'); + ajaxHandler.setErrorCallback(function () { + segment.starred = !segment.starred; + updateStarredSegment($root, segment, true); + }); + ajaxHandler.setCallback(function (response) { + segment.starred_by = response.starred_by; + updateStarSegmentTooltip($root, segment); + }); + ajaxHandler.send(); }); self.target.on('click', '.compareSegment', function (e) { e.stopPropagation(); e.preventDefault(); - var comparisonService = window.CoreHome.ComparisonsStoreInstance; + const $button = $(this); + if ($button.attr('data-state') === 'disabled') { + return false; + } + const comparisonService = window.CoreHome.ComparisonsStoreInstance; comparisonService.addSegmentComparison({ - segment: $(e.target).closest('li').data('definition'), + segment: $button.closest('li').data('definition'), }); closeAllOpenLists(); }); - self.target.on("click", ".segmentList li span.segname", function (e) { + self.target.on("click", ".segmentList li .segname", function (e) { let parentLi = $(this).parent(); if (parentLi.hasClass("grayed") !== true) { var segmentDefinition = $(parentLi).data("definition"); @@ -462,7 +507,7 @@ Segmentation = (function($) { // emulate a click when pressing enter on one of the segments or the add button self.target.on("keyup", ".segmentList li, .add_new_segment", function (event) { var keycode = (event.keyCode ? event.keyCode : (event.which ? event.which : event.key)); - if(keycode == '13'){ + if (keycode == '13'){ $(this).trigger('click'); } }); @@ -486,20 +531,18 @@ Segmentation = (function($) { self.target.on('keyup', ".segmentFilter", function (e) { var search = $(e.currentTarget).val(); - if (search == self.translations['General_Search']) { + if (search === self.translations['General_Search']) { search = ""; } + clearTimeout(self.filterTimer); + self.filterTimer = false; if (search.length >= 2) { - clearTimeout(self.filterTimer); - self.filterAllowed = true; self.filterTimer = setTimeout(function () { filterSegmentList(search); }, 500); - } - else { - self.filterTimer = false; - clearFilterSegmentList(); + } else { + self.filterTimer = setTimeout(clearFilterSegmentList, 500); } }); @@ -573,17 +616,93 @@ Segmentation = (function($) { } }); - // - // segment manipulation events - // - }; - var getAddOrBlockButtonHtml = function(){ - if(typeof addOrBlockButton === "undefined") { - var addOrBlockButton = self.editorTemplate.find("div.segment-add-or").clone(); + function getIsUserCanEditSegment(segment) { + if (self.segmentAccess !== 'write') { + return false; + } + return (segment.login === piwik.userLogin || piwik.hasSuperUserAccess); + } + + function getStarredByTitlePart(segment) { + const login = segment.starred_by || ''; + if (login === piwik.userLogin) { + return ' (' + self.translations['General_StarredByYou'] + ')' + } + return ' (' + self.translations['General_StarredBy'] + ' ' + login + ')' + } + + function getStarSegmentTitle(segment, canEdit) { + // Site-specific segments + if (segment.enable_only_idsite) { + if (canEdit) { + if (segment.starred) { + return self.translations['General_CanUnstarSiteSegment'] + ' ' + getStarredByTitlePart(segment); + } + return self.translations['General_CanStarSiteSegment']; + } else { + if (segment.starred) { + return self.translations['General_CanNotUnstarSiteSegment']; + } + return self.translations['General_CanNotStarSiteSegment']; + } + } + + // Global segments + if (canEdit) { + if (segment.starred) { + return self.translations['General_CanUnstarGlobalSegment'] + ' ' + getStarredByTitlePart(segment); } - return addOrBlockButton.clone(); + return self.translations['General_CanStarGlobalSegment']; + } + if (segment.starred) { + return self.translations['General_CanNotUnstarGlobalSegment']; + } + return self.translations['General_CanNotStarGlobalSegment']; + } + + function getEditSegmentTitle(segment, canEdit) { + // Site-specific segments + if (segment.enable_only_idsite) { + if (canEdit) { + return self.translations['General_CanEditSiteSegment']; + } else { + return self.translations['General_CanNotEditSiteSegment']; + } + } + + // Global segments + if (canEdit) { + return self.translations['General_CanEditGlobalSegment']; + } + return self.translations['General_CanNotEditGlobalSegment']; + } + + function updateStarSegmentTooltip($segment, segment) { + const $starButton = $segment.find('.starSegment'); + const canEdit = getIsUserCanEditSegment(segment); + addTooltip($starButton, getStarSegmentTitle(segment, canEdit)); + } + + function updateStarredSegment($segment, segment, isError = false) { + updateStarSegmentTooltip($segment, segment); + $segment.toggleClass('segmentStarred', segment.starred); + $segment.one('animationend', function avoidAnimationRepetition() { + $segment.removeClass('segmentStarAnimation'); + $segment.removeClass('segmentStarErrorAnimation'); + }); + $segment.toggleClass('segmentStarAnimation', !isError); + $segment.toggleClass('segmentStarErrorAnimation', isError); + } + + function addTooltip(element, title) { + $(element).attr('title', title).tooltip({ + track: true, + show: { delay: 700, duration: 200 }, // default from Tooltips.js + hide: false, + content: title, + }); }; function openEditFormGivenSegment(option) { @@ -638,7 +757,7 @@ Segmentation = (function($) { $(self.form).find('.enable_all_users_select > option[value="' + segment.enable_all_users + '"]').prop("selected",true); // Replace "Visible to me" by "Visible to $login" when user is super user - if(hasSuperUserAccessAndSegmentCreatedByAnotherUser(segment)) { + if (hasSuperUserAccessAndSegmentCreatedByAnotherUser(segment)) { $(self.form).find('.enable_all_users_select > option[value="' + 0 + '"]').text(segment.login); } $(self.form).find('.visible_to_website_select > option[value="'+segment.enable_only_idsite+'"]').prop("selected",true); @@ -731,10 +850,9 @@ Segmentation = (function($) { }; // determine if save or update should be performed - if(segmentId === ""){ + if (segmentId === "") { self.addMethod(params); - } - else{ + } else { jQuery.extend(params, { "idSegment": segmentId }); @@ -865,7 +983,7 @@ Segmentation = (function($) { var html = getListHtml(); - if(typeof self.content !== "undefined"){ + if (typeof self.content !== "undefined") { this.content.html($(html).html()); } else { this.target.append(html); @@ -881,6 +999,10 @@ Segmentation = (function($) { // Loading message var segmentIsSet = this.getSegment().length; toggleLoadingMessage(segmentIsSet); + + self.target.find('[title]').each(function () { + addTooltip(this, this.getAttribute('title')); + }); }; if (piwikHelper.isReportingPage()) { diff --git a/plugins/SegmentEditor/stylesheets/segmentation.less b/plugins/SegmentEditor/stylesheets/segmentation.less index 0522ac4e95d..6ee68ad987e 100644 --- a/plugins/SegmentEditor/stylesheets/segmentation.less +++ b/plugins/SegmentEditor/stylesheets/segmentation.less @@ -232,7 +232,13 @@ div.scrollable { min-width: 206px; } +.segmentationContainer hr { + margin: 10px 0; +} + .segmentationContainer .submenu ul { + display: flex; + flex-direction: column; color: @theme-color-text-light; float: none; font-size: 11px; @@ -243,16 +249,22 @@ div.scrollable { padding-top: 10px; } - .segmentationContainer .submenu ul li { padding: 2px 0 2px 0; margin: 3px 0 0 0; cursor: pointer; + order: 2; } -.segmentationContainer .submenu ul li:hover, -.segmentationContainer .submenu ul li:focus, -.segmentationContainer .submenu ul li:focus-within { +.segmentationContainer .submenu ul li[data-idsegment=""] { + order: 0; +} + +.segmentationContainer .submenu ul li.segmentStarred { + order: 1; +} + +.segmentationContainer .submenu ul li:hover { color: #255792; background: @color-silver-l95; outline: none; @@ -268,50 +280,138 @@ div.scrollable { margin-bottom: 5px; } +@keyframes starAnimation { + 0% { transform: scale(1) rotate(0deg); } + 50% { transform: scale(1.2) rotate(180deg); opacity: 1; } + 100% { transform: scale(1) rotate(360deg); } +} + +@keyframes starAnimationPath { + 0% { fill: black; } + 50% { fill: @theme-color-brand; stroke: @theme-color-brand; } + 100% { fill: black; } +} + +@keyframes unstarAnimation { + 0% { transform: scale(1) rotate(0deg); } + 50% { transform: scale(1.2) rotate(-180deg);} + 100% { transform: scale(1) rotate(-360deg); } +} + +@keyframes unstarAnimationPath { + 0% { fill: black; } + 100% { fill: transparent; } +} + +@keyframes starErrorAnimation { + 0% { opacity: 0.5; } + 5%, 15%, 25%, 35%, 45% { transform: translate(-2px)} + 10%, 20%, 30%, 40% { transform: translate(2px)} + 50% { opacity: 1; } + 100% { transform: translate(0); opacity: 0.5; } +} + +@keyframes starErrorAnimationPath { + 0% { stroke: black; fill: black; } + 25%, 75% { stroke: red; fill: red; } + 100% { stroke: black; fill: transparent; } +} + .segmentationContainer ul.submenu > li { - span.editSegment, span.compareSegment { - display: block; - float: right; - text-align: center; - margin-right: 4px; - font-weight: normal; + .editSegment, + .compareSegment, + .starSegment { + flex: none; + display: flex; width: 16px; height: 16px; - .opacity(0.5); + + // Button reset + padding: 0; + border: 0; + background: transparent; + cursor: pointer; + + font-weight: normal; + text-align: center; + opacity: 0.5; &:hover { - .opacity(1); + opacity: 1; + } + + &[data-state=disabled] { + opacity: 0.2; + cursor: not-allowed; + } + + &[data-state=active] { + border-radius: 50%; + background-color: white; + background-size: 70%; + background-position: center; + filter: invert(1); } } - span.editSegment { - background: url(plugins/SegmentEditor/images/edit_segment.png) no-repeat; + .editSegment { + background: url(plugins/SegmentEditor/images/edit_segment.svg) no-repeat; + background-size: cover; order: 3; } - span.compareSegment { + .starSegment { + order: 2; + } + + .segmentStarAnimation .starSegment { + animation: unstarAnimation 0.5s 1; + } + + .segmentStarAnimation .starSegment path { + animation: unstarAnimationPath 0.5s 1; + } + + .segmentStarred.segmentStarAnimation .starSegment { + animation-name: starAnimation; + } + + .segmentStarred.segmentStarAnimation .starSegment path { + animation-name: starAnimationPath; + } + + .segmentStarErrorAnimation .starSegment { + animation: starErrorAnimation 2s 1; + } + + .segmentStarErrorAnimation .starSegment path { + animation: starErrorAnimationPath 2s 1; + } + + .segmentStarred.segmentStarErrorAnimation .starSegment path { + animation-direction: reverse; + } + + .segmentStarred .starSegment path { + fill: black; + } + + .compareSegment { background: url(plugins/Morpheus/images/compare.svg) no-repeat; background-size: cover; order: 2; &.allVisitsCompareSegment { - margin-right: 24px; + margin-left: 20px; } - } - span.compareSegment.no-click { - pointer-events: none; - } - - li.segmentSelected, li.comparedSegment { - span.compareSegment { - pointer-events: none; - opacity: 0.2; + &.allVisitsCompareSegment--write { + margin-right: 20px; } } } html.comparisonsDisabled .segmentationContainer ul.submenu { - span.compareSegment { + .compareSegment { display: none; } } @@ -398,6 +498,8 @@ html.comparisonsDisabled .segmentationContainer ul.submenu { li { display: flex; + align-items: center; + gap: 4px; } } @@ -446,9 +548,7 @@ a.metric_category { .segmentSelected, .segmentSelected:hover, -.segmentEditorPanel .segmentationContainer .submenu li .segmentSelected, -.segmentEditorPanel .segmentationContainer .submenu li:focus, -.segmentEditorPanel .segmentationContainer .submenu li:focus-within { +.segmentEditorPanel .segmentationContainer .submenu li .segmentSelected { font-weight: bold; } @@ -523,9 +623,8 @@ a.metric_category { } .segname { - width: ~"calc(100% - 40px)"; + flex: auto; padding-right: 10px; - display: inline-block; order: 1; } diff --git a/plugins/SegmentEditor/tests/Integration/SegmentEditorTest.php b/plugins/SegmentEditor/tests/Integration/SegmentEditorTest.php index 6da40901e2c..17778de5cad 100644 --- a/plugins/SegmentEditor/tests/Integration/SegmentEditorTest.php +++ b/plugins/SegmentEditor/tests/Integration/SegmentEditorTest.php @@ -83,6 +83,8 @@ public function testAddAndGetSimpleSegment() 'auto_archive' => '0', 'ts_last_edit' => null, 'deleted' => '0', + 'starred' => '0', + 'starred_by' => null, ); $this->assertEquals($segment, $expected); @@ -113,6 +115,8 @@ public function testAddAndGetAnotherSegment() 'auto_archive' => '1', 'ts_last_edit' => null, 'deleted' => '0', + 'starred' => '0', + 'starred_by' => null, ); unset($segment['ts_created']); $this->assertEquals($segment, $expected); @@ -145,7 +149,7 @@ public function testUpdateSegment() $this->clearReArchiveList(); $updatedSegment = array( - 'idsegment' => $idSegment2, + 'idsegment' => (string) $idSegment2, 'name' => 'NEW name', 'definition' => 'searches==0', 'hash' => md5('searches==0'), @@ -156,6 +160,8 @@ public function testUpdateSegment() 'ts_created' => Date::now()->getDatetime(), 'login' => Piwik::getCurrentUserLogin(), 'deleted' => '0', + 'starred' => '0', + 'starred_by' => null, ); API::getInstance()->update( $idSegment2, @@ -178,11 +184,29 @@ public function testUpdateSegment() $this->assertEquals($newSegment, $updatedSegment); - // Check the other segmenet was not updated + // Check the other segment was not updated $newSegment = API::getInstance()->get($idSegment1); $this->assertEquals($newSegment['name'], $nameSegment1); } + public function testStarUnstarSegment() + { + // Set up initial conditions + $idSegment = API::getInstance()->add('hello', 'searches==0'); + $segment = API::getInstance()->get($idSegment); + $this->assertEquals('0', $segment['starred']); + + // Star segment + API::getInstance()->star($idSegment); + $starredSegment = API::getInstance()->get($idSegment); + $this->assertEquals('1', $starredSegment['starred']); + + // Unstar segment + API::getInstance()->unstar($idSegment); + $unstarredSegment = API::getInstance()->get($idSegment); + $this->assertEquals('0', $unstarredSegment['starred']); + } + public function testDeleteSegment() { $this->expectNotToPerformAssertions(); diff --git a/plugins/SegmentEditor/tests/UI/SegmentSelectorEditor_spec.js b/plugins/SegmentEditor/tests/UI/SegmentSelectorEditor_spec.js index 353ff8eeb58..ee6dcda15f1 100644 --- a/plugins/SegmentEditor/tests/UI/SegmentSelectorEditor_spec.js +++ b/plugins/SegmentEditor/tests/UI/SegmentSelectorEditor_spec.js @@ -43,6 +43,18 @@ describe("SegmentSelectorEditorTest", function () { expect(await page.screenshotSelector(selectorsToCapture)).to.matchImage('1_selector_open'); }); + it("should unstar all segments", async function() { + await page.click('.segmentList li:nth-child(2) .starSegment'); + await page.click('.segmentList li:nth-child(3) .starSegment'); + await page.click('.segmentList li:nth-child(4) .starSegment'); + expect(await page.screenshotSelector(selectorsToCapture)).to.matchImage('1_selector_unstarred'); + }); + + it("should star last segment", async function() { + await page.click('.segmentList li:last-child .starSegment'); + expect(await page.screenshotSelector(selectorsToCapture)).to.matchImage('1_selector_starred'); + }); + it("should open segment editor when edit link clicked for existing segment", async function() { await page.evaluate(function() { $('.segmentList .editSegment:first').click() diff --git a/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_1_selector_open.png b/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_1_selector_open.png index 48ac7f70794..21b562a0fa5 100644 --- a/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_1_selector_open.png +++ b/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_1_selector_open.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e586e785ae27693d9755ac7b8a27db91aa28c12077056c9208878656c1815355 -size 15993 +oid sha256:3927ef177014f6f43ce385be4bcd9dc8a77a390e7b2cc714c48d39ca9bc6e08b +size 16750 diff --git a/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_1_selector_starred.png b/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_1_selector_starred.png new file mode 100644 index 00000000000..4863d89d953 --- /dev/null +++ b/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_1_selector_starred.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6eb43ab8f5069cfd80e04bb93113983862ba307534bbc68eb8fafca4c7368338 +size 17291 diff --git a/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_1_selector_unstarred.png b/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_1_selector_unstarred.png new file mode 100644 index 00000000000..305536fcdd4 --- /dev/null +++ b/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_1_selector_unstarred.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0a79771f8de5c7cbc536127c2390ce3c1eee0fa6cb9a4ce1955dba0525a2f1c +size 17092 diff --git a/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_2_segment_editor_update.png b/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_2_segment_editor_update.png index 7d203da33a3..629c3bb61cd 100644 --- a/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_2_segment_editor_update.png +++ b/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_2_segment_editor_update.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7dd1dcc19eda71d47f719324a18181a827838ab4c4511ca6ff01c5066139d558 -size 35667 +oid sha256:bb703841188ab795a8f03eb4784d2b48362f15a72dbf609a54e57eb1157d1741 +size 35421 diff --git a/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_deleted.png b/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_deleted.png index 48ac7f70794..705e5debe0f 100644 --- a/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_deleted.png +++ b/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_deleted.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e586e785ae27693d9755ac7b8a27db91aa28c12077056c9208878656c1815355 -size 15993 +oid sha256:7522eac65db89e386be8bbdab4545a5a311c44c7d181137f016f763bf2940a73 +size 16703 diff --git a/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_enabled_create_realtime_segments_saved.png b/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_enabled_create_realtime_segments_saved.png index e7641ce2b2f..18711d8f528 100644 --- a/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_enabled_create_realtime_segments_saved.png +++ b/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_enabled_create_realtime_segments_saved.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f9b999f26ab808b11c9cc2c2e9e8388d66de22aab90533cce3cd6bcb27916fa8 -size 21289 +oid sha256:8974b57e944c677e6632962d2a19b5c824b41567b39701f3fd04618374a83b58 +size 22132 diff --git a/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_saved.png b/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_saved.png index 13eef5d9e7f..fd2e41adad1 100644 --- a/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_saved.png +++ b/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_saved.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1ff9036d74dbc4b3fac73d94247fd5eed415ad386a64df31cc66697fc9b9e379 -size 17846 +oid sha256:5eef4eb48a5c2e3ef8bb503f7a186aa4fe3cb2e53d3a9ffdfc9f067cb29bfe15 +size 18614 diff --git a/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_updated.png b/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_updated.png index 14c0c47ee82..787ea858eb4 100644 --- a/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_updated.png +++ b/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_updated.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c1c054fb1eb65d7da38e5fba720a81a52c4bfd36c1f3a3c53e57981fa8b2a400 -size 18507 +oid sha256:99ae645a4e18027b0ef49e7472129f05e83302b7c9be988432fffe5590ea1715 +size 18996 diff --git a/tests/PHPUnit/Integration/Segment/SegmentUnavailableTest.php b/tests/PHPUnit/Integration/Segment/SegmentUnavailableTest.php index 27b503ab155..f8e4f95a3f3 100644 --- a/tests/PHPUnit/Integration/Segment/SegmentUnavailableTest.php +++ b/tests/PHPUnit/Integration/Segment/SegmentUnavailableTest.php @@ -172,6 +172,8 @@ private function checkSegmentAvailable(string $definition, string $name, bool $s 'auto_archive' => 1, 'ts_last_edit' => null, 'deleted' => 0, + 'starred' => 0, + 'starred_by' => null, ], ]; } diff --git a/tests/UI/expected-screenshots/UIIntegrationTest_api_listing.png b/tests/UI/expected-screenshots/UIIntegrationTest_api_listing.png index d69f2910103..e80d62c975e 100644 --- a/tests/UI/expected-screenshots/UIIntegrationTest_api_listing.png +++ b/tests/UI/expected-screenshots/UIIntegrationTest_api_listing.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:031b27a45ed8600ff49178a02cb70bc806bd3b5900f6286da8c80ef4fcbaf9c9 -size 5007529 +oid sha256:ddb5a0e253e2b8c95631ac23f02908ed5cd67f8df0c797ffeaab30461ec110b2 +size 5019917