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 +111 -75 3.7.33.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' ],
@@ -103,9 +112,9 @@
103 112 */
104 113 public function scripts( $hook = '' ) {
105 114 $allowed_hooks = apply_filters(
106 115 'templately_admin_enqueue_allowed_hooks',
107 - [ 'index.php', 'edit.php', 'plugins.php', 'toplevel_page_templately', 'elementor', 'gutenberg', 'templately_page_templately_settings' ],
116 + [ 'index.php', 'edit.php', 'toplevel_page_templately', 'elementor', 'gutenberg', 'templately_page_templately_settings' ],
108 117 $hook
109 118 );
110 119
111 120 if ( !empty($hook) && ! in_array( $hook, $allowed_hooks, true ) ) {
@@ -148,41 +157,14 @@
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 - if ( 'plugins.php' === $hook ) {
170 - templately()->assets->enqueue(
171 - 'templately-inter',
172 - set_url_scheme( '//fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap' )
173 - );
174 - templately()->assets->enqueue( 'templately-tailwind', 'css/tailwind.css', [ 'templately-inter' ] );
175 - templately()->assets->enqueue( 'templately-deactivate-survey', 'css/deactivate-survey-style.css', [ 'templately-tailwind' ] );
176 - templately()->assets->enqueue( 'templately', 'js/deactivate-survey.js', [ 'regenerator-runtime' ], true );
177 - templately()->assets->localize( 'templately', 'templatelyDeactivateSurvey', [
178 - 'nonce' => wp_create_nonce( 'templately_nonce' ),
179 - 'pluginBasename' => TEMPLATELY_PLUGIN_BASENAME,
180 - 'reasons' => DeactivationSurvey::get_reasons_for_js(),
181 - ] );
182 - return;
183 - }
184 -
185 167 $templately = [];
186 168
187 169 if ( 'edit.php' === $hook ) {
188 170 global $current_screen;
@@ -212,8 +194,22 @@
212 194 $script_dependencies = ['regenerator-runtime'];
213 195 $_localize_handle = 'templately';
214 196 $_current_screen = 'templately';
215 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 +
216 212 if ( $hook === 'elementor' || $hook === 'gutenberg' ) {
217 213 $_current_screen = $hook;
218 214 $_localize_handle = 'templately-' . $hook;
219 215 $script_dependencies[] = $_localize_handle;
@@ -250,14 +246,50 @@
250 246 wp_enqueue_style('wp-components');
251 247
252 248 wp_enqueue_media();
253 249 templately()->assets->enqueue( 'templately', 'js/templately.js', $script_dependencies, true );
254 - 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 +
255 259 $tailwind_hooks = apply_filters( 'templately_tailwind_hooks', [ 'toplevel_page_templately', 'elementor', 'gutenberg', 'templately_page_templately_settings', 'edit.php' ] );
256 260 if ( in_array( $hook, $tailwind_hooks, true ) ) {
257 261 templately()->assets->enqueue( 'templately-tailwind', 'css/tailwind.css', ['templately', 'templately-inter'] );
258 262 }
259 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 +
260 292 /**
261 293 * @var Elementor|Gutenberg $platform
262 294 */
263 295 $platform = $this->platform( $_current_screen );
@@ -293,12 +325,8 @@
293 325 'is_globally_signed' => Login::is_globally_signed(),
294 326 'signed_as_global' => Login::signed_as_global(),
295 327 'current_screen' => $_current_screen,
296 328 'can_fsi' => current_user_can('install_plugins') && current_user_can('install_themes'),
297 - // Account-level administration: the Subscription screen. Mirrors the
298 - // subscription branch of `Checkout::checkout()`, so the sidebar can hide
299 - // it for non-admins rather than route them to a 403.
300 - 'can_manage_account' => current_user_can( 'manage_options' ),
301 329 'post_type' => get_post_type(),
302 330 'has_elementor' => rest_sanitize_boolean( is_plugin_active( 'elementor/elementor.php' ) ),
303 331 'has_elementor_pro' => rest_sanitize_boolean( is_plugin_active( 'elementor-pro/elementor-pro.php' ) ),
304 332 'theme' => $_current_screen == 'templately' ? 'light' : $platform->ui_theme(),
@@ -309,14 +337,24 @@
309 337 'allowed_mime_types' => get_allowed_mime_types(),
310 338 'is_free_user' => $global_user === false || isset( $global_user['plan'] ) && $global_user['plan'] == 'free',
311 339 'is_disconnected' => ! empty( $global_user['is_disconnected'] ),
312 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' ),
313 351 // Which Templately site the plugin talks to. `templatelyDeveloper.dev_api`
314 352 // carries the same flag, but only when developer mode is on and only from
315 353 // its own script handle — so anything rendered before that handle runs (the
316 354 // footer CTA, for one) linked to the live site while the plugin was pointed
317 355 // at dev. Kept here, on the main localized object, `webURL()` is right from
318 - // the first paint. See `react-src/utils/helper.js#webURL`.
356 + // the first paint. See `react-src/utils/helper.ts#webURL`.
319 357 'dev_api' => Helper::is_dev_api(),
320 358 ], $templately );
321 359
322 360 // Apply filter to allow network admin modifications
@@ -503,9 +541,9 @@
503 541 Helper::web_url() . '#pricing'
504 542 );
505 543 $holiday_text .= sprintf(
506 544 '<button class="button button-link dismiss-btn" data-dismiss="true">%1$s</button>',
507 - __('No, I\'ll Pay Full Price Later', 'templately'),
545 + __('No, I\'ll Pay Full Price Later', 'templately')
508 546 );
509 547 $holiday_text .= "</div>";
510 548
511 549
@@ -538,16 +576,29 @@
538 576 */
539 577 public function admin_menu() {
540 578 // TODO: Role Management
541 579
542 - 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 );
543 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.
544 595 add_submenu_page( 'templately', 'Templately', 'Template Library', 'delete_posts', 'templately', [
545 596 $this,
546 597 'display'
547 - ], '58.7' );
598 + ] );
548 599
549 - 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', '' );
550 601
551 602 // add_submenu_page( 'templately', 'Settings', 'Settings', 'administrator', 'templately-settings', [
552 603 // Settings::get_instance(),
553 604 // 'display'
@@ -555,9 +606,9 @@
555 606 }
556 607
557 608 public function display() {
558 609 Helper::views( 'template-library', [
559 - 'show_grid_skeleton' => $this->is_template_library_path(),
610 + 'show_grid_skeleton' => self::is_template_library_path(),
560 611 ] );
561 612 }
562 613
563 614 /**
@@ -563,17 +614,23 @@
563 614 /**
564 615 * Whether the current `?path=` targets the template-library grid
565 616 * (`TemplatesTypeLayout`, e.g. `elementor/packs`) — the only route the
566 617 * pre-mount skeleton's card grid actually matches. Every other route
567 - * (`clouds/*`, `downloads`, `favourites`, `subscription`,
618 + * (`clouds/*`, `downloads`, `favourites`, `manage-sites`, `subscription`,
568 619 * `purchased-items`, `ai-sites`, `settings/*`, item-detail pages, and
569 620 * Saved Templates) gets a generic shell instead, so a hard reload doesn't
570 621 * flash the wrong page shape before React mounts. See
571 622 * views/template-library.php.
572 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 + *
573 629 * @return bool
574 630 */
575 - private function is_template_library_path() {
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.
576 633 $path = isset( $_GET['path'] ) ? sanitize_text_field( wp_unslash( $_GET['path'] ) ) : '';
577 634
578 635 if ( $path === '' ) {
579 636 return true;
@@ -580,9 +637,9 @@
580 637 }
581 638
582 639 $segments = array_values( array_filter( explode( '/', trim( $path, '/' ) ) ) );
583 640
584 - $non_library_prefixes = [ 'clouds', 'downloads', 'favourites', 'subscription', 'purchased-items', 'ai-sites', 'settings' ];
641 + $non_library_prefixes = [ 'clouds', 'downloads', 'favourites', 'manage-sites', 'subscription', 'purchased-items', 'ai-sites', 'settings' ];
585 642
586 643 if ( in_array( $segments[0] ?? '', $non_library_prefixes, true ) ) {
587 644 return false;
588 645 }
@@ -599,12 +656,12 @@
599 656 * @return void
600 657 */
601 658 public static function has_no_elementor() {
602 659 $plugin_url = \wp_nonce_url( \self_admin_url( 'update.php?action=install-plugin&amp;plugin=elementor' ), 'install-plugin_elementor' );
603 - $button_text = 'Install Elementor';
660 + $button_text = __( 'Install Elementor', 'templately' );
604 661 if ( isset( Helper::get_plugins()['elementor/elementor.php'] ) ) {
605 662 $plugin_url = \wp_nonce_url( 'plugins.php?action=activate&amp;plugin=elementor/elementor.php', 'activate-plugin_elementor/elementor.php' );
606 - $button_text = 'Activate Elementor';
663 + $button_text = __( 'Activate Elementor', 'templately' );
607 664 }
608 665 $output = '<div class="notice notice-error">';
609 666 $output .= sprintf(
610 667 "<p><strong>%s</strong> %s <strong>%s</strong> %s &nbsp;&nbsp;<a class='button-primary' href='%s'>%s</a></p>",
@@ -612,12 +669,12 @@
612 669 __( 'requires', 'templately' ),
613 670 __( 'Elementor', 'templately' ),
614 671 __( 'plugin to be installed and activated. Please install Elementor to continue.', 'templately' ),
615 672 esc_url( $plugin_url ),
616 - __( $button_text, 'templately' )
673 + $button_text
617 674 );
618 675 $output .= '</div>';
619 - echo $output;
676 + echo wp_kses_post( $output );
620 677 }
621 678
622 679 public function header() {
623 680 Helper::views( 'header' );
@@ -637,27 +694,6 @@
637 694 'templately') . '">' . __('Templates Library', 'templately') . '</a>';
638 695
639 696 $links[] = $settingsLink;
640 697 return $links;
641 - }
642 -
643 -
644 - public function my_custom_footer_html() {
645 - global $current_screen;
646 -
647 - if ( ! $current_screen ) {
648 - return false;
649 - }
650 -
651 - if($current_screen->id !== 'dashboard'){
652 - return false;
653 - }
654 -
655 - $is_complete = get_user_meta(get_current_user_id(), 'templately_fsi_complete', true);
656 -
657 - if($is_complete && $is_complete !== 'done' && !wp_is_mobile()):
658 - ?>
659 - <div id="templately-fsi-feedback"></div>
660 - <?php
661 - endif;
662 698 }
663 699 }