FieldSettings.php
426 lines
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace AC\Acf\Service; |
| 6 | |
| 7 | use AC\Acf; |
| 8 | use AC\Acf\ColumnMatcher; |
| 9 | use AC\Acf\FieldGroupCache; |
| 10 | use AC\Asset\Location\Absolute; |
| 11 | use AC\Asset\Script; |
| 12 | use AC\Asset\Style; |
| 13 | use AC\Column; |
| 14 | use AC\ListScreen; |
| 15 | use AC\ListScreenRepository\Storage; |
| 16 | use AC\Registerable; |
| 17 | use AC\TableScreen; |
| 18 | use AC\Type\EditorUrlFactory; |
| 19 | use AC\Type\TableId; |
| 20 | use AC\Type\Uri; |
| 21 | use AC\Type\Url\Site; |
| 22 | use AC\Type\Url\UtmTags; |
| 23 | |
| 24 | class FieldSettings implements Registerable |
| 25 | { |
| 26 | private const PRO_ONLY_ACF_TYPES = [ |
| 27 | 'flexible_content', |
| 28 | 'gallery', |
| 29 | 'google_map', |
| 30 | 'group', |
| 31 | 'link', |
| 32 | 'relationship', |
| 33 | 'repeater', |
| 34 | 'taxonomy', |
| 35 | ]; |
| 36 | |
| 37 | private const SUPPORTED_ACF_TYPES = [ |
| 38 | 'button_group', |
| 39 | 'checkbox', |
| 40 | 'color_picker', |
| 41 | 'date_picker', |
| 42 | 'date_time_picker', |
| 43 | 'email', |
| 44 | 'file', |
| 45 | 'image', |
| 46 | 'number', |
| 47 | 'oembed', |
| 48 | 'page_link', |
| 49 | 'password', |
| 50 | 'post_object', |
| 51 | 'radio', |
| 52 | 'range', |
| 53 | 'select', |
| 54 | 'text', |
| 55 | 'textarea', |
| 56 | 'time_picker', |
| 57 | 'true_false', |
| 58 | 'url', |
| 59 | 'user', |
| 60 | 'wysiwyg', |
| 61 | ]; |
| 62 | |
| 63 | private Storage $storage; |
| 64 | |
| 65 | private FieldGroupCache $field_group_cache; |
| 66 | |
| 67 | private ColumnMatcher $column_matcher; |
| 68 | |
| 69 | private Absolute $location; |
| 70 | |
| 71 | public function __construct( |
| 72 | Storage $storage, |
| 73 | FieldGroupCache $field_group_cache, |
| 74 | ColumnMatcher $column_matcher, |
| 75 | Absolute $location |
| 76 | ) { |
| 77 | $this->storage = $storage; |
| 78 | $this->field_group_cache = $field_group_cache; |
| 79 | $this->column_matcher = $column_matcher; |
| 80 | $this->location = $location; |
| 81 | } |
| 82 | |
| 83 | public function register(): void |
| 84 | { |
| 85 | if (! Acf::is_active()) { |
| 86 | return; |
| 87 | } |
| 88 | |
| 89 | add_filter('acf/field_group/additional_field_settings_tabs', [$this, 'add_tab']); |
| 90 | add_action('acf/field_group/render_field_settings_tab/admin_columns', [$this, 'render_tab']); |
| 91 | add_action('acf/field_group/admin_enqueue_scripts', [$this, 'enqueue_scripts']); |
| 92 | } |
| 93 | |
| 94 | public function enqueue_scripts(): void |
| 95 | { |
| 96 | $style = new Style('ac-acf-field-settings', $this->location->with_suffix('assets/css/acf-field-settings.css')); |
| 97 | $style->enqueue(); |
| 98 | |
| 99 | $script = new Script('ac-acf-field-settings', $this->location->with_suffix('assets/js/acf-field-settings.js'), ['jquery']); |
| 100 | $script->enqueue(); |
| 101 | |
| 102 | wp_localize_script('ac-acf-field-settings', 'ac_acf_field_settings', [ |
| 103 | 'nonce' => wp_create_nonce('ac-ajax'), |
| 104 | 'adding' => __('Adding...', 'codepress-admin-columns'), |
| 105 | 'added' => __('Added', 'codepress-admin-columns'), |
| 106 | 'edit_column' => __('Edit column →', 'codepress-admin-columns'), |
| 107 | 'add_as_column' => $this->get_add_column_label(), |
| 108 | ]); |
| 109 | } |
| 110 | |
| 111 | public function add_tab(array $tabs): array |
| 112 | { |
| 113 | $tabs['admin_columns'] = __('Admin Columns', 'codepress-admin-columns'); |
| 114 | |
| 115 | return $tabs; |
| 116 | } |
| 117 | |
| 118 | private function get_add_column_label(): string |
| 119 | { |
| 120 | return __('Add as column', 'codepress-admin-columns'); |
| 121 | } |
| 122 | |
| 123 | public function render_tab(array $field): void |
| 124 | { |
| 125 | if ($this->is_pro_only_field($field)) { |
| 126 | $this->render_pro_only_upsell($field); |
| 127 | |
| 128 | return; |
| 129 | } |
| 130 | |
| 131 | if (! $this->is_field_supported($field)) { |
| 132 | acf_render_field_setting( |
| 133 | $field, |
| 134 | [ |
| 135 | 'label' => '', |
| 136 | 'type' => 'message', |
| 137 | 'name' => 'ac_unsupported', |
| 138 | 'message' => '<p style="color:#50575e;">' |
| 139 | . esc_html__('This field type cannot be used as an admin column.', 'codepress-admin-columns') |
| 140 | . '</p>', |
| 141 | ] |
| 142 | ); |
| 143 | |
| 144 | return; |
| 145 | } |
| 146 | |
| 147 | $this->render_table_screen_rows($field); |
| 148 | } |
| 149 | |
| 150 | private function render_pro_only_upsell(array $field): void |
| 151 | { |
| 152 | $url = (new UtmTags(new Site(Site::PAGE_ADDON_ACF), 'acf-field-settings-upsell'))->get_url(); |
| 153 | |
| 154 | acf_render_field_setting( |
| 155 | $field, |
| 156 | [ |
| 157 | 'label' => '', |
| 158 | 'type' => 'message', |
| 159 | 'name' => 'ac_pro_upsell', |
| 160 | 'message' => $this->get_upsell_html( |
| 161 | __('This field type is supported in Admin Columns Pro', 'codepress-admin-columns'), |
| 162 | __('Add this field as a column with sorting, filtering, and inline editing.', 'codepress-admin-columns'), |
| 163 | $url, |
| 164 | __('Learn more →', 'codepress-admin-columns') |
| 165 | ), |
| 166 | ] |
| 167 | ); |
| 168 | } |
| 169 | |
| 170 | private function render_table_screen_rows(array $field): void |
| 171 | { |
| 172 | $table_screens = $this->resolve_table_screens($field); |
| 173 | |
| 174 | if (! $table_screens) { |
| 175 | return; |
| 176 | } |
| 177 | |
| 178 | $meta_key = $field['name'] ?? ''; |
| 179 | $has_meta_key = $meta_key !== '' && $meta_key !== 'new_field'; |
| 180 | |
| 181 | $rows_html = ''; |
| 182 | |
| 183 | foreach ($table_screens as $table_screen) { |
| 184 | $table_id = (string)$table_screen->get_id(); |
| 185 | $label = $table_screen->get_labels()->get_plural(); |
| 186 | |
| 187 | if (! $has_meta_key) { |
| 188 | $rows_html .= $this->render_unavailable_row($label); |
| 189 | |
| 190 | continue; |
| 191 | } |
| 192 | |
| 193 | $match = $this->find_column_in_list_screens($table_screen->get_id(), $meta_key); |
| 194 | |
| 195 | if ($match) { |
| 196 | $url = EditorUrlFactory::create($table_screen->get_id(), false, $match['list_screen']->get_id()) |
| 197 | ->with_arg('open_columns', (string)$match['column']->get_id()); |
| 198 | |
| 199 | $rows_html .= $this->render_added_row($label, $url); |
| 200 | } else { |
| 201 | $rows_html .= $this->render_available_row($table_id, $label); |
| 202 | } |
| 203 | } |
| 204 | |
| 205 | $intro = '<p class="ac-acf-intro">' |
| 206 | . esc_html__('Add this field as a column in the list tables below. Once added, you can open that specific column in Admin Columns to configure it.', 'codepress-admin-columns') |
| 207 | . '</p>'; |
| 208 | |
| 209 | if (! $has_meta_key) { |
| 210 | $intro .= '<div class="ac-acf-notice">' |
| 211 | . sprintf( |
| 212 | esc_html__('Enter a %1$s under the %2$s tab to enable adding this field as a column.', 'codepress-admin-columns'), |
| 213 | '<strong>' . esc_html__('Field Name', 'codepress-admin-columns') . '</strong>', |
| 214 | '<strong>' . esc_html__('General', 'codepress-admin-columns') . '</strong>' |
| 215 | ) |
| 216 | . '</div>'; |
| 217 | } |
| 218 | |
| 219 | $helper = '<p class="ac-acf-helper">' |
| 220 | . esc_html__('Column settings are managed in Admin Columns after creation.', 'codepress-admin-columns') |
| 221 | . '</p>'; |
| 222 | |
| 223 | acf_render_field_setting( |
| 224 | $field, |
| 225 | [ |
| 226 | 'label' => __('Admin Columns', 'codepress-admin-columns'), |
| 227 | 'type' => 'message', |
| 228 | 'name' => 'ac_field_settings', |
| 229 | 'message' => $intro . '<div class="ac-acf-list">' . $rows_html . '</div>' . $helper, |
| 230 | ] |
| 231 | ); |
| 232 | |
| 233 | $this->render_upsell($field, $table_screens); |
| 234 | } |
| 235 | |
| 236 | private function render_added_row(string $label, Uri $editor_url): string |
| 237 | { |
| 238 | return sprintf( |
| 239 | '<div class="ac-acf-card ac-acf-card--added" data-state="added">' |
| 240 | . '<div class="ac-acf-card-main">' |
| 241 | . '<span class="ac-acf-card-name">%s</span>' |
| 242 | . '<span class="ac-acf-status"><span class="ac-acf-badge">✓</span><span class="ac-acf-card-status">%s</span></span>' |
| 243 | . '</div>' |
| 244 | . '<div class="ac-acf-card-actions">' |
| 245 | . '<a href="%s" target="_blank" class="ac-acf-link">%s</a>' |
| 246 | . '</div>' |
| 247 | . '</div>', |
| 248 | esc_html($label), |
| 249 | esc_html__('Added', 'codepress-admin-columns'), |
| 250 | esc_url((string)$editor_url), |
| 251 | esc_html__('Edit column →', 'codepress-admin-columns') |
| 252 | ); |
| 253 | } |
| 254 | |
| 255 | private function render_available_row(string $table_id, string $label): string |
| 256 | { |
| 257 | return sprintf( |
| 258 | '<div class="ac-acf-card" data-state="available" data-table-id="%s">' |
| 259 | . '<div class="ac-acf-card-main">' |
| 260 | . '<span class="ac-acf-card-name">%s</span>' |
| 261 | . '</div>' |
| 262 | . '<div class="ac-acf-card-actions">' |
| 263 | . '<button type="button" class="button ac-acf-add-column">%s</button>' |
| 264 | . '</div>' |
| 265 | . '</div>', |
| 266 | esc_attr($table_id), |
| 267 | esc_html($label), |
| 268 | esc_html($this->get_add_column_label()) |
| 269 | ); |
| 270 | } |
| 271 | |
| 272 | private function render_unavailable_row(string $label): string |
| 273 | { |
| 274 | return sprintf( |
| 275 | '<div class="ac-acf-card ac-acf-card--unavailable">' |
| 276 | . '<div class="ac-acf-card-main">' |
| 277 | . '<span class="ac-acf-card-name">%s</span>' |
| 278 | . '</div>' |
| 279 | . '<div class="ac-acf-card-actions">' |
| 280 | . '<button type="button" class="button" disabled>%s</button>' |
| 281 | . '</div>' |
| 282 | . '</div>', |
| 283 | esc_html($label), |
| 284 | esc_html($this->get_add_column_label()) |
| 285 | ); |
| 286 | } |
| 287 | |
| 288 | /** |
| 289 | * @param TableScreen[] $table_screens |
| 290 | */ |
| 291 | private function render_upsell(array $field, array $table_screens): void |
| 292 | { |
| 293 | $labels = []; |
| 294 | |
| 295 | foreach ($table_screens as $table_screen) { |
| 296 | $labels[] = strtolower($table_screen->get_labels()->get_plural()); |
| 297 | } |
| 298 | |
| 299 | $labels = array_slice($labels, 0, 3); |
| 300 | |
| 301 | if ($labels) { |
| 302 | $last = array_pop($labels); |
| 303 | $content_label = $labels |
| 304 | ? implode(', ', $labels) . ' & ' . $last |
| 305 | : $last; |
| 306 | |
| 307 | $description = sprintf( |
| 308 | __( |
| 309 | 'Make this column editable, sortable, and filterable – and manage your %s faster right from the overview.', |
| 310 | 'codepress-admin-columns' |
| 311 | ), |
| 312 | $content_label |
| 313 | ); |
| 314 | } else { |
| 315 | $description = __( |
| 316 | 'Make this column editable, sortable, and filterable – and manage your content faster right from the overview.', |
| 317 | 'codepress-admin-columns' |
| 318 | ); |
| 319 | } |
| 320 | |
| 321 | $url = (new UtmTags(new Site(Site::PAGE_ADDON_ACF), 'acf-field-settings'))->get_url(); |
| 322 | |
| 323 | acf_render_field_setting( |
| 324 | $field, |
| 325 | [ |
| 326 | 'label' => '', |
| 327 | 'type' => 'message', |
| 328 | 'name' => 'ac_upsell', |
| 329 | 'message' => $this->get_upsell_html( |
| 330 | __('Unlock powerful column features', 'codepress-admin-columns'), |
| 331 | $description, |
| 332 | $url, |
| 333 | __('Get Admin Columns Pro →', 'codepress-admin-columns') |
| 334 | ), |
| 335 | ] |
| 336 | ); |
| 337 | } |
| 338 | |
| 339 | private function get_upsell_html(string $title, string $description, string $url, string $link_text): string |
| 340 | { |
| 341 | return sprintf( |
| 342 | '<div class="ac-acf-upsell">' |
| 343 | . '<strong>%s</strong><br>' |
| 344 | . '%s <a href="%s" target="_blank">%s</a>' |
| 345 | . '</div>', |
| 346 | esc_html($title), |
| 347 | esc_html($description), |
| 348 | esc_url($url), |
| 349 | esc_html($link_text) |
| 350 | ); |
| 351 | } |
| 352 | |
| 353 | private function is_pro_only_field(array $field): bool |
| 354 | { |
| 355 | if ($field['type'] === 'select' && ! empty($field['multiple'])) { |
| 356 | return true; |
| 357 | } |
| 358 | |
| 359 | return $this->is_sub_field($field) || in_array($field['type'], self::PRO_ONLY_ACF_TYPES, true); |
| 360 | } |
| 361 | |
| 362 | private function is_field_supported(array $field): bool |
| 363 | { |
| 364 | return in_array($field['type'], self::SUPPORTED_ACF_TYPES, true); |
| 365 | } |
| 366 | |
| 367 | private function is_sub_field(array $field): bool |
| 368 | { |
| 369 | if (isset($field['parent_repeater']) |
| 370 | || isset($field['parent_group']) |
| 371 | || isset($field['parent_layout']) |
| 372 | || isset($field['_clone']) |
| 373 | ) { |
| 374 | return true; |
| 375 | } |
| 376 | |
| 377 | $parent = $field['parent'] ?? 0; |
| 378 | |
| 379 | if ($parent) { |
| 380 | $parent_field = acf_get_field($parent); |
| 381 | |
| 382 | if ($parent_field && in_array($parent_field['type'], ['group', 'repeater', 'flexible_content'], true)) { |
| 383 | return true; |
| 384 | } |
| 385 | } |
| 386 | |
| 387 | return false; |
| 388 | } |
| 389 | |
| 390 | /** |
| 391 | * @return TableScreen[] |
| 392 | */ |
| 393 | private function resolve_table_screens(array $field): array |
| 394 | { |
| 395 | static $cache = []; |
| 396 | |
| 397 | $parent = $field['parent'] ?? 0; |
| 398 | |
| 399 | if (! isset($cache[$parent])) { |
| 400 | $cache[$parent] = $this->field_group_cache->get_table_screens_for_group((int)$parent); |
| 401 | } |
| 402 | |
| 403 | return $cache[$parent]; |
| 404 | } |
| 405 | |
| 406 | /** |
| 407 | * @return array{list_screen: ListScreen, column: Column}|null |
| 408 | */ |
| 409 | private function find_column_in_list_screens(TableId $table_id, string $meta_key): ?array |
| 410 | { |
| 411 | foreach ($this->storage->find_all_by_table_id($table_id) as $list_screen) { |
| 412 | $column = $this->column_matcher->find_column($list_screen, $meta_key); |
| 413 | |
| 414 | if ($column) { |
| 415 | return [ |
| 416 | 'list_screen' => $list_screen, |
| 417 | 'column' => $column, |
| 418 | ]; |
| 419 | } |
| 420 | } |
| 421 | |
| 422 | return null; |
| 423 | } |
| 424 | |
| 425 | } |
| 426 |