PluginProbe
ZIP AI – AI Website Builder & AI Agent (Beta) / trunk
ZIP AI – AI Website Builder & AI Agent (Beta) vtrunk
0.0.10 0.0.9 trunk 0.0.4 0.0.5 0.0.6 0.0.7 0.0.8
zip-ai / inc / admin / snippet-admin.php

snippet-admin.php in ZIP AI – AI Website Builder & AI Agent (Beta) trunk, at inc/admin/snippet-admin.php

305 lines 8.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Snippet Admin - Mounts the React-based snippets management SPA.
4 *
5 * @since 0.1.0
6 * @package zip-ai
7 */
8
9 namespace ZipAI\MCP\Classes\Admin;
10
11 use ZipAI\MCP\Classes\Core\Snippet_Store;
12 use ZipAI\MCP\Classes\Core\Snippet_Executor;
13
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17
18 class Snippet_Admin {
19
20 /**
21 * Hook suffix of the registered submenu page.
22 *
23 * @var string
24 */
25 private $hook_suffix = '';
26
27 /**
28 * Wire admin hooks (submenu registration + asset enqueue).
29 *
30 * @return void
31 */
32 public function __construct() {
33 if ( ! is_admin() ) {
34 return;
35 }
36
37 add_action( 'admin_menu', array( $this, 'register_submenu' ) );
38 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
39 }
40
41 /**
42 * Register the snippets management submenu page.
43 *
44 * @return void
45 */
46 public function register_submenu() {
47 // Read capability: the page must stay reachable so an admin can see and
48 // disable live snippets even where authoring is denied. Write actions
49 // 403 from the REST layer with an explanatory message.
50 if ( ! Snippet_Store::current_user_can_read() ) {
51 return;
52 }
53
54 $this->hook_suffix = (string) add_submenu_page(
55 'options-general.php',
56 __( 'ZIP AI Code Snippets', 'zip-ai' ),
57 __( 'ZIP AI Code Snippets', 'zip-ai' ),
58 Snippet_Store::read_capability(),
59 'zip-ai-snippets',
60 array( $this, 'render_page' )
61 );
62 }
63
64 /**
65 * Read a @wordpress/scripts asset manifest (dependencies + version).
66 *
67 * The path is a generated build artifact that may be absent (not committed),
68 * so it is guarded with file_exists() and returns an empty array otherwise.
69 *
70 * @param string $path Absolute path to the generated *.asset.php file.
71 * @return array<int|string, mixed>
72 */
73 private function read_asset_manifest( $path ) {
74 return file_exists( $path ) ? (array) ( include $path ) : array();
75 }
76
77 /**
78 * Enqueue the snippets SPA bundle, styles, and localized config.
79 *
80 * @param string $hook_suffix Current admin page hook suffix.
81 * @return void
82 */
83 public function enqueue_assets( $hook_suffix ) {
84 if ( $this->hook_suffix !== $hook_suffix ) {
85 return;
86 }
87
88 // React SPA bundle.
89 $asset = $this->read_asset_manifest( ZIPAI_MCP_DIR . 'assets/js/dist/snippets-page.asset.php' );
90
91 $deps = array( 'react', 'react-dom' );
92 if ( isset( $asset['dependencies'] ) && is_array( $asset['dependencies'] ) ) {
93 $deps = array_map( static fn ( $d ): string => is_scalar( $d ) ? (string) $d : '', $asset['dependencies'] );
94 }
95 $version = isset( $asset['version'] ) && is_string( $asset['version'] ) ? $asset['version'] : ZIPAI_MCP_VERSION;
96
97 wp_enqueue_script(
98 'zip-ai-snippets-page',
99 ZIPAI_MCP_URL . 'assets/js/dist/snippets-page.js',
100 $deps,
101 $version,
102 true
103 );
104
105 // Single source of truth: enqueue the shared Tailwind/ZIP AI stylesheet built
106 // from src/styles/main.css. Tokens + utilities are scoped to both
107 // #chat-assistant-root and #zip-ai-snippets-root via tailwind.config.js.
108 wp_enqueue_style(
109 'zip-ai-snippets-page',
110 ZIPAI_MCP_URL . 'assets/css/dist/chat-assistant.css',
111 array(),
112 $version
113 );
114
115 // Mount point sits flush against the WP admin canvas. The SPA owns its
116 // own layout/padding via Tailwind — the `.wrap` chrome adds margins
117 // that fight the design, so we zero the margins WP injects on
118 // `#wpbody-content`'s default flow.
119 wp_add_inline_style(
120 'zip-ai-snippets-page',
121 '#wpbody-content { padding: 0; } #wpcontent { padding-left: 0; padding-top: 0; } #wpbody { padding-top: 0 !important; } #wpfooter { display: none !important; } #zip-ai-snippets-root-wrap { margin: 0; padding: 0; }'
122 );
123
124 // CodeMirror 6 editor.
125 $editor_asset = $this->read_asset_manifest( ZIPAI_MCP_DIR . 'assets/js/dist/snippet-editor.asset.php' );
126
127 $editor_deps = array();
128 if ( isset( $editor_asset['dependencies'] ) && is_array( $editor_asset['dependencies'] ) ) {
129 $editor_deps = array_map( static fn ( $d ): string => is_scalar( $d ) ? (string) $d : '', $editor_asset['dependencies'] );
130 }
131 $editor_version = isset( $editor_asset['version'] ) && is_string( $editor_asset['version'] ) ? $editor_asset['version'] : ZIPAI_MCP_VERSION;
132
133 wp_enqueue_script(
134 'zip-ai-snippet-editor',
135 ZIPAI_MCP_URL . 'assets/js/dist/snippet-editor.js',
136 $editor_deps,
137 $editor_version,
138 true
139 );
140
141 // Config for the React app.
142 wp_localize_script(
143 'zip-ai-snippets-page',
144 'eraSnippetConfig',
145 array(
146 'restUrl' => rest_url( 'zip-ai/v1/snippets' ),
147 'nonce' => wp_create_nonce( 'wp_rest' ),
148 'hooks' => array(
149 'php' => Snippet_Store::VALID_PHP_HOOKS,
150 'js' => Snippet_Store::VALID_JS_HOOKS,
151 'css' => Snippet_Store::VALID_CSS_HOOKS,
152 'html' => Snippet_Store::VALID_HTML_HOOKS,
153 ),
154 'scopes' => Snippet_Store::VALID_SCOPES,
155 'conditionTypes' => array(
156 array(
157 'value' => 'page',
158 'label' => __( 'Page Type', 'zip-ai' ),
159 ),
160 array(
161 'value' => 'user',
162 'label' => __( 'User State', 'zip-ai' ),
163 ),
164 array(
165 'value' => 'user_role',
166 'label' => __( 'User Role', 'zip-ai' ),
167 ),
168 array(
169 'value' => 'post_type',
170 'label' => __( 'Post Type', 'zip-ai' ),
171 ),
172 array(
173 'value' => 'post',
174 'label' => __( 'Specific Post / Page', 'zip-ai' ),
175 ),
176 array(
177 'value' => 'device',
178 'label' => __( 'Device', 'zip-ai' ),
179 ),
180 array(
181 'value' => 'url_pattern',
182 'label' => __( 'URL Pattern', 'zip-ai' ),
183 ),
184 ),
185 'conditionValues' => array(
186 'page' => array(
187 array(
188 'value' => 'home',
189 'label' => __( 'Homepage', 'zip-ai' ),
190 ),
191 array(
192 'value' => 'single',
193 'label' => __( 'Single Post', 'zip-ai' ),
194 ),
195 array(
196 'value' => 'page',
197 'label' => __( 'Page', 'zip-ai' ),
198 ),
199 array(
200 'value' => 'archive',
201 'label' => __( 'Archive', 'zip-ai' ),
202 ),
203 array(
204 'value' => '404',
205 'label' => __( '404 Page', 'zip-ai' ),
206 ),
207 array(
208 'value' => 'search',
209 'label' => __( 'Search Results', 'zip-ai' ),
210 ),
211 ),
212 'user' => array(
213 array(
214 'value' => 'logged_in',
215 'label' => __( 'Logged In', 'zip-ai' ),
216 ),
217 ),
218 'user_role' => self::get_user_roles(),
219 'post_type' => self::get_post_types(),
220 'device' => array(
221 array(
222 'value' => 'mobile',
223 'label' => __( 'Mobile', 'zip-ai' ),
224 ),
225 ),
226 // post + url_pattern are open-ended (no fixed list); UI uses
227 // autocomplete (post) or free-text input (url_pattern).
228 'post' => array(),
229 'url_pattern' => array(),
230 ),
231 'conditionOperators' => array(
232 'page' => array( 'is', 'is_not' ),
233 'user' => array( 'is', 'is_not' ),
234 'user_role' => array( 'is', 'is_not', 'in', 'not_in' ),
235 'post_type' => array( 'is', 'is_not', 'in', 'not_in' ),
236 'post' => array( 'is', 'is_not', 'in', 'not_in' ),
237 'device' => array( 'is', 'is_not' ),
238 'url_pattern' => array( 'is', 'is_not', 'contains', 'starts_with', 'ends_with', 'regex' ),
239 ),
240 'conditionMulti' => array(
241 // Types where the UI should render a chip multi-picker.
242 'user_role' => true,
243 'post_type' => true,
244 'post' => true,
245 ),
246 'safeModeActive' => Snippet_Executor::is_persistent_safe_mode(),
247 )
248 );
249 }
250
251 /**
252 * Render the React mount point.
253 *
254 * @return void
255 */
256 public function render_page() {
257 // Canvas-reset CSS is enqueued via wp_add_inline_style() in enqueue_assets().
258 echo '<div id="zip-ai-snippets-root-wrap"><div id="zip-ai-snippets-root"></div></div>';
259 }
260
261 /**
262 * Get all user roles as value/label pairs.
263 *
264 * @since 0.0.5
265 * @return array<int, array{value: string, label: string}>
266 */
267 private static function get_user_roles() {
268 $roles = wp_roles()->get_names();
269 $result = array();
270
271 foreach ( $roles as $slug => $name ) {
272 $result[] = array(
273 'value' => $slug,
274 'label' => translate_user_role( $name ),
275 );
276 }
277
278 return $result;
279 }
280
281 /**
282 * Get all public post types as value/label pairs.
283 *
284 * @since 0.0.5
285 * @return array<int, array{value: string, label: string}>
286 */
287 private static function get_post_types() {
288 $types = get_post_types( array( 'public' => true ), 'objects' );
289 $result = array();
290
291 foreach ( $types as $slug => $type ) {
292 if ( 'attachment' === $slug ) {
293 continue;
294 }
295 $singular = $type->labels->singular_name ?? '';
296 $result[] = array(
297 'value' => $slug,
298 'label' => is_string( $singular ) ? $singular : '',
299 );
300 }
301
302 return $result;
303 }
304 }
305