diff options
author | diosmosis <diosmosis@users.noreply.github.com> | 2018-08-03 01:57:13 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-08-03 01:57:13 +0300 |
commit | cb1d83db863938ace3ebdafd072dfd32e434fded (patch) | |
tree | 32d8e98d500b2167dcd9d04d92edfec21da9f5e9 | |
parent | 59e6f48c9d9112b7335e078f05d405264b46f0c5 (diff) |
Add reusable widget to display single metric w/ sparkline & evolution percent (+ other changes) (#13101)
* Add empty metric for single metric view.
* Add new isReusable property to widget metadata & if set to true, do not grey out the widget in the dashboard manager, even if the widget is used in the dashboard.
* Initial working version of single metric view.
* Get single metric view widget to work and look correctly (no series picker).
* Add series picker to single metric widget and add filter_last_period_evolution parameter.
* Persist metric change through dashboard widget parameter saving.
* Loading state for single metric view.
* Make new evolution param work on processed reports + tweak component implementation.
* Tweak CSS and make sure angular components are compiled in widget preview.
* Make component work with widget preview and avoid unnecessary widget reloads when multiple widgets of the same type are shown.
* Generalize JS lastN range period computing and use to create standalone sparkline angular component and get rid of need for "past-period" argument to single metric view.
* Add format_metrics: "1" to API.get method.
* Add escaping to _angularComponent.twig.
* hacky fix for formatting revenue columns
* Format past data values & allow evolution to be calculated for processed metrics.
* filter evolution changes
* Fix issue in subtable recursion for processed metric computation & metric formatting + add new processed metric compute hooks to fix bug in evolution calculation on subtables.
* remove isReusable property.
* attempting to change strategy
* simpler solution that does not require backend changes
* remove unneeded code + fix issue w/ formatted metrics
* remove some more unneeded code
* write UI test
* add new screenshots
* Add all goals to single metric view picker.
* move category
* fix test
* fixing more tests
* Fixing some UI tests.
* Update more screenshots.
* update two more screenshots
55 files changed, 901 insertions, 103 deletions
diff --git a/core/API/DataTablePostProcessor.php b/core/API/DataTablePostProcessor.php index a292139053..abfd57d9df 100644 --- a/core/API/DataTablePostProcessor.php +++ b/core/API/DataTablePostProcessor.php @@ -367,7 +367,11 @@ class DataTablePostProcessor $metricsToFormat = $this->apiInconsistencies->getPercentMetricsToFormat(); } - $dataTable->filter(array($this->formatter, 'formatMetrics'), array($this->report, $metricsToFormat)); + // 'all' is a special value that indicates we should format non-processed metrics that are identified + // by string, like 'revenue'. this should be removed when all metrics are using the `Metric` class. + $formatAll = $formatMetrics === 'all'; + + $dataTable->filter(array($this->formatter, 'formatMetrics'), array($this->report, $metricsToFormat, $formatAll)); return $dataTable; } @@ -421,16 +425,28 @@ class DataTablePostProcessor } foreach ($dataTable->getRows() as $row) { - if ($row->getColumn($name) === false) { // only compute the metric if it has not been computed already - $computedValue = $processedMetric->compute($row); - if ($computedValue !== false) { - $row->addColumn($name, $computedValue); - } - - $subtable = $row->getSubtable(); - if (!empty($subtable)) { - $this->computeProcessedMetrics($subtable); - } + if ($row->getColumn($name) !== false) { // only compute the metric if it has not been computed already + continue; + } + + $computedValue = $processedMetric->compute($row); + if ($computedValue !== false) { + $row->addColumn($name, $computedValue); + } + } + } + + foreach ($dataTable->getRows() as $row) { + $subtable = $row->getSubtable(); + if (!empty($subtable)) { + foreach ($processedMetrics as $name => $processedMetric) { + $processedMetric->beforeComputeSubtable($row); + } + + $this->computeProcessedMetrics($subtable); + + foreach ($processedMetrics as $name => $processedMetric) { + $processedMetric->afterComputeSubtable($row); } } } @@ -441,3 +457,4 @@ class DataTablePostProcessor $dataTable->filter(array($this, 'computeProcessedMetrics')); } } + diff --git a/core/DataTable/Filter/CalculateEvolutionFilter.php b/core/DataTable/Filter/CalculateEvolutionFilter.php index 73274c537d..2253760494 100644 --- a/core/DataTable/Filter/CalculateEvolutionFilter.php +++ b/core/DataTable/Filter/CalculateEvolutionFilter.php @@ -28,7 +28,7 @@ use Piwik\Site; * ((currentValue - pastValue) / pastValue) * 100 * * @api - * @deprecated since v2.10.0 + * @deprecated since v2.10.0 (use EvolutionMetric instead) */ class CalculateEvolutionFilter extends ColumnCallbackAddColumnPercentage { diff --git a/core/Metrics/Formatter.php b/core/Metrics/Formatter.php index 6608b802e3..e5b6e19426 100644 --- a/core/Metrics/Formatter.php +++ b/core/Metrics/Formatter.php @@ -7,6 +7,7 @@ */ namespace Piwik\Metrics; +use Piwik\Archive\DataTableFactory; use Piwik\Common; use Piwik\DataTable; use Piwik\NumberFormatter; @@ -200,9 +201,11 @@ class Formatter * @param DataTable $dataTable The table to format metrics for. * @param Report|null $report The report the table belongs to. * @param string[]|null $metricsToFormat Whitelist of names of metrics to format. + * @param boolean $formatAll If true, will also apply formatting to non-processed metrics like revenue. + * This parameter is not currently supported and subject to change. * @api */ - public function formatMetrics(DataTable $dataTable, Report $report = null, $metricsToFormat = null) + public function formatMetrics(DataTable $dataTable, Report $report = null, $metricsToFormat = null, $formatAll = false) { $metrics = $this->getMetricsToFormat($dataTable, $report); if (empty($metrics) @@ -231,10 +234,36 @@ class Formatter if ($columnValue !== false) { $row->setColumn($name, $metric->format($columnValue, $this)); } + } + } + + foreach ($dataTable->getRows() as $row) { + $subtable = $row->getSubtable(); + if (!empty($subtable)) { + $this->formatMetrics($subtable, $report, $metricsToFormat); + } + } + + $idSite = DataTableFactory::getSiteIdFromMetadata($dataTable); + if (empty($idSite)) { + // possible when using search in visualization + $idSite = Common::getRequestVar('idSite', 0, 'int'); + } - $subtable = $row->getSubtable(); - if (!empty($subtable)) { - $this->formatMetrics($subtable, $report, $metricsToFormat); + // @todo for matomo 4, should really use the Metric class to house this kind of logic + // format other metrics + if ($formatAll) { + foreach ($dataTable->getRows() as $row) { + foreach ($row->getColumns() as $column => $columnValue) { + if (strpos($column, 'revenue') === false + || !is_numeric($columnValue) + ) { + continue; + } + + if ($columnValue !== false) { + $row->setColumn($column, $this->getPrettyMoney($columnValue, $idSite)); + } } } } diff --git a/core/Plugin/ProcessedMetric.php b/core/Plugin/ProcessedMetric.php index 108612c805..cbac0c5582 100644 --- a/core/Plugin/ProcessedMetric.php +++ b/core/Plugin/ProcessedMetric.php @@ -67,4 +67,22 @@ abstract class ProcessedMetric extends Metric { return true; } + + /** + * @param Row $row + * @ignore + */ + public function beforeComputeSubtable(Row $row) + { + // empty + } + + /** + * @param Row $row + * @ignore + */ + public function afterComputeSubtable(Row $row) + { + // empty + } } diff --git a/core/Plugin/Report.php b/core/Plugin/Report.php index e0850586d9..5356286f80 100644 --- a/core/Plugin/Report.php +++ b/core/Plugin/Report.php @@ -934,6 +934,42 @@ class Report */ public static function getProcessedMetricsForTable(DataTable $dataTable, Report $report = null) { - return self::getMetricsForTable($dataTable, $report, 'Piwik\\Plugin\\ProcessedMetric'); + /** @var ProcessedMetric[] $metrics */ + $metrics = self::getMetricsForTable($dataTable, $report, 'Piwik\\Plugin\\ProcessedMetric'); + + // sort metrics w/ dependent metrics calculated before the metrics that depend on them + $result = []; + self::processedMetricDfs($metrics, function ($metricName) use (&$result, $metrics) { + $result[$metricName] = $metrics[$metricName]; + }); + return $result; + } + + /** + * @param ProcessedMetric[] $metrics + * @param $callback + * @param array $visited + */ + private static function processedMetricDfs($metrics, $callback, &$visited = [], $toVisit = null) + { + $toVisit = $toVisit === null ? $metrics : $toVisit; + foreach ($toVisit as $name => $metric) { + if (!empty($visited[$name])) { + continue; + } + + $visited[$name] = true; + + $dependentMetrics = []; + foreach ($metric->getDependentMetrics() as $metricName) { + if (!empty($metrics[$metricName])) { + $dependentMetrics[$metricName] = $metrics[$metricName]; + } + } + + self::processedMetricDfs($metrics, $callback, $visited, $dependentMetrics); + + $callback($name); + } } } diff --git a/core/Widget/WidgetConfig.php b/core/Widget/WidgetConfig.php index e50afaff7c..a99ca9ceb3 100644 --- a/core/Widget/WidgetConfig.php +++ b/core/Widget/WidgetConfig.php @@ -365,5 +365,4 @@ class WidgetConfig { return $this->isWide; } - }
\ No newline at end of file diff --git a/lang/en.json b/lang/en.json index 6b87cbdb36..69ac31e0f1 100644 --- a/lang/en.json +++ b/lang/en.json @@ -218,6 +218,7 @@ "ForExampleShort": "eg.", "Forums": "Forums", "FromReferrer": "from", + "Generic": "Generic", "GeneralInformation": "General Information", "GeneralSettings": "General settings", "GetStarted": "Get started", diff --git a/plugins/API/ProcessedReport.php b/plugins/API/ProcessedReport.php index 6caac672c3..ec4bfc986f 100644 --- a/plugins/API/ProcessedReport.php +++ b/plugins/API/ProcessedReport.php @@ -23,6 +23,7 @@ use Piwik\Metrics; use Piwik\Metrics\Formatter; use Piwik\Period; use Piwik\Piwik; +use Piwik\Plugin\Metric; use Piwik\Plugin\ReportsProvider; use Piwik\Site; use Piwik\Timer; diff --git a/plugins/API/lang/en.json b/plugins/API/lang/en.json index 7b37f23921..72dd837fae 100644 --- a/plugins/API/lang/en.json +++ b/plugins/API/lang/en.json @@ -11,6 +11,7 @@ "UserAuthentication": "User authentication", "UsingTokenAuth": "If you want to %1$s request data within a script, a crontab, etc. %2$s you need to add the parameter %3$s to the API calls URLs that require authentication.", "Glossary": "Glossary", - "LearnAboutCommonlyUsedTerms2": "Learn about the commonly used terms to make the most of Matomo Analytics." + "LearnAboutCommonlyUsedTerms2": "Learn about the commonly used terms to make the most of Matomo Analytics.", + "EvolutionMetricName": "%s Evolution" } }
\ No newline at end of file diff --git a/plugins/CoreHome/Categories/GenericCategory.php b/plugins/CoreHome/Categories/GenericCategory.php new file mode 100644 index 0000000000..13727cc51c --- /dev/null +++ b/plugins/CoreHome/Categories/GenericCategory.php @@ -0,0 +1,17 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ +namespace Piwik\Plugins\CoreHome\Categories; + +use Piwik\Category\Category; + +class GenericCategory extends Category +{ + protected $id = 'General_Generic'; + protected $order = 70; +} diff --git a/plugins/CoreHome/Columns/Metrics/EvolutionMetric.php b/plugins/CoreHome/Columns/Metrics/EvolutionMetric.php index 537e42a520..2df4e93171 100644 --- a/plugins/CoreHome/Columns/Metrics/EvolutionMetric.php +++ b/plugins/CoreHome/Columns/Metrics/EvolutionMetric.php @@ -10,6 +10,7 @@ namespace Piwik\Plugins\CoreHome\Columns\Metrics; use Piwik\DataTable; use Piwik\DataTable\Row; +use Piwik\Metrics; use Piwik\Metrics\Formatter; use Piwik\Piwik; use Piwik\Plugin\Metric; @@ -46,15 +47,23 @@ class EvolutionMetric extends ProcessedMetric private $pastData; /** + * The list of labels leading to the current subtable being processed. Used to get the proper subtable in + * $pastData. + * + * @var string[] + */ + private $labelPath = []; + + /** * Constructor. * * @param string|Metric $wrapped The metric used to calculate the evolution. - * @param DataTable $pastData The data in the past to use when calculating evolutions. + * @param DataTable|null $pastData The data in the past to use when calculating evolutions. * @param string|false $evolutionMetricName The name of the evolution processed metric. Defaults to * $wrapped's name with `'_evolution'` appended. * @param int $quotientPrecision The percent's quotient precision. */ - public function __construct($wrapped, DataTable $pastData, $evolutionMetricName = false, $quotientPrecision = 0) + public function __construct($wrapped, DataTable $pastData = null, $evolutionMetricName = false, $quotientPrecision = 0) { $this->wrapped = $wrapped; $this->pastData = $pastData; @@ -75,7 +84,13 @@ class EvolutionMetric extends ProcessedMetric public function getTranslatedName() { - return $this->wrapped instanceof Metric ? $this->wrapped->getTranslatedName() : $this->getName(); + if ($this->wrapped instanceof Metric) { + $metricName = $this->wrapped->getTranslatedName(); + } else { + $defaultMetricTranslations = Metrics::getDefaultMetricTranslations(); + $metricName = isset($defaultMetricTranslations[$this->wrapped]) ? $defaultMetricTranslations[$this->wrapped] : $this->wrapped; + } + return Piwik::translate('CoreHome_EvolutionMetricName', [$metricName]); } public function compute(Row $row) @@ -108,6 +123,16 @@ class EvolutionMetric extends ProcessedMetric return array($this->getWrappedName()); } + public function beforeComputeSubtable(Row $row) + { + $this->labelPath[] = $row->getColumn('label'); + } + + public function afterComputeSubtable(Row $row) + { + array_pop($this->labelPath); + } + protected function getWrappedName() { return $this->wrapped instanceof Metric ? $this->wrapped->getName() : $this->wrapped; @@ -118,6 +143,31 @@ class EvolutionMetric extends ProcessedMetric */ public function getPastRowFromCurrent(Row $row) { - return $this->pastData->getRowFromLabel($row->getColumn('label')); + $pastData = $this->getPastDataTable(); + if (empty($pastData)) { + return null; + } + + $label = $row->getColumn('label'); + return $label ? $pastData->getRowFromLabel($label) : $pastData->getFirstRow(); + } + + private function getPastDataTable() + { + $result = $this->pastData; + foreach ($this->labelPath as $label) { + $row = $result->getRowFromLabel($label); + if (empty($row)) { + return null; + } + + $subtable = $row->getSubtable(); + if (empty($subtable)) { + return null; + } + + $result = $subtable; + } + return $result; } }
\ No newline at end of file diff --git a/plugins/CoreHome/CoreHome.php b/plugins/CoreHome/CoreHome.php index 04fc917ec7..3905bdf78b 100644 --- a/plugins/CoreHome/CoreHome.php +++ b/plugins/CoreHome/CoreHome.php @@ -117,6 +117,7 @@ class CoreHome extends \Piwik\Plugin $stylesheets[] = "plugins/CoreHome/angularjs/period-date-picker/period-date-picker.component.less"; $stylesheets[] = "plugins/CoreHome/angularjs/period-selector/period-selector.directive.less"; $stylesheets[] = "plugins/CoreHome/angularjs/multipairfield/multipairfield.directive.less"; + $stylesheets[] = "plugins/CoreHome/angularjs/sparkline/sparkline.component.less"; $stylesheets[] = "plugins/CoreHome/angularjs/field-array/field-array.directive.less"; } @@ -207,6 +208,7 @@ class CoreHome extends \Piwik\Plugin $jsFiles[] = "plugins/CoreHome/angularjs/activity-indicator/activityindicator.directive.js"; $jsFiles[] = "plugins/CoreHome/angularjs/progressbar/progressbar.directive.js"; $jsFiles[] = "plugins/CoreHome/angularjs/alert/alert.directive.js"; + $jsFiles[] = "plugins/CoreHome/angularjs/sparkline/sparkline.component.js"; $jsFiles[] = "plugins/CoreHome/angularjs/siteselector/siteselector-model.service.js"; $jsFiles[] = "plugins/CoreHome/angularjs/siteselector/siteselector.controller.js"; @@ -441,6 +443,5 @@ class CoreHome extends \Piwik\Plugin $translationKeys[] = 'CoreHome_PageDownShortcutDescription'; $translationKeys[] = 'CoreHome_MacPageUp'; $translationKeys[] = 'CoreHome_MacPageDown'; - } } diff --git a/plugins/CoreHome/angularjs/common/services/periods.js b/plugins/CoreHome/angularjs/common/services/periods.js index d1b50917bd..1c02bca2df 100644 --- a/plugins/CoreHome/angularjs/common/services/periods.js +++ b/plugins/CoreHome/angularjs/common/services/periods.js @@ -167,40 +167,64 @@ addCustomPeriod('year', YearPeriod); // range period - function RangePeriod(startDate, endDate) { + function RangePeriod(startDate, endDate, childPeriodType) { this.startDate = startDate; this.endDate = endDate; + this.childPeriodType = childPeriodType; } - RangePeriod.parse = function parseRangePeriod(strDate) { - var dates = []; + RangePeriod.parse = function parseRangePeriod(strDate, childPeriodType) { + childPeriodType = childPeriodType || 'day'; if (/^previous/.test(strDate)) { - dates = getLastNRange(strDate.substring(8), 1); + var endDate = RangePeriod.getLastNRange(childPeriodType, 2).startDate; + return RangePeriod.getLastNRange(childPeriodType, strDate.substring(8), endDate); } else if (/^last/.test(strDate)) { - dates = getLastNRange(strDate.substring(4), 0); + return RangePeriod.getLastNRange(childPeriodType, strDate.substring(4)); } else { var parts = strDate.split(','); - dates[0] = parseDate(parts[0]); - dates[1] = parseDate(parts[1]); + return new RangePeriod(parseDate(parts[0]), parseDate(parts[1]), childPeriodType) } + }; - return new RangePeriod(dates[0], dates[1]); - - function getLastNRange(strAmount, extraDaysStart) { - var nAmount = Math.max(parseInt(strAmount) - 1, 0); - if (isNaN(nAmount)) { - throw new Error('Invalid range date: ' + strDate); - } + /** + * Returns a range representing the last N childPeriodType periods, including the current one. + * + * @param childPeriodType + * @param strAmount + * @param endDate + * @returns {RangePeriod} + */ + RangePeriod.getLastNRange = function (childPeriodType, strAmount, endDate) { + var nAmount = Math.max(parseInt(strAmount) - 1, 0); + if (isNaN(nAmount)) { + throw new Error('Invalid range date: ' + strDate); + } - var endDate = getToday(); - endDate.setDate(endDate.getDate() - extraDaysStart); + endDate = endDate ? parseDate(endDate) : getToday(); - var startDate = new Date(endDate.getTime()); + var startDate = new Date(endDate.getTime()); + if (childPeriodType === 'day') { startDate.setDate(startDate.getDate() - nAmount); + } else if (childPeriodType === 'week') { + startDate.setDate(startDate.getDate() - (nAmount * 7)); + } else if (childPeriodType === 'month') { + startDate.setMonth(startDate.getMonth() - nAmount); + } else if (childPeriodType === 'year') { + startDate.setFullYear(startDate.getFullYear() - nAmount); + } else { + throw new Error("Unknown period type '" + childPeriodType + "'."); + } + + if (childPeriodType !== 'day') { + var startPeriod = periods[childPeriodType].parse(startDate); + var endPeriod = periods[childPeriodType].parse(endDate); - return [startDate, endDate]; + startDate = startPeriod.getDateRange()[0]; + endDate = endPeriod.getDateRange()[1]; } + + return new RangePeriod(startDate, endDate, childPeriodType); }; RangePeriod.getDisplayText = function () { @@ -268,6 +292,10 @@ } function parseDate(strDate) { + if (strDate instanceof Date) { + return strDate; + } + if (strDate === 'today' || strDate === 'now' ) { diff --git a/plugins/CoreHome/angularjs/common/services/piwik-api.js b/plugins/CoreHome/angularjs/common/services/piwik-api.js index 54c07dc0d8..0f77fd7dbb 100644 --- a/plugins/CoreHome/angularjs/common/services/piwik-api.js +++ b/plugins/CoreHome/angularjs/common/services/piwik-api.js @@ -137,7 +137,7 @@ var hasBlockedContent = false; method: 'POST', url: url, responseType: requestFormat, - params: _mixinDefaultGetParams(getParams), + params: mixinDefaultGetParams(getParams), data: $.param(getPostParams(postParams)), timeout: requestPromise, headers: headers @@ -201,7 +201,7 @@ var hasBlockedContent = false; * @return {object} * @private */ - function _mixinDefaultGetParams (getParamsToMixin) { + function mixinDefaultGetParams (getParamsToMixin) { var segment = piwik.broadcast.getValueFromHash('segment', $window.location.href.split('#')[1]); // we have to decode the value manually because broadcast will not decode anything itself. if we don't, @@ -338,7 +338,8 @@ var hasBlockedContent = false; * @deprecated */ abort: abort, - abortAll: abortAll + abortAll: abortAll, + mixinDefaultGetParams: mixinDefaultGetParams }; } })();
\ No newline at end of file diff --git a/plugins/CoreHome/angularjs/sparkline/sparkline.component.js b/plugins/CoreHome/angularjs/sparkline/sparkline.component.js new file mode 100644 index 0000000000..4bf959875c --- /dev/null +++ b/plugins/CoreHome/angularjs/sparkline/sparkline.component.js @@ -0,0 +1,62 @@ +/*! + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +/** + * Displays a sparkline. 'module', 'action' and 'date' are required elements of the + * params attribute. + * + * Usage: + * <piwik-sparkline params="{'module': 'API', 'action': 'get', 'date': '...'}"></piwik-sparkline> + */ +(function () { + angular.module('piwikApp').component('piwikSparkline', { + template: '<img />', + bindings: { + params: '<' + }, + controller: SparklineController + }); + + SparklineController.$inject = ['$element', '$httpParamSerializer', 'piwikApi', 'piwik', 'piwikPeriods']; + + function SparklineController($element, $httpParamSerializer, piwikApi, piwik, piwikPeriods) { + var vm = this; + vm.$onChanges = $onChanges; + + function $onChanges() { + // done manually due to 'random' query param. since it changes the URL on each digest, depending on angular + // results in an infinite digest + $element.find('img').attr('src', getSparklineUrl()); + } + + function getSparklineUrl() { + var defaultParams = { + forceView: '1', + viewDataTable: 'sparkline', + widget: $element.closest('[widgetId]').length ? '1' : '0', + showtitle: '1', + colors: JSON.stringify(piwik.getSparklineColors()), + random: Date.now(), + date: getDefaultDate() + }; + + var urlParams = piwikApi.mixinDefaultGetParams($element.extend(defaultParams, vm.params)); + return '?' + $httpParamSerializer(urlParams); + } + + function getDefaultDate() { + if (piwik.period === 'range') { + return piwik.startDateString + ',' + piwik.endDateString; + } + + var dateRange = piwikPeriods.get('range').getLastNRange(piwik.period, 30, piwik.currentDateString).getDateRange(); + var startDateStr = $.datepicker.formatDate('yy-mm-dd', dateRange[0]); + var endDateStr = $.datepicker.formatDate('yy-mm-dd', dateRange[1]); + return startDateStr + ',' + endDateStr; + } + } +})(); diff --git a/plugins/CoreHome/angularjs/sparkline/sparkline.component.less b/plugins/CoreHome/angularjs/sparkline/sparkline.component.less new file mode 100644 index 0000000000..a31d68547e --- /dev/null +++ b/plugins/CoreHome/angularjs/sparkline/sparkline.component.less @@ -0,0 +1,8 @@ +[piwik-sparkline] { + display: inline-block; + + img { + width: 100px; + height: 25px; + } +}
\ No newline at end of file diff --git a/plugins/CoreHome/templates/_angularComponent.twig b/plugins/CoreHome/templates/_angularComponent.twig new file mode 100644 index 0000000000..2ac6cb7f90 --- /dev/null +++ b/plugins/CoreHome/templates/_angularComponent.twig @@ -0,0 +1,5 @@ +<{{ componentName|e('html') }} + {% for key, value in componentParameters %} + {{ key|e('html') }}="{{ value|e('html_attr') }}" + {% endfor %} +/>
\ No newline at end of file diff --git a/plugins/CoreHome/tests/UI/SingleMetricView_spec.js b/plugins/CoreHome/tests/UI/SingleMetricView_spec.js new file mode 100644 index 0000000000..4305c95ae8 --- /dev/null +++ b/plugins/CoreHome/tests/UI/SingleMetricView_spec.js @@ -0,0 +1,43 @@ +/*! + * Piwik - free/libre analytics platform + * + * Dashboard screenshot tests. + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +describe('SingleMetricView', function () { + this.timeout(0); + + var url = "?module=Widgetize&action=iframe&idSite=1&period=year&date=2012-08-09&moduleToWidgetize=Dashboard&" + + "actionToWidgetize=index&idDashboard=5"; + var rangeUrl = "?module=Widgetize&action=iframe&idSite=1&period=range&date=2012-08-07,2012-08-10&moduleToWidgetize=Dashboard&" + + "actionToWidgetize=index&idDashboard=5"; + + it('should load correctly', function (done) { + expect.screenshot("loaded").to.be.captureSelector('#widgetCoreVisualizationssingleMetricViewcolumn', function (page) { + page.load(url, 5000); + page.click('.dashboard-manager a.title'); + + page.mouseMove('.widgetpreview-categorylist>li:contains(Live!)'); // have to mouse move twice... otherwise Live! will just be highlighted + page.mouseMove('.widgetpreview-categorylist > li:contains(Generic)'); + + page.mouseMove('.widgetpreview-widgetlist li:contains(Metric)'); + page.click('.widgetpreview-widgetlist li:contains(Metric)'); + }, done); + }); + + it('should handle formatted metrics properly', function (done) { + expect.screenshot("formatted_metric").to.be.captureSelector('#widgetCoreVisualizationssingleMetricViewcolumn', function (page) { + page.mouseMove('#widgetCoreVisualizationssingleMetricViewcolumn .single-metric-view-picker'); + page.click('.jqplot-seriespicker-popover label:contains(Revenue)'); + }, done); + }); + + it('should handle range periods correctly', function (done) { + expect.screenshot("range").to.be.captureSelector('#widgetCoreVisualizationssingleMetricViewcolumn', function (page) { + page.load(rangeUrl, 8000); + }, done); + }); +}); diff --git a/plugins/CoreHome/tests/UI/expected-screenshots/SingleMetricView_formatted_metric.png b/plugins/CoreHome/tests/UI/expected-screenshots/SingleMetricView_formatted_metric.png new file mode 100644 index 0000000000..358cebf25c --- /dev/null +++ b/plugins/CoreHome/tests/UI/expected-screenshots/SingleMetricView_formatted_metric.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:313dada73a2be00e60b92b87103ff6034469ca4f5791a748e89c0c7416731c6e +size 8060 diff --git a/plugins/CoreHome/tests/UI/expected-screenshots/SingleMetricView_loaded.png b/plugins/CoreHome/tests/UI/expected-screenshots/SingleMetricView_loaded.png new file mode 100644 index 0000000000..2c5ba94ffd --- /dev/null +++ b/plugins/CoreHome/tests/UI/expected-screenshots/SingleMetricView_loaded.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63f8be060d07ca212107f46af079331be10d41d217200b7866b190be73810b08 +size 5584 diff --git a/plugins/CoreHome/tests/UI/expected-screenshots/SingleMetricView_range.png b/plugins/CoreHome/tests/UI/expected-screenshots/SingleMetricView_range.png new file mode 100644 index 0000000000..a8b656864e --- /dev/null +++ b/plugins/CoreHome/tests/UI/expected-screenshots/SingleMetricView_range.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d67e7660eef5bee9b595401e3500b48fbd36ddaede374d7bc3f59cb331ae56e7 +size 2952 diff --git a/plugins/CoreVisualizations/CoreVisualizations.php b/plugins/CoreVisualizations/CoreVisualizations.php index 5c633706e3..48117517a4 100644 --- a/plugins/CoreVisualizations/CoreVisualizations.php +++ b/plugins/CoreVisualizations/CoreVisualizations.php @@ -40,6 +40,7 @@ class CoreVisualizations extends \Piwik\Plugin public function getStylesheetFiles(&$stylesheets) { $stylesheets[] = "plugins/CoreVisualizations/angularjs/series-picker/series-picker.component.less"; + $stylesheets[] = "plugins/CoreVisualizations/angularjs/single-metric-view/single-metric-view.component.less"; $stylesheets[] = "plugins/CoreVisualizations/stylesheets/dataTableVisualizations.less"; $stylesheets[] = "plugins/CoreVisualizations/stylesheets/jqplot.css"; @@ -48,7 +49,7 @@ class CoreVisualizations extends \Piwik\Plugin public function getJsFiles(&$jsFiles) { $jsFiles[] = "plugins/CoreVisualizations/angularjs/series-picker/series-picker.component.js"; - $jsFiles[] = "plugins/CoreVisualizations/angularjs/series-picker/series-picker.component.js"; + $jsFiles[] = "plugins/CoreVisualizations/angularjs/single-metric-view/single-metric-view.component.js"; $jsFiles[] = "plugins/CoreVisualizations/javascripts/seriesPicker.js"; $jsFiles[] = "plugins/CoreVisualizations/javascripts/jqplot.js"; @@ -65,5 +66,6 @@ class CoreVisualizations extends \Piwik\Plugin $translationKeys[] = 'General_SaveImageOnYourComputer'; $translationKeys[] = 'General_ExportAsImage'; $translationKeys[] = 'General_NoDataForGraph'; + $translationKeys[] = 'General_EvolutionSummaryGeneric'; } } diff --git a/plugins/CoreVisualizations/Widgets/SingleMetricView.php b/plugins/CoreVisualizations/Widgets/SingleMetricView.php new file mode 100644 index 0000000000..b9ea39840e --- /dev/null +++ b/plugins/CoreVisualizations/Widgets/SingleMetricView.php @@ -0,0 +1,83 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ + +namespace Piwik\Plugins\CoreVisualizations\Widgets; + +use Piwik\API\Request; +use Piwik\Common; +use Piwik\View; +use Piwik\Widget\WidgetConfig; +use Piwik\Plugin\Manager as PluginManager; +use Piwik\Plugins\Goals\API as GoalsAPI; + +class SingleMetricView extends \Piwik\Widget\Widget +{ + public static function configure(WidgetConfig $config) + { + parent::configure($config); + + $column = Common::getRequestVar('column', '', 'string'); + + $config->addParameters(['column' => $column]); + $config->setCategoryId('General_Generic'); + $config->setName('General_Metric'); + $config->setIsWidgetizable(); + } + + public function render() + { + $column = Common::getRequestVar('column', 'nb_visits', 'string'); + + $goalMetrics = []; + $goals = []; + + $idSite = Common::getRequestVar('idSite'); + $idGoal = Common::getRequestVar('idGoal', false); + + $reportMetadata = Request::processRequest('API.getMetadata', [ + 'idSites' => $idSite, + 'apiModule' => 'API', + 'apiAction' => 'get', + ]); + $reportMetadata = reset($reportMetadata); + + $metricTranslations = array_merge($reportMetadata['metrics'], $reportMetadata['processedMetrics']); + $metricDocumentations = $reportMetadata['metricsDocumentation']; + + if (PluginManager::getInstance()->isPluginActivated('Goals')) { + $reportMetadata = Request::processRequest('API.getMetadata', [ + 'idSites' => $idSite, + 'apiModule' => 'Goals', + 'apiAction' => 'get', + ]); + $reportMetadata = reset($reportMetadata); + + $goalMetrics = array_merge( + array_keys($reportMetadata['metrics']), + array_keys($reportMetadata['processedMetrics']) + ); + $metricDocumentations = array_merge($metricDocumentations, $reportMetadata['metricsDocumentation']); + + $goals = GoalsAPI::getInstance()->getGoals($idSite); + } + + $view = new View("@CoreHome/_angularComponent.twig"); + $view->componentName = 'piwik-single-metric-view'; + $view->componentParameters = [ + 'metric' => json_encode($column), + 'id-goal' => $idGoal === false ? 'undefined' : $idGoal, + 'goal-metrics' => json_encode($goalMetrics), + 'goals' => json_encode($goals), + 'metric-translations' => json_encode($metricTranslations), + 'metric-documentations' => json_encode($metricDocumentations), + ]; + + return $view->render(); + } +}
\ No newline at end of file diff --git a/plugins/CoreVisualizations/angularjs/series-picker/series-picker.component.less b/plugins/CoreVisualizations/angularjs/series-picker/series-picker.component.less index e950b1eefb..9d4fdc8de9 100644 --- a/plugins/CoreVisualizations/angularjs/series-picker/series-picker.component.less +++ b/plugins/CoreVisualizations/angularjs/series-picker/series-picker.component.less @@ -6,6 +6,10 @@ piwik-series-picker { opacity: .55; } + &.open { // while open, make sure we're above other series picker icons + z-index: 1000; + } + > a { display: inline-block; opacity: 0; diff --git a/plugins/CoreVisualizations/angularjs/single-metric-view/single-metric-view.component.html b/plugins/CoreVisualizations/angularjs/single-metric-view/single-metric-view.component.html new file mode 100644 index 0000000000..7d7b0c2a9f --- /dev/null +++ b/plugins/CoreVisualizations/angularjs/single-metric-view/single-metric-view.component.html @@ -0,0 +1,20 @@ +<div class="singleMetricView" ng-class="{'loading': $ctrl.isLoading}"> + <piwik-sparkline + class="metric-sparkline" + params="{module: 'API', action: 'get', columns: $ctrl.metric}" + > + </piwik-sparkline> + <div class="metric-value"> + <span title="{{ $ctrl.metricDocumentation }}"> + <strong>{{ $ctrl.metricValue }}</strong> {{ ($ctrl.metricTranslation || '').toLowerCase() }} + </span> + <span class="metricEvolution" + ng-if="$ctrl.pastValue !== null" + title="{{ 'General_EvolutionSummaryGeneric'|translate:$ctrl.metricValue:$ctrl.getCurrentPeriod():$ctrl.pastValue:$ctrl.pastPeriod:$ctrl.metricChangePercent }}" + > + <span ng-class="{'positive-evolution': $ctrl.metricValueUnformatted > $ctrl.pastValueUnformatted, 'negative-evolution': $ctrl.metricValueUnformatted < $ctrl.pastValueUnformatted}"> + {{ $ctrl.metricChangePercent }} + </span> + </span> + </div> +</div>
\ No newline at end of file diff --git a/plugins/CoreVisualizations/angularjs/single-metric-view/single-metric-view.component.js b/plugins/CoreVisualizations/angularjs/single-metric-view/single-metric-view.component.js new file mode 100644 index 0000000000..d18e9d88ed --- /dev/null +++ b/plugins/CoreVisualizations/angularjs/single-metric-view/single-metric-view.component.js @@ -0,0 +1,262 @@ +/*! + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +/** + * Usage: + * <piwik-single-metric-view> + */ +(function () { + angular.module('piwikApp').component('piwikSingleMetricView', { + templateUrl: 'plugins/CoreVisualizations/angularjs/single-metric-view/single-metric-view.component.html?cb=' + piwik.cacheBuster, + bindings: { + metric: '<', + idGoal: '<', + metricTranslations: '<', + metricDocumentations: '<', + goals: '<', + goalMetrics: '<' + }, + controller: SingleMetricViewController + }); + + SingleMetricViewController.$inject = ['piwik', 'piwikApi', '$element', '$httpParamSerializer', '$compile', '$scope', 'piwikPeriods', '$q']; + + function SingleMetricViewController(piwik, piwikApi, $element, $httpParamSerializer, $compile, $scope, piwikPeriods, $q) { + var seriesPickerScope; + + var vm = this; + vm.metricValue = null; + vm.isLoading = false; + vm.metricTranslation = null; + vm.metricDocumentation = null; + vm.selectableColumns = []; + vm.responses = null; + vm.$onInit = $onInit; + vm.$onChanges = $onChanges; + vm.$onDestroy = $onDestroy; + vm.getCurrentPeriod = getCurrentPeriod; + vm.getMetricTranslation = getMetricTranslation; + vm.setMetric = setMetric; + + function $onInit() { + vm.selectedColumns = [vm.metric]; + if (piwik.period !== 'range') { + vm.pastPeriod = getPastPeriodStr(); + } + + setSelectableColumns(); + + createSeriesPicker(); + + $element.closest('.widgetContent') + .on('widget:destroy', function() { $scope.$parent.$destroy(); }) + .on('widget:reload', function() { $scope.$parent.$destroy(); }); + } + + function $onChanges(changes) { + if (changes.metric && changes.metric.previousValue !== changes.metric.currentValue) { + onMetricChanged(); + } + } + + function $onDestroy() { + $element.closest('.widgetContent').off('widget:destroy').off('widget:reload'); + destroySeriesPicker(); + } + + function fetchData() { + if (vm.responses && vm.responses.length && typeof vm.idGoal === 'undefined') { + return $q.resolve(); + } + + vm.isLoading = true; + + var promises = []; + + var apiModule = 'API'; + var apiAction = 'get'; + + var extraParams = {}; + if (vm.idGoal) { + extraParams.idGoal = vm.idGoal; + // the conversion rate added by the AddColumnsProcessedMetrics filter conflicts w/ the goals one, so don't run it + extraParams.filter_add_columns_when_show_all_columns = 0; + + apiModule = 'Goals'; + apiAction = 'get'; + } + + // first request for formatted data + promises.push(piwikApi.fetch($.extend({ + method: apiModule + '.' + apiAction, + format_metrics: 'all' + }, extraParams))); + + if (piwik.period !== 'range') { + // second request for unformatted data so we can calculate evolution + promises.push(piwikApi.fetch($.extend({ + method: apiModule + '.' + apiAction, + format_metrics: '0' + }, extraParams))); + + // third request for past data (unformatted) + promises.push(piwikApi.fetch($.extend({ + method: apiModule + '.' + apiAction, + date: getLastPeriodDate(), + format_metrics: '0', + }, extraParams))); + + // fourth request for past data (formatted for tooltip display) + promises.push(piwikApi.fetch($.extend({ + method: apiModule + '.' + apiAction, + date: getLastPeriodDate(), + format_metrics: 'all', + }, extraParams))); + } + + return $q.all(promises).then(function (responses) { + vm.responses = responses; + vm.isLoading = false; + }); + } + + function recalculateValues() { + // update display based on processed report metadata + setWidgetTitle(); + vm.metricDocumentation = getMetricDocumentation(); + + // update data + var currentData = vm.responses[0]; + vm.metricValue = currentData[vm.metric] || 0; + + if (vm.responses[1]) { + vm.metricValueUnformatted = vm.responses[1][vm.metric]; + + var pastData = vm.responses[2]; + vm.pastValueUnformatted = pastData[vm.metric] || 0; + + var evolution = piwik.helper.calculateEvolution(vm.metricValueUnformatted, vm.pastValueUnformatted); + vm.metricChangePercent = (evolution * 100).toFixed(2) + ' %'; + + var pastDataFormatted = vm.responses[3]; + vm.pastValue = pastDataFormatted[vm.metric] || 0; + } else { + vm.pastValue = null; + vm.metricChangePercent = null; + } + + // don't change the metric translation until data is fetched to avoid loading state confusion + vm.metricTranslation = getMetricTranslation(); + } + + function getLastPeriodDate() { + var RangePeriod = piwikPeriods.get('range'); + var result = RangePeriod.getLastNRange(piwik.period, 2, piwik.currentDateString).startDate; + return $.datepicker.formatDate('yy-mm-dd', result); + } + + function setWidgetTitle() { + var title = vm.getMetricTranslation(); + if (vm.idGoal) { + var goalName = vm.goals[vm.idGoal].name; + title = goalName + ' - ' + title; + } + + $element.closest('div.widget').find('.widgetTop > .widgetName > span').text(title); + } + + function getCurrentPeriod() { + if (piwik.startDateString === piwik.endDateString) { + return piwik.endDateString; + } + return piwik.startDateString + ', ' + piwik.endDateString; + } + + function createSeriesPicker() { + vm.selectedColumns = [vm.idGoal ? ('goal' + vm.idGoal + '_' + vm.metric) : vm.metric]; + + var $widgetName = $element.closest('div.widget').find('.widgetTop > .widgetName'); + + var $seriesPicker = $('<piwik-series-picker class="single-metric-view-picker" multiselect="false" ' + + 'selectable-columns="$ctrl.selectableColumns" selectable-rows="[]" selected-columns="$ctrl.selectedColumns" ' + + 'selected-rows="[]" on-select="$ctrl.setMetric(columns[0])" />'); + + seriesPickerScope = $scope.$new(); + $compile($seriesPicker)(seriesPickerScope); + + $widgetName.append($seriesPicker); + } + + function destroySeriesPicker() { + $element.closest('div.widget').find('.single-metric-view-picker').remove(); + + seriesPickerScope.$destroy(); + seriesPickerScope = null; + } + + function getMetricDocumentation() { + if (!vm.metricDocumentations || !vm.metricDocumentations[vm.metric]) { + return ''; + } + + return vm.metricDocumentations[vm.metric]; + } + + function getMetricTranslation() { + if (!vm.metricTranslations || !vm.metricTranslations[vm.metric]) { + return ''; + } + + return vm.metricTranslations[vm.metric]; + } + + function setSelectableColumns() { + var result = []; + Object.keys(vm.metricTranslations).forEach(function (column) { + result.push({ column: column, translation: vm.metricTranslations[column] }); + }); + + Object.keys(vm.goals).forEach(function (idgoal) { + var goal = vm.goals[idgoal]; + vm.goalMetrics.forEach(function (column) { + result.push({ + column: 'goal' + goal.idgoal + '_' + column, + translation: goal.name + ' - ' + vm.metricTranslations[column] + }); + }); + }); + + vm.selectableColumns = result; + } + + function onMetricChanged() { + fetchData().then(recalculateValues); + + // notify widget of parameter change so it is replaced + $element.closest('[widgetId]').trigger('setParameters', { column: vm.metric, idGoal: vm.idGoal }); + } + + function setMetric(newColumn) { + var m = newColumn.match(/^goal([0-9])_(.*)/); + if (m) { + vm.idGoal = m[1]; + newColumn = m[2]; + } else { + vm.idGoal = undefined; + } + + vm.metric = newColumn; + onMetricChanged(); + } + + function getPastPeriodStr() { + var startDate = piwikPeriods.get('range').getLastNRange(piwik.period, 2, piwik.currentDateString).startDate; + var dateRange = piwikPeriods.get(piwik.period).parse(startDate).getDateRange(); + return $.datepicker.formatDate('yy-mm-dd', dateRange[0]) + ',' + $.datepicker.formatDate('yy-mm-dd', dateRange[1]); + } + } +})(); diff --git a/plugins/CoreVisualizations/angularjs/single-metric-view/single-metric-view.component.less b/plugins/CoreVisualizations/angularjs/single-metric-view/single-metric-view.component.less new file mode 100644 index 0000000000..f091b530e2 --- /dev/null +++ b/plugins/CoreVisualizations/angularjs/single-metric-view/single-metric-view.component.less @@ -0,0 +1,61 @@ +.singleMetricView { + margin: 5px 12px 10px; + display: inline-block; + + &.loading { + opacity: 0.5; + } + + .metric-value { + display: inline-block; + font-size: 14px; + line-height: 25px; + vertical-align: top; + margin-left: 3px; + } + + .metric-sparkline { + display: inline-block; + vertical-align: top; + img { + width: 100px; + height: 25px; + } + } + + .metricEvolution { + font-weight: bold; + font-size: 12px; + + > span { + display: inline-block; + + &:not(.positive-evolution):not(.negative-evolution) { + margin-left: 8px; + } + } + + .positive-evolution::before { + content: ""; + background: url(plugins/MultiSites/images/arrow_up.png) no-repeat center center; + display: inline-block; + height: 7px; + width: 12px; + } + .negative-evolution::before { + content: ""; + background: url(plugins/MultiSites/images/arrow_down.png) no-repeat center center; + display: inline-block; + height: 7px; + width: 12px; + } + } +} + +[piwik-single-metric-view] { + display: block; +} + +.single-metric-view-picker { + margin-left: 6px; +}
\ No newline at end of file diff --git a/plugins/Dashboard/javascripts/dashboardObject.js b/plugins/Dashboard/javascripts/dashboardObject.js index c72f53a8c9..cb603be3a6 100644 --- a/plugins/Dashboard/javascripts/dashboardObject.js +++ b/plugins/Dashboard/javascripts/dashboardObject.js @@ -160,7 +160,6 @@ */ addWidget: function (uniqueId, columnNumber, widgetParameters, addWidgetOnTop, isHidden) { addWidgetTemplate(uniqueId, columnNumber, widgetParameters, addWidgetOnTop, isHidden); - reloadWidget(uniqueId); saveLayout(); }, @@ -445,10 +444,14 @@ /** * Reloads the widget with the given uniqueId * - * @param {String} uniqueId + * @param {String|jQuery} $widget */ - function reloadWidget(uniqueId) { - $('[widgetId="' + uniqueId + '"]', dashboardElement).dashboardWidget('reload', false, true); + function reloadWidget($widget) { + if (typeof widget === 'string') { + $widget = $('[widgetId="' + uniqueId + '"]', dashboardElement); + } + + $widget.dashboardWidget('reload', false, true); } /** @@ -469,15 +472,15 @@ return; } - var widgetContent = '<div class="sortable" widgetId="' + uniqueId + '"></div>'; + var $widgetContent = $('<div class="sortable" widgetId="' + uniqueId + '"></div>'); if (addWidgetOnTop) { - $('> .col:nth-child(' + columnNumber + ')', dashboardElement).prepend(widgetContent); + $('> .col:nth-child(' + columnNumber + ')', dashboardElement).prepend($widgetContent); } else { - $('> .col:nth-child(' + columnNumber + ')', dashboardElement).append(widgetContent); + $('> .col:nth-child(' + columnNumber + ')', dashboardElement).append($widgetContent); } - $('[widgetId="' + uniqueId + '"]', dashboardElement).dashboardWidget({ + return $widgetContent.dashboardWidget({ uniqueId: uniqueId, widgetParameters: widgetParameters, onChange: function () { diff --git a/plugins/Dashboard/javascripts/dashboardWidget.js b/plugins/Dashboard/javascripts/dashboardWidget.js index 6756b1d291..22c0538471 100644 --- a/plugins/Dashboard/javascripts/dashboardWidget.js +++ b/plugins/Dashboard/javascripts/dashboardWidget.js @@ -117,6 +117,8 @@ var self = this, currentWidget = this.element; + $('.widgetContent', currentWidget).trigger('widget:reload'); + function onWidgetLoadedReplaceElementWithContent(loadedContent) { var $widgetContent = $('.widgetContent', currentWidget); @@ -133,7 +135,7 @@ if (currentWidget.parents('body').length) { // there might be race conditions, eg widget might be just refreshed while whole dashboard is also // removed from DOM - piwikHelper.compileAngularComponents($widgetContent); + piwikHelper.compileAngularComponents($widgetContent, { forceNewScope: true }); } $widgetContent.removeClass('loading'); $widgetContent.trigger('widget:create', [self]); diff --git a/plugins/Dashboard/javascripts/widgetMenu.js b/plugins/Dashboard/javascripts/widgetMenu.js index 465e8001f5..c847edeb70 100644 --- a/plugins/Dashboard/javascripts/widgetMenu.js +++ b/plugins/Dashboard/javascripts/widgetMenu.js @@ -327,9 +327,9 @@ widgetsHelper.loadWidgetAjax = function (widgetUniqueId, widgetParameters, onWid for (var j = 0; j < widgets.length; j++) { var widgetName = widgets[j]["name"]; var widgetUniqueId = widgets[j]["uniqueId"]; - // var widgetParameters = widgets[j]["parameters"]; + var widgetCategoryId = widgets[j].category ? widgets[j].category.id : null; var widgetClass = ''; - if (!settings.isWidgetAvailable(widgetUniqueId)) { + if (!settings.isWidgetAvailable(widgetUniqueId) && widgetCategoryId !== 'General_Generic') { widgetClass += ' ' + settings.unavailableClass; } @@ -408,7 +408,7 @@ widgetsHelper.loadWidgetAjax = function (widgetUniqueId, widgetParameters, onWid var emptyWidgetHtml = require('piwik/UI/Dashboard').WidgetFactory.make( widgetUniqueId, - $('<div/>') + $('<span/>') .attr('title', _pk_translate("Dashboard_AddPreviewedWidget")) .text(_pk_translate('Dashboard_WidgetPreview')) ); @@ -418,6 +418,7 @@ widgetsHelper.loadWidgetAjax = function (widgetUniqueId, widgetParameters, onWid var widgetElement = $(document.getElementById(widgetUniqueId)); // document.getElementById needed for widgets with uniqueid like widgetOpens+Contact+Form $('.widgetContent', widgetElement).html($(response)); + piwikHelper.compileAngularComponents($('.widgetContent', widgetElement), { forceNewScope: true }); $('.widgetContent', widgetElement).trigger('widget:create'); settings.onPreviewLoaded(widgetUniqueId, widgetElement); $('.' + settings.widgetpreviewClass + ' .widgetTop', widgetPreview).on('click', function () { diff --git a/plugins/Dashboard/tests/UI/Dashboard_spec.js b/plugins/Dashboard/tests/UI/Dashboard_spec.js index c929f6783e..79b7eb997d 100644 --- a/plugins/Dashboard/tests/UI/Dashboard_spec.js +++ b/plugins/Dashboard/tests/UI/Dashboard_spec.js @@ -52,7 +52,6 @@ describe("Dashboard", function () { [] ]; - // TODO: should probably include an async lib testEnvironment.callController("Dashboard.saveLayout", {name: 'D4', layout: JSON.stringify(layout), idDashboard: 5, idSite: 2}, function () { // reset default widget selection testEnvironment.callController("Dashboard.saveLayoutAsDefault", {layout: 0}, function () { diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_create_new.png b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_create_new.png index 58490a4567..6d8cb7adeb 100644 --- a/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_create_new.png +++ b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_create_new.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4cc5ac318571f225047a6ea8099a36a3eddf84160490455e4be0cb2f1b1fedfa -size 47474 +oid sha256:4eed2ccd11a0fd8867a9f83721d674cdcc80d3b3fafaa3e28e237201480e0e65 +size 47578 diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_expanded.png b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_expanded.png index 5f26293303..39a1d09d71 100644 --- a/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_expanded.png +++ b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_expanded.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4fd2c5c78884be0cfd6c7bf620dc2f7aa5f82bea9de34a65fae33ef0ddadc5d8 -size 46904 +oid sha256:f581b26a963cf0b0318ef6277122a3c433803bf9a47c419085f491d27a9f0d0c +size 47830 diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_removed.png b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_removed.png index 4910980871..28a728eb9a 100644 --- a/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_removed.png +++ b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_removed.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ba89b3a3e64b84fd716d852a1b7c87b13328f7b8722ec67b0ff3f23a63a108c -size 429615 +oid sha256:6b3381ae6c02a78d3d04192425e289e2cc6d06c5422f2536b48326ac43ffaff7 +size 415347 diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_widget_list_shown.png b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_widget_list_shown.png index e015fafe22..800e666235 100644 --- a/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_widget_list_shown.png +++ b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_widget_list_shown.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2692f5b18e1643264b616159b4a8043fd4b33cfee9abeb89bfcaef19290677b7 -size 57021 +oid sha256:0189c5b0b15e78e0d575180b721e873fc17c441bf5a3418a2afa5068fd206e20 +size 57925 diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_widget_preview.png b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_widget_preview.png index 40665bf6a7..e8caefddbd 100644 --- a/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_widget_preview.png +++ b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_widget_preview.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:22a795cbcbbe4600add04a010b32d8bf6fc4ae353bc31e643b474a2e68ba327e -size 68744 +oid sha256:76d374477ef2b3898b21b764bab479bb0f297efa1d068b28d2fe7eaf43c8903f +size 69442 diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_loaded_token_auth.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_loaded_token_auth.png index 6d51d33409..48f26c8a2d 100644 --- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_loaded_token_auth.png +++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_loaded_token_auth.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e24de3ee34581a370f0e7b91e079d25ca411c0897964525551156259862bf3d5 -size 624303 +oid sha256:6cd315980242867325c54b263aa94c3fbd453ae93f23873c3f1d1e8bded128eb +size 616084 diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_removed.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_removed.png index 5d8f810e97..159b8b5b30 100644 --- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_removed.png +++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_removed.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4148a72a37f310fce630d18b5dd1dea6012afc27751bd8f954db37c432917e70 -size 624066 +oid sha256:cb7a5493c40df8442fee1b9119ef4a08b1bb9c5cb61e508d72edd46534e3c07b +size 615793 diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_reset.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_reset.png index 7b237f9bb3..58769a7eb8 100644 --- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_reset.png +++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:587356c3aa5d29f3ec2e719ae9eaabf1c9db176ffa7322f2af4e81d03a8a6066 -size 252483 +oid sha256:391fc9177fd7effde0c8c66d83d2570f5d84cbb0d58687f883843f26d9b905d4 +size 252398 diff --git a/plugins/Goals/API.php b/plugins/Goals/API.php index 5d091aa8a4..072f1b9b8e 100644 --- a/plugins/Goals/API.php +++ b/plugins/Goals/API.php @@ -427,7 +427,7 @@ class API extends \Piwik\Plugin\API 'date' => $date, 'idGoal' => $idGoal, 'columns' => $columns, - 'format_metrics' => 'bc' + 'format_metrics' => Common::getRequestVar('format_metrics', 'bc'), )); $tableSegmented->filter('Piwik\Plugins\Goals\DataTable\Filter\AppendNameToColumnNames', diff --git a/plugins/Morpheus/javascripts/piwikHelper.js b/plugins/Morpheus/javascripts/piwikHelper.js index dc8b27f50b..685c967d46 100644 --- a/plugins/Morpheus/javascripts/piwikHelper.js +++ b/plugins/Morpheus/javascripts/piwikHelper.js @@ -165,7 +165,7 @@ var piwikHelper = { var scope = null; if (options.scope) { scope = options.scope; - } else { + } else if (!options.forceNewScope) { // TODO: docs scope = angular.element($element).scope(); } if (!scope) { @@ -552,6 +552,19 @@ var piwikHelper = { piwikHelper.shortcuts[key] = description; Mousetrap.bind(key, callback); + }, + + calculateEvolution: function (currentValue, pastValue) { + var dividend = currentValue - pastValue; + var divisor = pastValue; + + if (dividend == 0) { + return 0; + } else if (divisor == 0) { + return 1; + } else { + return Math.round((dividend / divisor) * 1000) / 1000; + } } }; diff --git a/tests/PHPUnit/Integration/Category/CategoryListTest.php b/tests/PHPUnit/Integration/Category/CategoryListTest.php index 1a663c1918..9adf91d00d 100644 --- a/tests/PHPUnit/Integration/Category/CategoryListTest.php +++ b/tests/PHPUnit/Integration/Category/CategoryListTest.php @@ -35,6 +35,7 @@ class CategoryListTest extends IntegrationTestCase $this->assertSame(array( 'General_Actions', + 'General_Generic', 'General_Visitors', 'Dashboard_Dashboard', 'General_MultiSitesSummary', diff --git a/tests/PHPUnit/Integration/WidgetsListTest.php b/tests/PHPUnit/Integration/WidgetsListTest.php index f775b60dfa..92658fb48a 100644 --- a/tests/PHPUnit/Integration/WidgetsListTest.php +++ b/tests/PHPUnit/Integration/WidgetsListTest.php @@ -42,6 +42,7 @@ class WidgetsListTest extends IntegrationTestCase $numberOfWidgets = array( 'Dashboard_Dashboard' => 1, 'General_Actions' => 15, + 'General_Generic' => 1, 'General_Visitors' => 35, 'SEO' => 2, 'Goals_Goals' => 3, @@ -91,7 +92,7 @@ class WidgetsListTest extends IntegrationTestCase $perCategory = $this->getWidgetsPerCategory(WidgetsList::get()); // number of main categories - $this->assertEquals(10, count($perCategory)); + $this->assertEquals(11, count($perCategory)); $this->assertEquals($initialGoalsWidgets + 2, count($perCategory['Goals_Goals'])); // make sure widgets for that goal were added } @@ -105,7 +106,7 @@ class WidgetsListTest extends IntegrationTestCase $perCategory = $this->getWidgetsPerCategory(WidgetsList::get()); // number of main categories - $this->assertEquals(11, count($perCategory)); + $this->assertEquals(12, count($perCategory)); // check if each category has the right number of widgets $numberOfWidgets = array( @@ -127,12 +128,12 @@ class WidgetsListTest extends IntegrationTestCase $list = WidgetsList::get(); - $this->assertCount(11, $this->getWidgetsPerCategory($list)); + $this->assertCount(12, $this->getWidgetsPerCategory($list)); $list->remove('SEO', 'NoTeXiStInG'); $perCategory = $this->getWidgetsPerCategory($list); - $this->assertCount(11, $perCategory); + $this->assertCount(12, $perCategory); $this->assertArrayHasKey('SEO', $perCategory); $this->assertCount(2, $perCategory['SEO']); diff --git a/tests/PHPUnit/System/expected/test_apiGetReportMetadata__API.getWidgetMetadata.xml b/tests/PHPUnit/System/expected/test_apiGetReportMetadata__API.getWidgetMetadata.xml index a9d6028b21..3001779c4c 100644 --- a/tests/PHPUnit/System/expected/test_apiGetReportMetadata__API.getWidgetMetadata.xml +++ b/tests/PHPUnit/System/expected/test_apiGetReportMetadata__API.getWidgetMetadata.xml @@ -2998,6 +2998,26 @@ </widgets> </row> <row> + <name>Metric</name> + <category> + <id>General_Generic</id> + <name>Generic</name> + <order>70</order> + <icon /> + </category> + <subcategory /> + <module>CoreVisualizations</module> + <action>singleMetricView</action> + <order>99</order> + <parameters> + <module>CoreVisualizations</module> + <action>singleMetricView</action> + <column /> + </parameters> + <uniqueId>widgetCoreVisualizationssingleMetricViewcolumn</uniqueId> + <isWide>0</isWide> + </row> + <row> <name>Pie graph</name> <category> <id>ExampleUI_UiFramework</id> diff --git a/tests/UI/expected-screenshots/EmptySite_emptySiteDashboard_ignored.png b/tests/UI/expected-screenshots/EmptySite_emptySiteDashboard_ignored.png index 24c95541b5..1f9ba7033a 100644 --- a/tests/UI/expected-screenshots/EmptySite_emptySiteDashboard_ignored.png +++ b/tests/UI/expected-screenshots/EmptySite_emptySiteDashboard_ignored.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:40820c2899444f4032ce07bbb9eec646c6ccbe096651e93812fb482898bf2e7b -size 334580 +oid sha256:a70c48449966095dd010775096d364a43ca8c5da932ac770b9799dd46799c098 +size 325028 diff --git a/tests/UI/expected-screenshots/Theme_home.png b/tests/UI/expected-screenshots/Theme_home.png index 0d4cb8bf90..d3081f7378 100644 --- a/tests/UI/expected-screenshots/Theme_home.png +++ b/tests/UI/expected-screenshots/Theme_home.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3806904114450a57479a3f705d252d69e1a441025eb4dc767e829ea0495e45a9 -size 635944 +oid sha256:68db279e2f011563cf4bb50a1020520ea2d993b86917cbb73c35a35cde490243 +size 619743 diff --git a/tests/UI/expected-screenshots/UIIntegrationTest_admin_diagnostics_configfile.png b/tests/UI/expected-screenshots/UIIntegrationTest_admin_diagnostics_configfile.png index fcb1929342..a090cbfaa5 100644 --- a/tests/UI/expected-screenshots/UIIntegrationTest_admin_diagnostics_configfile.png +++ b/tests/UI/expected-screenshots/UIIntegrationTest_admin_diagnostics_configfile.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9135dfd13e06c4e6317e3d2660924e7a1dbc6dc73a9d2e0f684d4feecc159929 -size 3939232 +oid sha256:24aa51619de3fa59baa671e9a1df7e7c2e7a6459777f87836a464c4d50c5362b +size 4009728 diff --git a/tests/UI/expected-screenshots/UIIntegrationTest_admin_plugins.png b/tests/UI/expected-screenshots/UIIntegrationTest_admin_plugins.png index e2dcd20c2e..71cc492140 100644 --- a/tests/UI/expected-screenshots/UIIntegrationTest_admin_plugins.png +++ b/tests/UI/expected-screenshots/UIIntegrationTest_admin_plugins.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a3c13f917f3dd4719c3384440f0ffbd843820295e3d5d4a4589b53c49a0bef3a -size 1026966 +oid sha256:5eed1b04841b1aeb985d5de66ac36527fc0209da9d11b3403dca30b5c11be02c +size 1028028 diff --git a/tests/UI/expected-screenshots/UIIntegrationTest_admin_plugins_no_internet.png b/tests/UI/expected-screenshots/UIIntegrationTest_admin_plugins_no_internet.png index 5d6d7475c1..ff0100ba35 100644 --- a/tests/UI/expected-screenshots/UIIntegrationTest_admin_plugins_no_internet.png +++ b/tests/UI/expected-screenshots/UIIntegrationTest_admin_plugins_no_internet.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:36973a63d9f611e245f8fc008f05e8d0087568e3333797a0fd110c12482f4ce1 -size 1026395 +oid sha256:ce3e381230d857d7928d8e13e3c2e712375ad615471d534374be314ca458186f +size 1027505 diff --git a/tests/UI/expected-screenshots/UIIntegrationTest_admin_settings_general.png b/tests/UI/expected-screenshots/UIIntegrationTest_admin_settings_general.png index 5cba62a8cc..422f4334f6 100644 --- a/tests/UI/expected-screenshots/UIIntegrationTest_admin_settings_general.png +++ b/tests/UI/expected-screenshots/UIIntegrationTest_admin_settings_general.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:211305b8120858bbfe934dc9dfa9a4a1bafe8acb71cdc0b362503c7e423ca20f -size 739196 +oid sha256:7b8a3607abdd6c5086e0593c28fe4a861c4b3dba17572244d969e4eb8ec60662 +size 796811 diff --git a/tests/UI/expected-screenshots/UIIntegrationTest_dashboard1.png b/tests/UI/expected-screenshots/UIIntegrationTest_dashboard1.png index dbf125ee72..bb95db9a58 100644 --- a/tests/UI/expected-screenshots/UIIntegrationTest_dashboard1.png +++ b/tests/UI/expected-screenshots/UIIntegrationTest_dashboard1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3201441f000c294b9abf3c12a61953acddc90097bbf65cb5e76d3784681e34f4 -size 587779 +oid sha256:083aa10f4b121cd5ef0656a315b78647d17d9ec2cc7275fd4df25c3064bac4a4 +size 569781 diff --git a/tests/UI/expected-screenshots/UIIntegrationTest_dashboard2.png b/tests/UI/expected-screenshots/UIIntegrationTest_dashboard2.png index 44257d964d..3b3428fc28 100644 --- a/tests/UI/expected-screenshots/UIIntegrationTest_dashboard2.png +++ b/tests/UI/expected-screenshots/UIIntegrationTest_dashboard2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1cee56aa2d6879d8c934efac917b243e86e13db53a0ceebe347bf10a60676ff6 -size 1527381 +oid sha256:aa54cdc4eb158a1eb174407a0a3f9cf3bb2fbf0fa2549ee99c8f43bfad0d2b5a +size 1510200 diff --git a/tests/UI/expected-screenshots/UIIntegrationTest_dashboard3.png b/tests/UI/expected-screenshots/UIIntegrationTest_dashboard3.png index 1178965c86..55e42c75b6 100644 --- a/tests/UI/expected-screenshots/UIIntegrationTest_dashboard3.png +++ b/tests/UI/expected-screenshots/UIIntegrationTest_dashboard3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f07b1425e746481c9715c441935b814f7cfd47b768b7d6dc0d27440e49447df8 -size 511392 +oid sha256:50edcaec01cb532d20168b518aaf5b494bbc6873379830c9a7edfa52f507e8d1 +size 540096 diff --git a/tests/UI/expected-screenshots/UIIntegrationTest_dashboard4.png b/tests/UI/expected-screenshots/UIIntegrationTest_dashboard4.png index 9649ad72f1..544fecfcbf 100644 --- a/tests/UI/expected-screenshots/UIIntegrationTest_dashboard4.png +++ b/tests/UI/expected-screenshots/UIIntegrationTest_dashboard4.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b349a6a50a60880a23abe83acbeb42c46e70b733493e8fc14889bad83006aadc -size 289658 +oid sha256:3952943bd7d4822ebf480a447460d2b39b85ad2be8e2a2166d718a7ad23ee18f +size 297681 diff --git a/tests/UI/expected-screenshots/UIIntegrationTest_widgets_listing.png b/tests/UI/expected-screenshots/UIIntegrationTest_widgets_listing.png index 5b91cab99c..48af2fca63 100644 --- a/tests/UI/expected-screenshots/UIIntegrationTest_widgets_listing.png +++ b/tests/UI/expected-screenshots/UIIntegrationTest_widgets_listing.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af6c68a115d937e62cb6133f3978e003a2ef0c6b9d43202c3964d086ed8028f4 -size 184981 +oid sha256:7e63927e6f0a64a307ba38ec05d1dc8bd84a42537bdac307ba3e9552599235de +size 185756 |