PluginProbe
wpDataTables – WordPress Data Table, Dynamic Tables & Table Charts Plugin / trunk
wpDataTables – WordPress Data Table, Dynamic Tables & Table Charts Plugin vtrunk
6.5.1.7 6.5.1.6 6.5.1.5 6.5.1.4 6.5.1.3 6.5.1.2 6.5.1.1 6.5.0.9 6.5.0.8 6.5.0.7 6.5.0.6 trunk 3.4.2.40 3.4.2.41 3.4.2.42 3.4.2.43 3.4.2.44 3.4.2.45 3.4.2.46 3.4.2.47 3.4.2.48 3.4.2.49 3.4.2.50 6.3.2 6.3.3.1 All 47 releases
wpdatatables / Infrastructure / WP / MCP / Abilities / edit-chart.php

edit-chart.php in wpDataTables – WordPress Data Table, Dynamic Tables & Table Charts Plugin trunk, at Infrastructure/WP/MCP/Abilities/edit-chart.php

425 lines 17.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Ability: wpdatatables/edit-chart
4 *
5 * Updates an existing wpDataCharts configuration (title, type, dimensions, columns,
6 * source table, axes, display flags). Chart engine cannot be changed — create a new chart instead.
7 *
8 * @package wpDataTables_MCP_Server
9 * @since 0.2.0
10 */
11
12 defined( 'ABSPATH' ) or die('Access denied.');
13
14 add_action( 'wp_abilities_api_init', 'wdtmcp_register_edit_chart_ability' );
15
16 function wdtmcp_register_edit_chart_ability() {
17 wp_register_ability(
18 'wpdatatables/edit-chart',
19 array(
20 'label' => __( 'Edit Chart', 'wpdatatables' ),
21 'description' => __( 'Updates an existing wpDataChart. Change title, chart type (within the same engine), selected_columns, linked wpdatatable_id, size, responsive width, follow_filtering, group_chart, show_title, show_grid, tooltip, background_color, axis labels, range_type, row_range. Saves by rebuilding chart JSON from the merged settings (avoids empty Chart.js after type changes). Changing engine is blocked — create a new chart. REQUIRED: chart_id.', 'wpdatatables' ),
22 'category' => 'wpdatatables-data',
23
24 'input_schema' => array(
25 'type' => 'object',
26 'properties' => array(
27 'chart_id' => array(
28 'type' => 'integer',
29 'description' => 'Existing chart ID (from list-charts).',
30 ),
31 'title' => array(
32 'type' => 'string',
33 'description' => 'Chart title.',
34 ),
35 'type' => array(
36 'type' => 'string',
37 'description' => 'Chart type keyword (e.g. line, pie, column, bar) — resolved like create-chart. Stored JSON is rebuilt on save.',
38 ),
39 'wpdatatable_id' => array(
40 'type' => 'integer',
41 'description' => 'Source table ID. All selected_columns must exist on the new table.',
42 ),
43 'selected_columns' => array(
44 'type' => 'array',
45 'description' => 'Column orig_headers (min 2: categories + series).',
46 'items' => array( 'type' => 'string' ),
47 ),
48 'width' => array( 'type' => 'integer', 'description' => 'Width in px.' ),
49 'height' => array( 'type' => 'integer', 'description' => 'Height in px.' ),
50 'responsive_width' => array( 'type' => 'boolean', 'description' => 'Responsive width.' ),
51 'follow_filtering' => array( 'type' => 'boolean', 'description' => 'React to table filters.' ),
52 'group_chart' => array( 'type' => 'boolean', 'description' => 'Group rows with same label.' ),
53 'show_title' => array( 'type' => 'boolean', 'description' => 'Show chart title.' ),
54 'show_grid' => array( 'type' => 'boolean', 'description' => 'Show grid lines.' ),
55 'tooltip_enabled' => array( 'type' => 'boolean', 'description' => 'Enable tooltips.' ),
56 'background_color' => array(
57 'type' => 'string',
58 'description' => 'Background colour (hex, e.g. #FFFFFF).',
59 ),
60 'horizontal_axis_label' => array(
61 'type' => 'string',
62 'description' => 'Major / horizontal axis title.',
63 ),
64 'vertical_axis_label' => array(
65 'type' => 'string',
66 'description' => 'Minor / vertical axis title.',
67 ),
68 'range_type' => array(
69 'type' => 'string',
70 'description' => 'Row range mode (e.g. all, pick_rows, range — same as wpDataTables chart UI).',
71 ),
72 'row_range' => array(
73 'type' => 'array',
74 'description' => 'Row index boundaries when range_type is not "all".',
75 'items' => array( 'type' => 'integer' ),
76 ),
77 ),
78 'required' => array( 'chart_id' ),
79 ),
80
81 'output_schema' => array(
82 'type' => 'object',
83 'properties' => array(
84 'chart_id' => array( 'type' => 'integer', 'description' => 'Updated chart ID.' ),
85 'shortcode' => array( 'type' => 'string', 'description' => 'Embed shortcode.' ),
86 'updated' => array(
87 'type' => 'array',
88 'items' => array( 'type' => 'string' ),
89 'description' => 'Fields that were applied.',
90 ),
91 ),
92 ),
93
94 'execute_callback' => 'wdtmcp_execute_edit_chart',
95
96 'permission_callback' => function () {
97 return current_user_can( 'manage_options' );
98 },
99
100 'meta' => array(
101 'annotations' => array(
102 'instructions' => __( 'get-chart-info → edit-chart. Engine is fixed. Use generic type names (pie, line) like create-chart.', 'wpdatatables' ),
103 'readonly' => false,
104 'destructive' => false,
105 'idempotent' => true,
106 ),
107 ),
108 )
109 );
110 }
111
112 /**
113 * Ensure selected column headers exist on the table.
114 *
115 * @param int $wpdatatable_id
116 * @param array $selected_columns
117 * @return true|\WP_Error
118 */
119 function wdtmcp_validate_chart_columns_for_table( $wpdatatable_id, array $selected_columns ) {
120 if ( count( $selected_columns ) < 2 ) {
121 return new \WP_Error(
122 'wdtmcp_invalid_input',
123 __( 'At least two selected_columns are required (label and value).', 'wpdatatables' )
124 );
125 }
126
127 if ( ! class_exists( 'WPDataTable' ) ) {
128 return new \WP_Error( 'wdtmcp_missing_class', __( 'WPDataTable is not available.', 'wpdatatables' ) );
129 }
130
131 try {
132 $table = \WPDataTable::loadWpDataTable( (int) $wpdatatable_id, null, true );
133 } catch ( \Exception $e ) {
134 return new \WP_Error(
135 'wdtmcp_table_load_failed',
136 sprintf( __( 'Could not load source table: %s', 'wpdatatables' ), $e->getMessage() )
137 );
138 }
139
140 $headers = array();
141 foreach ( $table->getColumns() as $column ) {
142 $headers[] = $column->getOriginalHeader();
143 }
144
145 foreach ( $selected_columns as $column_header ) {
146 if ( ! in_array( $column_header, $headers, true ) ) {
147 return new \WP_Error(
148 'wdtmcp_invalid_input',
149 sprintf(
150 /* translators: 1: column, 2: table id */
151 __( 'Column "%1$s" was not found on table %2$d.', 'wpdatatables' ),
152 $column_header,
153 $wpdatatable_id
154 )
155 );
156 }
157 }
158
159 return true;
160 }
161
162 /**
163 * Export current chart object state to the array shape expected by WPDataChart::build( ..., false ).
164 * Used to fully regenerate json_render_data after loadFromDB + setters (avoids pie/bar desync).
165 *
166 * @param \WPDataChart $chart Chart instance (already patched).
167 * @return array
168 */
169 function wdtmcp_chart_construct_array_from_instance( \WPDataChart $chart ) {
170 $out = array(
171 'id' => (int) $chart->getId(),
172 'title' => (string) $chart->getTitle(),
173 'wpdatatable_id' => (int) $chart->getwpDataTableId(),
174 'engine' => (string) $chart->getEngine(),
175 'type' => (string) $chart->getType(),
176 'selected_columns' => $chart->getSelectedColumns(),
177 'range_type' => (string) $chart->getRangeType(),
178 'follow_filtering' => (bool) $chart->getFollowFiltering(),
179 'width' => (int) $chart->getWidth(),
180 'height' => (int) $chart->getHeight(),
181 'responsive_width' => (bool) $chart->isResponsiveWidth(),
182 'group_chart' => (bool) $chart->isGroupChart(),
183 'background_color' => (string) $chart->getBackgroundColor(),
184 'show_grid' => (bool) $chart->isShowGrid(),
185 'show_title' => (bool) $chart->isShowTitle(),
186 'tooltip_enabled' => (bool) $chart->isTooltipEnabled(),
187 'horizontal_axis_label' => (string) $chart->getMajorAxisLabel(),
188 'vertical_axis_label' => (string) $chart->getMinorAxisLabel(),
189 'vertical_axis_min' => null !== $chart->getVerticalAxisMin() ? (string) $chart->getVerticalAxisMin() : '',
190 'vertical_axis_max' => null !== $chart->getVerticalAxisMax() ? (string) $chart->getVerticalAxisMax() : '',
191 'series_type' => (string) $chart->getSeriesType(),
192 'loader' => (bool) $chart->isLoaderVisible(),
193 );
194
195 $rr = $chart->getRowRange();
196 if ( ! empty( $rr ) ) {
197 $out['range_data'] = $rr;
198 }
199
200 $series_data = $chart->getUserDefinedSeriesData();
201 if ( ! empty( $series_data ) && is_array( $series_data ) ) {
202 $out['series_data'] = $series_data;
203 }
204
205 $loader_cs = $chart->getChartLoaderColorSettings();
206 if ( '' !== (string) $loader_cs ) {
207 $out['chartLoaderColorSettings'] = $loader_cs;
208 }
209
210 $loader_anim = $chart->getChartLoaderAnimationColorSettings();
211 if ( '' !== (string) $loader_anim ) {
212 $out['chartLoaderAnimationColorSettings'] = $loader_anim;
213 }
214
215 return $out;
216 }
217
218 /**
219 * Execute wpdatatables/edit-chart.
220 *
221 * @param array $input
222 * @return array|\WP_Error
223 */
224 function wdtmcp_execute_edit_chart( $input ) {
225 $chart_id = isset( $input['chart_id'] ) ? (int) $input['chart_id'] : 0;
226 if ( $chart_id <= 0 ) {
227 return new \WP_Error( 'wdtmcp_invalid_input', __( 'chart_id is required.', 'wpdatatables' ) );
228 }
229
230 if ( ! class_exists( 'WPDataChart' ) || ! function_exists( 'wdtmcp_resolve_chart_type' ) ) {
231 return new \WP_Error(
232 'wdtmcp_missing_class',
233 __( 'Chart helpers are not loaded (create-chart must be registered).', 'wpdatatables' )
234 );
235 }
236
237 $row = \WPDataChart::getChartDataById( $chart_id );
238 if ( empty( $row ) ) {
239 return new \WP_Error(
240 'wdtmcp_not_found',
241 sprintf( __( 'Chart with ID %d was not found.', 'wpdatatables' ), $chart_id )
242 );
243 }
244
245 $stored_engine = isset( $row->engine ) ? strtolower( (string) $row->engine ) : '';
246 if ( isset( $input['engine'] ) ) {
247 $req_engine = strtolower( sanitize_text_field( $input['engine'] ) );
248 if ( $req_engine !== $stored_engine ) {
249 return new \WP_Error(
250 'wdtmcp_chart_engine_locked',
251 __( 'Changing chart engine is not supported. Create a new chart with the desired engine.', 'wpdatatables' )
252 );
253 }
254 }
255
256 $built = \WPDataChart::build(
257 array(
258 'id' => $chart_id,
259 'wpdatatable_id' => (int) $row->wpdatatable_id,
260 'engine' => $stored_engine,
261 ),
262 true
263 );
264
265 /** @var \WPDataChart $chart */
266 $chart = $built;
267 $updated = array();
268 $table_id = (int) $chart->getwpDataTableId();
269
270 if ( isset( $input['wpdatatable_id'] ) ) {
271 $new_tid = (int) $input['wpdatatable_id'];
272 if ( $new_tid > 0 && $new_tid !== $table_id ) {
273 global $wpdb;
274 $exists = (int) $wpdb->get_var(
275 $wpdb->prepare(
276 'SELECT COUNT(*) FROM ' . $wpdb->prefix . 'wpdatatables WHERE id = %d',
277 $new_tid
278 )
279 );
280 if ( $exists < 1 ) {
281 return new \WP_Error(
282 'wdtmcp_not_found',
283 sprintf( __( 'Table with ID %d was not found.', 'wpdatatables' ), $new_tid )
284 );
285 }
286 $chart->setwpDataTableId( $new_tid );
287 $table_id = $new_tid;
288 $chart->loadChildWPDataTable();
289 $updated[] = 'wpdatatable_id';
290 }
291 }
292
293 if ( isset( $input['title'] ) ) {
294 $chart->setTitle( sanitize_text_field( $input['title'] ) );
295 $updated[] = 'title';
296 }
297
298 if ( isset( $input['type'] ) && '' !== trim( (string) $input['type'] ) ) {
299 $resolved = wdtmcp_resolve_chart_type( $stored_engine, (string) $input['type'] );
300 if ( '' !== $resolved ) {
301 $chart->setType( $resolved );
302 $updated[] = 'type';
303 }
304 }
305
306 $columns_to_check = null;
307 if ( isset( $input['selected_columns'] ) && is_array( $input['selected_columns'] ) ) {
308 $cols = array_values(
309 array_filter(
310 array_map( 'sanitize_text_field', $input['selected_columns'] ),
311 function ( $h ) {
312 return '' !== $h;
313 }
314 )
315 );
316 $check = wdtmcp_validate_chart_columns_for_table( $table_id, $cols );
317 if ( is_wp_error( $check ) ) {
318 return $check;
319 }
320 $chart->setSelectedColumns( $cols );
321 $updated[] = 'selected_columns';
322 $columns_to_check = $cols;
323 }
324
325 if ( isset( $input['width'] ) ) {
326 $chart->setWidth( max( 50, min( 4000, (int) $input['width'] ) ) );
327 $updated[] = 'width';
328 }
329 if ( isset( $input['height'] ) ) {
330 $chart->setHeight( max( 50, min( 4000, (int) $input['height'] ) ) );
331 $updated[] = 'height';
332 }
333 if ( isset( $input['responsive_width'] ) ) {
334 $chart->setResponsiveWidth( (bool) $input['responsive_width'] );
335 $updated[] = 'responsive_width';
336 }
337 if ( isset( $input['follow_filtering'] ) ) {
338 $chart->setFollowFiltering( (bool) $input['follow_filtering'] );
339 $updated[] = 'follow_filtering';
340 }
341 if ( isset( $input['group_chart'] ) ) {
342 $chart->setGroupChart( (bool) $input['group_chart'] );
343 $updated[] = 'group_chart';
344 }
345 if ( isset( $input['show_title'] ) ) {
346 $chart->setShowTitle( (bool) $input['show_title'] );
347 $updated[] = 'show_title';
348 }
349 if ( isset( $input['show_grid'] ) ) {
350 $chart->setShowGrid( (bool) $input['show_grid'] );
351 $updated[] = 'show_grid';
352 }
353 if ( isset( $input['tooltip_enabled'] ) ) {
354 $chart->setTooltipEnabled( (bool) $input['tooltip_enabled'] );
355 $updated[] = 'tooltip_enabled';
356 }
357 if ( isset( $input['background_color'] ) ) {
358 $chart->setBackgroundColor( sanitize_text_field( $input['background_color'] ) );
359 $updated[] = 'background_color';
360 }
361 if ( isset( $input['horizontal_axis_label'] ) ) {
362 $chart->setMajorAxisLabel( sanitize_text_field( $input['horizontal_axis_label'] ) );
363 $updated[] = 'horizontal_axis_label';
364 }
365 if ( isset( $input['vertical_axis_label'] ) ) {
366 $chart->setMinorAxisLabel( sanitize_text_field( $input['vertical_axis_label'] ) );
367 $updated[] = 'vertical_axis_label';
368 }
369 if ( isset( $input['range_type'] ) && '' !== trim( (string) $input['range_type'] ) ) {
370 $chart->setRangeType( sanitize_text_field( $input['range_type'] ) );
371 $updated[] = 'range_type';
372 }
373 if ( isset( $input['row_range'] ) && is_array( $input['row_range'] ) ) {
374 $chart->setRowRange( array_values( array_map( 'intval', $input['row_range'] ) ) );
375 $updated[] = 'row_range';
376 }
377
378 // If table link changed but columns not supplied, ensure current columns still valid.
379 if ( in_array( 'wpdatatable_id', $updated, true ) && null === $columns_to_check ) {
380 $current = $chart->getSelectedColumns();
381 $check = wdtmcp_validate_chart_columns_for_table( $table_id, $current );
382 if ( is_wp_error( $check ) ) {
383 return new \WP_Error(
384 'wdtmcp_invalid_input',
385 sprintf(
386 /* translators: %d: table id */
387 __( 'wpdatatable_id changed; existing columns do not match table %d. Pass selected_columns.', 'wpdatatables' ),
388 $table_id
389 )
390 );
391 }
392 }
393
394 if ( empty( $updated ) ) {
395 return new \WP_Error(
396 'wdtmcp_invalid_input',
397 __( 'No editable fields provided. Pass at least one property to update besides chart_id.', 'wpdatatables' )
398 );
399 }
400
401 try {
402 $construct = wdtmcp_chart_construct_array_from_instance( $chart );
403 $chart = \WPDataChart::build( $construct, false );
404 if ( $chart->isResponsiveWidth() && (int) $chart->getWidth() < 200 ) {
405 $chart->setWidth( 400 );
406 }
407 if ( function_exists( 'wdtmcp_chart_save_persisting_table_rows' ) ) {
408 wdtmcp_chart_save_persisting_table_rows( $chart );
409 } else {
410 $chart->save();
411 }
412 } catch ( \Exception $e ) {
413 return new \WP_Error(
414 'wdtmcp_chart_save_failed',
415 sprintf( __( 'Failed to save chart: %s', 'wpdatatables' ), $e->getMessage() )
416 );
417 }
418
419 return array(
420 'chart_id' => $chart_id,
421 'shortcode' => $chart->getShortCode(),
422 'updated' => array_values( array_unique( $updated ) ),
423 );
424 }
425