PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / trunk
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! vtrunk
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 -85 3.7.5 → trunk 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,51 +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 - /**
175 - * plugins.php is not our screen, so the survey gets the scoped Tailwind
176 - * build rather than css/tailwind.css. The unscoped bundle is compiled
177 - * with `important: true` and no layer, so its utilities outranked
178 - * wp-admin's own rules — `.inline` beat `.notice.inline` and `.hidden`,
179 - * which collapsed the update notices in every plugin row into coloured
180 - * slivers and revealed the hidden auto-update error placeholders.
181 - * css/tailwind-survey.css carries the same rules prefixed with
182 - * `.templately-scope`, the container the modal mounts into.
183 - */
184 - templately()->assets->enqueue( 'templately-tailwind-survey', 'css/tailwind-survey.css', [ 'templately-inter' ] );
185 - templately()->assets->enqueue( 'templately-deactivate-survey', 'css/deactivate-survey-style.css', [ 'templately-tailwind-survey' ] );
186 - templately()->assets->enqueue( 'templately', 'js/deactivate-survey.js', [ 'regenerator-runtime' ], true );
187 - templately()->assets->localize( 'templately', 'templatelyDeactivateSurvey', [
188 - 'nonce' => wp_create_nonce( 'templately_nonce' ),
189 - 'pluginBasename' => TEMPLATELY_PLUGIN_BASENAME,
190 - 'reasons' => DeactivationSurvey::get_reasons_for_js(),
191 - ] );
192 - return;
193 - }
194 -
195 167 $templately = [];
196 168
197 169 if ( 'edit.php' === $hook ) {
198 170 global $current_screen;
@@ -222,8 +194,22 @@
222 194 $script_dependencies = ['regenerator-runtime'];
223 195 $_localize_handle = 'templately';
224 196 $_current_screen = 'templately';
225 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 +
226 212 if ( $hook === 'elementor' || $hook === 'gutenberg' ) {
227 213 $_current_screen = $hook;
228 214 $_localize_handle = 'templately-' . $hook;
229 215 $script_dependencies[] = $_localize_handle;
@@ -260,14 +246,50 @@
260 246 wp_enqueue_style('wp-components');
261 247
262 248 wp_enqueue_media();
263 249 templately()->assets->enqueue( 'templately', 'js/templately.js', $script_dependencies, true );
264 - 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 +
265 259 $tailwind_hooks = apply_filters( 'templately_tailwind_hooks', [ 'toplevel_page_templately', 'elementor', 'gutenberg', 'templately_page_templately_settings', 'edit.php' ] );
266 260 if ( in_array( $hook, $tailwind_hooks, true ) ) {
267 261 templately()->assets->enqueue( 'templately-tailwind', 'css/tailwind.css', ['templately', 'templately-inter'] );
268 262 }
269 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 +
270 292 /**
271 293 * @var Elementor|Gutenberg $platform
272 294 */
273 295 $platform = $this->platform( $_current_screen );
@@ -303,12 +325,8 @@
303 325 'is_globally_signed' => Login::is_globally_signed(),
304 326 'signed_as_global' => Login::signed_as_global(),
305 327 'current_screen' => $_current_screen,
306 328 'can_fsi' => current_user_can('install_plugins') && current_user_can('install_themes'),
307 - // Account-level administration: the Subscription screen. Mirrors the
308 - // subscription branch of `Checkout::checkout()`, so the sidebar can hide
309 - // it for non-admins rather than route them to a 403.
310 - 'can_manage_account' => current_user_can( 'manage_options' ),
311 329 'post_type' => get_post_type(),
312 330 'has_elementor' => rest_sanitize_boolean( is_plugin_active( 'elementor/elementor.php' ) ),
313 331 'has_elementor_pro' => rest_sanitize_boolean( is_plugin_active( 'elementor-pro/elementor-pro.php' ) ),
314 332 'theme' => $_current_screen == 'templately' ? 'light' : $platform->ui_theme(),
@@ -319,14 +337,24 @@
319 337 'allowed_mime_types' => get_allowed_mime_types(),
320 338 'is_free_user' => $global_user === false || isset( $global_user['plan'] ) && $global_user['plan'] == 'free',
321 339 'is_disconnected' => ! empty( $global_user['is_disconnected'] ),
322 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' ),
323 351 // Which Templately site the plugin talks to. `templatelyDeveloper.dev_api`
324 352 // carries the same flag, but only when developer mode is on and only from
325 353 // its own script handle — so anything rendered before that handle runs (the
326 354 // footer CTA, for one) linked to the live site while the plugin was pointed
327 355 // at dev. Kept here, on the main localized object, `webURL()` is right from
328 - // the first paint. See `react-src/utils/helper.js#webURL`.
356 + // the first paint. See `react-src/utils/helper.ts#webURL`.
329 357 'dev_api' => Helper::is_dev_api(),
330 358 ], $templately );
331 359
332 360 // Apply filter to allow network admin modifications
@@ -513,9 +541,9 @@
513 541 Helper::web_url() . '#pricing'
514 542 );
515 543 $holiday_text .= sprintf(
516 544 '<button class="button button-link dismiss-btn" data-dismiss="true">%1$s</button>',
517 - __('No, I\'ll Pay Full Price Later', 'templately'),
545 + __('No, I\'ll Pay Full Price Later', 'templately')
518 546 );
519 547 $holiday_text .= "</div>";
520 548
521 549
@@ -548,16 +576,29 @@
548 576 */
549 577 public function admin_menu() {
550 578 // TODO: Role Management
551 579
552 - 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 );
553 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.
554 595 add_submenu_page( 'templately', 'Templately', 'Template Library', 'delete_posts', 'templately', [
555 596 $this,
556 597 'display'
557 - ], '58.7' );
598 + ] );
558 599
559 - 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', '' );
560 601
561 602 // add_submenu_page( 'templately', 'Settings', 'Settings', 'administrator', 'templately-settings', [
562 603 // Settings::get_instance(),
563 604 // 'display'
@@ -565,9 +606,9 @@
565 606 }
566 607
567 608 public function display() {
568 609 Helper::views( 'template-library', [
569 - 'show_grid_skeleton' => $this->is_template_library_path(),
610 + 'show_grid_skeleton' => self::is_template_library_path(),
570 611 ] );
571 612 }
572 613
573 614 /**
@@ -573,17 +614,23 @@
573 614 /**
574 615 * Whether the current `?path=` targets the template-library grid
575 616 * (`TemplatesTypeLayout`, e.g. `elementor/packs`) — the only route the
576 617 * pre-mount skeleton's card grid actually matches. Every other route
577 - * (`clouds/*`, `downloads`, `favourites`, `subscription`,
618 + * (`clouds/*`, `downloads`, `favourites`, `manage-sites`, `subscription`,
578 619 * `purchased-items`, `ai-sites`, `settings/*`, item-detail pages, and
579 620 * Saved Templates) gets a generic shell instead, so a hard reload doesn't
580 621 * flash the wrong page shape before React mounts. See
581 622 * views/template-library.php.
582 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 + *
583 629 * @return bool
584 630 */
585 - 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.
586 633 $path = isset( $_GET['path'] ) ? sanitize_text_field( wp_unslash( $_GET['path'] ) ) : '';
587 634
588 635 if ( $path === '' ) {
589 636 return true;
@@ -590,9 +637,9 @@
590 637 }
591 638
592 639 $segments = array_values( array_filter( explode( '/', trim( $path, '/' ) ) ) );
593 640
594 - $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' ];
595 642
596 643 if ( in_array( $segments[0] ?? '', $non_library_prefixes, true ) ) {
597 644 return false;
598 645 }
@@ -609,12 +656,12 @@
609 656 * @return void
610 657 */
611 658 public static function has_no_elementor() {
612 659 $plugin_url = \wp_nonce_url( \self_admin_url( 'update.php?action=install-plugin&amp;plugin=elementor' ), 'install-plugin_elementor' );
613 - $button_text = 'Install Elementor';
660 + $button_text = __( 'Install Elementor', 'templately' );
614 661 if ( isset( Helper::get_plugins()['elementor/elementor.php'] ) ) {
615 662 $plugin_url = \wp_nonce_url( 'plugins.php?action=activate&amp;plugin=elementor/elementor.php', 'activate-plugin_elementor/elementor.php' );
616 - $button_text = 'Activate Elementor';
663 + $button_text = __( 'Activate Elementor', 'templately' );
617 664 }
618 665 $output = '<div class="notice notice-error">';
619 666 $output .= sprintf(
620 667 "<p><strong>%s</strong> %s <strong>%s</strong> %s &nbsp;&nbsp;<a class='button-primary' href='%s'>%s</a></p>",
@@ -622,12 +669,12 @@
622 669 __( 'requires', 'templately' ),
623 670 __( 'Elementor', 'templately' ),
624 671 __( 'plugin to be installed and activated. Please install Elementor to continue.', 'templately' ),
625 672 esc_url( $plugin_url ),
626 - __( $button_text, 'templately' )
673 + $button_text
627 674 );
628 675 $output .= '</div>';
629 - echo $output;
676 + echo wp_kses_post( $output );
630 677 }
631 678
632 679 public function header() {
633 680 Helper::views( 'header' );
@@ -647,27 +694,6 @@
647 694 'templately') . '">' . __('Templates Library', 'templately') . '</a>';
648 695
649 696 $links[] = $settingsLink;
650 697 return $links;
651 - }
652 -
653 -
654 - public function my_custom_footer_html() {
655 - global $current_screen;
656 -
657 - if ( ! $current_screen ) {
658 - return false;
659 - }
660 -
661 - if($current_screen->id !== 'dashboard'){
662 - return false;
663 - }
664 -
665 - $is_complete = get_user_meta(get_current_user_id(), 'templately_fsi_complete', true);
666 -
667 - if($is_complete && $is_complete !== 'done' && !wp_is_mobile()):
668 - ?>
669 - <div id="templately-fsi-feedback"></div>
670 - <?php
671 - endif;
672 698 }
673 699 }