Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.46% covered (success)
98.46%
384 / 390
71.43% covered (warning)
71.43%
5 / 7
CRAP
n/a
0 / 0
Automattic\Jetpack\PremiumAnalytics\get_dashboard_default_layout_for
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
Automattic\Jetpack\PremiumAnalytics\get_dashboard_default_layout_response
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
Automattic\Jetpack\PremiumAnalytics\register_dashboard_default_layout_route
94.12% covered (success)
94.12%
16 / 17
0.00% covered (danger)
0.00%
0 / 1
1.00
Automattic\Jetpack\PremiumAnalytics\get_dashboard_default_widget_instance
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
Automattic\Jetpack\PremiumAnalytics\get_dashboard_default_section_layouts
100.00% covered (success)
100.00%
328 / 328
100.00% covered (success)
100.00%
1 / 1
1
Automattic\Jetpack\PremiumAnalytics\get_dashboard_default_section_id_for
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
Automattic\Jetpack\PremiumAnalytics\seed_default_dashboard_layout
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2/**
3 * Dashboard Layout: Premium Analytics server-side defaults.
4 *
5 * Premium Analytics owns its dashboard, so it ships its own default layout
6 * rather than relying on the core dashboard endpoint (which is Gutenberg-only
7 * and returns the core dashboard's widgets). The default is served on the
8 * `dashboardSection` REST shape and a per-dashboard `default-layout` route; the
9 * frontend reads it from the section entity, so there is no server-seeded
10 * preference.
11 *
12 * @package automattic/jetpack-premium-analytics
13 */
14
15namespace Automattic\Jetpack\PremiumAnalytics;
16
17require_once __DIR__ . '/dashboard-grammar.php';
18require_once __DIR__ . '/rest-namespace.php';
19// Availability policy for default layout instances: defaults are read outside
20// the widget registry bootstrap, so the policy must be loaded here explicitly.
21require_once __DIR__ . '/widget-type-support.php';
22
23/**
24 * Identifier of the Premium Analytics dashboard, formatted as `<plugin>_<page>`
25 * to match the underscore form produced by the wp-build pipeline. Used as the
26 * `{name}` segment of the REST route.
27 */
28const DASHBOARD_NAME = 'jetpack-premium-analytics_dashboard';
29
30/**
31 * Filter through which the default layout for a dashboard is resolved.
32 */
33const DASHBOARD_DEFAULT_LAYOUT_FILTER = 'jetpack_premium_analytics_dashboard_default_layout';
34
35/**
36 * Preference keys used by the dashboard route's tabbed sections.
37 */
38const DASHBOARD_TRAFFIC_SECTION_ID     = 'traffic';
39const DASHBOARD_INSIGHTS_SECTION_ID    = 'insights';
40const DASHBOARD_SUBSCRIBERS_SECTION_ID = 'subscribers';
41const DASHBOARD_STORE_SECTION_ID       = 'store';
42
43/**
44 * Resolves the default layout registered for a dashboard.
45 *
46 * Returns a fresh evaluation of the filter chain each call, so callers always
47 * see the current code default rather than a hydrated copy.
48 *
49 * @param string $dashboard_name Identifier of the dashboard or dashboard section.
50 * @return array Array of widget instances (possibly empty).
51 */
52function get_dashboard_default_layout_for( $dashboard_name ) {
53    /**
54     * Filters the default dashboard layout served to users who have not
55     * customized theirs.
56     *
57     * Each entry should match the dashboard's widget instance shape: `uuid`,
58     * `type`, optional `attributes`, optional `placement`.
59     *
60     * @param array  $default_layout Default array of widget instances.
61     * @param string $dashboard_name Identifier of the dashboard or dashboard
62     *                               section receiving the default. Callbacks
63     *                               targeting a specific default should switch
64     *                               on this value.
65     */
66    $default = apply_filters( DASHBOARD_DEFAULT_LAYOUT_FILTER, array(), $dashboard_name );
67
68    return remove_unsupported_widget_items(
69        is_array( $default ) ? array_values( $default ) : array(),
70        'type',
71        get_widget_support_context()
72    );
73}
74
75/**
76 * REST callback returning the default layout for the requested dashboard.
77 *
78 * @param \WP_REST_Request $request REST request carrying the dashboard name.
79 * @return \WP_REST_Response Response wrapping the default layout array.
80 */
81function get_dashboard_default_layout_response( $request ) {
82    return rest_ensure_response( get_dashboard_default_layout_for( $request['name'] ) );
83}
84
85/**
86 * Registers the REST route that exposes per-dashboard default layouts.
87 *
88 * @return void
89 */
90function register_dashboard_default_layout_route() {
91    register_rest_route(
92        DASHBOARD_REST_NAMESPACE,
93        '/dashboards/(?P<name>' . get_dashboard_name_pattern() . ')/default-layout',
94        array(
95            'methods'             => \WP_REST_Server::READABLE,
96            'callback'            => __NAMESPACE__ . '\\get_dashboard_default_layout_response',
97            'permission_callback' => static function () {
98                return current_user_can( 'manage_options' );
99            },
100            'args'                => array(
101                'name' => array(
102                    'description' => __( 'Dashboard identifier as produced by the build pipeline.', 'jetpack-premium-analytics' ),
103                    'type'        => 'string',
104                ),
105            ),
106        )
107    );
108}
109
110/**
111 * Builds a widget instance for bundled dashboard defaults.
112 *
113 * @param string $uuid       Widget instance UUID.
114 * @param string $type       Widget type.
115 * @param int    $order      Widget placement order.
116 * @param int    $width      Widget placement width.
117 * @param int    $height     Widget placement height.
118 * @param array  $attributes Optional widget attributes.
119 * @return array Widget instance.
120 */
121function get_dashboard_default_widget_instance(
122    $uuid,
123    $type,
124    $order,
125    $width = 1,
126    $height = 1,
127    $attributes = array()
128) {
129    $widget = array(
130        'uuid' => $uuid,
131        'type' => $type,
132    );
133
134    if ( ! empty( $attributes ) ) {
135        $widget['attributes'] = $attributes;
136    }
137
138    $widget['placement'] = array(
139        'width'  => $width,
140        'height' => $height,
141        'order'  => $order,
142    );
143
144    return $widget;
145}
146
147/**
148 * Returns the bundled default widget layouts keyed by dashboard tab.
149 *
150 * @return array Map of tab IDs to widget layout arrays.
151 */
152function get_dashboard_default_section_layouts() {
153    return array(
154        DASHBOARD_TRAFFIC_SECTION_ID     => array(
155            get_dashboard_default_widget_instance(
156                'default-traffic-chart-widget-instance',
157                'jpa/traffic-chart',
158                0,
159                2,
160                2,
161                array(
162                    'granularity' => 'auto',
163                )
164            ),
165            get_dashboard_default_widget_instance(
166                'default-stats-top-posts-widget-instance',
167                'jpa/stats-top-posts',
168                1,
169                1,
170                2,
171                array(
172                    'max' => 10,
173                )
174            ),
175            get_dashboard_default_widget_instance(
176                'default-referrers-widget-instance',
177                'jpa/referrers',
178                2,
179                1,
180                2,
181                array(
182                    'max' => 10,
183                )
184            ),
185            get_dashboard_default_widget_instance(
186                'default-locations-widget-instance',
187                'jpa/locations',
188                3,
189                2,
190                2,
191                array(
192                    'max' => 10,
193                )
194            ),
195            get_dashboard_default_widget_instance(
196                'default-devices-widget-instance',
197                'jpa/devices',
198                4,
199                1,
200                2,
201                array(
202                    'max' => 5,
203                )
204            ),
205            get_dashboard_default_widget_instance(
206                'default-top-platforms-widget-instance',
207                'jpa/top-platforms',
208                5,
209                1,
210                2,
211                array(
212                    'max' => 10,
213                )
214            ),
215            get_dashboard_default_widget_instance(
216                'default-search-terms-widget-instance',
217                'jpa/search-terms',
218                6,
219                1,
220                2,
221                array(
222                    'max' => 10,
223                )
224            ),
225            get_dashboard_default_widget_instance(
226                'default-utm-insights-widget-instance',
227                'jpa/utm-insights',
228                7,
229                1,
230                2,
231                array(
232                    'utmDimension' => 'utm_source,utm_medium',
233                    'max'          => 10,
234                )
235            ),
236            get_dashboard_default_widget_instance(
237                'default-file-downloads-widget-instance',
238                'jpa/file-downloads',
239                8,
240                1,
241                2,
242                array(
243                    'max' => 10,
244                )
245            ),
246            get_dashboard_default_widget_instance(
247                'default-clicks-widget-instance',
248                'jpa/clicks',
249                9,
250                1,
251                2,
252                array(
253                    'max' => 10,
254                )
255            ),
256            get_dashboard_default_widget_instance(
257                'default-authors-widget-instance',
258                'jpa/authors',
259                10,
260                1,
261                2,
262                array(
263                    'max' => 7,
264                )
265            ),
266            get_dashboard_default_widget_instance(
267                'default-videopress-widget-instance',
268                'jpa/videopress',
269                11,
270                1,
271                2,
272                array(
273                    'max' => 7,
274                )
275            ),
276            get_dashboard_default_widget_instance(
277                'default-plan-usage-widget-instance',
278                'jpa/plan-usage',
279                12,
280                2,
281                1
282            ),
283        ),
284        DASHBOARD_INSIGHTS_SECTION_ID    => array(
285            get_dashboard_default_widget_instance(
286                'default-annual-highlights-widget-instance',
287                'jpa/annual-highlights',
288                0,
289                2,
290                1
291            ),
292            get_dashboard_default_widget_instance(
293                'default-all-time-stats-widget-instance',
294                'jpa/all-time-stats',
295                1,
296                2,
297                1
298            ),
299            get_dashboard_default_widget_instance(
300                'default-latest-post-widget-instance',
301                'jpa/latest-post',
302                2,
303                1,
304                1
305            ),
306            get_dashboard_default_widget_instance(
307                'default-posting-activity-widget-instance',
308                'jpa/posting-activity',
309                3,
310                2,
311                2
312            ),
313            get_dashboard_default_widget_instance(
314                'default-emails-widget-instance',
315                'jpa/stats-emails',
316                4,
317                1,
318                2,
319                array(
320                    'max'    => 10,
321                    'metric' => 'opens',
322                )
323            ),
324            get_dashboard_default_widget_instance(
325                'default-shares-widget-instance',
326                'jpa/shares',
327                5,
328                1,
329                2,
330                array(
331                    'max' => 10,
332                )
333            ),
334            get_dashboard_default_widget_instance(
335                'default-tags-widget-instance',
336                'jpa/tags',
337                6,
338                2,
339                2,
340                array(
341                    'max' => 10,
342                )
343            ),
344            get_dashboard_default_widget_instance(
345                'default-comments-widget-instance',
346                'jpa/comments',
347                7,
348                2,
349                2,
350                array(
351                    'max' => 10,
352                )
353            ),
354            get_dashboard_default_widget_instance(
355                'default-most-popular-day-widget-instance',
356                'jpa/most-popular-day',
357                8,
358                1,
359                1
360            ),
361            get_dashboard_default_widget_instance(
362                'default-most-popular-time-widget-instance',
363                'jpa/most-popular-time',
364                9,
365                1,
366                1
367            ),
368        ),
369        DASHBOARD_SUBSCRIBERS_SECTION_ID => array(
370            get_dashboard_default_widget_instance(
371                'default-subscriber-highlights-widget-instance',
372                'jpa/subscriber-highlights',
373                0,
374                4,
375                1,
376                array(
377                    'showTotal'  => true,
378                    'showPaid'   => true,
379                    'showFree'   => true,
380                    'showSocial' => true,
381                )
382            ),
383            get_dashboard_default_widget_instance(
384                'default-subscribers-chart-widget-instance',
385                'jpa/subscribers-chart',
386                1,
387                4,
388                1,
389                array(
390                    'granularity' => 'auto',
391                )
392            ),
393            get_dashboard_default_widget_instance(
394                'default-subscribers-list-widget-instance',
395                'jpa/subscribers-list',
396                2,
397                2,
398                2,
399                array(
400                    'max' => 6,
401                )
402            ),
403            get_dashboard_default_widget_instance(
404                'default-subscribers-emails-widget-instance',
405                'jpa/stats-emails',
406                3,
407                2,
408                2,
409                array(
410                    'max'    => 10,
411                    'metric' => 'opens',
412                )
413            ),
414        ),
415        DASHBOARD_STORE_SECTION_ID       => array(
416            get_dashboard_default_widget_instance(
417                'default-store-performance-widget-instance',
418                'jpa/store-performance',
419                0,
420                2,
421                1
422            ),
423            get_dashboard_default_widget_instance(
424                'default-total-sales-over-time-widget-instance',
425                'jpa/total-sales-over-time',
426                1,
427                1,
428                1
429            ),
430            get_dashboard_default_widget_instance(
431                'default-conversion-rate-widget-instance',
432                'jpa/conversion-rate',
433                2,
434                1,
435                1
436            ),
437            get_dashboard_default_widget_instance(
438                'default-orders-over-time-widget-instance',
439                'jpa/orders-over-time',
440                3,
441                1,
442                1
443            ),
444            get_dashboard_default_widget_instance(
445                'default-average-order-value-widget-instance',
446                'jpa/average-order-value',
447                4,
448                1,
449                1
450            ),
451            get_dashboard_default_widget_instance(
452                'default-top-performing-products-widget-instance',
453                'jpa/top-performing-products',
454                5,
455                1,
456                1
457            ),
458            get_dashboard_default_widget_instance(
459                'default-new-vs-returning-customer-widget-instance',
460                'jpa/new-vs-returning-customer',
461                6,
462                1,
463                1
464            ),
465            get_dashboard_default_widget_instance(
466                'default-payment-status-widget-instance',
467                'jpa/payment-status',
468                7,
469                1,
470                1
471            ),
472            get_dashboard_default_widget_instance(
473                'default-orders-fulfillment-widget-instance',
474                'jpa/orders-fulfillment',
475                8,
476                1,
477                1
478            ),
479        ),
480    );
481}
482
483/**
484 * Resolves a dashboard or section identifier to one of the bundled tab IDs.
485 *
486 * @param string $dashboard_name Dashboard or section identifier.
487 * @return string|null Bundled tab ID, or null when the identifier is unsupported.
488 */
489function get_dashboard_default_section_id_for( $dashboard_name ) {
490    $aliases = array(
491        DASHBOARD_NAME                   => DASHBOARD_TRAFFIC_SECTION_ID,
492        DASHBOARD_TRAFFIC_SECTION_ID     => DASHBOARD_TRAFFIC_SECTION_ID,
493        'analytics/traffic'              => DASHBOARD_TRAFFIC_SECTION_ID,
494        DASHBOARD_INSIGHTS_SECTION_ID    => DASHBOARD_INSIGHTS_SECTION_ID,
495        'analytics/insights'             => DASHBOARD_INSIGHTS_SECTION_ID,
496        DASHBOARD_SUBSCRIBERS_SECTION_ID => DASHBOARD_SUBSCRIBERS_SECTION_ID,
497        'analytics/subscribers'          => DASHBOARD_SUBSCRIBERS_SECTION_ID,
498        DASHBOARD_STORE_SECTION_ID       => DASHBOARD_STORE_SECTION_ID,
499        'woocommerce/store'              => DASHBOARD_STORE_SECTION_ID,
500    );
501
502    return $aliases[ $dashboard_name ] ?? null;
503}
504
505/**
506 * Seeds the bundled default layouts for the Premium Analytics dashboard tabs.
507 *
508 * Only contributes to the known Premium Analytics dashboard and tab aliases;
509 * other dashboards are left untouched so the filter can be reused if more
510 * dashboards are added later.
511 *
512 * @param array  $dashboard_layout Default layout from earlier callbacks.
513 * @param string $dashboard_name   Identifier of the dashboard receiving the
514 *                                 default.
515 * @return array The layout extended with the bundled widget instances.
516 */
517function seed_default_dashboard_layout( $dashboard_layout, $dashboard_name = '' ) {
518    $section_id = get_dashboard_default_section_id_for( $dashboard_name );
519
520    if ( null === $section_id ) {
521        return $dashboard_layout;
522    }
523
524    $uuids   = array_column( $dashboard_layout, 'uuid' );
525    $layouts = get_dashboard_default_section_layouts();
526
527    foreach ( $layouts[ $section_id ] as $widget ) {
528        if ( ! in_array( $widget['uuid'], $uuids, true ) ) {
529            $dashboard_layout[] = $widget;
530            $uuids[]            = $widget['uuid'];
531        }
532    }
533
534    return $dashboard_layout;
535}
536add_filter( DASHBOARD_DEFAULT_LAYOUT_FILTER, __NAMESPACE__ . '\\seed_default_dashboard_layout', 10, 2 );