Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
be396ae
adding display of selected reports
chippison Dec 21, 2025
480a0c0
adding title and subtitle for selected reports for translation
chippison Dec 21, 2025
769888e
persisting order of reports after drag and drop
chippison Dec 21, 2025
8bfd644
adding css
chippison Dec 21, 2025
79ea46f
removed the handle property so that we can use the whole li element f…
chippison Dec 21, 2025
e1b618b
Making selected reports list be its own vue component
chippison Dec 22, 2025
61f21e4
making generate report API create/add reports based on its order from…
chippison Dec 22, 2025
dc8bb90
adding correct texts for scheduled reports
chippison Dec 23, 2025
d7b23f5
making scheduled reports show notification after the refresh
chippison Dec 23, 2025
eb75431
commit built file
chippison Dec 23, 2025
3558046
added an icon to indicate the li was draggable
chippison Dec 23, 2025
dfc04ec
Add new tests for creating and updating scheduled reports; mostly for…
chippison Dec 23, 2025
7db4ccc
Adding parameter to enforce order; this will only be present on new s…
chippison Dec 24, 2025
313926a
adding new screenshots for testing; also refactored duplicated code w…
chippison Dec 24, 2025
5d33065
removed not needed ui tests; by reordering the list and testing persi…
chippison Dec 28, 2025
14d7d38
adding new screenshot for UIIntegrationTest_email_reports_editor.png;…
chippison Dec 29, 2025
c5b6ea1
adding a test that new parameter 'enforceOrder' is set to false by de…
chippison Dec 29, 2025
51bc2ee
transferring where we add enforceOrder parameter to get it from the s…
chippison Dec 29, 2025
2b46ebc
Add interfaces and comments to some variables so that its easier to u…
chippison Dec 29, 2025
1025e6e
cosmetic chnages to sortable selected reports
chippison Dec 30, 2025
5a1ca07
made sure that the selected reports section shows order based on cate…
chippison Dec 30, 2025
08acd09
adding new screenshots based on new changes
chippison Dec 31, 2025
5860435
removed unused code
chippison Jan 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions plugins/ScheduledReports/API.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
*/
class API extends \Piwik\Plugin\API
{
public const ENFORCE_ORDER_PARAMETER = ScheduledReports::ENFORCE_ORDER_PARAMETER;

public const VALIDATE_PARAMETERS_EVENT = 'ScheduledReports.validateReportParameters';
public const GET_REPORT_PARAMETERS_EVENT = 'ScheduledReports.getReportParameters';
public const GET_REPORT_METADATA_EVENT = 'ScheduledReports.getReportMetadata';
Expand Down Expand Up @@ -407,6 +409,10 @@ public function generateReport(
self::validateReportParameters($reportType, empty($parameters) ? $report['parameters'] : $parameters),
true
);
$parameters = $report['parameters'];
$enforceCustomOrder = is_array($parameters)
&& array_key_exists(self::ENFORCE_ORDER_PARAMETER, $parameters)
&& !empty($parameters[self::ENFORCE_ORDER_PARAMETER]);

$originalShowEvolutionWithinSelectedPeriod = Config::getInstance()->General['graphs_show_evolution_within_selected_period'];
$originalDefaultEvolutionGraphLastPeriodsAmount = Config::getInstance()->General['graphs_default_evolution_graph_last_days_amount'];
Expand All @@ -418,11 +424,26 @@ public function generateReport(
// available reports
$availableReportMetadata = \Piwik\Plugins\API\API::getInstance()->getReportMetadata($idSite);

// we need to lookup which reports metadata are registered in this report
$reportMetadata = [];
foreach ($availableReportMetadata as $metadata) {
if (in_array($metadata['uniqueId'], $report['reports'])) {
$reportMetadata[] = $metadata;
if ($enforceCustomOrder) {
// we need to lookup which reports metadata are registered in this report
// and keep the order defined
$reportMetadataByUniqueId = [];
foreach ($availableReportMetadata as $metadata) {
$reportMetadataByUniqueId[$metadata['uniqueId']] = $metadata;
}

foreach ($report['reports'] as $reportUniqueId) {
if (isset($reportMetadataByUniqueId[$reportUniqueId])) {
$reportMetadata[] = $reportMetadataByUniqueId[$reportUniqueId];
}
}
} else {
// fallback to default metadata order when the flag isn't set
foreach ($availableReportMetadata as $metadata) {
if (in_array($metadata['uniqueId'], $report['reports'], true)) {
$reportMetadata[] = $metadata;
}
}
}

Expand Down
12 changes: 12 additions & 0 deletions plugins/ScheduledReports/ScheduledReports.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@ class ScheduledReports extends \Piwik\Plugin
public const EVOLUTION_GRAPH_PARAMETER = 'evolutionGraph';
public const ADDITIONAL_EMAILS_PARAMETER = 'additionalEmails';
public const DISPLAY_FORMAT_PARAMETER = 'displayFormat';
public const ENFORCE_ORDER_PARAMETER = 'enforceOrder';
public const EMAIL_ME_PARAMETER_DEFAULT_VALUE = true;
public const EVOLUTION_GRAPH_PARAMETER_DEFAULT_VALUE = false;
public const ENFORCE_ORDER_PARAMETER_DEFAULT_VALUE = false;

public const EMAIL_TYPE = 'email';

Expand All @@ -55,6 +57,7 @@ class ScheduledReports extends \Piwik\Plugin
self::EVOLUTION_GRAPH_PARAMETER => false,
self::ADDITIONAL_EMAILS_PARAMETER => false,
self::DISPLAY_FORMAT_PARAMETER => true,
self::ENFORCE_ORDER_PARAMETER => false,
);

private static $managedReportTypes = array(
Expand Down Expand Up @@ -156,6 +159,9 @@ public function getClientSideTranslationKeys(&$translationKeys)
$translationKeys[] = 'ScheduledReports_AlsoSendReportToTheseEmails';
$translationKeys[] = 'ScheduledReports_ReportSchedule';
$translationKeys[] = 'ScheduledReports_SendingReport';
$translationKeys[] = 'ScheduledReports_SelectedReports';
$translationKeys[] = 'ScheduledReports_SelectedReportsHelp';
$translationKeys[] = "ScheduledReports_ReportAdded";
}

/**
Expand Down Expand Up @@ -216,6 +222,12 @@ public function validateReportParameters(&$parameters, $reportType)
if (isset($parameters[self::ADDITIONAL_EMAILS_PARAMETER])) {
$parameters[self::ADDITIONAL_EMAILS_PARAMETER] = self::checkAdditionalEmails($parameters[self::ADDITIONAL_EMAILS_PARAMETER]);
}

if (!isset($parameters[self::ENFORCE_ORDER_PARAMETER])) {
$parameters[self::ENFORCE_ORDER_PARAMETER] = self::ENFORCE_ORDER_PARAMETER_DEFAULT_VALUE;
} else {
$parameters[self::ENFORCE_ORDER_PARAMETER] = self::valueIsTrue($parameters[self::ENFORCE_ORDER_PARAMETER]);
}
}

// based on https://www.php.net/manual/en/filter.filters.validate.php -> FILTER_VALIDATE_BOOLEAN
Expand Down
7 changes: 5 additions & 2 deletions plugins/ScheduledReports/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,12 @@
"ReportHourWithUTC": "%s o'clock UTC",
"ReportIncludeNWebsites": "The report will include main metrics for all websites that have at least one visit (from the %s websites currently available).",
"ReportSent": "Report sent",
"ReportsIncluded": "Statistics included",
"ReportsIncluded": "Select the reports to include",
"SelectedReports": "Preview of your report",
"SelectedReportsHelp": "Drag and drop to re-order the report sections.",
"ReportType": "Send report via",
"ReportUpdated": "Report updated",
"ReportAdded": "Report added",
"Segment_Deletion_Error": "This segment cannot be deleted or made invisible to other users because it is used to generate email report(s) %s. Please retry after removing this segment from this report(s).",
"Segment_HelpScheduledReport": "You can select an existing custom segment to apply to data in this scheduled report. You may create and edit custom segments in your dashboard %1$s(click here to open)%2$s, then click on the \"%3$s\" box, then \"%4$s\".",
"SegmentAppliedToReports": "The segment '%s' is applied to the reports.",
Expand Down Expand Up @@ -62,4 +65,4 @@
"ReportSchedule": "Report Schedule",
"SendingReport": "Sending report…"
}
}
}
49 changes: 49 additions & 0 deletions plugins/ScheduledReports/stylesheets/scheduledreports.less
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,52 @@
}
}
}
.selectedReportsWrapper {
font-size: 1rem;
margin-top: 1.5rem;
margin-bottom: 2.5rem;

.selectedReportsHeading {
margin-bottom: 0.4rem;
}

.selectedReportsHelp {
margin-top: 0;
color: @color-silver;
}

.selectedReportsList {
list-style: none;
padding: 0;
margin: 0;

li {
display: flex;
align-items: center;
padding: 0.6rem 0.9rem;
border: 1px solid @color-silver-l85;
border-radius: 4px;
background: @color-white;
cursor: move;
margin-bottom: 0.6rem;

span.drag-icon {
padding-right: 15px;
font-size: 13px;
color: @color-silver-l60;
}
}

.selectedReportName {
flex: 1;
}

.selectedReportPlaceholder {
border: 1px dashed @color-silver;
border-radius: 4px;
background: @color-silver-l95;
height: 2.6rem;
margin-bottom: 0.6rem;
}
}
}
14 changes: 14 additions & 0 deletions plugins/ScheduledReports/tests/Integration/ApiTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ public function testAddReportGetReports()
'emailMe' => true,
'additionalEmails' => array('test@test.com', 't2@test.com'),
'evolutionGraph' => true,
'enforceOrder' => true,
),
);

Expand Down Expand Up @@ -208,6 +209,18 @@ public function testAddReportGetReports()
$this->assertReportsEqual($report, $data);
}

public function testAddReportDefaultsEnforceOrderToFalse()
{
$data = self::getDailyPDFReportData($this->idSite);
$idReport = self::addReport($data);

$reports = APIScheduledReports::getInstance()->getReports($this->idSite, $data['period'], $idReport);
$report = reset($reports);

$this->assertArrayHasKey('enforceOrder', $report['parameters']);
$this->assertFalse($report['parameters']['enforceOrder']);
}

/**
* @group Plugins
*/
Expand Down Expand Up @@ -1004,6 +1017,7 @@ private static function getMonthlyEmailReportData($idSite)
'emailMe' => false,
'additionalEmails' => array('blabla@ec.fr'),
'evolutionGraph' => false,
'enforceOrder' => true,
),
);
}
Expand Down
93 changes: 93 additions & 0 deletions plugins/ScheduledReports/tests/UI/ScheduledReports_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,97 @@ describe("ScheduledReports", function () {

expect(await page.screenshot({ fullPage: true })).to.matchImage('invalid_token');
});

describe('ManageScheduledReports', function () {
const manageReportsUrl = "?module=ScheduledReports&action=index&idSite=1&period=day&date=2013-01-23";
const createdReportName = "for testing";

// Helper function to open the report we will use for testing
async function openReportForTesting() {
await page.evaluate((description) => {
const rows = Array.from(document.querySelectorAll('#entityEditContainer tbody tr'));

for (const row of rows) {
if (!row.textContent || row.textContent.indexOf(description) === -1) {
continue;
}
const editButton = row.querySelector('button[title="Edit"]');
editButton.click();
}
}, createdReportName);

await page.waitForSelector('#addEditReport', { visible: true });
await page.waitForSelector('.selectedReportsList li', { visible: true });
}

it("should show selected reports when creating a new report", async function () {
await page.goto(manageReportsUrl);
await page.waitForNetworkIdle();

await page.waitForSelector('#add-report');
await page.click('#add-report');
await page.waitForSelector('#addEditReport', { visible: true });

const reportCheckboxes = await page.$$(
'div[name="reportsList"]:not([style*="display: none"]) .listReports input[type="checkbox"]',
);

const selectedReportIds = [];
// Click the first 4 checkboxes
for (const checkbox of reportCheckboxes.slice(0, 4)) {
await checkbox.click();
const uniqueId = await checkbox.evaluate((input) => input.id );
if (uniqueId) {
selectedReportIds.push(uniqueId);
}
}
const selectedReportsWrapper = await page.$('.selectedReportsWrapper');
expect(await selectedReportsWrapper.screenshot()).to.matchImage('selected_reports');
});

it("should persist manually reordered selected reports when saving a report", async function () {
await openReportForTesting();

const initialOrder = await page.$$eval(
'.selectedReportsList li',
(items) => items.map((item) => item.getAttribute('data-unique-id')),
);
const expectedOrder = initialOrder.slice().reverse();

// Reorder the selected reports via DOM manipulation
await page.evaluate((newOrder) => {
const list = document.querySelector('.selectedReportsList');
newOrder.forEach((uniqueId) => {
const item = list.querySelector(`li[data-unique-id="${uniqueId}"]`);
if (item) {
list.appendChild(item);
}
});

const jq = window.jQuery || window.$;
const $list = jq('.selectedReportsList');
// Get sortable instance and call stop handler manually,
// so that we simulate the emitted reorder event
const stopHandler = $list.sortable('option', 'stop');
Copy link
Member

Choose a reason for hiding this comment

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

We should avoid using jQuery UI where possible. The short / mid term plan is to fully remove it, not to add more usage of it. See #16033

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the info on this @sgiehl.
I'll not add in new jquery code in the future

Copy link
Contributor

Choose a reason for hiding this comment

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

Interesting info.
It's noted on my side too! 👍

if (typeof stopHandler === 'function') {
stopHandler();
}
}, expectedOrder);

await page.click('.matomo-save-button .btn');
await page.waitForNetworkIdle();
await page.waitForTimeout(500);

await openReportForTesting();
const persistedOrder = await page.$$eval(
'.selectedReportsList li',
(items) => items.map((item) => item.getAttribute('data-unique-id')),
);

expect(persistedOrder).to.deep.equal(expectedOrder);

const selectedReportsWrapper = await page.$('.selectedReportsWrapper');
expect(await selectedReportsWrapper.screenshot()).to.matchImage('reorder_persisted');
});
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading