diff --git a/.distignore b/.distignore index dcb821b..7e68d54 100644 --- a/.distignore +++ b/.distignore @@ -16,4 +16,12 @@ LICENSE.md license.txt phpcs.xml.dist phpunit.xml.dist +phpstan.neon.dist README.md +package.json +package-lock.json +node_modules +src +webpack.config.js +.eslintrc.js +.prettierrc.js diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..7a2b479 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + extends: [ 'plugin:@wordpress/eslint-plugin/recommended' ], + globals: { + jQuery: 'readonly', + }, +}; diff --git a/.gitignore b/.gitignore index 83579b7..250e7ce 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,14 @@ vendor/ logs *.log + +############ +## Build artifacts +############ + +*.map +*.asset.php +.build-entry.js +.build-script.js +.backup-source.js +.backup-source.css diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3888d75..518fca4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,6 +30,27 @@ It is recommended to run integration tests and PHPCodeSniffer locally before com * `vendor/bin/phpunit`: Run the integration tests. * `vendor/bin/phpcs`: Check against the WordPress Coding Standards. +### JavaScript and CSS Build Pipeline + +This plugin uses [@wordpress/scripts](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/) for building, linting, and minifying JavaScript and CSS assets. Both source and minified versions are located in `wp-multi-network/assets/`. + +To work with JavaScript and CSS: + +* `npm install`: Install Node.js dependencies (Node.js 20+ required) +* `npm run build`: Build minified production-ready assets from source files +* `npm run start`: Start development mode with file watching and hot reload +* `npm run lint:js`: Lint JavaScript files +* `npm run lint:css`: Lint CSS files +* `npm run format`: Auto-format JavaScript and CSS files + +The asset structure: +* **Source files** (edit these): `wp-multi-network.js`, `wp-multi-network.css`, `wp-multi-network-rtl.css` (unminified, for debugging) +* **Minified files** (auto-generated): `wp-multi-network.min.js`, `wp-multi-network.min.css`, `wp-multi-network-rtl.min.css` (production) + +The plugin automatically loads source files when `WP_SCRIPT_DEBUG` is enabled, otherwise it loads minified versions. + +**Important:** Edit the non-minified source files in `wp-multi-network/assets/`. The minified `.min.js` and `.min.css` files are automatically generated by the build process and should not be edited directly. + ### Writing Integration Tests * Integration tests go into the `tests/integration/tests` directory. diff --git a/package.json b/package.json new file mode 100644 index 0000000..5f944a2 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "wp-multi-network", + "version": "2.7.0", + "description": "A Network Management Interface for global administrators in WordPress Multisite installations", + "private": true, + "scripts": { + "build": "npm run backup-source && wp-scripts build && npm run rename-assets && npm run restore-source && npm run generate-rtl-source && npm run cleanup-assets", + "backup-source": "cp wp-multi-network/assets/js/wp-multi-network.js .backup-source.js && cp wp-multi-network/assets/css/wp-multi-network.css .backup-source.css", + "restore-source": "mv .backup-source.js wp-multi-network/assets/js/wp-multi-network.js && mv .backup-source.css wp-multi-network/assets/css/wp-multi-network.css", + "rename-assets": "mv wp-multi-network/assets/css/style-wp-multi-network.min.css wp-multi-network/assets/css/wp-multi-network.min.css 2>/dev/null || true && mv wp-multi-network/assets/css/style-wp-multi-network.min-rtl.css wp-multi-network/assets/css/wp-multi-network-rtl.min.css 2>/dev/null || true", + "generate-rtl-source": "npx rtlcss wp-multi-network/assets/css/wp-multi-network.css wp-multi-network/assets/css/wp-multi-network-rtl.css", + "cleanup-assets": "rm -f wp-multi-network/assets/js/*.asset.php", + "start": "wp-scripts start", + "lint:js": "wp-scripts lint-js wp-multi-network/assets/js/", + "lint:css": "wp-scripts lint-style wp-multi-network/assets/css/", + "format": "wp-scripts format wp-multi-network/assets/", + "check-engines": "wp-scripts check-engines", + "check-licenses": "wp-scripts check-licenses" + }, + "devDependencies": { + "@babel/runtime": "^7.28.4", + "@wordpress/scripts": "^28.0.0" + }, + "engines": { + "node": ">=20.0.0", + "npm": ">=10.0.0" + } +} diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..0cdbab8 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,63 @@ +/** + * Custom Webpack Configuration for WP Multi Network + * + * This configuration extends @wordpress/scripts default webpack config + * to customize the output location and naming of built assets. + * + * NOTE: Due to how webpack handles CSS extraction from JavaScript imports, + * the CSS files are generated with auto-generated chunk names that include + * a 'style-' prefix (e.g., 'style-wp-multi-network.css'). To maintain + * backward compatibility with the existing plugin structure, we use a + * post-build rename script (see package.json 'rename-assets') to rename + * these files to match the expected names ('wp-multi-network.css'). + * + * @see https://webpack.js.org/configuration/ + */ + +const defaultConfig = require( '@wordpress/scripts/config/webpack.config' ); +const MiniCssExtractPlugin = require( 'mini-css-extract-plugin' ); +const RtlCssPlugin = require( 'rtlcss-webpack-plugin' ); +const path = require( 'path' ); + +/** + * Helper function to generate CSS filename based on chunk name + * + * @param {Object} pathData - Webpack path data object + * @param {string} suffix - Optional suffix for the filename (e.g., '-rtl') + * @return {string} The CSS filename + */ +function getCssFilename( pathData, suffix = '' ) { + if ( pathData.chunk && pathData.chunk.name ) { + // The chunk name includes '.min', so we just add the suffix and extension + return 'css/' + pathData.chunk.name + suffix + '.css'; + } + return 'css/[name]' + suffix + '.css'; +} + +module.exports = { + ...defaultConfig, + entry: { + 'wp-multi-network.min': path.resolve( process.cwd(), '.build-script.js' ), + }, + output: { + path: path.resolve( process.cwd(), 'wp-multi-network/assets' ), + filename: 'js/[name].js', + }, + plugins: [ + // Remove default CSS plugins and replace with custom configuration + // to place CSS files in the css/ subdirectory with proper naming. + // @wordpress/scripts generates CSS chunks with auto-generated names + // (e.g., 'style-wp-multi-network') which we need to customize. + ...defaultConfig.plugins.filter( + ( plugin ) => + ! ( plugin instanceof MiniCssExtractPlugin ) && + ! ( plugin instanceof RtlCssPlugin ) + ), + new MiniCssExtractPlugin( { + filename: ( pathData ) => getCssFilename( pathData ), + } ), + new RtlCssPlugin( { + filename: ( pathData ) => getCssFilename( pathData, '-rtl' ), + } ), + ], +}; diff --git a/wp-multi-network/assets/css/wp-multi-network-rtl.css b/wp-multi-network/assets/css/wp-multi-network-rtl.css new file mode 100644 index 0000000..110ecab --- /dev/null +++ b/wp-multi-network/assets/css/wp-multi-network-rtl.css @@ -0,0 +1,156 @@ +th.column-title { + width: 35%; +} +th.column-path { + width: 15%; +} +th.column-blogs { + width: 10%; +} +td.column-domain { + white-space: nowrap; + overflow: hidden; +} + +.edit-network label span.scheme { + display: inline-block; +} + +.edit-network .form-field label input[type="text"] { + width: 50%; + display: inline-block; +} + +#poststuff #wpmn-edit-network-assign-sites .inside { + margin: 0; + padding: 0; +} + +#wpmn-edit-network-publish .submitbox, +#wpmn-move-site-publish .submitbox { + margin: 10px -12px -12px; +} + +#wpmn-edit-network-publish #major-publishing-actions, +#wpmn-move-site-publish #major-publishing-actions { + margin-top: 10px; +} + +#misc-publishing-actions #network:before, +#misc-publishing-actions #sites:before { + font: normal 20px/1 dashicons; + speak: none; + display: inline-block; + padding: 0 0 0 2px; + top: 0; + right: -1px; + position: relative; + vertical-align: top; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-decoration: none !important; +} + +#misc-publishing-actions #network:before { + content: '\f319'; +} + +#misc-publishing-actions #sites:before { + content: '\f325'; +} + +table.widefat.move-site, +table.widefat.assign-sites { + border: none; + box-shadow: none; +} + +table.move-site thead th, +table.assign-sites thead th { + text-align: center; + font-size: 13px; + font-weight: 600; + background: #f4f4f4; +} + +.networks tbody td, +.networks tbody th { + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .1); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .1); + padding: 10px 9px; +} + +.networks .current th.check-column { + border-right: 4px solid #00a0d2; +} + +.networks .current td, +.networks .current th { + background-color: #f7fcfe; + padding: 10px 9px; +} + +table.assign-sites select[multiple] { + width: 100%; + height: 80px !important; + margin: 0; +} + +table.assign-sites .button { + margin: 0 5px; + width: 35%; + height: 80px; +} + +table.assign-sites td.column-actions { + width: 16%; + padding: 8px 0; +} + +table.assign-sites td.column-actions .assign { + float: right; + content: "\f341"; +} + +table.assign-sites td.column-actions .unassign { + float: left; +} + +table.assign-sites td.column-available { + padding-left: 0; +} + +table.assign-sites td.column-assigned { + padding-right: 0; +} +table.assign-sites td.column-available, +table.assign-sites td.column-assigned { + width: 42%; +} + +table.assign-sites td.column-assigned { + text-align: left; +} + +div.network-delete { + background: #fff; + border-right: 4px solid #dc3232; + -webkit-box-shadow: 0 1px 1px 0 rgba( 0, 0, 0, 0.1 ); + box-shadow: 0 1px 1px 0 rgba( 0, 0, 0, 0.1 ); + margin: 5px 0 2px; + padding: 1px 12px; +} + +div.network-delete p { + margin: 0.5em 0; + padding: 2px; +} + +ul.delete-sites { + list-style: square; + margin: 10px 20px; +} + +ul.delete-sites li { + margin: 0; +} diff --git a/wp-multi-network/assets/css/wp-multi-network.min-rtl.css b/wp-multi-network/assets/css/wp-multi-network.min-rtl.css new file mode 100644 index 0000000..b0b73d2 --- /dev/null +++ b/wp-multi-network/assets/css/wp-multi-network.min-rtl.css @@ -0,0 +1 @@ +th.column-title{width:35%}th.column-path{width:15%}th.column-blogs{width:10%}td.column-domain{overflow:hidden;white-space:nowrap}.edit-network label span.scheme{display:inline-block}.edit-network .form-field label input[type=text]{display:inline-block;width:50%}#poststuff #wpmn-edit-network-assign-sites .inside{margin:0;padding:0}#wpmn-edit-network-publish .submitbox,#wpmn-move-site-publish .submitbox{margin:10px -12px -12px}#wpmn-edit-network-publish #major-publishing-actions,#wpmn-move-site-publish #major-publishing-actions{margin-top:10px}#misc-publishing-actions #network:before,#misc-publishing-actions #sites:before{font:normal 20px/1 dashicons;speak:none;display:inline-block;right:-1px;padding:0 0 0 2px;position:relative;top:0;vertical-align:top;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-decoration:none!important}#misc-publishing-actions #network:before{content:"\f319"}#misc-publishing-actions #sites:before{content:"\f325"}table.widefat.assign-sites,table.widefat.move-site{border:none;box-shadow:none}table.assign-sites thead th,table.move-site thead th{background:#f4f4f4;font-size:13px;font-weight:600;text-align:center}.networks tbody td,.networks tbody th{box-shadow:inset 0 -1px 0 rgba(0,0,0,.1);padding:10px 9px}.networks .current th.check-column{border-right:4px solid #00a0d2}.networks .current td,.networks .current th{background-color:#f7fcfe;padding:10px 9px}table.assign-sites select[multiple]{height:80px!important;margin:0;width:100%}table.assign-sites .button{height:80px;margin:0 5px;width:35%}table.assign-sites td.column-actions{padding:8px 0;width:16%}table.assign-sites td.column-actions .assign{content:"\f341";float:right}table.assign-sites td.column-actions .unassign{float:left}table.assign-sites td.column-available{padding-left:0}table.assign-sites td.column-assigned{padding-right:0}table.assign-sites td.column-assigned,table.assign-sites td.column-available{width:42%}table.assign-sites td.column-assigned{text-align:left}div.network-delete{background:#fff;border-right:4px solid #dc3232;box-shadow:0 1px 1px 0 rgba(0,0,0,.1);margin:5px 0 2px;padding:1px 12px}div.network-delete p{margin:.5em 0;padding:2px}ul.delete-sites{list-style:square;margin:10px 20px}ul.delete-sites li{margin:0} diff --git a/wp-multi-network/assets/css/wp-multi-network.min.css b/wp-multi-network/assets/css/wp-multi-network.min.css new file mode 100644 index 0000000..175a66d --- /dev/null +++ b/wp-multi-network/assets/css/wp-multi-network.min.css @@ -0,0 +1 @@ +th.column-title{width:35%}th.column-path{width:15%}th.column-blogs{width:10%}td.column-domain{overflow:hidden;white-space:nowrap}.edit-network label span.scheme{display:inline-block}.edit-network .form-field label input[type=text]{display:inline-block;width:50%}#poststuff #wpmn-edit-network-assign-sites .inside{margin:0;padding:0}#wpmn-edit-network-publish .submitbox,#wpmn-move-site-publish .submitbox{margin:10px -12px -12px}#wpmn-edit-network-publish #major-publishing-actions,#wpmn-move-site-publish #major-publishing-actions{margin-top:10px}#misc-publishing-actions #network:before,#misc-publishing-actions #sites:before{font:normal 20px/1 dashicons;speak:none;display:inline-block;left:-1px;padding:0 2px 0 0;position:relative;top:0;vertical-align:top;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-decoration:none!important}#misc-publishing-actions #network:before{content:"\f319"}#misc-publishing-actions #sites:before{content:"\f325"}table.widefat.assign-sites,table.widefat.move-site{border:none;box-shadow:none}table.assign-sites thead th,table.move-site thead th{background:#f4f4f4;font-size:13px;font-weight:600;text-align:center}.networks tbody td,.networks tbody th{box-shadow:inset 0 -1px 0 rgba(0,0,0,.1);padding:10px 9px}.networks .current th.check-column{border-left:4px solid #00a0d2}.networks .current td,.networks .current th{background-color:#f7fcfe;padding:10px 9px}table.assign-sites select[multiple]{height:80px!important;margin:0;width:100%}table.assign-sites .button{height:80px;margin:0 5px;width:35%}table.assign-sites td.column-actions{padding:8px 0;width:16%}table.assign-sites td.column-actions .assign{content:"\f341";float:left}table.assign-sites td.column-actions .unassign{float:right}table.assign-sites td.column-available{padding-right:0}table.assign-sites td.column-assigned{padding-left:0}table.assign-sites td.column-assigned,table.assign-sites td.column-available{width:42%}table.assign-sites td.column-assigned{text-align:right}div.network-delete{background:#fff;border-left:4px solid #dc3232;box-shadow:0 1px 1px 0 rgba(0,0,0,.1);margin:5px 0 2px;padding:1px 12px}div.network-delete p{margin:.5em 0;padding:2px}ul.delete-sites{list-style:square;margin:10px 20px}ul.delete-sites li{margin:0} diff --git a/wp-multi-network/assets/js/wp-multi-network.js b/wp-multi-network/assets/js/wp-multi-network.js index 0d11752..a4794fa 100644 --- a/wp-multi-network/assets/js/wp-multi-network.js +++ b/wp-multi-network/assets/js/wp-multi-network.js @@ -1,48 +1,42 @@ -jQuery( document ).ready( - function ( $ ) { +/** + * WP Multi Network Admin Script + */ - $( '.if-js-closed' ) - .removeClass( 'if-js-closed' ) - .addClass( 'closed' ); - $( '.postbox' ).children( 'h3' ).click( - function () { - if ( $( this.parentNode ).hasClass( 'closed' ) ) { - $( this.parentNode ).removeClass( 'closed' ); - } else { - $( this.parentNode ).addClass( 'closed' ); - } - } - ); +jQuery( document ).ready( function ( $ ) { + $( '.if-js-closed' ).removeClass( 'if-js-closed' ).addClass( 'closed' ); - /* Handle clicks to add/remove sites to/from selected list */ - $( 'input[name=assign]' ).click( - function () { - move( 'from', 'to' ); - } - ); + $( '.postbox' ) + .children( 'h3' ) + .click( function () { + if ( $( this.parentNode ).hasClass( 'closed' ) ) { + $( this.parentNode ).removeClass( 'closed' ); + } else { + $( this.parentNode ).addClass( 'closed' ); + } + } ); - $( 'input[name=unassign]' ).click( - function () { - move( 'to', 'from' ); - } - ); + /* Handle clicks to add/remove sites to/from selected list */ + $( 'input[name=assign]' ).click( function () { + move( 'from', 'to' ); + } ); - /* Select all sites in "selected" box when submitting */ - $( '#edit-network-form' ).submit( - function () { - $( '#to' ).children( 'option:enabled' ).attr( 'selected', true ); - $( '#from' ).children( 'option:enabled' ).attr( 'selected', true ); - } - ); + $( 'input[name=unassign]' ).click( function () { + move( 'to', 'from' ); + } ); - function move( from, to ) { - jQuery( '#' + from ).children( 'option:selected' ).each( - function () { - jQuery( '#' + to ).append( jQuery( this ).clone() ); - jQuery( this ).remove(); - } - ); - } + /* Select all sites in "selected" box when submitting */ + $( '#edit-network-form' ).submit( function () { + $( '#to' ).children( 'option:enabled' ).attr( 'selected', true ); + $( '#from' ).children( 'option:enabled' ).attr( 'selected', true ); + } ); + + function move( from, to ) { + jQuery( '#' + from ) + .children( 'option:selected' ) + .each( function () { + jQuery( '#' + to ).append( jQuery( this ).clone() ); + jQuery( this ).remove(); + } ); } -); +} ); diff --git a/wp-multi-network/assets/js/wp-multi-network.min.js b/wp-multi-network/assets/js/wp-multi-network.min.js new file mode 100644 index 0000000..c004779 --- /dev/null +++ b/wp-multi-network/assets/js/wp-multi-network.min.js @@ -0,0 +1 @@ +(()=>{var e={822:()=>{jQuery(document).ready(function(e){function t(e,t){jQuery("#"+e).children("option:selected").each(function(){jQuery("#"+t).append(jQuery(this).clone()),jQuery(this).remove()})}e(".if-js-closed").removeClass("if-js-closed").addClass("closed"),e(".postbox").children("h3").click(function(){e(this.parentNode).hasClass("closed")?e(this.parentNode).removeClass("closed"):e(this.parentNode).addClass("closed")}),e("input[name=assign]").click(function(){t("from","to")}),e("input[name=unassign]").click(function(){t("to","from")}),e("#edit-network-form").submit(function(){e("#to").children("option:enabled").attr("selected",!0),e("#from").children("option:enabled").attr("selected",!0)})})}},t={};function o(n){var r=t[n];if(void 0!==r)return r.exports;var s=t[n]={exports:{}};return e[n](s,s.exports,o),s.exports}o.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return o.d(t,{a:t}),t},o.d=(e,t)=>{for(var n in t)o.o(t,n)&&!o.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},o.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{"use strict";o(822)})()})(); \ No newline at end of file diff --git a/wp-multi-network/includes/classes/class-wp-ms-networks-admin.php b/wp-multi-network/includes/classes/class-wp-ms-networks-admin.php index c3a0a75..afec0e6 100644 --- a/wp-multi-network/includes/classes/class-wp-ms-networks-admin.php +++ b/wp-multi-network/includes/classes/class-wp-ms-networks-admin.php @@ -165,8 +165,12 @@ public function enqueue_scripts( $page = '' ) { return; } - wp_register_style( 'wp-multi-network', wpmn()->plugin_url . 'assets/css/wp-multi-network.css', array(), wpmn()->asset_version ); - wp_register_script( 'wp-multi-network', wpmn()->plugin_url . 'assets/js/wp-multi-network.js', array( 'jquery', 'post' ), wpmn()->asset_version, true ); + // Determine if we should load source or minified assets based on WP_SCRIPT_DEBUG. + $suffix = ( defined( 'WP_SCRIPT_DEBUG' ) && WP_SCRIPT_DEBUG ) ? '' : '.min'; + $asset_version = ( defined( 'WP_SCRIPT_DEBUG' ) && WP_SCRIPT_DEBUG ) ? time() : wpmn()->asset_version; + + wp_register_style( 'wp-multi-network', wpmn()->plugin_url . 'assets/css/wp-multi-network' . $suffix . '.css', array(), $asset_version ); + wp_register_script( 'wp-multi-network', wpmn()->plugin_url . 'assets/js/wp-multi-network' . $suffix . '.js', array( 'jquery', 'post' ), $asset_version, true ); wp_enqueue_style( 'wp-multi-network' ); wp_enqueue_script( 'wp-multi-network' );