PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / 3.8.0
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! v3.8.0
3.8.0 3.7.5 3.7.4 3.7.3 3.7.2 1-final 3.7.1 3.7.0 3.6.8 3.6.7 3.6.6 3.6.5 3.6.4 3.6.3 3.6.2 3.6.1 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 All 112 releases
← All changes | includes/Core/Admin.php +151 -53 3.7.13.8.0 View file →
@@ -4,15 +4,17 @@
4 4 use Exception;
5 5 use PriyoMukul\WPNotice\Notices;
6 6 use PriyoMukul\WPNotice\Utils\CacheBank;
7 7 use PriyoMukul\WPNotice\Utils\NoticeRemover;
8 -use Templately\API\Login;
9 -use Templately\Core\Importer\FullSiteImport;
8 +use Templately\Modules\Auth\REST\Login;
9 +use Templately\Modules\FullSiteImport\FullSiteImport;
10 +use Templately\Admin\Roles;
11 +use Templately\Core\Capabilities;
10 12 use Templately\Utils\Base;
11 13 use Templately\Utils\Helper;
12 14 use Templately\Utils\Options;
13 -use Templately\Core\Platform\Elementor;
14 -use Templately\Core\Platform\Gutenberg;
15 +use Templately\Modules\ElementorIntegration\Platform\Elementor;
16 +use Templately\Modules\GutenbergIntegration\Platform\Gutenberg;
15 17
16 18 class Admin extends Base {
17 19
18 20 /**
@@ -38,9 +40,11 @@
38 40 }
39 41
40 42 add_filter( 'plugin_action_links_' . TEMPLATELY_PLUGIN_BASENAME, [$this, 'handleActionLinks'], 10, 2 );
41 43
42 - add_action( 'admin_footer', [$this, 'my_custom_footer_html'] );
44 + // Post-import feedback mount point moved to
45 + // modules/post-import-feedback/Widget.php::render_mount_point() (spec 027) —
46 + // hooked independently on admin_footer.
43 47
44 48 // Remove OLD notice from 1.0.0 (if other WPDeveloper plugin has notice)
45 49 NoticeRemover::get_instance( '1.0.0' );
46 50 }
@@ -80,9 +84,14 @@
80 84 'show_ui' => true,
81 85 'show_in_menu' => false,
82 86 'query_var' => true,
83 87 'rewrite' => [ 'slug' => 'templately-library' ],
88 + // Kept in sync with the theme-builder module's own registration of this
89 + // same post type (modules/theme-builder/Source.php) — whichever runs last
90 + // wins, so both must carry the administrator-only capability map.
84 91 'capability_type' => 'post',
92 + 'capabilities' => Roles::builder_post_type_capabilities(),
93 + 'map_meta_cap' => true,
85 94 'has_archive' => true,
86 95 'hierarchical' => false,
87 96 'menu_position' => 10,
88 97 'supports' => [ 'title', 'editor', 'author', 'thumbnail', 'custom-field' ],
@@ -148,22 +157,11 @@
148 157 // }
149 158
150 159 if('index.php' === $hook){
151 160 // need separate condition so we can return if page is index.php
152 -
153 - $is_complete = get_user_meta(get_current_user_id(), 'templately_fsi_complete', true);
154 - if($is_complete && $is_complete !== 'done' && !wp_is_mobile()){
155 - $user = Options::get_instance()->get('user');
156 - $email = isset($user['email']) ? $user['email'] : '';
157 -
158 - templately()->assets->enqueue( 'templately', 'css/dashboard-style.css', [] );
159 - templately()->assets->enqueue( 'templately', 'js/dashboard.js', [], true );
160 - templately()->assets->localize( 'templately', 'templately', [
161 - 'email' => $email,
162 - 'nonce' => wp_create_nonce( 'templately_nonce' ),
163 - ] );
164 -
165 - }
161 + // Post-import feedback widget enqueue/localize moved to
162 + // modules/post-import-feedback/Widget.php (spec 027) — hooked independently
163 + // on admin_enqueue_scripts.
166 164 return;
167 165 }
168 166
169 167 $templately = [];
@@ -196,8 +194,22 @@
196 194 $script_dependencies = ['regenerator-runtime'];
197 195 $_localize_handle = 'templately';
198 196 $_current_screen = 'templately';
199 197
198 + // The webpack "commons" chunk (splitChunks) holds the react-src core + vendor code SHARED
199 + // across the main bundle AND every module bundle — extracted once instead of duplicated
200 + // into each. It MUST load before any of them. Making the main 'templately' handle depend
201 + // on it means every module bundle (which depends on 'templately') gets it transitively, in
202 + // the right order. Its own .asset.php lists no deps (DEWP doesn't attribute externals to
203 + // shared chunks), so declare the WordPress script globals its shared code references here.
204 + if ( file_exists( TEMPLATELY_PATH . 'assets/js/templately-commons.js' ) ) {
205 + templately()->assets->register( 'templately-commons', 'js/templately-commons.js', [
206 + 'react', 'react-dom', 'react-jsx-runtime', 'wp-element', 'wp-hooks', 'wp-i18n',
207 + 'wp-api-fetch', 'wp-url', 'wp-data', 'wp-components', 'lodash',
208 + ], true );
209 + $script_dependencies[] = 'templately-commons';
210 + }
211 +
200 212 if ( $hook === 'elementor' || $hook === 'gutenberg' ) {
201 213 $_current_screen = $hook;
202 214 $_localize_handle = 'templately-' . $hook;
203 215 $script_dependencies[] = $_localize_handle;
@@ -234,14 +246,50 @@
234 246 wp_enqueue_style('wp-components');
235 247
236 248 wp_enqueue_media();
237 249 templately()->assets->enqueue( 'templately', 'js/templately.js', $script_dependencies, true );
238 - templately()->assets->enqueue( 'templately', 'css/templately.css', ['templately-dmsans', 'templately-inter'] );
250 + // splitChunks moves the shared component CSS into the commons chunk's stylesheet
251 + // (templately-commons.css) and leaves the per-entry templately.css empty/absent. Register
252 + // the shared CSS under the SAME 'templately' style handle so every stylesheet that depends
253 + // on it (templately-tailwind, templately-admin) still resolves — WP won't enqueue a style
254 + // whose dependency handle is unregistered. Fall back to templately.css if a build didn't
255 + // split (no commons chunk).
256 + $main_css = file_exists( TEMPLATELY_PATH . 'assets/css/templately-commons.css' ) ? 'css/templately-commons.css' : 'css/templately.css';
257 + templately()->assets->enqueue( 'templately', $main_css, ['templately-dmsans', 'templately-inter'] );
258 +
239 259 $tailwind_hooks = apply_filters( 'templately_tailwind_hooks', [ 'toplevel_page_templately', 'elementor', 'gutenberg', 'templately_page_templately_settings', 'edit.php' ] );
240 260 if ( in_array( $hook, $tailwind_hooks, true ) ) {
241 261 templately()->assets->enqueue( 'templately-tailwind', 'css/tailwind.css', ['templately', 'templately-inter'] );
242 262 }
243 263
264 + // MODULE STYLES. @wordpress/scripts splits extracted CSS by ORIGIN, not by feature:
265 + // `{entry}.css` holds CSS reached from a stylesheet entry point, while
266 + // `style-{entry}.css` holds CSS `import`ed from JS. Every module's SCSS is imported from
267 + // its own index (e.g. modules/auth/assets/js/index.tsx imports ../scss/style.scss), so
268 + // with the modules folded into the `templately` graph ALL of it now lands in
269 + // style-templately.css — a file nothing enqueued.
270 + //
271 + // This is the regression that shipped the sign-in screen completely unstyled: the header
272 + // and banner came from templately.css and looked fine, so the page rendered but the
273 + // module-owned views did not. Before the merge these styles were emitted per module to
274 + // assets/css/modules/*.css and picked up by enqueue_module_assets()'s glob; that glob is
275 + // gone, and this is its replacement.
276 + //
277 + // ORDER MATTERS, and this must stay AFTER the Tailwind enqueue. The retired glob ran at
278 + // the very end of this method — after Tailwind — so module rules beat Tailwind utilities
279 + // on equal specificity. Enqueueing it earlier silently inverts that: the AI sidebar chat
280 + // bubble lost its overflow-wrap to a Tailwind utility and long URLs broke out of the
281 + // bubble (caught by 020-ai-chat-bubble-overflow.spec.js, not by eye).
282 + if ( file_exists( TEMPLATELY_PATH . 'assets/css/style-templately.css' ) ) {
283 + templately()->assets->enqueue( 'templately-modules', 'css/style-templately.css', [ 'templately' ] );
284 + }
285 +
286 + // Module assets are no longer enqueued separately: every feature module compiles into
287 + // the 'templately' bundle via react-src/generated/moduleManifest.ts (Constitution XIII).
288 + // The former enqueue_module_assets() globbed assets/js/modules/*.js and loaded all of
289 + // them unconditionally wherever this ran, so folding them into the main handle is a
290 + // parity change, not a scope change.
291 +
244 292 /**
245 293 * @var Elementor|Gutenberg $platform
246 294 */
247 295 $platform = $this->platform( $_current_screen );
@@ -289,8 +337,25 @@
289 337 'allowed_mime_types' => get_allowed_mime_types(),
290 338 'is_free_user' => $global_user === false || isset( $global_user['plan'] ) && $global_user['plan'] == 'free',
291 339 'is_disconnected' => ! empty( $global_user['is_disconnected'] ),
292 340 'tour_status' => Options::get_instance()->get( 'tour_status', [] ),
341 + // Host-capability map (spec 053) — the ONE delivery point for every
342 + // Templately bundle (admin SPA and editor surfaces all localize here).
343 + // Always sourced from the registry, never hand-built (FR-025).
344 + 'caps' => Capabilities::get_instance()->get_map(),
345 + // Mirrors the account-scoped server gate so the client can hide what it
346 + // would only be 403'd for: Subscription (a plan purchase is made FOR the
347 + // account, and Checkout gates `subscription` on `manage_options`).
348 + // Hidden rather than shown-and-refused — offering a button that cannot
349 + // work is worse than not offering it.
350 + 'can_manage_account' => current_user_can( 'manage_options' ),
351 + // Which Templately site the plugin talks to. `templatelyDeveloper.dev_api`
352 + // carries the same flag, but only when developer mode is on and only from
353 + // its own script handle — so anything rendered before that handle runs (the
354 + // footer CTA, for one) linked to the live site while the plugin was pointed
355 + // at dev. Kept here, on the main localized object, `webURL()` is right from
356 + // the first paint. See `react-src/utils/helper.ts#webURL`.
357 + 'dev_api' => Helper::is_dev_api(),
293 358 ], $templately );
294 359
295 360 // Apply filter to allow network admin modifications
296 361 $templately = apply_filters( 'templately_admin_localized_data', $templately );
@@ -368,9 +433,9 @@
368 433 'class' => 'dismiss-btn'
369 434 ]
370 435 ],
371 436 'support' => [
372 - 'link' => 'https://templately.com/?support=open',
437 + 'link' => Helper::web_url( '', [ 'support' => 'open' ] ),
373 438 'attributes' => [
374 439 'target' => '_blank'
375 440 ],
376 441 'label' => __( 'I need help', 'templately' ),
@@ -432,9 +497,9 @@
432 497 'thumbnail' => templately()->assets->icon( 'logos/logo-full.svg' ),
433 498 'html' => $_notice_text_spring_savings_2026,
434 499 'links' => [
435 500 'later' => [
436 - 'link' => 'https://templately.com/#pricing',
501 + 'link' => Helper::web_url() . '#pricing',
437 502 'target' => '_blank',
438 503 'label' => __( 'Upgrade To Pro Now', 'templately' ),
439 504 'attributes' => [
440 505 'class' => 'button button-primary',
@@ -472,13 +537,13 @@
472 537 $holiday_text .= "<div class='wpnotice-button-wrapper'>";
473 538 $holiday_text .= sprintf(
474 539 '<a class="button button-primary" target="_blank" href="%2$s">%1$s</a>',
475 540 $crown . __('GET PRO Lifetime Access', 'templately'),
476 - 'https://templately.com/#pricing'
541 + Helper::web_url() . '#pricing'
477 542 );
478 543 $holiday_text .= sprintf(
479 544 '<button class="button button-link dismiss-btn" data-dismiss="true">%1$s</button>',
480 - __('No, I\'ll Pay Full Price Later', 'templately'),
545 + __('No, I\'ll Pay Full Price Later', 'templately')
481 546 );
482 547 $holiday_text .= "</div>";
483 548
484 549
@@ -511,16 +576,29 @@
511 576 */
512 577 public function admin_menu() {
513 578 // TODO: Role Management
514 579
515 - add_menu_page( 'Templately', 'Templately', 'delete_posts', 'templately', '', templately()->assets->icon( 'logos/logo-icon.svg' ), '58.7' );
580 + // 58.7 is the TOP-LEVEL slot: a float is correct and always has been
581 + // there, and the fractional part is what keeps Templately from
582 + // colliding with another plugin sitting at the same integer position.
583 + add_menu_page( 'Templately', 'Templately', 'delete_posts', 'templately', '', templately()->assets->icon( 'logos/logo-icon.svg' ), 58.7 );
516 584
585 + // The submenu calls pass NO position on purpose. `58.7` was copied down
586 + // from the top-level slot, where it never meant anything — a submenu
587 + // $position is an index within THIS menu, and 58.7 is past the end of a
588 + // four-item list, so every WordPress we support already just appended.
589 + // It was not merely useless though: before WP 6.0, $position had to be
590 + // an int (`is_int()`, not `is_numeric()` — a float fails it too), so
591 + // each call emitted `_doing_it_wrong` on every admin load of a
592 + // WP_DEBUG site inside the advertised floor (readme: WP 5.0) and then
593 + // discarded the value anyway. Omitting it appends — verified identical
594 + // ordering on WP 5.9 and 7.0 — and is silent on every version.
517 595 add_submenu_page( 'templately', 'Templately', 'Template Library', 'delete_posts', 'templately', [
518 596 $this,
519 597 'display'
520 - ], '58.7' );
598 + ] );
521 599
522 - add_submenu_page( 'templately', 'Theme Builder', 'Theme Builder', 'delete_posts', 'edit.php?post_type=templately_library', '', '58.7' );
600 + add_submenu_page( 'templately', 'Theme Builder', 'Theme Builder', Roles::CAP_BUILDER, 'edit.php?post_type=templately_library', '' );
523 601
524 602 // add_submenu_page( 'templately', 'Settings', 'Settings', 'administrator', 'templately-settings', [
525 603 // Settings::get_instance(),
526 604 // 'display'
@@ -527,12 +605,53 @@
527 605 // ], '58.7' );
528 606 }
529 607
530 608 public function display() {
531 - Helper::views( 'template-library' );
609 + Helper::views( 'template-library', [
610 + 'show_grid_skeleton' => self::is_template_library_path(),
611 + ] );
532 612 }
533 613
534 614 /**
615 + * Whether the current `?path=` targets the template-library grid
616 + * (`TemplatesTypeLayout`, e.g. `elementor/packs`) — the only route the
617 + * pre-mount skeleton's card grid actually matches. Every other route
618 + * (`clouds/*`, `downloads`, `favourites`, `manage-sites`, `subscription`,
619 + * `purchased-items`, `ai-sites`, `settings/*`, item-detail pages, and
620 + * Saved Templates) gets a generic shell instead, so a hard reload doesn't
621 + * flash the wrong page shape before React mounts. See
622 + * views/template-library.php.
623 + *
624 + * Public + static (it was private on the pre-modular branch) purely so the
625 + * path parsing can be unit-tested on its own: it reads nothing but `$_GET`
626 + * and touches no instance state, so there is nothing for an Admin instance
627 + * to contribute, and constructing one would register every admin hook.
628 + *
629 + * @return bool
630 + */
631 + public static function is_template_library_path() {
632 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only routing hint for a skeleton; no state is changed.
633 + $path = isset( $_GET['path'] ) ? sanitize_text_field( wp_unslash( $_GET['path'] ) ) : '';
634 +
635 + if ( $path === '' ) {
636 + return true;
637 + }
638 +
639 + $segments = array_values( array_filter( explode( '/', trim( $path, '/' ) ) ) );
640 +
641 + $non_library_prefixes = [ 'clouds', 'downloads', 'favourites', 'manage-sites', 'subscription', 'purchased-items', 'ai-sites', 'settings' ];
642 +
643 + if ( in_array( $segments[0] ?? '', $non_library_prefixes, true ) ) {
644 + return false;
645 + }
646 +
647 + // `/:platform/:type` (the grid) vs `/:platform/:type/:slug` (item
648 + // detail, its own skeleton shape) or `/:platform/templates` (Saved
649 + // Templates, a table) — only the 2-segment form matches the grid.
650 + return count( $segments ) === 2 && $segments[1] !== 'templates';
651 + }
652 +
653 + /**
535 654 * If Elementor doesn't exists.
536 655 *
537 656 * @return void
538 657 */
@@ -537,12 +656,12 @@
537 656 * @return void
538 657 */
539 658 public static function has_no_elementor() {
540 659 $plugin_url = \wp_nonce_url( \self_admin_url( 'update.php?action=install-plugin&amp;plugin=elementor' ), 'install-plugin_elementor' );
541 - $button_text = 'Install Elementor';
660 + $button_text = __( 'Install Elementor', 'templately' );
542 661 if ( isset( Helper::get_plugins()['elementor/elementor.php'] ) ) {
543 662 $plugin_url = \wp_nonce_url( 'plugins.php?action=activate&amp;plugin=elementor/elementor.php', 'activate-plugin_elementor/elementor.php' );
544 - $button_text = 'Activate Elementor';
663 + $button_text = __( 'Activate Elementor', 'templately' );
545 664 }
546 665 $output = '<div class="notice notice-error">';
547 666 $output .= sprintf(
548 667 "<p><strong>%s</strong> %s <strong>%s</strong> %s &nbsp;&nbsp;<a class='button-primary' href='%s'>%s</a></p>",
@@ -550,12 +669,12 @@
550 669 __( 'requires', 'templately' ),
551 670 __( 'Elementor', 'templately' ),
552 671 __( 'plugin to be installed and activated. Please install Elementor to continue.', 'templately' ),
553 672 esc_url( $plugin_url ),
554 - __( $button_text, 'templately' )
673 + $button_text
555 674 );
556 675 $output .= '</div>';
557 - echo $output;
676 + echo wp_kses_post( $output );
558 677 }
559 678
560 679 public function header() {
561 680 Helper::views( 'header' );
@@ -575,27 +694,6 @@
575 694 'templately') . '">' . __('Templates Library', 'templately') . '</a>';
576 695
577 696 $links[] = $settingsLink;
578 697 return $links;
579 - }
580 -
581 -
582 - public function my_custom_footer_html() {
583 - global $current_screen;
584 -
585 - if ( ! $current_screen ) {
586 - return false;
587 - }
588 -
589 - if($current_screen->id !== 'dashboard'){
590 - return false;
591 - }
592 -
593 - $is_complete = get_user_meta(get_current_user_id(), 'templately_fsi_complete', true);
594 -
595 - if($is_complete && $is_complete !== 'done' && !wp_is_mobile()):
596 - ?>
597 - <div id="templately-fsi-feedback"></div>
598 - <?php
599 - endif;
600 698 }
601 699 }