├── .gitignore ├── CHANGELOG.md ├── README.md ├── acf-agency-workflow.php ├── functions.php └── index.php /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.sublime-* -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | ## [1.3.0] - 2020-06-29 5 | ### Fixed 6 | - Fixed displaying JSON locations that are not available. 7 | 8 | ### Added 9 | - Added multiple WP_ENV values that are considered a development environment. 10 | - Added a filter for users to add even more values. 11 | - Added a requirement check: A WP_ENV must be defined. 12 | - Added a requirement check: Set JSON locations must exist. 13 | - Added an update check on the plugins page. 14 | 15 | ### Removed 16 | - Removed the 'Default' JSON Save Path option. 17 | 18 | ## [1.2.1] - 2020-03-04 19 | ### Fixed 20 | - Fixed undefined index notices on non-local. 21 | 22 | ## [1.2.0] - 2020-02-04 23 | ### Added 24 | - Added CHANGELOG.md. 25 | - Added translatable text. 26 | - Sorted JSON Save Path choices. 27 | - Added warning on non-local Custom Fields subpages. 28 | 29 | ### Removed 30 | - Removed hiding the Custom Fields menu. 31 | 32 | ## [1.1.0] - 2020-01-27 33 | ### Added 34 | - Added parent theme support. 35 | 36 | ## [1.0.1] - 2020-01-22 37 | ### Added 38 | - Added README.md with install guide. 39 | 40 | ### Fixed 41 | - Fixed bug where fields got deleted before they got synced. 42 | 43 | ## [1.0.0] - 2019-10-23 44 | ### Added 45 | - Initial version. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ACF Agency Workflow 2 | 3 | ## What do you get? 4 | 5 | ### Git your fields 6 | 7 | You already use git for collaboration and version control. Let's put your custom fields in Git! We've made the ACF JSON *cache* the source of truth by forcefully syncing them to your database when you load the Dashboard or Field Groups page. If you've added, modified or deleted a field, then you have something to commit. If you pulled in new, modified or deleted fields, then you have something to sync. Every developer on your team now works with the same Field Groups. 8 | 9 | ### Store your fields with your themes and plugins 10 | 11 | You already separate your website's looks from it's functionality (themes vs plugins). You can now do the same with your custom fields. For example: Store the header settings with your theme, the client's address fields with their core functionality plugin, and those API settings with your API plugin. Made a mistake? No problem. Edit the Field Group, choose a new location, save, and it's been moved. 12 | 13 | ## Requirements 14 | 15 | This plugin was developed with: 16 | 17 | - PHP 7.3 18 | - WordPress 5.3.2 19 | - Advanced Custom Fields PRO 5.8.7 20 | 21 | Requirements may be lowered after proper testing. 22 | 23 | ## Installation 24 | 25 | 1. Back up any existing Field Groups as a precaution. 26 | 1. Define `WP_ENV` as `local` on your local environment. (`wp-config-local.php` perhaps?) 27 | 1. Download & activate ACF PRO. 28 | 1. Download & activate ACF Agency Workflow. 29 | 1. Set ACF local JSON load locations. 30 | 31 | ## How to 32 | 33 | ### Set an ACF local JSON location in a theme 34 | 35 | Create an `acf-json` folder in your theme. 36 | 37 | ### Set an ACF local JSON location in a child theme 38 | 39 | Create an `acf-json` folder in your child theme. 40 | 41 | ### Set an ACF local JSON location in a parent theme 42 | 43 | Create an `acf-json` folder in your parent theme.\ 44 | Add this code to your parent theme's `functions.php`. 45 | 46 | ``` 47 | /** 48 | * ACF Local JSON location for a parent theme. 49 | */ 50 | 51 | add_filter( 'acf/settings/load_json', function ( $paths ) { 52 | 53 | $paths[] = get_template_directory().'/acf-json'; 54 | 55 | return $paths; 56 | }); 57 | ``` 58 | 59 | ### Set an ACF local JSON location in a plugin 60 | 61 | Create an `acf-json` folder in your plugin.\ 62 | Add this code to your plugin's main file. (`plugin-name/plugin-name.php`) 63 | 64 | ``` 65 | /** 66 | * ACF local JSON location for a plugin. 67 | */ 68 | 69 | add_filter( 'acf/settings/load_json', function ( $paths ) { 70 | 71 | $paths[] = plugin_dir_path( __FILE__ ).'acf-json'; 72 | 73 | return $paths; 74 | }); 75 | ``` 76 | 77 | ### Add custom WP_ENV values for local 78 | 79 | If you have a `WP_ENV` defined locally that AAW does not consider "local", you can add yours to the list with this filter.\ 80 | Add this code to a custom plugin or the theme's `functions.php`. 81 | 82 | ``` 83 | add_filter( 'aaw_local_envs', function ( $local_envs ) { 84 | 85 | return array_merge( 86 | $local_envs, 87 | ['lokaal'] // local in Dutch 88 | ); 89 | }); 90 | ``` 91 | 92 | ## Contributing 93 | 94 | Found a bug? Anything you'd like to ask, add, change, or have added or changed? Please open an issue so we can talk about it. 95 | 96 | ## Disclaimer 97 | 98 | The author(s) are not responsible for lost fields or other data. This plugin deletes things, so it can be dangerous. Backups are a good idea. Git can be your backup too. 99 | 100 | ## Author 101 | 102 | [Tim Brugman](https://github.com/Brugman) 103 | -------------------------------------------------------------------------------- /acf-agency-workflow.php: -------------------------------------------------------------------------------- 1 | 'acf-field-group' ] ); 36 | 37 | if ( !$is_dashboard && !$is_field_groups_page ) 38 | return; 39 | 40 | // Add & update field groups. 41 | aaw_act_on_added_json(); 42 | 43 | // Remove field groups. 44 | aaw_act_on_removed_json(); 45 | }); 46 | 47 | /** 48 | * Respond to Field Group Editor changes. 49 | */ 50 | 51 | add_action( 'acf/delete_field_group', function ( $field_group ) { 52 | // Delete field group from JSON. 53 | aaw_delete_field_group_from_json( $field_group['key'] ); 54 | }); 55 | 56 | /** 57 | * Replace the FG trash buttons with delete buttons. 58 | */ 59 | 60 | add_filter( 'page_row_actions', function ( $actions, $post ) { 61 | 62 | if ( $post->post_type == 'acf-field-group' ) 63 | { 64 | // Remove trash. 65 | unset( $actions['trash'] ); 66 | // Add delete. 67 | $actions['delete'] = ''.__( 'Delete Permanently' ).''; 68 | } 69 | 70 | return $actions; 71 | 72 | }, 10, 2 ); 73 | 74 | /** 75 | * Display JSON save locations list. 76 | */ 77 | 78 | add_action( 'acf/render_field_group_settings', function ( $field_group ) { 79 | 80 | $choices = []; 81 | 82 | $path_to_plugins = dirname( dirname( __FILE__ ) ); 83 | $path_to_themes = dirname( get_stylesheet_directory() ); 84 | 85 | $load_dirs = acf_get_setting('load_json'); 86 | 87 | if ( empty( $load_dirs ) ) 88 | return; 89 | 90 | foreach ( $load_dirs as $load_dir ) 91 | { 92 | $display_title = $load_dir; 93 | 94 | if ( strpos( $load_dir, $path_to_plugins ) !== false ) 95 | { 96 | $display_title = __( 'Plugin', 'acf-agency-workflow' ).': '.substr( str_replace( $path_to_plugins, '', $load_dir ), 1 ); 97 | } 98 | 99 | if ( strpos( $load_dir, $path_to_themes ) !== false ) 100 | { 101 | $label = __( 'Theme', 'acf-agency-workflow' ); 102 | if ( is_child_theme() ) 103 | { 104 | if ( strpos( $load_dir, get_stylesheet_directory() ) !== false ) 105 | $label = __( 'Theme (child)', 'acf-agency-workflow' ); 106 | 107 | if ( strpos( $load_dir, get_template_directory() ) !== false ) 108 | $label = __( 'Theme (parent)', 'acf-agency-workflow' ); 109 | } 110 | 111 | $display_title = $label.': '.substr( str_replace( $path_to_themes, '', $load_dir ), 1 ); 112 | } 113 | 114 | $choices[ $load_dir ] = $display_title; 115 | 116 | if ( file_exists( $load_dir.'/'.$field_group['key'].'.json' ) ) 117 | $load_dir_selected = $load_dir; 118 | } 119 | 120 | asort( $choices ); 121 | 122 | acf_render_field_wrap([ 123 | 'label' => __( 'JSON Save Path', 'acf-agency-workflow' ), 124 | 'instructions' => __( 'Where do you want the Field Group saved?', 'acf-agency-workflow' ), 125 | 'type' => 'select', 126 | 'name' => 'json_save_path', 127 | 'prefix' => 'acf_field_group', 128 | 'value' => $load_dir_selected ?? '', 129 | 'choices' => $choices, 130 | ]); 131 | }); 132 | 133 | /** 134 | * Get selected JSON save location. 135 | */ 136 | 137 | $global_preferred_save_path = false; 138 | 139 | add_action( 'acf/update_field_group', function ( $field_group ) { 140 | 141 | // Skip everything if this is not a FGE location move. 142 | if ( !isset( $field_group['json_save_path'] ) ) 143 | return $field_group; 144 | 145 | // Delete JSON cache for this field group. 146 | aaw_delete_field_group_from_json( $field_group['key'] ); 147 | 148 | // Reset save location. 149 | global $global_preferred_save_path; 150 | $global_preferred_save_path = false; 151 | 152 | // Store save location. 153 | if ( $field_group['json_save_path'] != 'default' ) 154 | $global_preferred_save_path = $field_group['json_save_path']; 155 | 156 | return $field_group; 157 | 158 | }, 1, 1 ); 159 | 160 | /** 161 | * Remove locations that don't exist. 162 | */ 163 | 164 | add_filter( 'acf/settings/load_json', function ( $paths ) { 165 | 166 | return array_filter( $paths, function ( $path ) { 167 | return file_exists( $path ); 168 | }); 169 | 170 | }, 20, 1 ); 171 | 172 | /** 173 | * Set selected JSON save location. 174 | */ 175 | 176 | add_action( 'acf/settings/save_json', function ( $path ) { 177 | 178 | global $global_preferred_save_path; 179 | 180 | // Set save location. 181 | if ( $global_preferred_save_path ) 182 | return $global_preferred_save_path; 183 | 184 | return $path; 185 | 186 | }, 999 ); 187 | 188 | /** 189 | * Remove save path value before it gets written to JSON. 190 | */ 191 | 192 | add_filter( 'acf/prepare_field_group_for_export', function ( $field_group ) { 193 | 194 | if ( isset( $field_group['json_save_path'] ) ) 195 | unset( $field_group['json_save_path'] ); 196 | 197 | return $field_group; 198 | }); 199 | 200 | /** 201 | * Notice: Sync feedback. 202 | */ 203 | 204 | $aaw_feedback = []; 205 | 206 | add_action( 'admin_notices', function () { 207 | 208 | global $aaw_feedback; 209 | 210 | if ( empty( $aaw_feedback ) ) 211 | return; 212 | 213 | foreach ( $aaw_feedback as $feedback ) 214 | printf( '
'.__( 'ACF Agency Workflow is not ready!', 'acf-agency-workflow' ).'
'; 230 | $feedback .= ''.__( 'Non-development environment detected!', 'acf-agency-workflow' ).'
'; 254 | $feedback .= ''.__( 'You are not on a development environment. Please do not add or edit Field Groups. If you do not know why this message is here, contact the developer who installed ACF Agency Workflow before continuing.', 'acf-agency-workflow' ).'
'; 255 | printf( 'There is a new version of ACF Agency Workflow available.
'; 276 | 277 | if ( aaw_env_is_dev() ) 278 | { 279 | $feedback .= 'Current version: '.aaw_current_version().', latest version: '.$new_version['version'].'.
'; 280 | $feedback .= 'Find out what\'s new in the changelog.
'; 281 | $feedback .= 'Updating has to be done manually. Download the ZIP file or tarball, and replace the contents of the plugin folder. Or git pull
from inside the plugin\'s directory.
Please ask your website developer to perform the update.
'; 286 | } 287 | 288 | printf( ''.__( 'The following ACF Field Groups were synced from their JSON cache:', 'acf-agency-workflow' ).'
'; 94 | $aaw_feedback['synced'] .= ''.__( 'One or more ACF Field Groups were synced from their JSON cache.', 'acf-agency-workflow' ).'
'; 102 | } 103 | } 104 | 105 | function aaw_get_unsynced_field_groups() 106 | { 107 | // Get all FGs, also the unsynced json ones. 108 | $groups = acf_get_field_groups(); 109 | // No FGs? No sync needed. 110 | if ( empty( $groups ) ) 111 | return false; 112 | 113 | $unsynced_groups = []; 114 | 115 | foreach ( $groups as $group ) 116 | { 117 | // TODO: double check acf_maybe_get 118 | 119 | // Not local? No sync needed. 120 | if ( !isset( $group['local'] ) ) 121 | continue; 122 | // Not json? No sync needed. 123 | if ( $group['local'] !== 'json' ) 124 | continue; 125 | // No id? Sync needed! 126 | if ( !$group['ID'] ) 127 | $unsynced_groups[ $group['key'] ] = $group; 128 | // Has ID, but modified date is newer than the db? Sync needed! 129 | if ( $group['ID'] && $group['modified'] && $group['modified'] > get_post_modified_time( 'U', true, $group['ID'], true ) ) 130 | $unsynced_groups[ $group['key'] ] = $group; 131 | } 132 | // Empty sync list? No sync needed. 133 | if ( empty( $unsynced_groups ) ) 134 | return false; 135 | 136 | return $unsynced_groups; 137 | } 138 | 139 | function aaw_sync_field_groups( $unsynced_groups = [] ) 140 | { 141 | // Required stuff copied from ACF. 142 | acf_disable_filters(); 143 | acf_enable_filter( 'local' ); 144 | acf_update_setting( 'json', false ); 145 | 146 | $synced_groups = []; 147 | 148 | foreach ( $unsynced_groups as $group ) 149 | { 150 | // Add the fields. 151 | $group['fields'] = acf_get_fields( $group ); 152 | // Sync. 153 | $synced_groups[] = acf_import_field_group( $group ); 154 | } 155 | 156 | return $synced_groups; 157 | } 158 | 159 | function aaw_act_on_removed_json() 160 | { 161 | $group_keys_db = aaw_get_field_group_keys_from_db(); 162 | 163 | if ( empty( $group_keys_db ) ) 164 | return; 165 | 166 | $group_keys_json = aaw_get_field_group_keys_from_json(); 167 | 168 | $group_keys_delete_from_db = array_diff( $group_keys_db, $group_keys_json ); 169 | 170 | if ( empty( $group_keys_delete_from_db ) ) 171 | return; 172 | 173 | aaw_delete_field_groups_from_db( $group_keys_delete_from_db ); 174 | 175 | global $aaw_feedback; 176 | $aaw_feedback['removed'] = ''.__( 'One or more ACF Field Groups were removed because their JSON cache was removed.', 'acf-agency-workflow' ).'
'; 177 | } 178 | 179 | function aaw_get_field_group_keys_from_db() 180 | { 181 | $post_ids = get_posts([ 182 | 'post_type' => 'acf-field-group', 183 | 'posts_per_page' => -1, 184 | 'post_status' => ['publish','acf-disabled'], 185 | 'fields' => 'ids', 186 | ]); 187 | 188 | if ( empty( $post_ids ) ) 189 | return []; 190 | 191 | $keys = []; 192 | 193 | foreach ( $post_ids as $post_id ) 194 | $keys[] = get_post_field( 'post_name', $post_id ); 195 | 196 | return $keys; 197 | } 198 | 199 | function aaw_get_field_group_keys_from_json() 200 | { 201 | $load_dirs = acf_get_setting('load_json'); 202 | 203 | if ( empty( $load_dirs ) ) 204 | return []; 205 | 206 | $keys = []; 207 | 208 | foreach ( $load_dirs as $load_dir ) 209 | { 210 | $json_files = glob( $load_dir.'/*.json' ); 211 | foreach ( $json_files as $json_file ) 212 | { 213 | // A. Actually check the key inside. 214 | // $group_json = file_get_contents( $json_file ); 215 | // $group = json_decode( $group_json ); 216 | // $keys[] = $group->key; 217 | 218 | // B. Take the filename. 219 | $keys[] = pathinfo( $json_file, PATHINFO_FILENAME ); 220 | } 221 | } 222 | 223 | return $keys; 224 | } 225 | 226 | function aaw_delete_field_groups_from_db( $keys = [] ) 227 | { 228 | if ( empty( $keys ) ) 229 | return; 230 | 231 | foreach ( $keys as $key ) 232 | acf_delete_field_group( $key ); 233 | } 234 | 235 | function aaw_delete_field_group_from_json( $key = false ) 236 | { 237 | if ( !$key ) 238 | return; 239 | 240 | $load_dirs = acf_get_setting('load_json'); 241 | 242 | if ( empty( $load_dirs ) ) 243 | return; 244 | 245 | foreach ( $load_dirs as $load_dir ) 246 | { 247 | $current_path = $load_dir.'/'.$key.'.json'; 248 | if ( file_exists( $current_path ) ) 249 | unlink( $current_path ); 250 | } 251 | } 252 | 253 | function aaw_plugin_main_file() 254 | { 255 | return dirname( __FILE__ ).'/acf-agency-workflow.php'; 256 | } 257 | 258 | function aaw_current_version() 259 | { 260 | return get_plugin_data( aaw_plugin_main_file(), false, false )['Version']; 261 | } 262 | 263 | function aaw_latest_version() 264 | { 265 | $repo = 'Brugman/acf-agency-workflow'; 266 | $latest_version_service = 'https://github-repo-latest-version.timbr.dev/'; 267 | 268 | $remote_data = @file_get_contents( $latest_version_service.$repo ); 269 | 270 | if ( !$remote_data ) 271 | return false; 272 | 273 | $remote_data = json_decode( $remote_data, true ); 274 | 275 | if ( isset( $remote_data['error'] ) ) 276 | return false; 277 | 278 | return $remote_data; 279 | } 280 | 281 | function aaw_new_version_available() 282 | { 283 | $current_version = aaw_current_version(); 284 | $latest_version_data = aaw_latest_version(); 285 | 286 | if ( !$latest_version_data ) 287 | return false; 288 | 289 | $latest_version = $latest_version_data['version']; 290 | 291 | if ( version_compare( $latest_version, $current_version ) > 0 ) 292 | return $latest_version_data; 293 | 294 | return false; 295 | } 296 | 297 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 |