Compare commits

...

320 Commits

Author SHA1 Message Date
Mike Wilkerson 37dd5688cb Release 5.0.0-alpha2
fix zip bundle
2024-08-09 17:47:27 -07:00
Mike Wilkerson c9ae274579 rebuild admin bundle 2024-08-09 17:47:04 -07:00
Mike Wilkerson b691390264 remove unused var 2024-08-09 17:31:01 -07:00
Mike Wilkerson 36c1e8f77b add eslint dep for react 2024-08-09 17:30:57 -07:00
Mike Wilkerson 316c58a7df fix lodash dev deps for jest tests 2024-08-09 17:27:20 -07:00
Mike Wilkerson d32cbd07ce run tests on push to any branc 2024-08-09 16:46:59 -07:00
Mike Wilkerson dbb8091b2f prepare for 5.0.0-alpha1 2024-08-09 16:44:11 -07:00
Mike Wilkerson 1a07d19a32
Bug fixes and cleanup (#225)
* wire-up faApiUrl to front end for icon chooser

- also, auto-formatting

* refactor queryCache

* refactor building prefixed keys

* implement clearQueryCache()

* run clearQueryCache() when fetching kits data

* remove console log

* make access token refresh more robust

* refactor access token getter code

* ignore .zed project settings

* add comments

* remove unused

* update phpactor exclusions

* auto-formatting

* sniff fixes

* update phpactor config

* add return type

* adding some more return types

* update doc

* throw NoAccessTokenException

* indicate internal-onlyuse

* remove obsolete code

* improve classic editor TinyMCE plugin, toward 1:1 relationship between icon chooser and TinyMCE instance

* remove obsolete injection of icon chooser container div for classic editor

* update plugin-nu description and version

* disable phpactor phpcs

* distinguish between TinyMCE editors by editor_id

* increase height of property section on style modal to better fit custom color picker

...and auto-formatting

* initial re-write of self-hosting exception messages.

* include missing algo name in exception message

* auto-formatting and cleaning

* update all JS bundles to use lodash-es deep imports to avoid overwriting global lodash

* rebuild block-editor bundle

* rebuild classic-editor bundle

* rebuild icon-chooser bundle

* rebuild admin bundle

* auto-formatting

* update more admin bundle lodash exports to use lodash-es

* rebuild admin bundle

* change the timing of when to load block assets

solves the problem with the global lodash _ being reset to undefined

* rearrange

* auto-formatting

* manual formatting of doc comments

* add block_init.php to phpcs config

* add back a missing lodash has import

* rebuild admin bundle

* in admin bundle, migrate to using the lodash that ships with WordPress

* update block-editor to use the WordPress built-in lodash

* rebuild admin bundle

* use WP built-in lodash for icon-chooser bundle

* use WP built-in lodash for classic-editor bundle

* make the retrieval of an access token conditional on using a kit

* rebuild icon-chooser bundle

* remove custom fixups for globals like lodash and moment

now that we're using @wordpress/scripts more conventionally, we can rely
on those to externalize such libraries and ensure that our bundles don't
load conflicting globals

* WIP: fixing up phpcs errors and warnings

* rename block init file to satisfy phpcs
2024-08-09 16:41:44 -07:00
Mike Wilkerson 91876e08a9 update package-lock.json for each js bundle 2024-08-06 09:21:59 -07:00
Mike Wilkerson 88cc4e5665 rebuild docs 2024-08-05 16:35:09 -07:00
Mike Wilkerson 8243353436 bump version to 5.0.0-alpha1 2024-08-05 16:34:21 -07:00
Mike Wilkerson edf482a028 update icon-chooser bundle version and build 2024-08-05 16:29:44 -07:00
Mike Wilkerson f6e59f5ec9 update classic-editor bundle version 2024-08-05 16:29:03 -07:00
Mike Wilkerson ef1742dc58 update block-editor bundle version 2024-08-05 16:28:36 -07:00
Mike Wilkerson 20063c1d07 update admin bundle version and build 2024-08-05 16:28:03 -07:00
Mike Wilkerson aa3b839563 load classic-editor bundle only when tiny_mce is being loaded 2024-08-02 16:53:53 -07:00
Mike Wilkerson 21ac410c06 register js bundle scripts before enqueue 2024-08-02 16:34:10 -07:00
Mike Wilkerson 1e244800b8 remove compat-js bundle 2024-08-02 16:00:19 -07:00
Mike Wilkerson 3e35bb8542 remove obsolete dist packing code 2024-08-02 15:59:57 -07:00
Mike Wilkerson 6bc6fcc73e rebuild admin bundle 2024-08-02 15:46:58 -07:00
Mike Wilkerson fe4caa6f05 remove obsolete enableIconChooser 2024-08-02 15:46:22 -07:00
Mike Wilkerson e2a381aa46 update min WP and PHP requirements 2024-08-02 15:45:02 -07:00
Mike Wilkerson 41916f26de update version for conflict detection script 2024-08-02 15:39:51 -07:00
Mike Wilkerson 9144938132 rebuild admin bundle 2024-08-02 15:38:36 -07:00
Mike Wilkerson e6a1e3fd7e remove v3deprecation code from back end 2024-08-02 15:38:30 -07:00
Mike Wilkerson a34bb86e05 remove v3deprecation code from admin bundle 2024-08-02 15:38:16 -07:00
Mike Wilkerson d4a129ce11 Add a try again message for token endpoint failure 2024-08-02 12:26:55 -07:00
Mike Wilkerson 66c7f56f73 update release steps in DEVELOPMENT.md 2024-08-02 12:06:14 -07:00
Mike Wilkerson f80232bdda update composer.json for release build steps 2024-08-02 12:03:46 -07:00
Mike Wilkerson c98bee8f72 update dist zip-building script 2024-08-02 11:56:33 -07:00
Mike Wilkerson 72c7145183 phpcs cleanup 2024-08-01 17:32:33 -07:00
Mike Wilkerson 6401f05187 add self-hosting exceptions 2024-08-01 17:31:25 -07:00
Mike Wilkerson afab9e1921 use wp_mkdir_p for creating the svg styles subdir 2024-08-01 17:03:47 -07:00
Mike Wilkerson 97ab23c8dd ignore phpcs warning for file system function in test code 2024-08-01 16:10:15 -07:00
Mike Wilkerson 9cd170dda9 migrate to using WP_Filesystem methods for filesystem access 2024-08-01 16:05:46 -07:00
Mike Wilkerson 4023b2a951 auto-formatting 2024-08-01 16:05:26 -07:00
Mike Wilkerson 5212553aa0 add some more exits if accessed directly 2024-08-01 15:53:12 -07:00
Mike Wilkerson a49c6d131e maybe exit before sourcing in other code 2024-08-01 15:50:51 -07:00
Mike Wilkerson 64c99ca121 use php8.2 for phpcs 2024-08-01 14:46:16 -07:00
Mike Wilkerson 52de0c2334 phpcs cleanup 2024-07-31 11:43:30 -07:00
Mike Wilkerson 2393f726e3 phpcs fixes 2024-07-30 17:24:39 -07:00
Mike Wilkerson 3b39366491 fix loader test to run on init 2024-07-30 17:03:35 -07:00
Mike Wilkerson 45828de260 remove configurations for obsolete versions of php or WordPress 2024-07-30 16:22:31 -07:00
Mike Wilkerson c9ffc2c946 auto-formatting 2024-07-30 16:12:51 -07:00
Mike Wilkerson 7be40f4f4f TEMP: disable phpcbf for font-awesome.php 2024-07-30 16:11:58 -07:00
Mike Wilkerson 95247b62e5 auto-formatting 2024-07-30 15:28:08 -07:00
Mike Wilkerson cdf1856b01 update deps in composer.json 2024-07-30 15:27:44 -07:00
Mike Wilkerson 880999413c add mock to config controller test
and auto-format
2024-07-30 15:21:12 -07:00
Mike Wilkerson 84576b67f6 return svg styles module as a singleton to make it mockable
also, auto-formatting
2024-07-30 15:20:23 -07:00
Mike Wilkerson 4c6b22feb9 rebuild block-editor bundle 2024-07-30 14:03:16 -07:00
Frances Botsford 1d129cb40e update block add hover preview 2024-07-30 15:59:18 -04:00
Mike Wilkerson c42981c20c rebuild block-editor bundle with example 2024-07-30 15:44:48 -04:00
Mike Wilkerson 62cb96ab26 wire-up an example in JS 2024-07-30 15:43:40 -04:00
Mike Wilkerson 3bb1f4d27c add phpactor config for neovim 2024-07-28 17:33:38 -07:00
Mike Wilkerson 41b3d793c6 gitignore php-cs-fixer cache 2024-07-28 17:33:08 -07:00
Mike Wilkerson df5b707106 move phpcs def to help with auto-formatting in neovim 2024-07-28 17:32:29 -07:00
Mike Wilkerson 141e1cd6f9 php auto-formatting 2024-07-27 17:25:43 -07:00
Mike Wilkerson bfdba35325 phpcbf auto-formatting 2024-07-27 17:15:00 -07:00
Mike Wilkerson b333c00625 auto-formatting classic-editor source 2024-07-27 17:08:54 -07:00
Mike Wilkerson dde37f05c5 add prettierrc for classic-editor bundle 2024-07-27 17:08:22 -07:00
Mike Wilkerson b60c166dce rebuild admin bundle 2024-07-27 17:06:37 -07:00
Mike Wilkerson e9f6c1e58b auto-formatting admin bundle 2024-07-27 17:06:11 -07:00
Mike Wilkerson 5753c7ec5a add prettierrc to admin bundle 2024-07-27 17:05:37 -07:00
Mike Wilkerson 321a025a05 rebuild block-editor bundle 2024-07-26 17:34:30 -07:00
Mike Wilkerson caadb6667f fix deprecation warning by using ToolbarDropdownMenu 2024-07-26 17:33:46 -07:00
Mike Wilkerson 1bcaca0fa1 rebuild block-editor bundle 2024-07-26 17:29:51 -07:00
Mike Wilkerson 1c2df5bf08 more linting 2024-07-26 17:14:54 -07:00
Mike Wilkerson 863e59397b WIP: clean up and linting 2024-07-26 17:14:08 -07:00
Mike Wilkerson ad7f8a59b2 WIP: fix linting and isAnimationSelected for No Animation 2024-07-26 17:09:29 -07:00
Mike Wilkerson aa4475b447 remove obsolete iconSizer 2024-07-26 17:03:29 -07:00
Mike Wilkerson f2c93369a6 WIP: fixing linter warnings and adding i18n 2024-07-26 17:02:35 -07:00
Mike Wilkerson 87cacd81f8 config prettier and auto-format 2024-07-26 16:53:23 -07:00
Mike Wilkerson c68c10d76d rebuild block-editor bundle 2024-07-26 15:39:24 -07:00
Mike Wilkerson 73491d742c improve loading of CSS 2024-07-26 15:38:21 -07:00
Frances Botsford c7ec2080f6 adjust alignment toolbar buttons 2024-07-26 17:13:30 -04:00
Mike Wilkerson 893273cb80 rebuild block-editor bundle 2024-07-26 14:06:14 -07:00
Mike Wilkerson 48e4024bfd add justification attribute to block icon 2024-07-26 14:05:56 -07:00
Mike Wilkerson dd906264f2 add @wordpress/icons to package.json 2024-07-26 14:01:49 -07:00
Mike Wilkerson f8396c5b85 rebuild block-editor bundle 2024-07-26 14:00:26 -07:00
Mike Wilkerson d8f1b7447f add alignment dropdown menu 2024-07-26 14:00:10 -07:00
Mike Wilkerson f692699109 make flip reset match rotation reset, having no selection indicator 2024-07-26 11:18:11 -07:00
Mike Wilkerson c10a330178 rebuild block-editor bundle 2024-07-26 11:00:40 -07:00
Mike Wilkerson 80702268a1 remove obsolete commented code 2024-07-26 10:54:33 -07:00
Mike Wilkerson a68f16a98c remove some "supports" from block.json 2024-07-26 10:10:58 -07:00
Mike Wilkerson f5220d9a74 update deriveAttributes to use allow list for picking style properties
and update the corresponding transform allow list processing
2024-07-26 09:04:03 -07:00
Mike Wilkerson 3578a13834 rebuild block-editor bundle 2024-07-25 17:19:41 -07:00
Mike Wilkerson baac7032c6 fix derivation of fontSize attribute for re-styling sized richText icon 2024-07-25 17:18:44 -07:00
Mike Wilkerson fb7a71c58f rebuild block-editor bundle 2024-07-25 17:10:02 -07:00
Mike Wilkerson 5e913a6c96 tweak theme alpha functions test code 2024-07-25 17:09:30 -07:00
Mike Wilkerson ba92440749 change enqueue rules for svg styles
load it only when tech is webfont or we're skipping the kit load
2024-07-25 17:09:11 -07:00
Mike Wilkerson 39c3df9e21 refactor init and conditional loading of block assets
- refactor is_gutenberge_page to be useful elsewhere
- register svg styles separately, the one place where there's concern about
  cdn vs selfhost
- make svg-with-js.css a dependence of the editorStyles of block-edit so
  those styles are loaded within the content iframe of the block editor
- on the front end load svg styles for webfont kit, but not svg kit
2024-07-25 16:55:15 -07:00
Mike Wilkerson 664a724a19 add inline_style registration of base styles for both block and rich text icons 2024-07-25 12:35:32 -07:00
Mike Wilkerson 4074e8669e fix building and loading of editorStyles 2024-07-25 12:34:42 -07:00
Mike Wilkerson 3abdabaed9 tweak metadata names in block registration 2024-07-25 10:56:55 -07:00
Mike Wilkerson f0327d5540 rebuild block-editor bundle 2024-07-25 10:47:33 -07:00
Mike Wilkerson 0d308439c4 update rich text class 2024-07-25 10:47:10 -07:00
Frances Botsford b9ecd57f11 fixup on popover, adjust preview w background 2024-07-25 13:44:49 -04:00
Frances Botsford 90b1ffc264 remove redundant size header 2024-07-25 13:43:51 -04:00
Frances Botsford 20641d924f adjust class names, cleanup, and add close to color picker 2024-07-25 13:28:20 -04:00
Mike Wilkerson 8613cabcba rebuild block-editor bundle 2024-07-25 10:09:08 -07:00
Mike Wilkerson d72c4ad9c8 rebuild block-editor bundle 2024-07-25 10:07:52 -07:00
Mike Wilkerson 2bb4489acf add common wrapper class to block icon 2024-07-25 10:07:52 -07:00
Mike Wilkerson cc34b1fbdc add common class to rich text wrapper element 2024-07-25 10:07:52 -07:00
Mike Wilkerson 4ecf12352d change the rich text icon class name 2024-07-25 10:07:52 -07:00
Mike Wilkerson 48c2548984 rebuild block-editor bundle 2024-07-25 10:07:52 -07:00
Mike Wilkerson fe5103946a include backgroundColor in context passed into the modal 2024-07-25 10:07:52 -07:00
Mike Wilkerson 7778111a5b fix flying t-shirt size highlights by moving modal out of the popover 2024-07-25 10:07:52 -07:00
Mike Wilkerson f8a9cf7d2d remove default fontSize setting in inline style 2024-07-25 10:07:52 -07:00
Mike Wilkerson e2084b9f73 remove obsolete fa-icon-chooser-react dep from admin bundle 2024-07-25 10:07:52 -07:00
Mike Wilkerson 7abe59d3de remove obsolete webpack config comments 2024-07-25 10:07:52 -07:00
Frances Botsford 4e8145805b position custom color picker 2024-07-25 09:31:42 -04:00
Mike Wilkerson f0845edadf rebuild block-editor bundle 2024-07-24 18:53:04 -07:00
Mike Wilkerson b35480f31e remove popoverAnchor fly-away hack now that we have isObjectActive 2024-07-24 18:51:54 -07:00
Mike Wilkerson b65140f1b5 rebuild block-editor bundle 2024-07-24 17:41:03 -07:00
Mike Wilkerson 2ab384e97c change insert/replace logic with isObjectActive 2024-07-24 17:40:46 -07:00
Mike Wilkerson bb6e547c42 fix icon replacement: distinguishing between initial placement versus style update 2024-07-24 16:56:33 -07:00
Mike Wilkerson dd96275149 change how rich text icons are inserted so as not to overwrite other text 2024-07-24 16:43:14 -07:00
Mike Wilkerson 4586a86a43 set isActive for the toolbar button based on isFormatIconFocused 2024-07-24 16:25:58 -07:00
Mike Wilkerson 3fa5a2edde move rich text toolbar button
also fix corner case where replacement was undefined
2024-07-24 16:21:30 -07:00
Mike Wilkerson 2664ee7715 rebuild block-editor bundle 2024-07-24 14:47:46 -07:00
Mike Wilkerson e3527d94f6 fix change of icon after richText icon attributes reorg 2024-07-24 14:47:01 -07:00
Mike Wilkerson a7a8b1e24b reorganize attributes state for richText icon 2024-07-24 14:44:58 -07:00
Mike Wilkerson fe60c40aa6 rebuild block-editor bundle 2024-07-24 13:26:30 -07:00
Mike Wilkerson be70bee04d pass through context to IconModifier from richText 2024-07-24 13:25:56 -07:00
Mike Wilkerson d6caae06c2 fix popover anchor when block has only a rich text icon 2024-07-24 12:33:03 -07:00
Mike Wilkerson c2bd4366f7 remove obsolete css file 2024-07-24 11:20:45 -07:00
Mike Wilkerson 7d1b97b8e9 guard invocations of md5(): require only string arg 2024-07-24 10:35:48 -07:00
Mike Wilkerson 2a23c456fa rebuild block-editor bundle 2024-07-24 09:58:22 -07:00
Mike Wilkerson 65a9536b3b fix color picker and its default 2024-07-24 09:58:07 -07:00
Mike Wilkerson df97a4e410 rebuild block-editor bundle 2024-07-24 09:32:07 -07:00
Mike Wilkerson 6c902672ba fix color picker logic 2024-07-24 09:31:48 -07:00
Mike Wilkerson 55a6072f07 rebuild block-editor bundle 2024-07-24 09:06:01 -07:00
Mike Wilkerson 3c231ec818 fix aria-selected for colors and import ColorPicker 2024-07-24 09:05:34 -07:00
Frances Botsford 5502c0bc3c color palette styling 2024-07-24 09:10:25 -04:00
Mike Wilkerson a5b20dd3fb rebuild classic-editor bundle 2024-07-23 21:36:08 -07:00
Mike Wilkerson 6aec76eea1 migrate classic-editor React mount to use createRoot 2024-07-23 21:35:42 -07:00
Mike Wilkerson 12f21799a5 rebuild admin bundle 2024-07-23 21:33:09 -07:00
Mike Wilkerson 5e7803ddbf re-work conditional enqueue of kit and associated conflict detection loading 2024-07-23 21:29:22 -07:00
Mike Wilkerson ff9ed748d2 migrate conflict detection React mount to use createRoot() 2024-07-23 21:27:45 -07:00
Mike Wilkerson 6b9813da3e migrate admin bundle to use createRoot() 2024-07-23 21:02:40 -07:00
Mike Wilkerson 69a3598bd2 remove integrity_key condition 2024-07-23 20:48:03 -07:00
Mike Wilkerson d84e9d895f add filters the alpha theme 2024-07-23 20:23:10 -07:00
Mike Wilkerson cb273214a1 complete initial happy path wire-up of loading additional svg styles 2024-07-23 20:21:26 -07:00
Mike Wilkerson 27d59a1133 WIP: re-work setup for loading additional svg support styles 2024-07-23 18:03:50 -07:00
Mike Wilkerson 3401eb8209 add pict model 2024-07-23 17:12:05 -07:00
Mike Wilkerson 7b935a35a7 WIP: initial wire-up of setup_selfhosting for svg support styles 2024-07-23 15:30:49 -07:00
Mike Wilkerson d927fef895 rebuild block-editor bundle 2024-07-23 13:24:32 -07:00
Mike Wilkerson 6595a6b79a refactor size setting 2024-07-23 13:24:13 -07:00
Mike Wilkerson e400876263 initial wire-up of size changing 2024-07-23 13:19:38 -07:00
Mike Wilkerson f8cd72d6b9 refactor skip_enqeue_kit() 2024-07-23 12:59:04 -07:00
Mike Wilkerson af29688b69 WIP: add SVG support styles manager class 2024-07-23 12:59:04 -07:00
Mike Wilkerson 05ffa35287 add the font_awesome_skip_enqueue_kit filter 2024-07-23 12:59:04 -07:00
Mike Wilkerson 12a3e0129c remove unnecessary kses filter 2024-07-23 12:59:04 -07:00
Frances Botsford 48ad4df08f switch back to FontSizePicker 2024-07-23 15:55:52 -04:00
Frances Botsford ae68864134 cleanup 2024-07-23 15:09:58 -04:00
Frances Botsford 8d96ad5e38 add standard class names 2024-07-23 15:09:58 -04:00
Mike Wilkerson b58a9b683e rebuild block-editor bundle 2024-07-22 16:08:06 -07:00
Mike Wilkerson 7c97d4aa2b remove obsolete resetSize in updateTransform() 2024-07-22 16:07:51 -07:00
Mike Wilkerson 2a7f16e188 rebuild block-editor bundle 2024-07-22 14:47:11 -07:00
Mike Wilkerson 37c573bbac finish wiring up new IconSizer 2024-07-22 14:46:52 -07:00
Mike Wilkerson c2d853c5d8 refactor IconSizer 2024-07-22 14:26:23 -07:00
Mike Wilkerson e89b66a447 WIP: IconSizer 2024-07-22 14:23:05 -07:00
Mike Wilkerson 222f762972 rebuild block-editor bundle 2024-07-19 17:32:12 -07:00
Mike Wilkerson 5ba1e7d3ee WIP: refactoring
- move Colors into its own module
- factor out some constants that are shared across modules
2024-07-19 17:31:55 -07:00
Mike Wilkerson c589a4e655 remove obsolete consts 2024-07-19 16:24:25 -07:00
Mike Wilkerson 1190c1b245 remove obsolete CSS rules for selected-layer class 2024-07-19 16:24:16 -07:00
Mike Wilkerson 967611d56b rebuild block-editor bundle 2024-07-19 16:18:06 -07:00
Mike Wilkerson 985ff40f46 add temporary styling for animation button selection 2024-07-19 16:17:47 -07:00
Mike Wilkerson 44eeb43314 wire-up animation button selected state 2024-07-19 16:17:26 -07:00
Mike Wilkerson 65e66100a9 handle fawp-selected for flips 2024-07-19 12:26:26 -07:00
Mike Wilkerson c63d83d509 clean up some obsolete stuf 2024-07-19 12:19:43 -07:00
Mike Wilkerson 67240ab31a rebuild block-editor bundle 2024-07-19 11:59:47 -07:00
Mike Wilkerson 8282c241a1 remove some old stuff 2024-07-19 11:59:41 -07:00
Mike Wilkerson 93d665068a migrate flipping to use power transforms 2024-07-19 11:57:20 -07:00
Mike Wilkerson c341484d75 wireup selected class for size buttons 2024-07-19 11:49:34 -07:00
Mike Wilkerson 90b74c0fcd tweak temp fawp-selected style again 2024-07-19 11:49:03 -07:00
Mike Wilkerson 6ce68e98c0 tweak temp fawp-selected rule 2024-07-19 11:36:50 -07:00
Mike Wilkerson 78c9a0e579 refactor color setting logic 2024-07-19 11:34:53 -07:00
Mike Wilkerson dc2ca8db1e get custom color selection working 2024-07-19 11:30:41 -07:00
Mike Wilkerson 8921dff900 WIP: initial Colors subpanel 2024-07-19 11:12:22 -07:00
Mike Wilkerson cd0599f516 remove obsolete setRotation 2024-07-19 10:26:18 -07:00
Mike Wilkerson 6c1e2f8065 remove obsolete setSize 2024-07-19 10:25:54 -07:00
Mike Wilkerson 4909fe3ce9 rename settings as editorSettings 2024-07-19 10:24:17 -07:00
Mike Wilkerson 34f0df58a9 add temporary style for fawp-selected 2024-07-19 10:22:48 -07:00
Mike Wilkerson 94da769ee2 wire-up selected class for rotation 2024-07-19 10:22:33 -07:00
Mike Wilkerson 8f02868210 re-work rotation and custom rotation in terms of power transforms 2024-07-19 10:09:27 -07:00
Mike Wilkerson dc0377864f WIP: re-working sizing in terms of power transforms 2024-07-19 09:43:24 -07:00
Mike Wilkerson 1d52efa413 WIP: initial refactor of SettingsTabPanel 2024-07-19 09:35:42 -07:00
Mike Wilkerson 8c2dd1e077 rebuild block-editor bundle 2024-07-19 09:18:13 -07:00
Mike Wilkerson 6701d60343 use icons 2024-07-19 09:10:28 -07:00
Mike Wilkerson fdc388a470 WIP: initial wire-up of tab panel 2024-07-19 08:53:33 -07:00
Mike Wilkerson f57c1e1a36 remove obsolete OptionalTooltip 2024-07-19 08:40:17 -07:00
Mike Wilkerson b7a5f1524e rebuild block-editor bundle 2024-07-18 16:55:12 -07:00
Mike Wilkerson 9661a0c06a remove a lot more layer and mask settings stuff 2024-07-18 16:53:24 -07:00
Mike Wilkerson 47933db097 use DOMParser instead of setting innerHTML 2024-07-18 16:44:42 -07:00
Mike Wilkerson dcf4ec68a5 only pick valid transform props 2024-07-18 16:33:07 -07:00
Mike Wilkerson 7961ff6732 handle power transforms for rich text icons 2024-07-18 16:24:28 -07:00
Mike Wilkerson cec1310cf1 render nothing when no iconLayers are present 2024-07-18 16:04:49 -07:00
Mike Wilkerson bf7d12a247 preserve styling when changing rich text icon 2024-07-18 15:09:29 -07:00
Mike Wilkerson c4788a3270 handle rich text icon replacement 2024-07-18 15:05:18 -07:00
Mike Wilkerson 0cae4d9c4d refactoring 2024-07-18 14:50:32 -07:00
Mike Wilkerson ad191cef5f WIP: wiring up richTextIcon to use the IconModifier UI 2024-07-18 14:47:21 -07:00
Mike Wilkerson c81c4eb9ac rebuild block-editor bundle 2024-07-18 13:14:31 -07:00
frances botsford 28dba72972
Start of icon styling UI (#222) 2024-07-18 16:08:42 -04:00
Mike Wilkerson ac580f0483 rebuild block-editor bundle 2024-07-18 10:02:01 -07:00
Mike Wilkerson 96dd2cca10 fix rich text icon popover anchor 2024-07-18 09:58:57 -07:00
Mike Wilkerson 71aeffb6a6 remove the mutation observer: no longer needed
apparently, we don't need it any more after switching rendering implemenations.

much cleaner and less hacky without it.
2024-07-17 16:12:04 -07:00
Mike Wilkerson 2e60ca7374 clean up old SVG rendering 2024-07-17 15:55:40 -07:00
Mike Wilkerson 0150ed2549 use react component to render rich text icon as well to be DRY 2024-07-17 15:54:25 -07:00
Mike Wilkerson 1f1fb37657 refactoring transformation of icon chooser select event into iconDefinition 2024-07-17 15:53:49 -07:00
Mike Wilkerson e637fabf05 update renderIcon to parameterize the wrapperElement 2024-07-17 15:11:04 -07:00
Mike Wilkerson b75206a7c7 more cleanup 2024-07-17 15:00:43 -07:00
Mike Wilkerson 56ceed3f50 remove onKeyDown event handling from toolbar button 2024-07-17 14:56:24 -07:00
Mike Wilkerson cb50d24cb0 inital cuts to remove layer and mask support for now 2024-07-17 14:50:48 -07:00
Mike Wilkerson 6dcd0115e1 rebuild block-editor bundle 2024-07-17 12:23:56 -07:00
Mike Wilkerson 469d357aee fix alignment of object replacement chars and replacement formats
when adding multiple rich text icons within a single rich text value,
we have to make sure that replacement formats cover every character index
for the replacement, including the additional zero-width space character
we add.
2024-07-17 12:23:32 -07:00
Mike Wilkerson 20982c8298 add SCRIPT_DEBUG to debugging config 2024-07-17 10:45:09 -07:00
Mike Wilkerson 1e495372a3 rebuild block-editor bundle 2024-07-17 10:16:13 -07:00
Mike Wilkerson 8cc40de9ed tweak the mutation observer to be constrained by the new rich text class 2024-07-17 10:15:46 -07:00
Mike Wilkerson a65c673d9a clean up rich text icon and class naming 2024-07-17 09:53:20 -07:00
Mike Wilkerson c79717c3e9 change the toolbar icon 2024-07-17 09:40:11 -07:00
Mike Wilkerson 89a44524ed rebuild block-editor bundle 2024-07-16 17:30:23 -07:00
Mike Wilkerson 4f9aa8239b renaming classes 2024-07-16 17:27:37 -07:00
Mike Wilkerson 82d5967668 fix the focus control 2024-07-16 17:22:24 -07:00
Mike Wilkerson e37ae25e00 use insertObject to get the object replacement text instead of hardcoding 2024-07-16 17:00:34 -07:00
Mike Wilkerson a56e97b43c renaming as richTextIcon 2024-07-16 16:18:52 -07:00
Mike Wilkerson 4572faef1f cleanup 2024-07-16 16:16:36 -07:00
Mike Wilkerson feb6361736 use create from html instead 2024-07-16 16:08:23 -07:00
Mike Wilkerson 120832c469 refine mutation observer again 2024-07-16 14:17:19 -07:00
Mike Wilkerson be2b5fdb0a WIP: checkpoint for inline svg 2024-07-16 13:13:02 -07:00
Mike Wilkerson c0c78df962 tweak mutation observer 2024-07-15 17:38:19 -07:00
Mike Wilkerson 364ca87007 rename mutation observer script 2024-07-15 17:24:47 -07:00
Mike Wilkerson 2701535f6f re-org and bugfix for mutation observer 2024-07-15 17:23:26 -07:00
Mike Wilkerson ee50efa9c7 update package-lock for block-editor 2024-07-15 14:27:29 -07:00
Frances Botsford 22e070411c remove inline-block display from block-editor icon 2024-07-12 15:20:11 -04:00
Frances Botsford 70fdba2352 add needed icon assets 2024-07-12 15:19:40 -04:00
Mike Wilkerson e70129a2be rebuild admin production bundle 2024-07-11 13:01:21 -07:00
Mike Wilkerson f338f240aa re-build class-editor production bundle 2024-07-11 13:00:40 -07:00
Mike Wilkerson 2a4d41638f rebuild block-editor production bundle 2024-07-11 12:59:51 -07:00
Mike Wilkerson 23e49c06b6 re-build production icon chooser bundle 2024-07-11 12:57:21 -07:00
Mike Wilkerson 680400845c use 0.8.0-1 pre-release of fa-icon chooser 2024-07-11 12:56:59 -07:00
Mike Wilkerson ac237af948 add .rgignore for ripgrep 2024-07-11 12:26:53 -07:00
Mike Wilkerson b1c7534a34 use div for block type icons and span for inline format SVGs 2024-07-11 12:26:42 -07:00
Mike Wilkerson 894474acf1 re-write the inlineSvgFormat Edit component 2024-07-10 16:26:51 -07:00
Mike Wilkerson 5d6df89805 fix typos in size change labels 2024-07-10 15:27:33 -07:00
Mike Wilkerson 494482003f add mask support 2024-07-10 15:04:26 -07:00
Mike Wilkerson d3510b744b tweak style when hovering over inline format API SVG 2024-07-09 17:07:17 -07:00
Mike Wilkerson d28c214ac8 re-fix the inline SVG mutation observer 2024-07-09 17:00:57 -07:00
Mike Wilkerson a6655404d6 handle power transforms rotation 2024-07-09 16:49:00 -07:00
Mike Wilkerson 22f0364cc1 use forward/back language instead of up/down 2024-07-09 16:31:39 -07:00
Mike Wilkerson 66d86ea032 support inverse 2024-07-09 16:29:26 -07:00
Mike Wilkerson cd0a2437d8 WIP: add support for flipping and reseting in power transforms 2024-07-09 16:15:14 -07:00
Mike Wilkerson 36990ffa7e WIP: adding support for power transforms 2024-07-09 16:01:13 -07:00
Mike Wilkerson 1b17bc5f19 when removing all but the last layer, set the last one to be the selected one 2024-07-09 15:21:46 -07:00
Mike Wilkerson db5d730cb8 do not highlight a single layer 2024-07-09 15:19:08 -07:00
Mike Wilkerson 7898c65336 fix block validation and saving 2024-07-09 15:04:41 -07:00
Mike Wilkerson 9cba654a44 ignore npmrc 2024-07-09 14:50:48 -07:00
Mike Wilkerson 691210eaff support animation options 2024-07-09 13:16:54 -07:00
Mike Wilkerson 459169eae3 support the flip option 2024-07-09 12:48:34 -07:00
Mike Wilkerson 77cc775e3f support setting size options 2024-07-09 12:34:46 -07:00
Mike Wilkerson 08790b411a support rotation options, including custom rotation 2024-07-09 12:30:29 -07:00
Mike Wilkerson d44a348a6e set color 2024-07-09 11:50:45 -07:00
Mike Wilkerson 60ad435fa3 add tooltip for adding a layer 2024-07-09 11:38:03 -07:00
Mike Wilkerson 17ee62852e remove debug code 2024-07-09 11:37:56 -07:00
Mike Wilkerson 45971a076b toolips for options buttons 2024-07-09 11:31:19 -07:00
Mike Wilkerson 6f268f8257 consolidate renderBlock and renderIcon 2024-07-09 10:45:37 -07:00
Mike Wilkerson 5b4f267809 WIP: making layers selectable 2024-07-09 10:40:44 -07:00
Mike Wilkerson dc08faeac2 move button styling to CSS 2024-07-09 10:40:30 -07:00
Mike Wilkerson 3af737cb13 rename 2024-07-08 16:46:23 -07:00
Mike Wilkerson 163d62cc2f WIP: switching to Modal for editing icon 2024-07-08 16:45:04 -07:00
Mike Wilkerson 86a0d76c01 use unique customEvent for modal open with inline SVG format types 2024-07-08 16:22:34 -07:00
Mike Wilkerson 3d993b73f6 factor out generateRandomString 2024-07-08 16:21:12 -07:00
Mike Wilkerson 624bd5b6b5 fix the MutationObserver to skip SVGs. Only repaint when it's a span with the specific classname 2024-07-08 16:20:22 -07:00
Mike Wilkerson 03c1b36de1 include authorization header in handleQuery requests and never cache responses with errors 2024-07-08 15:32:42 -07:00
Mike Wilkerson 79497dc33d simplify custom event usage 2024-07-07 13:27:45 -07:00
Mike Wilkerson 43d2af0954 formatting 2024-07-07 12:03:12 -07:00
Mike Wilkerson cad7ea5cf8 WIP: wiring up parameterized modal open events 2024-07-07 11:45:19 -07:00
Mike Wilkerson 0961ea0d2e update modalOpenEvent for inline SVG 2024-07-06 19:32:04 -07:00
Mike Wilkerson cc6912f76c cleanup previous openModalEvent 2024-07-06 19:26:38 -07:00
Mike Wilkerson f98b7108fb update tinyMCE media button to use inline SVG and not a global opener function 2024-07-06 19:21:42 -07:00
Mike Wilkerson eaaf17cee4 update classic editor integration for openEvent param 2024-07-06 19:20:55 -07:00
Mike Wilkerson 895cf3f98b make openEvent a prop for the IconChooser 2024-07-06 19:19:01 -07:00
Mike Wilkerson 12b1ae58e0 WIP: adding a separate component for showing single icon layer 2024-07-06 17:50:32 -07:00
Mike Wilkerson 62b41725a9 refactor for single vs multi-layer 2024-07-06 15:03:53 -07:00
Mike Wilkerson 584ce00de0 rename iconLayersModifier 2024-07-06 14:46:51 -07:00
Mike Wilkerson c497bd5eaa remove layers 2024-06-29 17:37:59 -07:00
Mike Wilkerson 0b283fb94d handle moving icon layers up and down 2024-06-29 17:13:02 -07:00
Mike Wilkerson 55f50a4503 WIP: adding an IconModifier that supports multiple layers 2024-06-29 16:24:08 -07:00
Mike Wilkerson c92e444b4c complete refactor of rendering 2024-06-27 20:43:03 -07:00
Mike Wilkerson 70808432f6 WIP: refactoring renderBlock 2024-06-27 20:19:46 -07:00
Mike Wilkerson bb03f1bc10 WIP: moving toward layer support for blocks 2024-06-27 20:12:52 -07:00
Mike Wilkerson ba0cc30689 WIP: initial dropdown for icon layers 2024-06-27 17:43:59 -07:00
Mike Wilkerson 0e638a241f Change Icon Chooser to directly query FA API using access token fetched from WP sever 2024-06-23 10:46:11 -07:00
Mike Wilkerson 4ef9a5ace6 use sessionStorage to cache handleQuery when cache: true 2024-06-22 22:24:23 -07:00
Mike Wilkerson a33bf4f14f DRYing out preparation of editor UI icons from FA icons 2024-06-22 19:21:48 -07:00
Mike Wilkerson 08360cac41 add an initial inline UI for working with the fa-inline-format icons 2024-06-22 12:37:50 -07:00
Mike Wilkerson 7f76018078 distinguish the format API svg icons from the icon blocks via CSS class
this is to make the block editor mutation observer replace/repaint only the format API icons.
2024-06-22 11:45:26 -07:00
Mike Wilkerson 28d92d71e5 remove render.php and add more styling attributes 2024-06-19 14:52:02 -07:00
Mike Wilkerson 5080d6d7c9 rework icon block to refactor and use FA react component 2024-06-19 14:40:40 -07:00
Mike Wilkerson 2ea7ae7ab6 WIP: refactoring edit and save to use svgIcon 2024-06-19 10:37:24 -07:00
Mike Wilkerson 7bdfcc95f0 WIP: refining edit and save 2024-06-19 10:18:20 -07:00
Mike Wilkerson 7451ed5a0d tweak icon block metadata 2024-06-19 10:15:19 -07:00
Mike Wilkerson 29d4b98dde build icon-chooser bundle 2024-06-19 09:47:35 -07:00
Mike Wilkerson 03a78640ec build classic-editor bundle 2024-06-19 09:47:23 -07:00
Mike Wilkerson e8a9ace07b WIP: wiring up a basic functioning SVG icon block 2024-06-17 10:59:42 -07:00
Mike Wilkerson 2b4619dc91 WIP: experiment with inline SVGs in tinymce 2024-06-16 16:24:14 -07:00
Mike Wilkerson f47665753e cleanup the setup of the icon chooser in the Classic Editor 2024-06-16 12:38:35 -07:00
Mike Wilkerson 6f30480816 WIP: refactoring the classic editor setup 2024-06-15 20:41:56 -07:00
Mike Wilkerson fcda85ef18 WIP: further refactoring 2024-06-15 15:44:19 -07:00
Mike Wilkerson 0a0880b6aa WIP: some refactoring 2024-06-15 12:04:38 -07:00
Mike Wilkerson feef64d03f WIP: initial fa-icon-block bundle 2024-06-11 10:15:13 -07:00
Mike Wilkerson a4d86174ea remove obsolete import 2024-06-11 10:12:05 -07:00
Mike Wilkerson 42d2a3546e add comment 2024-06-11 10:11:56 -07:00
Mike Wilkerson 95332e3ab8 WIP: back end wire-up of fa-icon block 2024-06-10 22:23:06 -07:00
Mike Wilkerson bdcded2036 add example of a toolbar button 2024-06-10 20:31:16 -07:00
Mike Wilkerson fb9cc8c6b0 update mutation observer to handle childList even when adding a SPAN or SVG 2024-06-10 20:26:36 -07:00
Mike Wilkerson 6f62232580 remove obsolete code 2024-06-10 20:15:25 -07:00
Mike Wilkerson 0dc2f91ea8 wrap in an outer span.fa-icon 2024-06-10 20:13:53 -07:00
Mike Wilkerson 6cc765f661 rebuild admin bundle 2024-06-10 15:41:15 -07:00
Mike Wilkerson 13699d146c prototype inserting inline SVGs
(requires local build of fa-icon-chooser)
2024-06-10 15:41:15 -07:00
231 changed files with 48853 additions and 17562 deletions

View File

@ -2,33 +2,33 @@ name: Jest
on:
push:
branches: [ master ]
branches:
- "**"
pull_request:
branches: [ master ]
branches: [master]
jobs:
jest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: '18'
check-latest: true
- uses: actions/setup-node@v1
with:
node-version: "18"
check-latest: true
- name: Cache node_module
id: node-modules-cache
uses: actions/cache@v2
with:
path: admin/node_modules
key: ${{ runner.os }}-node-modules-${{ hashFiles('admin/package-lock.json') }}
- name: Cache node_module
id: node-modules-cache
uses: actions/cache@v2
with:
path: admin/node_modules
key: ${{ runner.os }}-node-modules-${{ hashFiles('admin/package-lock.json') }}
- name: Install node dependencies
if: steps.node-modules-cache.outputs.cache-hit != 'true'
run: cd admin && npm install
- name: Install node dependencies
if: steps.node-modules-cache.outputs.cache-hit != 'true'
run: cd admin && npm install
- name: Run Jest
run: cd admin && npm run test
- name: Run Jest
run: cd admin && npm run test

View File

@ -2,9 +2,10 @@ name: PHP Tests
on:
push:
branches: [ master ]
branches:
- "**"
pull_request:
branches: [ master ]
branches: [master]
env:
# Seems like we should be able to use these in the services section below
@ -18,257 +19,254 @@ env:
jobs:
build:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:5.7
env:
MYSQL_ROOT_PASSWORD: somewordpress
MYSQL_DATABASE: wordpressdb
MYSQL_USER: wordpress
MYSQL_PASSWORD: password
ports:
- 3306:3306
image: mysql:5.7
env:
MYSQL_ROOT_PASSWORD: somewordpress
MYSQL_DATABASE: wordpressdb
MYSQL_USER: wordpress
MYSQL_PASSWORD: password
ports:
- 3306:3306
strategy:
matrix:
php: ['7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3']
wordpress: [latest]
include:
- php: '5.6'
wordpress: 5.2.5
- php: '8.3'
wordpress: trunk
matrix:
php: ["7.4", "8.0", "8.1", "8.2", "8.3"]
wordpress: [latest]
include:
- php: "8.3"
wordpress: trunk
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v3
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
- name: Validate composer.json and composer.lock
id: composer-lock
run: |
if [ ${{ matrix.php }} == '8.2' ]; then
composer validate
LOCK_FILE=composer.lock
COMPOSER_FILE=composer.json
else
COMPOSER=composer-php${{ matrix.php }}.json composer validate
LOCK_FILE=composer-php${{ matrix.php }}.lock
COMPOSER_FILE=composer-php${{ matrix.php }}.json
fi
echo "COMPOSER_LOCK_HASH=$(md5sum $LOCK_FILE | cut -d' ' -f1)" >> $GITHUB_OUTPUT
echo "COMPOSER_FILE=${COMPOSER_FILE}" >> $GITHUB_OUTPUT
- name: Resolve Cache Date
id: cache-date
run: echo "DATE=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Cache Composer packages
id: composer-cache
uses: actions/cache@v3
with:
path: vendor
key: ${{ runner.os }}-php-b-${{ steps.composer-lock.outputs.COMPOSER_LOCK_HASH }}
- name: Install dependencies
if: steps.composer-cache.outputs.cache-hit != 'true'
run: COMPOSER=${{ steps.composer-lock.outputs.COMPOSER_FILE }} composer install --prefer-dist --no-progress
- name: Cache APT Sources Daily
id: apt-sources-cache
uses: actions/cache@v3
with:
path: /tmp/apt-sources-${{ steps.cache-date.outputs.DATE }}
key: ${{ runner.os }}-apt-sources-${{ steps.cache-date.outputs.DATE }}
- name: Cache APT Packages Daily
id: apt-packages-cache
uses: actions/cache@v3
with:
path: /tmp/apt-packages-${{ steps.cache-date.outputs.DATE }}
key: ${{ runner.os }}-apt-packages-${{ steps.cache-date.outputs.DATE }}
- name: Update APT Sources
if: steps.apt-sources-cache.outputs.cache-hit != 'true'
run: |
sudo apt-get update
mkdir -p /tmp/apt-sources-${{ steps.cache-date.outputs.DATE }}
sudo cp -R /var/lib/apt/lists/* /tmp/apt-sources-${{ steps.cache-date.outputs.DATE }}
- name: Download APT Packages
if: steps.apt-packages-cache.outputs.cache-hit != 'true'
run: |
sudo cp -R /tmp/apt-sources-${{ steps.cache-date.outputs.DATE }}/* /var/lib/apt/lists
sudo apt-get install -y --download-only subversion mysql-client
sudo mkdir -p /tmp/apt-packages-${{ steps.cache-date.outputs.DATE }}
sudo cp -R /var/cache/apt/archives/*.deb /tmp/apt-packages-${{ steps.cache-date.outputs.DATE }}
- name: Install OS Packages
run: sudo dpkg -i /tmp/apt-packages-${{ steps.cache-date.outputs.DATE }}/*.deb
- name: Verify DB
run: mysql --user=root --password=${MYSQL_ROOT_PASSWORD} --host=${MYSQL_HOST} --port=${MYSQL_PORT} --protocol=tcp -e 'SHOW DATABASES;'
- name: Resolve WordPress Version
run: |
curl -s https://api.wordpress.org/core/version-check/1.7/ > /tmp/wp-latest.json
LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//' | head -1)
if [ "${{ matrix.wordpress }}" == 'latest' ]; then
VERSION=$LATEST_VERSION
echo "WORDPRESS_VERSION=$VERSION" >> $GITHUB_OUTPUT
echo "WORDPRESS_VERSION_IS_TRUNK=0" >> $GITHUB_OUTPUT
echo "WORDPRESS_CORE_DIR=/tmp/$VERSION" >> $GITHUB_OUTPUT
echo "WP_TESTS_TAG=branches/$VERSION" >> $GITHUB_OUTPUT
elif [ "${{ matrix.wordpress }}" == 'trunk' ]; then
VERSION=trunk-$(date +'%Y-%m-%d')
echo "WORDPRESS_VERSION=$VERSION" >> $GITHUB_OUTPUT
echo "WORDPRESS_VERSION_IS_TRUNK=1" >> $GITHUB_OUTPUT
echo "WORDPRESS_CORE_DIR=/tmp/$VERSION" >> $GITHUB_OUTPUT
echo "WP_TESTS_TAG=trunk" >> $GITHUB_OUTPUT
else
VERSION=${{ matrix.wordpress }}
echo "WORDPRESS_VERSION=$VERSION" >> $GITHUB_OUTPUT
echo "WORDPRESS_VERSION_IS_TRUNK=0" >> $GITHUB_OUTPUT
echo "WORDPRESS_CORE_DIR=/tmp/$VERSION" >> $GITHUB_OUTPUT
fi
if [[ $VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then
WP_BRANCH=${VERSION%\-*}
echo "WP_TESTS_TAG=branches/$WP_BRANCH" >> $GITHUB_OUTPUT
elif [[ $VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then
echo "WP_TESTS_TAG=branches/$VERSION" >> $GITHUB_OUTPUT
elif [[ $VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then
if [[ $VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then
# version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x
echo "WP_TESTS_TAG=tags/${VERSION%??}" >> $GITHUB_OUTPUT
- name: Validate composer.json and composer.lock
id: composer-lock
run: |
if [ ${{ matrix.php }} == '8.2' ]; then
composer validate
LOCK_FILE=composer.lock
COMPOSER_FILE=composer.json
else
echo "WP_TESTS_TAG=tags/$VERSION" >> $GITHUB_OUTPUT
COMPOSER=composer-php${{ matrix.php }}.json composer validate
LOCK_FILE=composer-php${{ matrix.php }}.lock
COMPOSER_FILE=composer-php${{ matrix.php }}.json
fi
elif [[ $VERSION == 'nightly' || $VERSION == 'trunk' ]]; then
echo "WP_TESTS_TAG=trunk" >> $GITHUB_OUTPUT
else
echo "WP_TESTS_TAG=tags/$LATEST_VERSION" >> $GITHUB_OUTPUT
fi
echo "WP_TESTS_DIR=/tmp/test/$VERSION" >> $GITHUB_OUTPUT
id: wordpress-version
echo "COMPOSER_LOCK_HASH=$(md5sum $LOCK_FILE | cut -d' ' -f1)" >> $GITHUB_OUTPUT
echo "COMPOSER_FILE=${COMPOSER_FILE}" >> $GITHUB_OUTPUT
- name: Show WordPress Version
run: echo "The current WordPress version is ${{ steps.wordpress-version.outputs.WORDPRESS_VERSION }}, and WP_TESTS_TAG=${{ steps.wordpress-version.outputs.WP_TESTS_TAG }}"
- name: Resolve Cache Date
id: cache-date
run: echo "DATE=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Cache WordPress Core Installation
id: wordpress-cache
uses: actions/cache@v3
with:
path: ${{ steps.wordpress-version.outputs.WORDPRESS_CORE_DIR }}
key: ${{ runner.os }}-wordpress-${{ steps.wordpress-version.outputs.WORDPRESS_VERSION }}') }}
- name: Cache Composer packages
id: composer-cache
uses: actions/cache@v3
with:
path: vendor
key: ${{ runner.os }}-php-b-${{ steps.composer-lock.outputs.COMPOSER_LOCK_HASH }}
- name: Install WordPress Core
if: steps.wordpress-cache.outputs.cache-hit != 'true'
run: |
mkdir -p ${{ steps.wordpress-version.outputs.WORDPRESS_CORE_DIR }}
if [ "${{ steps.wordpress-version.outputs.WORDPRESS_VERSION_IS_TRUNK }}" == "1" ]; then
mkdir -p /tmp/wordpress-nightly
curl https://wordpress.org/nightly-builds/wordpress-latest.zip > /tmp/wordpress-nightly.zip
unzip -q /tmp/wordpress-nightly.zip -d /tmp/wordpress-nightly/
mv /tmp/wordpress-nightly/wordpress/* ${{ steps.wordpress-version.outputs.WORDPRESS_CORE_DIR }}
else
curl -s https://wordpress.org/wordpress-${{ steps.wordpress-version.outputs.WORDPRESS_VERSION }}.tar.gz > /tmp/wordpress.tar.gz
tar --strip-components=1 -zxmf /tmp/wordpress.tar.gz -C ${{ steps.wordpress-version.outputs.WORDPRESS_CORE_DIR }}
fi
curl https://raw.github.com/markoheijnen/wp-mysqli/master/db.php > ${{ steps.wordpress-version.outputs.WORDPRESS_CORE_DIR }}/wp-content/db.php
- name: Install dependencies
if: steps.composer-cache.outputs.cache-hit != 'true'
run: COMPOSER=${{ steps.composer-lock.outputs.COMPOSER_FILE }} composer install --prefer-dist --no-progress
- name: Cache WordPress Test Installation
id: wordpress-test-cache
uses: actions/cache@v3
with:
path: ${{ steps.wordpress-version.outputs.WP_TESTS_DIR }}
key: ${{ runner.os }}-wordpress-test-${{ steps.wordpress-version.outputs.WORDPRESS_VERSION }}') }}
- name: Cache APT Sources Daily
id: apt-sources-cache
uses: actions/cache@v3
with:
path: /tmp/apt-sources-${{ steps.cache-date.outputs.DATE }}
key: ${{ runner.os }}-apt-sources-${{ steps.cache-date.outputs.DATE }}
- name: Install WordPress Test
if: steps.wordpress-test-cache.outputs.cache-hit != 'true'
run: |
mkdir -p ${{ steps.wordpress-version.outputs.WP_TESTS_DIR }}
DB_USER=${MYSQL_USER}
DB_PASS=${MYSQL_PASSWORD}
DB_NAME=${MYSQL_DATABASE}
DB_HOST=${MYSQL_HOST}:${MYSQL_PORT}
WP_TESTS_DIR=${{ steps.wordpress-version.outputs.WP_TESTS_DIR }}
svn co --quiet https://develop.svn.wordpress.org/${{ steps.wordpress-version.outputs.WP_TESTS_TAG }}/tests/phpunit/includes/ $WP_TESTS_DIR/includes
svn co --quiet https://develop.svn.wordpress.org/${{ steps.wordpress-version.outputs.WP_TESTS_TAG }}/tests/phpunit/data/ $WP_TESTS_DIR/data
curl https://develop.svn.wordpress.org/${{ steps.wordpress-version.outputs.WP_TESTS_TAG }}/wp-tests-config-sample.php > $WP_TESTS_DIR/wp-tests-config.php
# remove all forward slashes in the end
WP_CORE_DIR=$(echo ${{ steps.wordpress-version.outputs.WORDPRESS_CORE_DIR }} | sed "s:/\+$::")
sed -i "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php
sed -i "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php
sed -i "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php
sed -i "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php
sed -i "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php
- name: Cache APT Packages Daily
id: apt-packages-cache
uses: actions/cache@v3
with:
path: /tmp/apt-packages-${{ steps.cache-date.outputs.DATE }}
key: ${{ runner.os }}-apt-packages-${{ steps.cache-date.outputs.DATE }}
- name: Run PHPUnit Main Tests
# only run the output tests on the newer versions of php and phpunit, cause
# they're trickier
run: |
if [ "8.3" == ${{ matrix.php }} ]; then
PHP_UNIT_ARGS=""
else
PHP_UNIT_ARGS="--exclude-group output"
fi
- name: Update APT Sources
if: steps.apt-sources-cache.outputs.cache-hit != 'true'
run: |
sudo apt-get update
mkdir -p /tmp/apt-sources-${{ steps.cache-date.outputs.DATE }}
sudo cp -R /var/lib/apt/lists/* /tmp/apt-sources-${{ steps.cache-date.outputs.DATE }}
WP_PLUGIN_DIR="$(pwd)" \
COMPOSER_VENDOR_DIR="$WP_PLUGIN_DIR/vendor" \
WP_TESTS_DIR=${{ steps.wordpress-version.outputs.WP_TESTS_DIR }} \
vendor/bin/phpunit $PHP_UNIT_ARGS --group slow
- name: Download APT Packages
if: steps.apt-packages-cache.outputs.cache-hit != 'true'
run: |
sudo cp -R /tmp/apt-sources-${{ steps.cache-date.outputs.DATE }}/* /var/lib/apt/lists
sudo apt-get install -y --download-only subversion mysql-client
sudo mkdir -p /tmp/apt-packages-${{ steps.cache-date.outputs.DATE }}
sudo cp -R /var/cache/apt/archives/*.deb /tmp/apt-packages-${{ steps.cache-date.outputs.DATE }}
- name: Run PHPUnit Loader Tests
run: |
WP_PLUGIN_DIR="$(pwd)" \
COMPOSER_VENDOR_DIR="$WP_PLUGIN_DIR/vendor" \
WP_TESTS_DIR=${{ steps.wordpress-version.outputs.WP_TESTS_DIR }} \
vendor/bin/phpunit --config phpunit-loader.xml.dist --filter FontAwesomeLoaderTestLifecycle
- name: Install OS Packages
run: sudo dpkg -i /tmp/apt-packages-${{ steps.cache-date.outputs.DATE }}/*.deb
WP_PLUGIN_DIR="$(pwd)" \
COMPOSER_VENDOR_DIR="$WP_PLUGIN_DIR/vendor" \
WP_TESTS_DIR=${{ steps.wordpress-version.outputs.WP_TESTS_DIR }} \
vendor/bin/phpunit --config phpunit-loader.xml.dist --filter FontAwesomeLoaderTestRedundantVersions
- name: Verify DB
run: mysql --user=root --password=${MYSQL_ROOT_PASSWORD} --host=${MYSQL_HOST} --port=${MYSQL_PORT} --protocol=tcp -e 'SHOW DATABASES;'
WP_PLUGIN_DIR="$(pwd)" \
COMPOSER_VENDOR_DIR="$WP_PLUGIN_DIR/vendor" \
WP_TESTS_DIR=${{ steps.wordpress-version.outputs.WP_TESTS_DIR }} \
vendor/bin/phpunit --config phpunit-loader.xml.dist --filter FontAwesomeLoaderTestBasic
- name: Resolve WordPress Version
run: |
curl -s https://api.wordpress.org/core/version-check/1.7/ > /tmp/wp-latest.json
LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//' | head -1)
if [ "${{ matrix.wordpress }}" == 'latest' ]; then
VERSION=$LATEST_VERSION
echo "WORDPRESS_VERSION=$VERSION" >> $GITHUB_OUTPUT
echo "WORDPRESS_VERSION_IS_TRUNK=0" >> $GITHUB_OUTPUT
echo "WORDPRESS_CORE_DIR=/tmp/$VERSION" >> $GITHUB_OUTPUT
echo "WP_TESTS_TAG=branches/$VERSION" >> $GITHUB_OUTPUT
elif [ "${{ matrix.wordpress }}" == 'trunk' ]; then
VERSION=trunk-$(date +'%Y-%m-%d')
echo "WORDPRESS_VERSION=$VERSION" >> $GITHUB_OUTPUT
echo "WORDPRESS_VERSION_IS_TRUNK=1" >> $GITHUB_OUTPUT
echo "WORDPRESS_CORE_DIR=/tmp/$VERSION" >> $GITHUB_OUTPUT
echo "WP_TESTS_TAG=trunk" >> $GITHUB_OUTPUT
else
VERSION=${{ matrix.wordpress }}
echo "WORDPRESS_VERSION=$VERSION" >> $GITHUB_OUTPUT
echo "WORDPRESS_VERSION_IS_TRUNK=0" >> $GITHUB_OUTPUT
echo "WORDPRESS_CORE_DIR=/tmp/$VERSION" >> $GITHUB_OUTPUT
fi
- name: Run PHPUnit Multisite Tests
run: |
WP_PLUGIN_DIR="$(pwd)" \
COMPOSER_VENDOR_DIR="$WP_PLUGIN_DIR/vendor" \
WP_TESTS_DIR=${{ steps.wordpress-version.outputs.WP_TESTS_DIR }} \
vendor/bin/phpunit --config phpunit-multisite.xml.dist
if [[ $VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then
WP_BRANCH=${VERSION%\-*}
echo "WP_TESTS_TAG=branches/$WP_BRANCH" >> $GITHUB_OUTPUT
elif [[ $VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then
echo "WP_TESTS_TAG=branches/$VERSION" >> $GITHUB_OUTPUT
elif [[ $VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then
if [[ $VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then
# version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x
echo "WP_TESTS_TAG=tags/${VERSION%??}" >> $GITHUB_OUTPUT
else
echo "WP_TESTS_TAG=tags/$VERSION" >> $GITHUB_OUTPUT
fi
elif [[ $VERSION == 'nightly' || $VERSION == 'trunk' ]]; then
echo "WP_TESTS_TAG=trunk" >> $GITHUB_OUTPUT
else
echo "WP_TESTS_TAG=tags/$LATEST_VERSION" >> $GITHUB_OUTPUT
fi
echo "WP_TESTS_DIR=/tmp/test/$VERSION" >> $GITHUB_OUTPUT
id: wordpress-version
WP_PLUGIN_DIR="$(pwd)" \
COMPOSER_VENDOR_DIR="$WP_PLUGIN_DIR/vendor" \
WP_TESTS_DIR=${{ steps.wordpress-version.outputs.WP_TESTS_DIR }} \
vendor/bin/phpunit --config phpunit-multisite-network-admin.xml.dist
- name: Show WordPress Version
run: echo "The current WordPress version is ${{ steps.wordpress-version.outputs.WORDPRESS_VERSION }}, and WP_TESTS_TAG=${{ steps.wordpress-version.outputs.WP_TESTS_TAG }}"
- name: Run PHPUnit Multisite SLOW Tests
run: |
WP_PLUGIN_DIR="$(pwd)" \
COMPOSER_VENDOR_DIR="$WP_PLUGIN_DIR/vendor" \
WP_TESTS_DIR=${{ steps.wordpress-version.outputs.WP_TESTS_DIR }} \
vendor/bin/phpunit --config phpunit-multisite.xml.dist --group slow
- name: Cache WordPress Core Installation
id: wordpress-cache
uses: actions/cache@v3
with:
path: ${{ steps.wordpress-version.outputs.WORDPRESS_CORE_DIR }}
key: ${{ runner.os }}-wordpress-${{ steps.wordpress-version.outputs.WORDPRESS_VERSION }}') }}
WP_PLUGIN_DIR="$(pwd)" \
COMPOSER_VENDOR_DIR="$WP_PLUGIN_DIR/vendor" \
WP_TESTS_DIR=${{ steps.wordpress-version.outputs.WP_TESTS_DIR }} \
vendor/bin/phpunit --config phpunit-multisite-network-admin.xml.dist --group slow
- name: Install WordPress Core
if: steps.wordpress-cache.outputs.cache-hit != 'true'
run: |
mkdir -p ${{ steps.wordpress-version.outputs.WORDPRESS_CORE_DIR }}
if [ "${{ steps.wordpress-version.outputs.WORDPRESS_VERSION_IS_TRUNK }}" == "1" ]; then
mkdir -p /tmp/wordpress-nightly
curl https://wordpress.org/nightly-builds/wordpress-latest.zip > /tmp/wordpress-nightly.zip
unzip -q /tmp/wordpress-nightly.zip -d /tmp/wordpress-nightly/
mv /tmp/wordpress-nightly/wordpress/* ${{ steps.wordpress-version.outputs.WORDPRESS_CORE_DIR }}
else
curl -s https://wordpress.org/wordpress-${{ steps.wordpress-version.outputs.WORDPRESS_VERSION }}.tar.gz > /tmp/wordpress.tar.gz
tar --strip-components=1 -zxmf /tmp/wordpress.tar.gz -C ${{ steps.wordpress-version.outputs.WORDPRESS_CORE_DIR }}
fi
curl https://raw.github.com/markoheijnen/wp-mysqli/master/db.php > ${{ steps.wordpress-version.outputs.WORDPRESS_CORE_DIR }}/wp-content/db.php
- name: Maybe run phpcs
run: |
if [ ${{ matrix.php }} == '8.3' ] && [ ${{ matrix.wordpress }} == latest ]; then
composer phpcs
echo
echo "Skipping phpcs"
fi
- name: Cache WordPress Test Installation
id: wordpress-test-cache
uses: actions/cache@v3
with:
path: ${{ steps.wordpress-version.outputs.WP_TESTS_DIR }}
key: ${{ runner.os }}-wordpress-test-${{ steps.wordpress-version.outputs.WORDPRESS_VERSION }}') }}
- name: Install WordPress Test
if: steps.wordpress-test-cache.outputs.cache-hit != 'true'
run: |
mkdir -p ${{ steps.wordpress-version.outputs.WP_TESTS_DIR }}
DB_USER=${MYSQL_USER}
DB_PASS=${MYSQL_PASSWORD}
DB_NAME=${MYSQL_DATABASE}
DB_HOST=${MYSQL_HOST}:${MYSQL_PORT}
WP_TESTS_DIR=${{ steps.wordpress-version.outputs.WP_TESTS_DIR }}
svn co --quiet https://develop.svn.wordpress.org/${{ steps.wordpress-version.outputs.WP_TESTS_TAG }}/tests/phpunit/includes/ $WP_TESTS_DIR/includes
svn co --quiet https://develop.svn.wordpress.org/${{ steps.wordpress-version.outputs.WP_TESTS_TAG }}/tests/phpunit/data/ $WP_TESTS_DIR/data
curl https://develop.svn.wordpress.org/${{ steps.wordpress-version.outputs.WP_TESTS_TAG }}/wp-tests-config-sample.php > $WP_TESTS_DIR/wp-tests-config.php
# remove all forward slashes in the end
WP_CORE_DIR=$(echo ${{ steps.wordpress-version.outputs.WORDPRESS_CORE_DIR }} | sed "s:/\+$::")
sed -i "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php
sed -i "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php
sed -i "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php
sed -i "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php
sed -i "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php
- name: Run PHPUnit Main Tests
# only run the output tests on the newer versions of php and phpunit, cause
# they're trickier
run: |
if [ "8.3" == ${{ matrix.php }} ]; then
PHP_UNIT_ARGS=""
else
PHP_UNIT_ARGS="--exclude-group output"
fi
WP_PLUGIN_DIR="$(pwd)" \
COMPOSER_VENDOR_DIR="$WP_PLUGIN_DIR/vendor" \
WP_TESTS_DIR=${{ steps.wordpress-version.outputs.WP_TESTS_DIR }} \
vendor/bin/phpunit $PHP_UNIT_ARGS --group slow
- name: Run PHPUnit Loader Tests
run: |
WP_PLUGIN_DIR="$(pwd)" \
COMPOSER_VENDOR_DIR="$WP_PLUGIN_DIR/vendor" \
WP_TESTS_DIR=${{ steps.wordpress-version.outputs.WP_TESTS_DIR }} \
vendor/bin/phpunit --config phpunit-loader.xml.dist --filter FontAwesomeLoaderTestLifecycle
WP_PLUGIN_DIR="$(pwd)" \
COMPOSER_VENDOR_DIR="$WP_PLUGIN_DIR/vendor" \
WP_TESTS_DIR=${{ steps.wordpress-version.outputs.WP_TESTS_DIR }} \
vendor/bin/phpunit --config phpunit-loader.xml.dist --filter FontAwesomeLoaderTestRedundantVersions
WP_PLUGIN_DIR="$(pwd)" \
COMPOSER_VENDOR_DIR="$WP_PLUGIN_DIR/vendor" \
WP_TESTS_DIR=${{ steps.wordpress-version.outputs.WP_TESTS_DIR }} \
vendor/bin/phpunit --config phpunit-loader.xml.dist --filter FontAwesomeLoaderTestBasic
- name: Run PHPUnit Multisite Tests
run: |
WP_PLUGIN_DIR="$(pwd)" \
COMPOSER_VENDOR_DIR="$WP_PLUGIN_DIR/vendor" \
WP_TESTS_DIR=${{ steps.wordpress-version.outputs.WP_TESTS_DIR }} \
vendor/bin/phpunit --config phpunit-multisite.xml.dist
WP_PLUGIN_DIR="$(pwd)" \
COMPOSER_VENDOR_DIR="$WP_PLUGIN_DIR/vendor" \
WP_TESTS_DIR=${{ steps.wordpress-version.outputs.WP_TESTS_DIR }} \
vendor/bin/phpunit --config phpunit-multisite-network-admin.xml.dist
- name: Run PHPUnit Multisite SLOW Tests
run: |
WP_PLUGIN_DIR="$(pwd)" \
COMPOSER_VENDOR_DIR="$WP_PLUGIN_DIR/vendor" \
WP_TESTS_DIR=${{ steps.wordpress-version.outputs.WP_TESTS_DIR }} \
vendor/bin/phpunit --config phpunit-multisite.xml.dist --group slow
WP_PLUGIN_DIR="$(pwd)" \
COMPOSER_VENDOR_DIR="$WP_PLUGIN_DIR/vendor" \
WP_TESTS_DIR=${{ steps.wordpress-version.outputs.WP_TESTS_DIR }} \
vendor/bin/phpunit --config phpunit-multisite-network-admin.xml.dist --group slow
- name: Maybe run phpcs
run: |
if [ ${{ matrix.php }} == '8.2' ] && [ ${{ matrix.wordpress }} == latest ]; then
composer phpcs
echo
echo "Skipping phpcs"
fi

3
.gitignore vendored
View File

@ -20,3 +20,6 @@ webpack-stats.json
admin/src/playwright/.auth/
admin/artifacts/
admin/test-results/
.npmrc
.php-cs-fixer.cache
.zed/

11
.phpactor.json Normal file
View File

@ -0,0 +1,11 @@
{
"$schema": "/phpactor.schema.json",
"language_server": {
"diagnostic_exclude_paths": ["**/wp-dist", "**/integrations/**/vendor"]
},
"indexer": {
"exclude_patterns": ["**/wp-dist", "**/integrations/**/vendor"]
},
"php_code_sniffer.enabled": false,
"php_code_sniffer.args": ["--standard=.phpcs.xml"]
}

View File

@ -16,10 +16,13 @@
<arg name="extensions" value="php"/>
<file>includes</file>
<file>tests</file>
<!-- TODO: maybe re-enable this. It's crashing phpcbf. -->
<!--
<file>font-awesome.php</file>
-->
<file>font-awesome-init.php</file>
<file>block-editor/font-awesome-icon-block-init.php</file>
<file>index.php</file>
<file>v3shims.php</file>
<file>defines.php</file>
<file>admin/index.php</file>
<file>admin/views/main.php</file>

6
.rgignore Normal file
View File

@ -0,0 +1,6 @@
block-editor/build
icon-chooser/build
classic-editor/build
admin/build
compat-js/build
docs/

View File

@ -41,7 +41,7 @@ to install any development dependencies inside the container.
The `integration` option does _not_ mount this plugin code in the container. Instead, it expects you
to install the plugin. You could do so by uploading a zip file on the Add New Plugin page in
WordPress admin: build a zip using `composer dist` or by downloading one from the [plugin's WordPress
WordPress admin: build a zip using the release steps below or by downloading one from the [plugin's WordPress
plugin directory entry](https://wordpress.org/plugins/font-awesome/). Or you could install directly
from the WordPress plugin directory by searching for plugins by author "fontawesome".
@ -456,27 +456,6 @@ brew install composer
```
</details>
<details>
<summary>The WordPress 4 compat bundle</summary>
For older versions of WordPress, we build and load this separate "compat-js" bundle. It includes some WordPress dependencies that are available at runtime on newer versions of WordPress, but which this plugin provides itself when it detects that
it's being loaded on older versions of WordPress.
This bundle will probably not change very much, so it may not be necessary to rebuild at all.
If you're doing development work only in WordPress 5, you can skip this altogether.
If you do need to update what's in this bundle, though, then you just build another
static production build like this:
```
$ cd compat-js
$ npm install
$ npm run build
```
This will create `compat-js/build/compat.js`, which the plugin looks for and
enqueues automatically when it detects that it's running under WordPress 4.
</details>
<details>
<summary>If you have an older version of Docker or one that doesn't support host.docker.internal</summary>
@ -820,7 +799,7 @@ When `mod_security` is enabled, it'll look like this:
3. Update the plugin version const in `includes/class-fontawesome.php`
4. Update the versions in `admin/package.json` and `compat-js/package.json`
4. Update the versions in `admin/package.json`, `block-editor/package.json`, `classic-editor/package.json`, `icon-chooser/package.json`
5. Wait on changing the "Stable Tag" in `readme.txt` until after we've made the changes in the `svn` repo below.
@ -845,7 +824,7 @@ When `mod_security` is enabled, it'll look like this:
- `git add docs` to stage them for commit (and eventually commit them)
7. Build production admin app and WordPress distribution layout into `wp-dist`
7. Build production distribution archive.
```bash
bin/composer dist
@ -856,20 +835,7 @@ the default dev `latest` container, which you should be running via `bin/dev`.
This will cause everything to be built inside the container, which will hopefully
keep the built assets more consistent, regardless of the host environment.)
This will delete the previous build assets and produce the following:
`admin/build`: production build of the admin UI React app. This needs to be committed, so that it
can be included in the composer package (which is really just a pull of this repo)
`compat-js/build`: production build of the compatibility JS bundled. This also needs to be committed.
8. Build the zip file
```bash
bin/make-wp-dist-zip
```
This builds the following:
This will delete the previous build assets and produce:
`wp-dist/`: the contents of this directory contains everything that will be used in
subsequent steps to both build an installable zip file, and to copy into the
@ -1347,7 +1313,7 @@ wp --allow-root core update --version=5.4 /tmp/wordpress-5.4-latest.zip
# Analyze Webpack Bundle
The webpack configs for both `admin/` and `compat-js/` include the `BundleAnalyzerPlugin`,
The webpack configs for the `admin/` JavaScript bundle includes the `BundleAnalyzerPlugin`,
which produces a corresponding `webpack-stats.html` file in the corresponding
directory on each build.

12
admin/.prettierrc Normal file
View File

@ -0,0 +1,12 @@
{
"bracketSameLine": false,
"htmlWhitespaceSensitivity": "css",
"printWidth": 160,
"quoteProps": "consistent",
"semi": false,
"singleAttributePerLine": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "none",
"useTabs": false
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
"use strict";(self.webpackChunkfont_awesome_admin=self.webpackChunkfont_awesome_admin||[]).push([[250],{1250:(e,o,s)=>{s.r(o),s.d(o,{default:()=>r});var t=s(1083);const r=e=>t.A.get(e).then((e=>e.status>=200||e.satus<=299?e.data:(console.error(e),Promise.reject("Font Awesome plugin unexpected response for Icon Chooser")))).catch((e=>(console.error(e),Promise.reject(e))))}}]);

View File

@ -1 +0,0 @@
"use strict";(self.webpackChunkfont_awesome_admin=self.webpackChunkfont_awesome_admin||[]).push([[268],{9268:(e,a,r)=>{r.r(a),r.d(a,{default:()=>n});var t=r(634),o=r.n(t);const n=e=>async(a,r)=>{try{const{apiNonce:t,rootUrl:n,restApiNamespace:s}=e;return o().use(o().createRootURLMiddleware(n)),o().use(o().createNonceMiddleware(t)),await o()({path:`${s}/api`,method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:a.replace(/\s+/g," "),variables:r})})}catch(e){throw console.error("CAUGHT:",e),new Error(e)}}}}]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
"use strict";(self.webpackChunkfont_awesome_admin=self.webpackChunkfont_awesome_admin||[]).push([[331],{3331:(e,t,s)=>{s.r(t),s.d(t,{scopeCss:()=>T});const r="-shadowcsshost",c="-shadowcssslotted",o="-shadowcsscontext",n=")(?:\\(((?:\\([^)(]*\\)|[^)(]*)+?)\\))?([^,{]*)",l=new RegExp("("+r+n,"gim"),a=new RegExp("("+o+n,"gim"),i=new RegExp("("+c+n,"gim"),p=r+"-no-combinator",h=/-shadowcsshost-no-combinator([^\s]*)/,u=[/::shadow/g,/::content/g],g=/-shadowcsshost/gim,d=/:host/gim,m=/::slotted/gim,f=/:host-context/gim,_=/\/\*\s*[\s\S]*?\*\//g,$=/\/\*\s*#\s*source(Mapping)?URL=[\s\S]+?\*\//g,x=/(\s*)([^;\{\}]+?)(\s*)((?:{%BLOCK%}?\s*;?)|(?:\s*;))/g,w=/([{}])/g,b=/(^.*?[^\\])??((:+)(.*)|$)/,S="%BLOCK%",W=(e,t)=>{const s=k(e);let r=0;return s.escapedString.replace(x,((...e)=>{const c=e[2];let o="",n=e[4],l="";n&&n.startsWith("{"+S)&&(o=s.blocks[r++],n=n.substring(8),l="{");const a=t({selector:c,content:o});return`${e[1]}${a.selector}${e[3]}${l}${a.content}${n}`}))},k=e=>{const t=e.split(w),s=[],r=[];let c=0,o=[];for(let e=0;e<t.length;e++){const n=t[e];"}"===n&&c--,c>0?o.push(n):(o.length>0&&(r.push(o.join("")),s.push(S),o=[]),s.push(n)),"{"===n&&c++}return o.length>0&&(r.push(o.join("")),s.push(S)),{escapedString:s.join(""),blocks:r}},O=(e,t,s)=>e.replace(t,((...e)=>{if(e[2]){const t=e[2].split(","),r=[];for(let c=0;c<t.length;c++){const o=t[c].trim();if(!o)break;r.push(s(p,o,e[3]))}return r.join(",")}return p+e[3]})),j=(e,t,s)=>e+t.replace(r,"")+s,E=(e,t,s)=>t.indexOf(r)>-1?j(e,t,s):e+t+s+", "+t+" "+e+s,R=(e,t)=>e.replace(b,((e,s="",r,c="",o="")=>s+t+c+o)),C=(e,t,s,r,c)=>W(e,(e=>{let c=e.selector,o=e.content;return"@"!==e.selector[0]?c=((e,t,s,r)=>e.split(",").map((e=>r&&e.indexOf("."+r)>-1?e.trim():((e,t)=>!(e=>(e=e.replace(/\[/g,"\\[").replace(/\]/g,"\\]"),new RegExp("^("+e+")([>\\s~+[.,{:][\\s\\S]*)?$","m")))(t).test(e))(e,t)?((e,t,s)=>{const r="."+(t=t.replace(/\[is=([^\]]*)\]/g,((e,...t)=>t[0]))),c=e=>{let c=e.trim();if(!c)return"";if(e.indexOf(p)>-1)c=((e,t,s)=>{if(g.lastIndex=0,g.test(e)){const t=`.${s}`;return e.replace(h,((e,s)=>R(s,t))).replace(g,t+" ")}return t+" "+e})(e,t,s);else{const t=e.replace(g,"");t.length>0&&(c=R(t,r))}return c},o=(e=>{const t=[];let s,r=0;return s=(e=e.replace(/(\[[^\]]*\])/g,((e,s)=>{const c=`__ph-${r}__`;return t.push(s),r++,c}))).replace(/(:nth-[-\w]+)(\([^)]+\))/g,((e,s,c)=>{const o=`__ph-${r}__`;return t.push(c),r++,s+o})),{content:s,placeholders:t}})(e);let n,l="",a=0;const i=/( |>|\+|~(?!=))\s*/g;let u=!((e=o.content).indexOf(p)>-1);for(;null!==(n=i.exec(e));){const t=n[1],s=e.slice(a,n.index).trim();u=u||s.indexOf(p)>-1,l+=`${u?c(s):s} ${t} `,a=i.lastIndex}const d=e.substring(a);return u=u||d.indexOf(p)>-1,l+=u?c(d):d,m=o.placeholders,l.replace(/__ph-(\d+)__/g,((e,t)=>m[+t]));var m})(e,t,s).trim():e.trim())).join(", "))(e.selector,t,s,r):(e.selector.startsWith("@media")||e.selector.startsWith("@supports")||e.selector.startsWith("@page")||e.selector.startsWith("@document"))&&(o=C(e.content,t,s,r)),{selector:c.replace(/\s{2,}/g," ").trim(),content:o}})),T=(e,t,s)=>{const n=t+"-h",h=t+"-s",g=e.match($)||[];e=e.replace(_,"");const x=[];if(s){const t=e=>{const t=`/*!@___${x.length}___*/`,s=`/*!@${e.selector}*/`;return x.push({placeholder:t,comment:s}),e.selector=t+e.selector,e};e=W(e,(e=>"@"!==e.selector[0]?t(e):e.selector.startsWith("@media")||e.selector.startsWith("@supports")||e.selector.startsWith("@page")||e.selector.startsWith("@document")?(e.content=W(e.content,t),e):e))}const w=((e,t,s,n,h)=>{const g=((e,t)=>{const s="."+t+" > ",r=[];return e=e.replace(i,((...e)=>{if(e[2]){const t=e[2].trim(),c=e[3],o=s+t+c;let n="";for(let t=e[4]-1;t>=0;t--){const s=e[5][t];if("}"===s||","===s)break;n=s+n}const l=n+o,a=`${n.trimRight()}${o.trim()}`;if(l.trim()!==a.trim()){const e=`${a}, ${l}`;r.push({orgSelector:l,updatedSelector:e})}return o}return p+e[3]})),{selectors:r,cssText:e}})(e=(e=>O(e,a,E))(e=(e=>O(e,l,j))(e=e.replace(f,o).replace(d,r).replace(m,c))),n);return e=(e=>u.reduce(((e,t)=>e.replace(t," ")),e))(e=g.cssText),t&&(e=C(e,t,s,n)),{cssText:(e=(e=e.replace(/-shadowcsshost-no-combinator/g,`.${s}`)).replace(/>\s*\*\s+([^{, ]+)/gm," $1 ")).trim(),slottedSelectors:g.selectors}})(e,t,n,h);return e=[w.cssText,...g].join("\n"),s&&x.forEach((({placeholder:t,comment:s})=>{e=e.replace(t,s)})),w.slottedSelectors.forEach((t=>{e=e.replace(t.orgSelector,t.updatedSelector)})),e}}}]);

View File

@ -8,4 +8,3 @@
.FGrSfvJewATz8TfOqA_j th.dDmxKRAWr1lhLPK3Z838,td.dDmxKRAWr1lhLPK3Z838{background-color:#ffe2e2}
.heZgRJQYY60l5e4s0W_4 th{vertical-align:top}.heZgRJQYY60l5e4s0W_4 th .cBkIuJWm4fbhOOHopdph{font-weight:700}.heZgRJQYY60l5e4s0W_4 code{font-size:10px}.qxjS23M34RH041PZzC82,.L1uULhjJTYD39y7vA6HC{margin-top:.5rem}.JL6BMdxHE5CPnMDBfHe8{display:flex}
.n4FnenNNXVSnogTxvEag{background-color:#fdfdf3;border:1px solid #000;display:inline-block;padding:1.5em}.SkXS_7p2VPLT73VFRBky{background-color:#0000;border-radius:5px;padding:.5rem}.SkXS_7p2VPLT73VFRBky:hover{cursor:pointer}.SkXS_7p2VPLT73VFRBky .OLbojYNxmUgjVLX8SD7b{margin-left:1em}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
(self.webpackChunkfont_awesome_admin=self.webpackChunkfont_awesome_admin||[]).push([[438],{5325:(r,e,n)=>{var t=n(6131);r.exports=function(r,e){return!(null==r||!r.length)&&t(r,e,0)>-1}},9905:r=>{r.exports=function(r,e,n){for(var t=-1,c=null==r?0:r.length;++t<c;)if(n(e,r[t]))return!0;return!1}},3915:(r,e,n)=>{var t=n(8859),c=n(5325),i=n(9905),o=n(4932),a=n(7301),f=n(9219);r.exports=function(r,e,n,u){var s=-1,l=c,v=!0,p=r.length,x=[],h=e.length;if(!p)return x;n&&(e=o(e,a(n))),u?(l=i,v=!1):e.length>=200&&(l=f,v=!1,e=new t(e));r:for(;++s<p;){var g=r[s],m=null==n?g:n(g);if(g=u||0!==g?g:0,v&&m==m){for(var z=h;z--;)if(e[z]===m)continue r;x.push(g)}else l(e,m,u)||x.push(g)}return x}},6131:(r,e,n)=>{var t=n(2523),c=n(5463),i=n(6959);r.exports=function(r,e,n){return e==e?i(r,e,n):t(r,c,n)}},5463:r=>{r.exports=function(r){return r!=r}},1437:(r,e,n)=>{var t=n(2552),c=n(346);r.exports=function(r){return c(r)&&"[object RegExp]"==t(r)}},9302:(r,e,n)=>{var t=n(3488),c=n(6757),i=n(2865);r.exports=function(r,e){return i(c(r,e,t),r+"")}},8024:(r,e,n)=>{var t=n(5288);r.exports=function(r,e){for(var n=-1,c=r.length,i=0,o=[];++n<c;){var a=r[n],f=e?e(a):a;if(!n||!t(f,u)){var u=f;o[i++]=0===a?0:a}}return o}},6959:r=>{r.exports=function(r,e,n){for(var t=n-1,c=r.length;++t<c;)if(r[t]===e)return t;return-1}},6245:(r,e,n)=>{var t=n(3915),c=n(3120),i=n(9302),o=n(3693),a=i((function(r,e){return o(r)?t(r,c(e,1,o,!0)):[]}));r.exports=a},3693:(r,e,n)=>{var t=n(4894),c=n(346);r.exports=function(r){return c(r)&&t(r)}},2404:(r,e,n)=>{var t=n(270);r.exports=function(r,e){return t(r,e)}},9607:(r,e,n)=>{var t=n(1437),c=n(7301),i=n(6009),o=i&&i.isRegExp,a=o?c(o):t;r.exports=a},3054:(r,e,n)=>{var t=n(8024);r.exports=function(r){return r&&r.length?t(r):[]}},2516:(r,e,n)=>{var t=n(7556),c=n(8754),i=n(9698),o=n(3805),a=n(9607),f=n(1993),u=n(3912),s=n(1489),l=n(3222),v=/\w*$/;r.exports=function(r,e){var n=30,p="...";if(o(e)){var x="separator"in e?e.separator:x;n="length"in e?s(e.length):n,p="omission"in e?t(e.omission):p}var h=(r=l(r)).length;if(i(r)){var g=u(r);h=g.length}if(n>=h)return r;var m=n-f(p);if(m<1)return p;var z=g?c(g,0,m).join(""):r.slice(0,m);if(void 0===x)return z+p;if(g&&(m+=z.length-m),a(x)){if(r.slice(m).search(x)){var M,d=z;for(x.global||(x=RegExp(x.source,l(v.exec(x))+"g")),x.lastIndex=0;M=x.exec(d);)var w=M.index;z=z.slice(0,void 0===w?m:w)}}else if(r.indexOf(t(x),m)!=m){var k=z.lastIndexOf(x);k>-1&&(z=z.slice(0,k))}return z+p}},7897:(r,e,n)=>{"use strict";n.d(e,{GEE:()=>i,Nfw:()=>c,SGM:()=>t,wRm:()=>o});var t={prefix:"far",iconName:"circle-check",icon:[512,512,[61533,"check-circle"],"f058","M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209c9.4-9.4 9.4-24.6 0-33.9s-24.6-9.4-33.9 0l-111 111-47-47c-9.4-9.4-24.6-9.4-33.9 0s-9.4 24.6 0 33.9l64 64c9.4 9.4 24.6 9.4 33.9 0L369 209z"]},c={prefix:"far",iconName:"square",icon:[448,512,[9632,9723,9724,61590],"f0c8","M384 80c8.8 0 16 7.2 16 16V416c0 8.8-7.2 16-16 16H64c-8.8 0-16-7.2-16-16V96c0-8.8 7.2-16 16-16H384zM64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64z"]},i={prefix:"far",iconName:"circle",icon:[512,512,[128308,128309,128992,128993,128994,128995,128996,9679,9898,9899,11044,61708,61915],"f111","M464 256A208 208 0 1 0 48 256a208 208 0 1 0 416 0zM0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256z"]},o={prefix:"far",iconName:"circle-question",icon:[512,512,[62108,"question-circle"],"f059","M464 256A208 208 0 1 0 48 256a208 208 0 1 0 416 0zM0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256zm169.8-90.7c7.9-22.3 29.1-37.3 52.8-37.3h58.3c34.9 0 63.1 28.3 63.1 63.1c0 22.6-12.1 43.5-31.7 54.8L280 264.4c-.2 13-10.9 23.6-24 23.6c-13.3 0-24-10.7-24-24V250.5c0-8.6 4.6-16.5 12.1-20.8l44.3-25.4c4.7-2.7 7.6-7.7 7.6-13.1c0-8.4-6.8-15.1-15.1-15.1H222.6c-3.4 0-6.4 2.1-7.5 5.3l-.4 1.2c-4.4 12.5-18.2 19-30.6 14.6s-19-18.2-14.6-30.6l.4-1.2zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"]}}}]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
"use strict";(self.webpackChunkfont_awesome_admin=self.webpackChunkfont_awesome_admin||[]).push([[56],{6056:(e,s,a)=>{a.r(s)}}]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
"use strict";(self.webpackChunkfont_awesome_admin=self.webpackChunkfont_awesome_admin||[]).push([[875],{2875:(i,t,e)=>{e.r(t),e.d(t,{fa_icon:()=>o});var s=e(858),n=e(9327);let o=class{constructor(i){(0,s.r)(this,i),this.pro=!1,this.loading=!1}componentWillLoad(){if(this.iconUpload)return void(this.iconDefinition={prefix:this.stylePrefix,iconName:this.iconUpload.name,icon:[parseInt(`${this.iconUpload.width}`),parseInt(`${this.iconUpload.height}`),[],this.iconUpload.unicode.toString(16),this.iconUpload.pathData]});if(this.icon)return void(this.iconDefinition=this.icon);if(!this.svgApi)return void console.error(`${n.C}: fa-icon: svgApi prop is needed but is missing`,this);if(!this.stylePrefix||!this.name)return void console.error(`${n.C}: fa-icon: the 'stylePrefix' and 'name' props are needed to render this icon but not provided.`,this);if(!this.familyStylePathSegment)return void console.error(`${n.C}: fa-icon: the 'familyStylePathSegment' prop is required to render this icon but not provided.`,this);const{findIconDefinition:i}=this.svgApi,t=i&&i({prefix:this.stylePrefix,iconName:this.name});if(t)return void(this.iconDefinition=t);if(!this.pro)return void console.error(`${n.C}: fa-icon: 'pro' prop is false but no free icon is available`,this);if(!this.svgFetchBaseUrl)return void console.error(`${n.C}: fa-icon: 'svgFetchBaseUrl' prop is absent but is necessary for fetching icon`,this);if(!this.kitToken)return void console.error(`${n.C}: fa-icon: 'kitToken' prop is absent but is necessary for accessing icon`,this);this.loading=!0;const e=`${this.svgFetchBaseUrl}/${this.familyStylePathSegment}/${this.name}.svg?token=${this.kitToken}`,s=n.l.get(this,"svgApi.library");"function"==typeof this.getUrlText?this.getUrlText(e).then((i=>{const t={iconName:this.name,prefix:this.stylePrefix,icon:(0,n.p)(i)};s&&s.add(t),this.iconDefinition=Object.assign({},t)})).catch((i=>{console.error(`${n.C}: fa-icon: failed when using 'getUrlText' to fetch icon`,i,this)})).finally((()=>{this.loading=!1})):console.error(`${n.C}: fa-icon: 'getUrlText' prop is absent but is necessary for fetching icon`,this)}buildSvg(i,t){if(!i)return;const[e,o,,,r]=n.l.get(i,"icon",[]),h=["svg-inline--fa"];this.class&&h.push(this.class),t&&h.push(t),this.size&&h.push(`fa-${this.size}`);const a=h.join(" ");return Array.isArray(r)?(0,s.h)("svg",{class:a,xmlns:"http://www.w3.org/2000/svg",viewBox:`0 0 ${e} ${o}`},(0,s.h)("path",{fill:"currentColor",class:"fa-primary",d:r[1]}),(0,s.h)("path",{fill:"currentColor",class:"fa-secondary",d:r[0]})):(0,s.h)("svg",{class:a,xmlns:"http://www.w3.org/2000/svg",viewBox:`0 0 ${e} ${o}`},(0,s.h)("path",{fill:"currentColor",d:r}))}render(){return this.iconDefinition?this.buildSvg(this.iconDefinition):(0,s.h)(s.f,null)}};o.style=""}}]);

View File

@ -0,0 +1 @@
"use strict";(self.webpackChunkfont_awesome_admin=self.webpackChunkfont_awesome_admin||[]).push([[897],{897:(c,e,a)=>{a.d(e,{GEE:()=>n,Nfw:()=>f,SGM:()=>i,wRm:()=>r});var i={prefix:"far",iconName:"circle-check",icon:[512,512,[61533,"check-circle"],"f058","M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209c9.4-9.4 9.4-24.6 0-33.9s-24.6-9.4-33.9 0l-111 111-47-47c-9.4-9.4-24.6-9.4-33.9 0s-9.4 24.6 0 33.9l64 64c9.4 9.4 24.6 9.4 33.9 0L369 209z"]},f={prefix:"far",iconName:"square",icon:[448,512,[9632,9723,9724,61590],"f0c8","M384 80c8.8 0 16 7.2 16 16V416c0 8.8-7.2 16-16 16H64c-8.8 0-16-7.2-16-16V96c0-8.8 7.2-16 16-16H384zM64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64z"]},n={prefix:"far",iconName:"circle",icon:[512,512,[128308,128309,128992,128993,128994,128995,128996,9679,9898,9899,11044,61708,61915],"f111","M464 256A208 208 0 1 0 48 256a208 208 0 1 0 416 0zM0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256z"]},r={prefix:"far",iconName:"circle-question",icon:[512,512,[62108,"question-circle"],"f059","M464 256A208 208 0 1 0 48 256a208 208 0 1 0 416 0zM0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256zm169.8-90.7c7.9-22.3 29.1-37.3 52.8-37.3h58.3c34.9 0 63.1 28.3 63.1 63.1c0 22.6-12.1 43.5-31.7 54.8L280 264.4c-.2 13-10.9 23.6-24 23.6c-13.3 0-24-10.7-24-24V250.5c0-8.6 4.6-16.5 12.1-20.8l44.3-25.4c4.7-2.7 7.6-7.7 7.6-13.1c0-8.4-6.8-15.1-15.1-15.1H222.6c-3.4 0-6.4 2.1-7.5 5.3l-.4 1.2c-4.4 12.5-18.2 19-30.6 14.6s-19-18.2-14.6-30.6l.4-1.2zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"]}}}]);

View File

@ -0,0 +1 @@
<?php return array('dependencies' => array('lodash', 'react', 'react-dom', 'wp-dom-ready', 'wp-element', 'wp-i18n'), 'version' => '216e55b4ec5691929633');

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -2,7 +2,7 @@ const defaultConfig = require('@wordpress/scripts/config/jest-e2e.config')
require('dotenv').config({ path: '../.env' })
process.env.WP_BASE_URL = `http://${process.env.WP_DOMAIN}`
process.env.WP_USERNAME=process.env.WP_ADMIN_USERNAME
process.env.WP_PASSWORD=process.env.WP_ADMIN_PASSWORD
process.env.WP_USERNAME = process.env.WP_ADMIN_USERNAME
process.env.WP_PASSWORD = process.env.WP_ADMIN_PASSWORD
module.exports = defaultConfig;
module.exports = defaultConfig

992
admin/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,8 @@
{
"name": "font-awesome-admin",
"version": "4.5.0",
"version": "5.0.0",
"private": true,
"dependencies": {
"@fortawesome/fa-icon-chooser-react": "^0.7.0",
"@fortawesome/fontawesome-svg-core": "^6.2.0",
"@fortawesome/free-regular-svg-icons": "^6.2.0",
"@fortawesome/free-solid-svg-icons": "^6.2.0",
@ -19,7 +18,7 @@
"web-vitals": "^4.0.1"
},
"scripts": {
"build": "wp-scripts build --webpack-no-externals",
"build": "wp-scripts build",
"check-engines": "wp-scripts check-engines",
"check-licenses": "wp-scripts check-licenses",
"format": "wp-scripts format",
@ -29,7 +28,7 @@
"lint:md:js": "wp-scripts lint-md-js",
"lint:pkg-json": "wp-scripts lint-pkg-json",
"packages-update": "wp-scripts packages-update",
"start": "wp-scripts start --webpack-no-externals",
"start": "wp-scripts start",
"test:e2e": "wp-scripts test-e2e",
"test:playwright": "npx playwright test",
"test:unit": "wp-scripts test-unit-js",
@ -69,6 +68,8 @@
"@wordpress/scripts": "wp-6.5",
"decode-uri-component": "^0.4.1",
"dotenv": "^14.3.2",
"eslint-config-react-app": "^7.0.1",
"lodash": "^4.17.21",
"mysql2": "^3.9.9",
"react": "18.3.1",
"react-dom": "18.3.1",

View File

@ -1,5 +1,5 @@
import './src/playwright/support/env.js'
import { defineConfig, devices } from '@playwright/test';
import { defineConfig, devices } from '@playwright/test'
const testDir = 'src/playwright'
const baseURL = `http://${process.env.WP_DOMAIN}`
@ -8,7 +8,7 @@ const adminStorageStatePath = 'src/playwright/.auth/state.json'
export default defineConfig({
use: {
baseURL,
baseURL
},
projects: [
{ name: 'auth', testDir, testMatch: 'setup/auth.js' },
@ -18,7 +18,7 @@ export default defineConfig({
testMatch: 'setup/reset.js',
use: {
storageState: adminStorageStatePath
},
}
},
{
name: 'setupProKit',
@ -27,10 +27,7 @@ export default defineConfig({
use: {
storageState: adminStorageStatePath
},
dependencies: [
'auth',
'reset'
]
dependencies: ['auth', 'reset']
},
{
name: 'with-proKit-chromium',
@ -39,7 +36,7 @@ export default defineConfig({
...devices['Desktop Chrome'],
storageState: adminStorageStatePath
},
dependencies: ['setupProKit'],
dependencies: ['setupProKit']
},
{
name: 'withAuth-chromium',
@ -48,8 +45,7 @@ export default defineConfig({
...devices['Desktop Chrome'],
storageState: adminStorageStatePath
},
dependencies: ['auth', 'reset'],
dependencies: ['auth', 'reset']
}
]
})

View File

@ -3,51 +3,73 @@ import PropTypes from 'prop-types'
import styles from './Alert.module.css'
import classnames from 'classnames'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import {
faInfoCircle,
faThumbsUp,
faSpinner,
faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'
import { faInfoCircle, faThumbsUp, faSpinner, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'
function getIcon(props = {}){
switch(props.type){
function getIcon(props = {}) {
switch (props.type) {
case 'info':
return <FontAwesomeIcon icon={ faInfoCircle } title='info' fixedWidth />
return (
<FontAwesomeIcon
icon={faInfoCircle}
title="info"
fixedWidth
/>
)
case 'warning':
return <FontAwesomeIcon icon={ faExclamationTriangle } title='warning' fixedWidth />
return (
<FontAwesomeIcon
icon={faExclamationTriangle}
title="warning"
fixedWidth
/>
)
case 'pending':
return <FontAwesomeIcon icon={ faSpinner } title='pending' spin fixedWidth />
return (
<FontAwesomeIcon
icon={faSpinner}
title="pending"
spin
fixedWidth
/>
)
case 'success':
return <FontAwesomeIcon icon={ faThumbsUp } title='success' fixedWidth />
return (
<FontAwesomeIcon
icon={faThumbsUp}
title="success"
fixedWidth
/>
)
default:
return <FontAwesomeIcon icon={ faExclamationTriangle } title='warning' fixedWidth />
return (
<FontAwesomeIcon
icon={faExclamationTriangle}
title="warning"
fixedWidth
/>
)
}
}
function Alert(props = {}) {
return <div className={ classnames(styles['alert'], styles[`alert-${ props.type }`]) } role="alert">
<div className={ styles['alert-icon'] }>
{ getIcon(props) }
</div>
<div className={ styles['alert-message'] }>
<h2 className={ styles['alert-title'] }>
{ props.title }
</h2>
<div className={ styles['alert-copy'] }>
{ props.children }
return (
<div
className={classnames(styles['alert'], styles[`alert-${props.type}`])}
role="alert"
>
<div className={styles['alert-icon']}>{getIcon(props)}</div>
<div className={styles['alert-message']}>
<h2 className={styles['alert-title']}>{props.title}</h2>
<div className={styles['alert-copy']}>{props.children}</div>
</div>
</div>
</div>
)
}
Alert.propTypes = {
title: PropTypes.string.isRequired,
type: PropTypes.oneOf(['info', 'warning', 'success', 'pending']),
children: PropTypes.oneOfType([
PropTypes.object,
PropTypes.string,
PropTypes.arrayOf(PropTypes.element)
]).isRequired
children: PropTypes.oneOfType([PropTypes.object, PropTypes.string, PropTypes.arrayOf(PropTypes.element)]).isRequired
}
export default Alert

View File

@ -1,23 +1,15 @@
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import {
addPendingOption,
checkPreferenceConflicts
} from './store/actions'
import { addPendingOption, checkPreferenceConflicts } from './store/actions'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import {
faDotCircle,
faCheckSquare,
faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons'
import { faDotCircle, faCheckSquare, faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons'
import { faCircle, faSquare } from '@fortawesome/free-regular-svg-icons'
import styles from './CdnConfigView.module.css'
import sharedStyles from './App.module.css'
import classnames from 'classnames'
import has from 'lodash/has'
import size from 'lodash/size'
import { has, size, get } from 'lodash'
import Alert from './Alert'
import PropTypes from 'prop-types'
import get from 'lodash/get'
import { __ } from '@wordpress/i18n'
const UNSPECIFIED = ''
@ -30,20 +22,22 @@ export default function CdnConfigView({ useOption, handleSubmit }) {
const pseudoElements = useOption('pseudoElements')
const isVersion6 = !!version.match(/^6\./)
const pendingOptions = useSelector(state => state.pendingOptions)
const pendingOptionConflicts = useSelector(state => state.pendingOptionConflicts)
const hasChecked = useSelector(state => state.preferenceConflictDetection.hasChecked)
const preferenceCheckSuccess = useSelector(state => state.preferenceConflictDetection.success)
const preferenceCheckMessage = useSelector(state => state.preferenceConflictDetection.message)
const pendingOptions = useSelector((state) => state.pendingOptions)
const pendingOptionConflicts = useSelector((state) => state.pendingOptionConflicts)
const hasChecked = useSelector((state) => state.preferenceConflictDetection.hasChecked)
const preferenceCheckSuccess = useSelector((state) => state.preferenceConflictDetection.success)
const preferenceCheckMessage = useSelector((state) => state.preferenceConflictDetection.message)
const versionOptions = useSelector(state => {
const { releases: { available, latest_version_5, latest_version_6 } } = state
const versionOptions = useSelector((state) => {
const {
releases: { available, latest_version_5, latest_version_6 }
} = state
return available.reduce((acc, version) => {
if( latest_version_5 === version ) {
acc[version] = `${ version } (latest 5.x)`
} else if( latest_version_6 === version ) {
acc[version] = `${ version } (latest)`
if (latest_version_5 === version) {
acc[version] = `${version} (latest 5.x)`
} else if (latest_version_6 === version) {
acc[version] = `${version} (latest)`
} else {
acc[version] = version
}
@ -54,7 +48,7 @@ export default function CdnConfigView({ useOption, handleSubmit }) {
const dispatch = useDispatch()
function handleOptionChange(change = {}, check = true) {
const pendingTechnology = get( change, 'technology' )
const pendingTechnology = get(change, 'technology')
const adjustedChange = pendingTechnology
? 'webfont' === pendingTechnology
@ -67,23 +61,33 @@ export default function CdnConfigView({ useOption, handleSubmit }) {
}
function getDetectionStatusForOption(option) {
if(has(pendingOptions, option)) {
if ( hasChecked && ! preferenceCheckSuccess ) {
return <Alert title={ __( 'Error checking preferences', 'font-awesome' ) } type='warning'>
<p>{ preferenceCheckMessage }</p>
</Alert>
if (has(pendingOptions, option)) {
if (hasChecked && !preferenceCheckSuccess) {
return (
<Alert
title={__('Error checking preferences', 'font-awesome')}
type="warning"
>
<p>{preferenceCheckMessage}</p>
</Alert>
)
} else if (has(pendingOptionConflicts, option)) {
return <Alert title={ __( 'Preference Conflict', 'font-awesome' ) } type='warning'>
{
size(pendingOptionConflicts[option]) > 1
? <div>
{ __( 'This change might cause problems for these themes or plugins', 'font-awesome' ) }: { pendingOptionConflicts[option].join(', ') }.
return (
<Alert
title={__('Preference Conflict', 'font-awesome')}
type="warning"
>
{size(pendingOptionConflicts[option]) > 1 ? (
<div>
{__('This change might cause problems for these themes or plugins', 'font-awesome')}: {pendingOptionConflicts[option].join(', ')}.
</div>
: <div>
{ __( 'This change might cause problems for the theme or plugin', 'font-awesome' ) }: { pendingOptionConflicts[option][0] }.
</div>
}
</Alert>
) : (
<div>
{__('This change might cause problems for the theme or plugin', 'font-awesome')}: {pendingOptionConflicts[option][0]}.
</div>
)}
</Alert>
)
} else {
return null
}
@ -92,184 +96,225 @@ export default function CdnConfigView({ useOption, handleSubmit }) {
}
}
return <div className={ classnames(styles['options-setter']) }>
<form onSubmit={ e => e.preventDefault() }>
<div className={ classnames( sharedStyles['flex'], sharedStyles['flex-row'] ) }>
<div className={ styles['option-header'] }>Icons</div>
<div className={ styles['option-choice-container'] }>
<div className={ styles['option-choices'] }>
<div className={ styles['option-choice'] }>
return (
<div className={classnames(styles['options-setter'])}>
<form onSubmit={(e) => e.preventDefault()}>
<div className={classnames(sharedStyles['flex'], sharedStyles['flex-row'])}>
<div className={styles['option-header']}>Icons</div>
<div className={styles['option-choice-container']}>
<div className={styles['option-choices']}>
<div className={styles['option-choice']}>
<input
id="code_edit_icons_pro"
name="code_edit_icons"
type="radio"
checked={ usePro }
onChange={ () => handleOptionChange({ usePro: true }) }
className={ classnames(sharedStyles['sr-only'], sharedStyles['input-radio-custom']) }
checked={usePro}
onChange={() => handleOptionChange({ usePro: true })}
className={classnames(sharedStyles['sr-only'], sharedStyles['input-radio-custom'])}
/>
<label htmlFor="code_edit_icons_pro" className={ styles['option-label'] }>
<span className={ sharedStyles['relative'] }>
<FontAwesomeIcon
icon={ faDotCircle }
className={ sharedStyles['checked-icon'] }
size="lg"
fixedWidth
/>
<FontAwesomeIcon
icon={ faCircle }
className={ sharedStyles['unchecked-icon'] }
size="lg"
fixedWidth
/>
</span>
<span className={ styles['option-label-text'] }>
Pro
</span>
<label
htmlFor="code_edit_icons_pro"
className={styles['option-label']}
>
<span className={sharedStyles['relative']}>
<FontAwesomeIcon
icon={faDotCircle}
className={sharedStyles['checked-icon']}
size="lg"
fixedWidth
/>
<FontAwesomeIcon
icon={faCircle}
className={sharedStyles['unchecked-icon']}
size="lg"
fixedWidth
/>
</span>
<span className={styles['option-label-text']}>Pro</span>
</label>
</div>
<div className={ styles['option-choice'] }>
<div className={styles['option-choice']}>
<input
id="code_edit_icons_free"
name="code_edit_icons"
type="radio"
checked={ ! usePro }
onChange={ () => handleOptionChange({ usePro: false }) }
className={ classnames(sharedStyles['sr-only'], sharedStyles['input-radio-custom']) }
checked={!usePro}
onChange={() => handleOptionChange({ usePro: false })}
className={classnames(sharedStyles['sr-only'], sharedStyles['input-radio-custom'])}
/>
<label htmlFor="code_edit_icons_free" className={ styles['option-label'] }>
<span className={ sharedStyles['relative'] }>
<label
htmlFor="code_edit_icons_free"
className={styles['option-label']}
>
<span className={sharedStyles['relative']}>
<FontAwesomeIcon
icon={ faDotCircle }
icon={faDotCircle}
size="lg"
fixedWidth
className={ sharedStyles['checked-icon'] }
className={sharedStyles['checked-icon']}
/>
<FontAwesomeIcon
icon={ faCircle }
icon={faCircle}
size="lg"
fixedWidth
className={ sharedStyles['unchecked-icon'] }
className={sharedStyles['unchecked-icon']}
/>
</span>
<span className={ styles['option-label-text'] }>
Free
</span>
<span className={styles['option-label-text']}>Free</span>
</label>
</div>
</div>
{ usePro &&
isVersion6 &&
<Alert title={ __( 'Heads up! Pro Version 6 is not available from CDN', 'font-awesome' ) } type='warning'>
<p>You can, however, use a Kit. Make sure you have a paid subscription and select "Use a Kit" above. We'll walk you through the other details from there.</p>
{usePro && isVersion6 && (
<Alert
title={__('Heads up! Pro Version 6 is not available from CDN', 'font-awesome')}
type="warning"
>
<p>
You can, however, use a Kit. Make sure you have an active Font Awesome subscription and select "Use a Kit" above. We'll walk you through the
other details from there.
</p>
</Alert>
}
{ usePro &&
!isVersion6 &&
<Alert title={ __( 'Heads up! Pro requires a Font Awesome subscription', 'font-awesome' ) } type='info'>
<p>And you need to add your WordPress site to the allowed domains for your CDN.</p>
)}
{usePro && !isVersion6 && (
<Alert
title={__('Heads up! Pro requires a Font Awesome subscription', 'font-awesome')}
type="info"
>
<p>And you need to add your WordPress site to the allowed domains for your CDN.</p>
<ul>
<li>
<a rel="noopener noreferrer" target="_blank" href="https://fontawesome.com/account/cdn">{ __( 'Manage my allowed domains', 'font-awesome' ) }<FontAwesomeIcon icon={faExternalLinkAlt} style={{marginLeft: '.5em'}} /></a>
<a
rel="noopener noreferrer"
target="_blank"
href="https://fontawesome.com/account/cdn"
>
{__('Manage my allowed domains', 'font-awesome')}
<FontAwesomeIcon
icon={faExternalLinkAlt}
style={{ marginLeft: '.5em' }}
/>
</a>
</li>
<li>
<a rel="noopener noreferrer" target="_blank" href="https://fontawesome.com/pro">{ __( 'Get Pro', 'font-awesome' ) }<FontAwesomeIcon icon={faExternalLinkAlt} style={{marginLeft: '.5em'}} /></a>
<a
rel="noopener noreferrer"
target="_blank"
href="https://fontawesome.com/pro"
>
{__('Get Pro', 'font-awesome')}
<FontAwesomeIcon
icon={faExternalLinkAlt}
style={{ marginLeft: '.5em' }}
/>
</a>
</li>
</ul>
</Alert>
}
{ getDetectionStatusForOption('usePro') }
)}
{getDetectionStatusForOption('usePro')}
</div>
</div>
<hr className={ styles['option-divider'] }/>
<div className={ classnames( sharedStyles['flex'], sharedStyles['flex-row'] ) }>
<div className={ styles['option-header'] }>{ __( 'Technology', 'font-awesome' ) }</div>
<div className={ styles['option-choice-container'] }>
<div className={ styles['option-choices'] }>
<div className={ styles['option-choice'] }>
<hr className={styles['option-divider']} />
<div className={classnames(sharedStyles['flex'], sharedStyles['flex-row'])}>
<div className={styles['option-header']}>{__('Technology', 'font-awesome')}</div>
<div className={styles['option-choice-container']}>
<div className={styles['option-choices']}>
<div className={styles['option-choice']}>
<input
id="code_edit_tech_svg"
name="code_edit_tech"
type="radio"
checked={ technology === 'svg' }
onChange={ () => handleOptionChange({ technology: 'svg' }) }
className={ classnames(sharedStyles['sr-only'], sharedStyles['input-radio-custom']) }
checked={technology === 'svg'}
onChange={() => handleOptionChange({ technology: 'svg' })}
className={classnames(sharedStyles['sr-only'], sharedStyles['input-radio-custom'])}
/>
<label htmlFor="code_edit_tech_svg" className={ styles['option-label'] }>
<span className={ sharedStyles['relative'] }>
<label
htmlFor="code_edit_tech_svg"
className={styles['option-label']}
>
<span className={sharedStyles['relative']}>
<FontAwesomeIcon
icon={ faDotCircle }
className={ sharedStyles['checked-icon'] }
icon={faDotCircle}
className={sharedStyles['checked-icon']}
size="lg"
fixedWidth
/>
<FontAwesomeIcon
icon={ faCircle }
className={ sharedStyles['unchecked-icon'] }
icon={faCircle}
className={sharedStyles['unchecked-icon']}
size="lg"
fixedWidth
/>
</span>
<span className={ styles['option-label-text'] }>
{ __( 'SVG', 'font-awesome' ) }
</span>
<span className={styles['option-label-text']}>{__('SVG', 'font-awesome')}</span>
</label>
</div>
<div className={ styles['option-choice'] }>
<div className={styles['option-choice']}>
<input
id="code_edit_tech_webfont"
name="code_edit_tech"
type="radio"
checked={ technology === 'webfont' }
onChange={ () => handleOptionChange({
technology: 'webfont',
pseudoElements: false
}) }
className={ classnames(sharedStyles['sr-only'], sharedStyles['input-radio-custom']) }
checked={technology === 'webfont'}
onChange={() =>
handleOptionChange({
technology: 'webfont',
pseudoElements: false
})
}
className={classnames(sharedStyles['sr-only'], sharedStyles['input-radio-custom'])}
/>
<label htmlFor="code_edit_tech_webfont" className={ styles['option-label'] }>
<span className={ sharedStyles['relative'] }>
<FontAwesomeIcon
icon={ faDotCircle }
size="lg"
fixedWidth
className={ sharedStyles['checked-icon'] }
/>
<FontAwesomeIcon
icon={ faCircle }
size="lg"
fixedWidth
className={ sharedStyles['unchecked-icon'] }
/>
</span>
<span className={ styles['option-label-text'] }>
{ __( 'Web Font', 'font-awesome' ) }
{
technology === 'webfont' &&
<span className={styles['option-label-explanation']}>
{ __( 'CSS Pseudo-elements are enabled by default with Web Font', 'font-awesome' ) }
</span>
}
</span>
<label
htmlFor="code_edit_tech_webfont"
className={styles['option-label']}
>
<span className={sharedStyles['relative']}>
<FontAwesomeIcon
icon={faDotCircle}
size="lg"
fixedWidth
className={sharedStyles['checked-icon']}
/>
<FontAwesomeIcon
icon={faCircle}
size="lg"
fixedWidth
className={sharedStyles['unchecked-icon']}
/>
</span>
<span className={styles['option-label-text']}>
{__('Web Font', 'font-awesome')}
{technology === 'webfont' && (
<span className={styles['option-label-explanation']}>
{__('CSS Pseudo-elements are enabled by default with Web Font', 'font-awesome')}
</span>
)}
</span>
</label>
</div>
</div>
{ getDetectionStatusForOption('technology') }
{getDetectionStatusForOption('technology')}
</div>
</div>
<div className={ classnames( sharedStyles['flex'], sharedStyles['flex-row'] ) }>
<div className={ styles['option-header'] }></div>
<div className={ styles['option-choice-container'] } style={{marginTop: '1em'}}>
{ technology === 'svg' &&
<div className={classnames(sharedStyles['flex'], sharedStyles['flex-row'])}>
<div className={styles['option-header']}></div>
<div
className={styles['option-choice-container']}
style={{ marginTop: '1em' }}
>
{technology === 'svg' && (
<>
<input
id="code_edit_features_pseudo_elements"
name="code_edit_features"
type="checkbox"
checked={ pseudoElements }
checked={pseudoElements}
onChange={() => handleOptionChange({ pseudoElements: !pseudoElements })}
className={classnames(sharedStyles['sr-only'], sharedStyles['input-checkbox-custom'])}
/>
<label htmlFor="code_edit_features_pseudo_elements" className={styles['option-label']}>
<label
htmlFor="code_edit_features_pseudo_elements"
className={styles['option-label']}
>
<span className={sharedStyles['relative']}>
<FontAwesomeIcon
icon={faCheckSquare}
@ -285,113 +330,129 @@ export default function CdnConfigView({ useOption, handleSubmit }) {
/>
</span>
<span className={styles['option-label-text']}>
{ __( 'Enable CSS Pseudo-elements with SVG', 'font-awesome' ) }
{__('Enable CSS Pseudo-elements with SVG', 'font-awesome')}
<span className={styles['option-label-explanation']}>
{ __( 'May cause performance issues.', 'font-awesome' ) } <a rel="noopener noreferrer" target="_blank" style={{marginLeft: '.5em'}} href="https://fontawesome.com/how-to-use/on-the-web/advanced/css-pseudo-elements">
{ __( 'Learn more', 'font-awesome' ) } <FontAwesomeIcon icon={faExternalLinkAlt} style={{marginLeft: '.5em'}} />
{__('May cause performance issues.', 'font-awesome')}{' '}
<a
rel="noopener noreferrer"
target="_blank"
style={{ marginLeft: '.5em' }}
href="https://fontawesome.com/how-to-use/on-the-web/advanced/css-pseudo-elements"
>
{__('Learn more', 'font-awesome')}{' '}
<FontAwesomeIcon
icon={faExternalLinkAlt}
style={{ marginLeft: '.5em' }}
/>
</a>
</span>
</span>
</label>
{ getDetectionStatusForOption('pseudoElements') }
{getDetectionStatusForOption('pseudoElements')}
</>
}
)}
</div>
</div>
<hr className={ styles['option-divider'] }/>
<div className={ classnames( sharedStyles['flex'], sharedStyles['flex-row'] ) }>
<div className={ styles['option-header'] }>Version</div>
<div className={ styles['option-choice-container'] }>
<div className={ styles['option-choices'] }>
<hr className={styles['option-divider']} />
<div className={classnames(sharedStyles['flex'], sharedStyles['flex-row'])}>
<div className={styles['option-header']}>Version</div>
<div className={styles['option-choice-container']}>
<div className={styles['option-choices']}>
<select
className={ styles['version-select'] }
className={styles['version-select']}
name="version"
onChange={ e => handleOptionChange({ version: e.target.value }) }
value={ version }
onChange={(e) => handleOptionChange({ version: e.target.value })}
value={version}
>
{
Object.keys(versionOptions).map((version, index) => {
return <option key={ index } value={ version }>
{ version === UNSPECIFIED ? '-' : versionOptions[version] }
{Object.keys(versionOptions).map((version, index) => {
return (
<option
key={index}
value={version}
>
{version === UNSPECIFIED ? '-' : versionOptions[version]}
</option>
})
}
)
})}
</select>
</div>
{ getDetectionStatusForOption('version') }
{getDetectionStatusForOption('version')}
</div>
</div>
<hr className={ styles['option-divider'] }/>
<div className={ classnames( sharedStyles['flex'], sharedStyles['flex-row'], styles['features'] ) }>
<div className={ styles['option-header'] }>Older Version Compatibility</div>
<div className={ styles['option-choice-container'] }>
<div className={ styles['option-choices'] }>
<div className={ styles['option-choice'] }>
<hr className={styles['option-divider']} />
<div className={classnames(sharedStyles['flex'], sharedStyles['flex-row'], styles['features'])}>
<div className={styles['option-header']}>Older Version Compatibility</div>
<div className={styles['option-choice-container']}>
<div className={styles['option-choices']}>
<div className={styles['option-choice']}>
<input
id="code_edit_compat_on"
name="code_edit_compat_on"
type="radio"
value={ compat }
checked={ compat }
onChange={ () => handleOptionChange({ compat: ! compat }) }
className={ classnames(sharedStyles['sr-only'], sharedStyles['input-radio-custom']) }
value={compat}
checked={compat}
onChange={() => handleOptionChange({ compat: !compat })}
className={classnames(sharedStyles['sr-only'], sharedStyles['input-radio-custom'])}
/>
<label htmlFor="code_edit_compat_on" className={ styles['option-label'] }>
<span className={ sharedStyles['relative'] }>
<label
htmlFor="code_edit_compat_on"
className={styles['option-label']}
>
<span className={sharedStyles['relative']}>
<FontAwesomeIcon
icon={ faDotCircle }
className={ sharedStyles['checked-icon'] }
icon={faDotCircle}
className={sharedStyles['checked-icon']}
size="lg"
fixedWidth
/>
<FontAwesomeIcon
icon={ faCircle }
className={ sharedStyles['unchecked-icon'] }
icon={faCircle}
className={sharedStyles['unchecked-icon']}
size="lg"
fixedWidth
/>
</span>
<span className={ styles['option-label-text'] }>
{ __( 'On', 'font-awesome' ) }
</span>
<span className={styles['option-label-text']}>{__('On', 'font-awesome')}</span>
</label>
</div>
<div className={ styles['option-choice'] }>
<div className={styles['option-choice']}>
<input
id="code_edit_v4_compat_off"
name="code_edit_v4_compat_off"
type="radio"
value={ ! compat }
checked={ ! compat }
onChange={ () => handleOptionChange({ compat: ! compat }) }
className={ classnames(sharedStyles['sr-only'], sharedStyles['input-radio-custom']) }
value={!compat}
checked={!compat}
onChange={() => handleOptionChange({ compat: !compat })}
className={classnames(sharedStyles['sr-only'], sharedStyles['input-radio-custom'])}
/>
<label htmlFor="code_edit_v4_compat_off" className={ styles['option-label'] }>
<span className={ sharedStyles['relative'] }>
<FontAwesomeIcon
icon={ faDotCircle }
size="lg"
fixedWidth
className={ sharedStyles['checked-icon'] }
/>
<FontAwesomeIcon
icon={ faCircle }
size="lg"
fixedWidth
className={ sharedStyles['unchecked-icon'] }
/>
</span>
<span className={ styles['option-label-text'] }>
{ __( 'Off', 'font-awesome' ) }
</span>
<label
htmlFor="code_edit_v4_compat_off"
className={styles['option-label']}
>
<span className={sharedStyles['relative']}>
<FontAwesomeIcon
icon={faDotCircle}
size="lg"
fixedWidth
className={sharedStyles['checked-icon']}
/>
<FontAwesomeIcon
icon={faCircle}
size="lg"
fixedWidth
className={sharedStyles['unchecked-icon']}
/>
</span>
<span className={styles['option-label-text']}>{__('Off', 'font-awesome')}</span>
</label>
</div>
</div>
{ getDetectionStatusForOption('compat') }
{getDetectionStatusForOption('compat')}
</div>
</div>
</form>
</div>
</div>
)
}
CdnConfigView.propTypes = {

View File

@ -6,9 +6,15 @@ import classnames from 'classnames'
import { faSpinner } from '@fortawesome/free-solid-svg-icons'
import { __ } from '@wordpress/i18n'
export default function CheckingOptionStatusIndicator(){
return <span className={ styles['checking-option-status-indicator'] }>
<FontAwesomeIcon spin className={ classnames(sharedStyles['icon']) } icon={ faSpinner }/>
&nbsp;{ __( 'checking for preference conflicts', 'font-awesome' ) }...
</span>
export default function CheckingOptionStatusIndicator() {
return (
<span className={styles['checking-option-status-indicator']}>
<FontAwesomeIcon
spin
className={classnames(sharedStyles['icon'])}
icon={faSpinner}
/>
&nbsp;{__('checking for preference conflicts', 'font-awesome')}...
</span>
)
}

View File

@ -2,9 +2,7 @@ import React from 'react'
import { useSelector } from 'react-redux'
import styles from './ClientPreferencesView.module.css'
import sharedStyles from './App.module.css'
import find from 'lodash/find'
import has from 'lodash/has'
import size from 'lodash/size'
import { find, has, size } from 'lodash'
import classnames from 'classnames'
import { __, sprintf } from '@wordpress/i18n'
@ -12,7 +10,7 @@ const UNSPECIFIED_INDICATOR = '-'
function formatVersionPreference(versionPreference = []) {
return versionPreference
.map(pref => `${pref[1]}${pref[0]}`)
.map((pref) => `${pref[1]}${pref[0]}`)
.join(
sprintf(
/* translators: 1: space */
@ -23,96 +21,73 @@ function formatVersionPreference(versionPreference = []) {
}
export default function ClientPreferencesView() {
const clientPreferences = useSelector(state => state.clientPreferences)
const conflicts = useSelector(state => state.preferenceConflicts)
const clientPreferences = useSelector((state) => state.clientPreferences)
const conflicts = useSelector((state) => state.preferenceConflicts)
const hasAdditionalClients = size(clientPreferences)
const hasConflicts = size(conflicts)
return <div className={ styles['client-requirements'] }>
<h3 className={ sharedStyles['section-title'] }>{ __( 'Registered themes or plugins', 'font-awesome' ) }</h3>
{
hasAdditionalClients
?
return (
<div className={styles['client-requirements']}>
<h3 className={sharedStyles['section-title']}>{__('Registered themes or plugins', 'font-awesome')}</h3>
{hasAdditionalClients ? (
<div>
<p className={sharedStyles['explanation']}>
{
__(
'Below is the list of active themes or plugins using Font Awesome that have opted-in to share information about the settings they are expecting.',
'font-awesome'
)
}
{ hasConflicts
? <span className={sharedStyles['explanation']}>
{ __( 'The highlights show where the settings are mismatched. You might want to adjust your settings to match, or your icons may not work as expected.', 'font-awesome' ) }
</span>
: null
}</p>
<table className={ classnames( 'widefat', 'striped' ) }>
{__(
'Below is the list of active themes or plugins using Font Awesome that have opted-in to share information about the settings they are expecting.',
'font-awesome'
)}
{hasConflicts ? (
<span className={sharedStyles['explanation']}>
{__(
'The highlights show where the settings are mismatched. You might want to adjust your settings to match, or your icons may not work as expected.',
'font-awesome'
)}
</span>
) : null}
</p>
<table className={classnames('widefat', 'striped')}>
<thead>
<tr className={ sharedStyles['table-header'] }>
<th>{ __( 'Name', 'font-awesome' ) }</th>
<th className={ classnames({ [styles.conflicted]: !! conflicts['usePro'] }) }>{ __( 'Icons', 'font-awesome' ) }</th>
<th className={ classnames({ [styles.conflicted]: !! conflicts['technology'] }) }>{ __( 'Technology', 'font-awesome' ) }</th>
<th className={ classnames({ [styles.conflicted]: !! conflicts['version'] }) }>{ __( 'Version', 'font-awesome' ) }</th>
<th className={ classnames({ [styles.conflicted]: !! conflicts['compat'] }) }>{ __( 'V4 Compat', 'font-awesome' ) }</th>
<th className={ classnames({ [styles.conflicted]: !! conflicts['pseudoElements'] }) }>{ __( 'CSS Pseudo-elements', 'font-awesome' ) }</th>
<tr className={sharedStyles['table-header']}>
<th>{__('Name', 'font-awesome')}</th>
<th className={classnames({ [styles.conflicted]: !!conflicts['usePro'] })}>{__('Icons', 'font-awesome')}</th>
<th className={classnames({ [styles.conflicted]: !!conflicts['technology'] })}>{__('Technology', 'font-awesome')}</th>
<th className={classnames({ [styles.conflicted]: !!conflicts['version'] })}>{__('Version', 'font-awesome')}</th>
<th className={classnames({ [styles.conflicted]: !!conflicts['compat'] })}>{__('V4 Compat', 'font-awesome')}</th>
<th className={classnames({ [styles.conflicted]: !!conflicts['pseudoElements'] })}>{__('CSS Pseudo-elements', 'font-awesome')}</th>
</tr>
</thead>
<tbody>
{
Object.values(clientPreferences).map((client, index) => {
const clientHasConflict = optionName => !!find(conflicts[optionName], c => c === client.name)
{Object.values(clientPreferences).map((client, index) => {
const clientHasConflict = (optionName) => !!find(conflicts[optionName], (c) => c === client.name)
return <tr key={ index }>
<td>{ client.name }</td>
<td
className={
classnames({ [styles.conflicted]: clientHasConflict('usePro') })
}>
{ has(client, 'usePro')
? client.usePro ? 'Pro' : 'Free'
: UNSPECIFIED_INDICATOR
}
</td>
<td
className={ classnames({ [styles.conflicted]: clientHasConflict('technology') }) }>
{ has(client, 'technology')
? client.technology
: UNSPECIFIED_INDICATOR
}
</td>
<td
className={ classnames({ [styles.conflicted]: clientHasConflict('version') }) }>
{ has(client, 'version')
? formatVersionPreference(client.version)
: UNSPECIFIED_INDICATOR
}
</td>
<td
className={ classnames({ [styles.conflicted]: clientHasConflict('compat') }) }>
{ has(client, 'compat')
? client.compat ? 'true' : 'false'
: UNSPECIFIED_INDICATOR
}
</td>
<td
className={ classnames({ [styles.conflicted]: clientHasConflict('pseudoElements') }) }>
{ has(client, 'pseudoElements')
? client.pseudoElements ? 'true' : 'false'
: UNSPECIFIED_INDICATOR
}
</td>
</tr>
})
}
return (
<tr key={index}>
<td>{client.name}</td>
<td className={classnames({ [styles.conflicted]: clientHasConflict('usePro') })}>
{has(client, 'usePro') ? (client.usePro ? 'Pro' : 'Free') : UNSPECIFIED_INDICATOR}
</td>
<td className={classnames({ [styles.conflicted]: clientHasConflict('technology') })}>
{has(client, 'technology') ? client.technology : UNSPECIFIED_INDICATOR}
</td>
<td className={classnames({ [styles.conflicted]: clientHasConflict('version') })}>
{has(client, 'version') ? formatVersionPreference(client.version) : UNSPECIFIED_INDICATOR}
</td>
<td className={classnames({ [styles.conflicted]: clientHasConflict('compat') })}>
{has(client, 'compat') ? (client.compat ? 'true' : 'false') : UNSPECIFIED_INDICATOR}
</td>
<td className={classnames({ [styles.conflicted]: clientHasConflict('pseudoElements') })}>
{has(client, 'pseudoElements') ? (client.pseudoElements ? 'true' : 'false') : UNSPECIFIED_INDICATOR}
</td>
</tr>
)
})}
</tbody>
</table>
</div>
:
<p className={ sharedStyles['explanation'] }>
{ __( 'No active themes or plugins have requested preferences for Font Awesome.', 'font-awesome' ) }
</p>
}
</div>
) : (
<p className={sharedStyles['explanation']}>{__('No active themes or plugins have requested preferences for Font Awesome.', 'font-awesome')}</p>
)}
</div>
)
}

View File

@ -5,8 +5,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCheckCircle, faCog, faExclamationTriangle, faGrin, faSkull, faThumbsUp, faTimesCircle } from '@fortawesome/free-solid-svg-icons'
import { ADMIN_TAB_TROUBLESHOOT } from './store/reducers'
import ConflictDetectionTimer from './ConflictDetectionTimer'
import size from 'lodash/size'
import has from 'lodash/has'
import { has, size } from 'lodash'
import { __ } from '@wordpress/i18n'
import ErrorBoundary from './ErrorBoundary'
@ -19,50 +18,50 @@ import ErrorBoundary from './ErrorBoundary'
const STATUS = {
running: {
code: 'Running',
display: __( 'Running', 'font-awesome' )
display: __('Running', 'font-awesome')
},
done: {
code: 'Done',
display: __( 'Done', 'font-awesome' )
display: __('Done', 'font-awesome')
},
submitting: {
code: 'Submitting',
display: __( 'Submitting', 'font-awesome' )
display: __('Submitting', 'font-awesome')
},
none: {
code: 'None',
display: __( 'None', 'font-awesome' )
display: __('None', 'font-awesome')
},
error: {
code: 'Error',
display: __( 'Error', 'font-awesome' )
display: __('Error', 'font-awesome')
},
expired: {
code: 'Expired',
display: __( 'Expired', 'font-awesome' )
display: __('Expired', 'font-awesome')
},
ready: {
code: 'Ready',
display: __( 'Ready', 'font-awesome' )
display: __('Ready', 'font-awesome')
},
stopped: {
code: 'Stopped',
display: __( 'Stopped', 'font-awesome' )
display: __('Stopped', 'font-awesome')
},
stopping: {
code: 'Stopping',
display: __( 'Stopping', 'font-awesome' )
display: __('Stopping', 'font-awesome')
},
restarting: {
code: 'Restarting',
display: __( 'Restarting', 'font-awesome' )
display: __('Restarting', 'font-awesome')
}
}
const STYLES = {
container: {
position: 'fixed',
fontFamily:'"Helvetica Neue",Helvetica,Arial,sans-serif',
fontFamily: '"Helvetica Neue",Helvetica,Arial,sans-serif',
right: '10px',
bottom: '10px',
width: '450px',
@ -82,7 +81,7 @@ const STYLES = {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '5px 20px',
padding: '5px 20px',
color: '#CAECFF'
},
content: {
@ -109,15 +108,15 @@ const STYLES = {
color: '#fff'
},
tally: {
display: 'flex',
display: 'flex',
alignItems: 'center',
margin: '.5em 0',
margin: '.5em 0',
textAlign: 'center'
},
count: {
flexBasis: '1em',
marginRight: '5px',
fontWeight: '600',
fontWeight: '600',
fontSize: '20px'
},
timerRow: {
@ -125,7 +124,7 @@ const STYLES = {
alignItems: 'center',
backgroundColor: '#0064B1',
padding: '10px 20px',
color: '#fff',
color: '#fff',
fontWeight: '600'
},
button: {
@ -133,7 +132,7 @@ const STYLES = {
border: '0',
padding: '5px',
backgroundColor: 'transparent',
color: '#fff',
color: '#fff',
opacity: '.7',
cursor: 'pointer'
},
@ -144,73 +143,59 @@ const STYLES = {
}
}
function withErrorBoundary( Component ) {
function withErrorBoundary(Component) {
return class extends ErrorBoundary {
render() {
return <div style={ STYLES.container }>
{
!!this.state.error
? <div style={ STYLES.badness }>
<FontAwesomeIcon icon={ faExclamationTriangle } />
{
__( ' Whoops, this is embarrassing! Some unexpected error has occurred. There might be some additional diagnostic information in the JavaScript console.', 'font-awesome' )
}
return (
<div style={STYLES.container}>
{!!this.state.error ? (
<div style={STYLES.badness}>
<FontAwesomeIcon icon={faExclamationTriangle} />
{__(
' Whoops, this is embarrassing! Some unexpected error has occurred. There might be some additional diagnostic information in the JavaScript console.',
'font-awesome'
)}
</div>
: <Component />
}
</div>
) : (
<Component />
)}
</div>
)
}
}
}
function ConflictDetectionReporter() {
const dispatch = useDispatch()
const settingsPageUrl = useSelector(state => state.settingsPageUrl)
const settingsPageUrl = useSelector((state) => state.settingsPageUrl)
const troubleshootTabUrl = `${settingsPageUrl}&tab=ts`
const activeAdminTab = useSelector(state => state.activeAdminTab )
const activeAdminTab = useSelector((state) => state.activeAdminTab)
const currentlyOnPluginAdminPage = window.location.href.startsWith(settingsPageUrl)
const currentlyOnTroubleshootTab = currentlyOnPluginAdminPage && activeAdminTab === ADMIN_TAB_TROUBLESHOOT
const userAttemptedToStopScanner = useSelector(state => state.userAttemptedToStopScanner)
const userAttemptedToStopScanner = useSelector((state) => state.userAttemptedToStopScanner)
const unregisteredClients = useSelector(
state => state.unregisteredClients
)
const unregisteredClients = useSelector((state) => state.unregisteredClients)
const unregisteredClientsBeforeDetection = useSelector(
state => state.unregisteredClientDetectionStatus.unregisteredClientsBeforeDetection
)
const unregisteredClientsBeforeDetection = useSelector((state) => state.unregisteredClientDetectionStatus.unregisteredClientsBeforeDetection)
const recentConflictsDetected = useSelector(
state => state.unregisteredClientDetectionStatus.recentConflictsDetected
)
const recentConflictsDetected = useSelector((state) => state.unregisteredClientDetectionStatus.recentConflictsDetected)
const expired = useSelector(
state => !state.showConflictDetectionReporter
)
const expired = useSelector((state) => !state.showConflictDetectionReporter)
const restarting = useSelector(
state => expired && state.conflictDetectionScannerStatus.isSubmitting
)
const restarting = useSelector((state) => expired && state.conflictDetectionScannerStatus.isSubmitting)
const scannerReady = useSelector(
state => state.conflictDetectionScannerStatus.hasSubmitted && state.conflictDetectionScannerStatus.success
)
const scannerReady = useSelector((state) => state.conflictDetectionScannerStatus.hasSubmitted && state.conflictDetectionScannerStatus.success)
const scannerIsStopping = useSelector(
state => userAttemptedToStopScanner
&& !state.conflictDetectionScannerStatus.hasSubmitted
)
const scannerIsStopping = useSelector((state) => userAttemptedToStopScanner && !state.conflictDetectionScannerStatus.hasSubmitted)
const userStoppedScannerSuccessfully = useSelector(
state => userAttemptedToStopScanner
&& !scannerIsStopping
&& state.conflictDetectionScannerStatus.success
(state) => userAttemptedToStopScanner && !scannerIsStopping && state.conflictDetectionScannerStatus.success
)
const runStatus = useSelector(state => {
const runStatus = useSelector((state) => {
const { isSubmitting, hasSubmitted, success } = state.unregisteredClientDetectionStatus
if (userAttemptedToStopScanner) {
if ( scannerIsStopping ) {
if (scannerIsStopping) {
return STATUS.stopping
} else if (userStoppedScannerSuccessfully) {
return STATUS.stopped
@ -221,131 +206,201 @@ function ConflictDetectionReporter() {
}
} else if (restarting) {
return STATUS.restarting
} else if ( expired ) {
} else if (expired) {
return STATUS.expired
} else if (scannerReady) {
return STATUS.ready
} else if ( success && 0 === size( unregisteredClients ) ) {
} else if (success && 0 === size(unregisteredClients)) {
return STATUS.none
} else if ( success ) {
} else if (success) {
return STATUS.done
} else if( isSubmitting ) {
} else if (isSubmitting) {
return STATUS.submitting
} else if( !hasSubmitted ){
} else if (!hasSubmitted) {
return STATUS.running
} else {
return STATUS.error
}
})
const errorMessage = useSelector(
state => state.unregisteredClientDetectionStatus.message
)
const errorMessage = useSelector((state) => state.unregisteredClientDetectionStatus.message)
function stopScanner() {
dispatch(userAttemptToStopScanner())
dispatch(setConflictDetectionScanner({ enable: false }))
}
const expiredOrStoppedDiv =
const expiredOrStoppedDiv = (
<div>
<h2 style={ STYLES.tally }><span>{ size( unregisteredClients ) }</span> <span>&nbsp;{ __( 'Results to Review', 'font-awesome' ) }</span></h2>
<p style={ STYLES.p }>
{
currentlyOnTroubleshootTab
? __( 'Manage results or restart the scanner here on the Troubleshoot tab.', 'font-awesome' )
: <>
{
__( 'Manage results or restart the scanner on the Troubleshoot tab.', 'font-awesome' )
} <a href={ troubleshootTabUrl } style={ STYLES.link }>{ __('Go', 'font-awesome' ) }</a>
</>
}
<h2 style={STYLES.tally}>
<span>{size(unregisteredClients)}</span> <span>&nbsp;{__('Results to Review', 'font-awesome')}</span>
</h2>
<p style={STYLES.p}>
{currentlyOnTroubleshootTab ? (
__('Manage results or restart the scanner here on the Troubleshoot tab.', 'font-awesome')
) : (
<>
{__('Manage results or restart the scanner on the Troubleshoot tab.', 'font-awesome')}{' '}
<a
href={troubleshootTabUrl}
style={STYLES.link}
>
{__('Go', 'font-awesome')}
</a>
</>
)}
</p>
</div>
)
const stoppingOrSubmittingDiv =
const stoppingOrSubmittingDiv = (
<div>
<div style={ STYLES.status }>
<h2 style={ STYLES.h2 }><FontAwesomeIcon icon={ faCog } size="sm" spin /> <span>{ runStatus.display }</span></h2>
<div style={STYLES.status}>
<h2 style={STYLES.h2}>
<FontAwesomeIcon
icon={faCog}
size="sm"
spin
/>{' '}
<span>{runStatus.display}</span>
</h2>
</div>
</div>
)
return (
<>
<div style={ STYLES.header }>
<h1 style={ STYLES.h1 }>{ __( 'Font Awesome Conflict Scanner', 'font-awesome' ) }</h1>
<p style={ STYLES.adminEyesOnly }>{ __( 'only admins can see this box', 'font-awesome' ) }</p>
<div style={STYLES.header}>
<h1 style={STYLES.h1}>{__('Font Awesome Conflict Scanner', 'font-awesome')}</h1>
<p style={STYLES.adminEyesOnly}>{__('only admins can see this box', 'font-awesome')}</p>
</div>
<div style={ STYLES.content }>
<div style={STYLES.content}>
{
{
None:
None: (
<div>
<div style={ STYLES.status }>
<h2 style={ STYLES.h2 }><FontAwesomeIcon icon={ faGrin } size="sm" /> <span>{ __( 'All clear!', 'font-awesome' ) }</span></h2>
<p style={ STYLES.p }>{ __( 'No new conflicts found on this page.', 'font-awesome' ) }</p>
<div style={STYLES.status}>
<h2 style={STYLES.h2}>
<FontAwesomeIcon
icon={faGrin}
size="sm"
/>{' '}
<span>{__('All clear!', 'font-awesome')}</span>
</h2>
<p style={STYLES.p}>{__('No new conflicts found on this page.', 'font-awesome')}</p>
</div>
</div>,
Running:
</div>
),
Running: (
<div>
<div style={ STYLES.status }>
<h2 style={ STYLES.h2 }><FontAwesomeIcon icon={ faCog } size="sm" spin /> <span>{ __( 'Scanning', 'font-awesome' ) }...</span></h2>
<div style={STYLES.status}>
<h2 style={STYLES.h2}>
<FontAwesomeIcon
icon={faCog}
size="sm"
spin
/>{' '}
<span>{__('Scanning', 'font-awesome')}...</span>
</h2>
</div>
</div>,
Restarting:
</div>
),
Restarting: (
<div>
<div style={ STYLES.status }>
<h2 style={ STYLES.h2 }><FontAwesomeIcon icon={ faCog } size="sm" spin /> <span>{ __( 'Restarting', 'font-awesome' ) }...</span></h2>
<div style={STYLES.status}>
<h2 style={STYLES.h2}>
<FontAwesomeIcon
icon={faCog}
size="sm"
spin
/>{' '}
<span>{__('Restarting', 'font-awesome')}...</span>
</h2>
</div>
</div>,
Ready:
</div>
),
Ready: (
<div>
<div>
<h2 style={ STYLES.h2 }><FontAwesomeIcon icon={ faThumbsUp } size="sm" /> { __( 'Proton pack charged!', 'font-awesome' ) }</h2>
<p style={ STYLES.p }>{ __( 'Wander through the pages of your web site and this scanner will track progress.', 'font-awesome' ) }</p>
<h2 style={STYLES.h2}>
<FontAwesomeIcon
icon={faThumbsUp}
size="sm"
/>{' '}
{__('Proton pack charged!', 'font-awesome')}
</h2>
<p style={STYLES.p}>{__('Wander through the pages of your web site and this scanner will track progress.', 'font-awesome')}</p>
</div>
</div>,
</div>
),
Submitting: stoppingOrSubmittingDiv,
Stopping: stoppingOrSubmittingDiv,
Done:
Done: (
<div>
<div style={ STYLES.status }>
<h2 style={ STYLES.h2 }><FontAwesomeIcon icon={ faCheckCircle } size="sm" /> <span>{ __( 'Page scan complete', 'font-awesome' ) }</span></h2>
<div style={STYLES.status}>
<h2 style={STYLES.h2}>
<FontAwesomeIcon
icon={faCheckCircle}
size="sm"
/>{' '}
<span>{__('Page scan complete', 'font-awesome')}</span>
</h2>
</div>
<p style={ STYLES.tally }><span style={ STYLES.count }>{ size( Object.keys( recentConflictsDetected ).filter(k => ! has(unregisteredClientsBeforeDetection, k) ) ) }</span> <span>{ __( 'new conflicts found on this page', 'font-awesome' ) }</span></p>
<p style={ STYLES.tally }><span style={ STYLES.count }>{ size( unregisteredClients ) }</span> <span>total found</span>
{
currentlyOnTroubleshootTab ?
<span>&nbsp;({ __( 'manage conflicts here on the Troubleshoot tab', 'font-awesome' ) })</span>
: <span>&nbsp;(<a href={ troubleshootTabUrl } style={ STYLES.link }>{ __( 'manage', 'font-awesome' ) }</a>)</span>
}
<p style={STYLES.tally}>
<span style={STYLES.count}>{size(Object.keys(recentConflictsDetected).filter((k) => !has(unregisteredClientsBeforeDetection, k)))}</span>{' '}
<span>{__('new conflicts found on this page', 'font-awesome')}</span>
</p>
</div>,
Expired: expiredOrStoppedDiv,
Stopped: expiredOrStoppedDiv,
Error:
<div>
<h2 style={ STYLES.h2 }><FontAwesomeIcon icon={ faSkull } /> <span>{ __( 'Don\'t cross the streams! It would be bad.', 'font-awesome' ) }</span></h2>
<p style={ STYLES.p }>
{ errorMessage }
<p style={STYLES.tally}>
<span style={STYLES.count}>{size(unregisteredClients)}</span> <span>total found</span>
{currentlyOnTroubleshootTab ? (
<span>&nbsp;({__('manage conflicts here on the Troubleshoot tab', 'font-awesome')})</span>
) : (
<span>
&nbsp;(
<a
href={troubleshootTabUrl}
style={STYLES.link}
>
{__('manage', 'font-awesome')}
</a>
)
</span>
)}
</p>
</div>
),
Expired: expiredOrStoppedDiv,
Stopped: expiredOrStoppedDiv,
Error: (
<div>
<h2 style={STYLES.h2}>
<FontAwesomeIcon icon={faSkull} /> <span>{__("Don't cross the streams! It would be bad.", 'font-awesome')}</span>
</h2>
<p style={STYLES.p}>{errorMessage}</p>
</div>
)
}[runStatus.code]
}
</div>
<div style={ STYLES.timerRow }>
<div style={STYLES.timerRow}>
<span>
<ConflictDetectionTimer addDescription>
<button style={ STYLES.button } title={ __( 'Stop timer', 'font-awesome' ) } onClick={() => stopScanner()}>
<FontAwesomeIcon icon={ faTimesCircle } size="lg" />
<button
style={STYLES.button}
title={__('Stop timer', 'font-awesome')}
onClick={() => stopScanner()}
>
<FontAwesomeIcon
icon={faTimesCircle}
size="lg"
/>
</button>
</ConflictDetectionTimer>
</span>
{
{
Expired: __( 'Timer expired', 'font-awesome' ),
Stopped: __( 'Timer stopped', 'font-awesome' ),
Expired: __('Timer expired', 'font-awesome'),
Stopped: __('Timer stopped', 'font-awesome'),
Restarting: null
}[runStatus.code]
}
@ -354,4 +409,4 @@ function ConflictDetectionReporter() {
)
}
export default withErrorBoundary( ConflictDetectionReporter )
export default withErrorBoundary(ConflictDetectionReporter)

View File

@ -11,15 +11,15 @@ import createInterpolateElement from './createInterpolateElement'
export default function ConflictDetectionScannerSection() {
const dispatch = useDispatch()
const detectConflictsUntil = useSelector(state => state.detectConflictsUntil)
const nowMs = (new Date()).valueOf()
const detectingConflicts = (new Date(detectConflictsUntil * 1000)) > nowMs
const { isSubmitting, hasSubmitted, message, success } = useSelector(state => state.conflictDetectionScannerStatus)
const showConflictDetectionReporter = useSelector(state => state.showConflictDetectionReporter)
const detectConflictsUntil = useSelector((state) => state.detectConflictsUntil)
const nowMs = new Date().valueOf()
const detectingConflicts = new Date(detectConflictsUntil * 1000) > nowMs
const { isSubmitting, hasSubmitted, message, success } = useSelector((state) => state.conflictDetectionScannerStatus)
const showConflictDetectionReporter = useSelector((state) => state.showConflictDetectionReporter)
const store = useStore()
useEffect(() => {
if(showConflictDetectionReporter && !isConflictDetectionReporterMounted()) {
if (showConflictDetectionReporter && !isConflictDetectionReporterMounted()) {
// We are not setting up the reporting hook, because the conflict scanner
// script is not actually going to run when it's initially activated from
// this view. The conflict scanner box will appear to alert the user,
@ -29,53 +29,66 @@ export default function ConflictDetectionScannerSection() {
mountConflictDetectionReporter(store)
}
}, [ showConflictDetectionReporter, store ])
}, [showConflictDetectionReporter, store])
return <div>
<h2 className={ sharedStyles['section-title'] }>{ __( 'Detect Conflicts with Other Versions of Font Awesome', 'font-awesome' ) }</h2>
<div className={sharedStyles['explanation']}>
<p>
{ __( 'If you are having trouble loading Font Awesome icons on your WordPress site, it may be because other themes or plugins are loading conflicting versions of Font Awesome. You can use our conflict scanner to detect other versions of Font Awesome running on your site.', 'font-awesome' ) }
</p>
return (
<div>
<h2 className={sharedStyles['section-title']}>{__('Detect Conflicts with Other Versions of Font Awesome', 'font-awesome')}</h2>
<div className={sharedStyles['explanation']}>
<p>
{__(
'If you are having trouble loading Font Awesome icons on your WordPress site, it may be because other themes or plugins are loading conflicting versions of Font Awesome. You can use our conflict scanner to detect other versions of Font Awesome running on your site.',
'font-awesome'
)}
</p>
<p>
{
createInterpolateElement(
__( 'Enable the scanner below and a box will appear in the bottom corner of your window while it runs for 10 minutes (only you and other admins can see the box). While the scanner is running, browse your site, especially the pages having trouble to catch any <noWrap>Slimers - *ahem* - conflicts</noWrap> in the scanner.', 'font-awesome' ),
<p>
{createInterpolateElement(
__(
'Enable the scanner below and a box will appear in the bottom corner of your window while it runs for 10 minutes (only you and other admins can see the box). While the scanner is running, browse your site, especially the pages having trouble to catch any <noWrap>Slimers - *ahem* - conflicts</noWrap> in the scanner.',
'font-awesome'
),
{
noWrap: <span style={{ whiteSpace: "nowrap" }} />
noWrap: <span style={{ whiteSpace: 'nowrap' }} />
}
)
}
</p>
</div>
<div className={sharedStyles['scanner-actions']}>
{
detectingConflicts
? <button className={sharedStyles['faPrimary']} disabled >
{ __( 'Scanner running', 'font-awesome' ) }: <ConflictDetectionTimer />
</button>
: <button className="button button-primary" disabled={ isSubmitting } onClick={() => dispatch(setConflictDetectionScanner({ enable: true }))}>
{
sprintf(
__( 'Enable scanner for %d minutes', 'font-awesome' ),
CONFLICT_DETECTION_SCANNER_DURATION_MIN
)
}
</button>
}
<div className={sharedStyles['scanner-runstatus']}>
{
isSubmitting
? <FontAwesomeIcon icon={ faSpinner } spin />
: hasSubmitted
? success
? <FontAwesomeIcon icon={ faCheck } />
: <><FontAwesomeIcon icon={ faSkull } /> <span>{ message }</span></>
: null
}
)}
</p>
</div>
<div className={sharedStyles['scanner-actions']}>
{detectingConflicts ? (
<button
className={sharedStyles['faPrimary']}
disabled
>
{__('Scanner running', 'font-awesome')}: <ConflictDetectionTimer />
</button>
) : (
<button
className="button button-primary"
disabled={isSubmitting}
onClick={() => dispatch(setConflictDetectionScanner({ enable: true }))}
>
{sprintf(__('Enable scanner for %d minutes', 'font-awesome'), CONFLICT_DETECTION_SCANNER_DURATION_MIN)}
</button>
)}
<div className={sharedStyles['scanner-runstatus']}>
{isSubmitting ? (
<FontAwesomeIcon
icon={faSpinner}
spin
/>
) : hasSubmitted ? (
success ? (
<FontAwesomeIcon icon={faCheck} />
) : (
<>
<FontAwesomeIcon icon={faSkull} /> <span>{message}</span>
</>
)
) : null}
</div>
</div>
<hr className={sharedStyles['section-divider']} />
</div>
<hr className={ sharedStyles['section-divider'] }/>
</div>
)
}

View File

@ -2,8 +2,7 @@ import React, { useState, useEffect } from 'react'
import PropTypes from 'prop-types'
import { useSelector, useDispatch } from 'react-redux'
import sharedStyles from './App.module.css'
import padStart from 'lodash/padStart'
import dropWhile from 'lodash/dropWhile'
import { padStart, dropWhile } from 'lodash'
import { __, sprintf } from '@wordpress/i18n'
const SECONDS_PER_DAY = 60 * 60 * 24
@ -12,39 +11,39 @@ const SECONDS_PER_MINUTE = 60
export function timerString(durationSeconds) {
const days = Math.floor(durationSeconds / SECONDS_PER_DAY)
const hours = Math.floor((durationSeconds - (days * SECONDS_PER_DAY)) / SECONDS_PER_HOUR)
const hours = Math.floor((durationSeconds - days * SECONDS_PER_DAY) / SECONDS_PER_HOUR)
const minutes = Math.floor((durationSeconds - (days * SECONDS_PER_DAY + hours * SECONDS_PER_HOUR)) / SECONDS_PER_MINUTE)
const seconds = durationSeconds - (days * SECONDS_PER_DAY + hours * SECONDS_PER_HOUR + minutes * SECONDS_PER_MINUTE)
return dropWhile(
[days, hours, minutes, seconds].reduce((acc, unit, index) => {
if(0 === index && unit !== 0){
if (0 === index && unit !== 0) {
acc.push(unit.toString())
} else {
acc.push(padStart(unit.toString(), 2, '0'))
}
return acc
}, []),
part => part.match(/^[0]+$/)
(part) => part.match(/^[0]+$/)
).join(':')
}
function secondsRemaining(endTime) {
const now = Math.floor((new Date()) / 1000)
const now = Math.floor(new Date() / 1000)
const remaining = endTime - now
return remaining < 0 ? 0 : remaining
}
export default function ConflictDetectionTimer({ addDescription, children }) {
const detectConflictsUntil = useSelector(state => state.detectConflictsUntil)
const detectConflictsUntil = useSelector((state) => state.detectConflictsUntil)
const [timeRemaining, setTimer] = useState(secondsRemaining(detectConflictsUntil))
const dispatch = useDispatch()
useEffect(() => {
let timeoutId = null
if(secondsRemaining(detectConflictsUntil) > 0) {
if (secondsRemaining(detectConflictsUntil) > 0) {
timeoutId = setTimeout(() => setTimer(secondsRemaining(detectConflictsUntil)), 1000)
} else {
setTimer(timerString(0))
@ -53,25 +52,21 @@ export default function ConflictDetectionTimer({ addDescription, children }) {
})
}
return () => timeoutId && clearTimeout( timeoutId )
return () => timeoutId && clearTimeout(timeoutId)
}, [detectConflictsUntil, timeRemaining, dispatch])
return timeRemaining <= 0 ? null : <span className={ sharedStyles['conflict-detection-timer'] }>
{ timerString( timeRemaining ) }
{
!!addDescription &&
(
timeRemaining > 60
/* translators: 1: space */
? sprintf( __( '%1$sminutes left to browse your site for trouble', 'font-awesome' ), ' ' )
/* translators: 1: space */
: sprintf( __( '%1$sseconds left to browse your site for trouble', 'font-awesome' ), ' ' )
)
}
{
children
}
return timeRemaining <= 0 ? null : (
<span className={sharedStyles['conflict-detection-timer']}>
{timerString(timeRemaining)}
{!!addDescription &&
(timeRemaining > 60
? /* translators: 1: space */
sprintf(__('%1$sminutes left to browse your site for trouble', 'font-awesome'), ' ')
: /* translators: 1: space */
sprintf(__('%1$sseconds left to browse your site for trouble', 'font-awesome'), ' '))}
{children}
</span>
)
}
ConflictDetectionTimer.propTypes = {

View File

@ -13,10 +13,10 @@ class ErrorBoundary extends React.Component {
componentDidCatch(error, errorInfo) {
console.group(ERROR_REPORT_PREAMBLE)
console.log( error )
console.log( errorInfo )
console.log(error)
console.log(errorInfo)
console.groupEnd()
this.setState({error, errorInfo})
this.setState({ error, errorInfo })
}
render() {

View File

@ -3,18 +3,17 @@ import styles from './ErrorFallbackView.module.css'
import Alert from './Alert'
import { __ } from '@wordpress/i18n'
export const fatalAlert = <Alert title={ __( 'Whoops, this is embarrassing', 'font-awesome' ) } type='warning'>
<p>
{
__( 'Some unexpected error has occurred. There might be some additional diagnostic information in the JavaScript console.', 'font-awesome' )
}
</p>
</Alert>
export const fatalAlert = (
<Alert
title={__('Whoops, this is embarrassing', 'font-awesome')}
type="warning"
>
<p>{__('Some unexpected error has occurred. There might be some additional diagnostic information in the JavaScript console.', 'font-awesome')}</p>
</Alert>
)
function ErrorFallbackView() {
return <div className={ styles['error-fallback'] }>
{ fatalAlert }
</div>
return <div className={styles['error-fallback']}>{fatalAlert}</div>
}
export default ErrorFallbackView

View File

@ -9,30 +9,30 @@ import { setActiveAdminTab } from './store/actions'
import { __ } from '@wordpress/i18n'
export default function FontAwesomeAdminView() {
const activeAdminTab = useSelector(state => state.activeAdminTab || ADMIN_TAB_SETTINGS )
const activeAdminTab = useSelector((state) => state.activeAdminTab || ADMIN_TAB_SETTINGS)
const dispatch = useDispatch()
return (
<div className={ classnames(styles['font-awesome-admin-view']) }>
return (
<div className={classnames(styles['font-awesome-admin-view'])}>
<h1>Font Awesome</h1>
<div className={styles['tab-header']}>
<button
<button
onClick={() => dispatch(setActiveAdminTab(ADMIN_TAB_SETTINGS))}
disabled={ activeAdminTab === ADMIN_TAB_SETTINGS }
disabled={activeAdminTab === ADMIN_TAB_SETTINGS}
>
{ __( 'Settings', 'font-awesome' ) }
{__('Settings', 'font-awesome')}
</button>
<button
onClick={() => dispatch(setActiveAdminTab(ADMIN_TAB_TROUBLESHOOT))}
disabled={ activeAdminTab === ADMIN_TAB_TROUBLESHOOT }
disabled={activeAdminTab === ADMIN_TAB_TROUBLESHOOT}
>
{ __( 'Troubleshoot', 'font-awesome' ) }
{__('Troubleshoot', 'font-awesome')}
</button>
</div>
{
{
[ADMIN_TAB_SETTINGS]: <SettingsTab/>,
[ADMIN_TAB_TROUBLESHOOT]: <TroubleshootTab/>
[ADMIN_TAB_SETTINGS]: <SettingsTab />,
[ADMIN_TAB_TROUBLESHOOT]: <TroubleshootTab />
}[activeAdminTab]
}
</div>

View File

@ -5,115 +5,109 @@ import styles from './KitSelectView.module.css'
import PropTypes from 'prop-types'
import { useSelector } from 'react-redux'
import Alert from './Alert'
import get from 'lodash/get'
import has from 'lodash/has'
import size from 'lodash/size'
import { get, has, size } from 'lodash'
import { __ } from '@wordpress/i18n'
import createInterpolateElement from './createInterpolateElement'
export default function KitConfigView({ kitToken }) {
const kitTokenIsActive = useSelector(state => get(state, 'options.kitToken') === kitToken)
const kitTokenApiData = useSelector(state => (state.kits || []).find(k => k.token === kitToken))
const pendingOptionConflicts = useSelector(state => state.pendingOptionConflicts)
const hasChecked = useSelector(state => state.preferenceConflictDetection.hasChecked)
const preferenceCheckSuccess = useSelector(state => state.preferenceConflictDetection.success)
const kitTokenIsActive = useSelector((state) => get(state, 'options.kitToken') === kitToken)
const kitTokenApiData = useSelector((state) => (state.kits || []).find((k) => k.token === kitToken))
const pendingOptionConflicts = useSelector((state) => state.pendingOptionConflicts)
const hasChecked = useSelector((state) => state.preferenceConflictDetection.hasChecked)
const preferenceCheckSuccess = useSelector((state) => state.preferenceConflictDetection.success)
const technology = useSelector(state =>
kitTokenIsActive
? state.options.technology
: kitTokenApiData.technologySelected === 'svg'
? 'svg'
: 'webfont'
)
const technology = useSelector((state) => (kitTokenIsActive ? state.options.technology : kitTokenApiData.technologySelected === 'svg' ? 'svg' : 'webfont'))
const usePro = useSelector(state =>
kitTokenIsActive
? state.options.usePro
: kitTokenApiData.licenseSelected === 'pro'
)
const usePro = useSelector((state) => (kitTokenIsActive ? state.options.usePro : kitTokenApiData.licenseSelected === 'pro'))
const compat = useSelector(state =>
kitTokenIsActive
? state.options.compat
: kitTokenApiData.shimEnabled
)
const compat = useSelector((state) => (kitTokenIsActive ? state.options.compat : kitTokenApiData.shimEnabled))
const version = useSelector(state =>
kitTokenIsActive
? state.options.version
: kitTokenApiData.version
)
const version = useSelector((state) => (kitTokenIsActive ? state.options.version : kitTokenApiData.version))
function getDetectionStatusForOption(option) {
if ( hasChecked && preferenceCheckSuccess && has(pendingOptionConflicts, option) ) {
return <Alert title={ __( 'Preference Conflict', 'font-awesome' ) } type='warning'>
{
size(pendingOptionConflicts[option]) > 1
? <div>
{ __( 'This change might cause problems for these themes or plugins:', 'font-awesome' ) } { pendingOptionConflicts[option].join(', ') }.
</div>
: <div>
{ __( 'This change might cause problems for the theme or plugin:', 'font-awesome' ) } { pendingOptionConflicts[option][0] }.
if (hasChecked && preferenceCheckSuccess && has(pendingOptionConflicts, option)) {
return (
<Alert
title={__('Preference Conflict', 'font-awesome')}
type="warning"
>
{size(pendingOptionConflicts[option]) > 1 ? (
<div>
{__('This change might cause problems for these themes or plugins:', 'font-awesome')} {pendingOptionConflicts[option].join(', ')}.
</div>
}
</Alert>
) : (
<div>
{__('This change might cause problems for the theme or plugin:', 'font-awesome')} {pendingOptionConflicts[option][0]}.
</div>
)}
</Alert>
)
} else {
return null
}
}
return (!kitTokenIsActive && !kitTokenApiData)
? <Alert type="warning" title={ __('Oh no! We could not find the kit data for the selected kit token.', 'font-awesome' )}>
{
__( 'Try reloading.', 'font-awesome' )
}
</Alert>
: <div className={ styles['kit-config-view-container'] }>
<table className={ styles['selected-kit-settings'] }>
<tbody>
<tr>
<th className={ styles['label'] }>{ __( 'Icons', 'font-awesome' ) }</th>
<td className={ styles['value'] }>
{ usePro ? 'Pro' : 'Free' }
{ getDetectionStatusForOption('usePro') }
</td>
</tr>
<tr>
<th className={ styles['label'] }>{ __( 'Technology', 'font-awesome' ) }</th>
<td className={ styles['value'] }>
{ technology }
{ getDetectionStatusForOption('technology') }
</td>
</tr>
<tr>
<th className={ styles['label'] }>{ __( 'Version', 'font-awesome' ) }</th>
<td className={ styles['value'] }>
{ version }
{ getDetectionStatusForOption('version') }
</td>
</tr>
<tr>
<th className={ styles['label'] }>{ __( 'Older Version Compatibility', 'font-awesome' ) }</th>
<td className={ styles['value'] }>
{ compat ? 'On' : 'Off' }
{ getDetectionStatusForOption('compat') }
</td>
</tr>
</tbody>
</table>
<p className={ styles['tip-text'] }>
{
createInterpolateElement(
__( 'Make changes on <a>fontawesome.com/kits <externalLinkIcon/></a>', 'font-awesome' ),
{
// eslint-disable-next-line jsx-a11y/anchor-has-content
a: <a target="_blank" rel="noopener noreferrer" href="https://fontawesome.com/kits" />,
externalLinkIcon: <FontAwesomeIcon icon={faExternalLinkAlt} style={{marginLeft: '.5em'}} />
}
)
}
</p>
</div>
return !kitTokenIsActive && !kitTokenApiData ? (
<Alert
type="warning"
title={__('Oh no! We could not find the kit data for the selected kit token.', 'font-awesome')}
>
{__('Try reloading.', 'font-awesome')}
</Alert>
) : (
<div className={styles['kit-config-view-container']}>
<table className={styles['selected-kit-settings']}>
<tbody>
<tr>
<th className={styles['label']}>{__('Icons', 'font-awesome')}</th>
<td className={styles['value']}>
{usePro ? 'Pro' : 'Free'}
{getDetectionStatusForOption('usePro')}
</td>
</tr>
<tr>
<th className={styles['label']}>{__('Technology', 'font-awesome')}</th>
<td className={styles['value']}>
{technology}
{getDetectionStatusForOption('technology')}
</td>
</tr>
<tr>
<th className={styles['label']}>{__('Version', 'font-awesome')}</th>
<td className={styles['value']}>
{version}
{getDetectionStatusForOption('version')}
</td>
</tr>
<tr>
<th className={styles['label']}>{__('Older Version Compatibility', 'font-awesome')}</th>
<td className={styles['value']}>
{compat ? 'On' : 'Off'}
{getDetectionStatusForOption('compat')}
</td>
</tr>
</tbody>
</table>
<p className={styles['tip-text']}>
{createInterpolateElement(__('Make changes on <a>fontawesome.com/kits <externalLinkIcon/></a>', 'font-awesome'), {
// eslint-disable-next-line jsx-a11y/anchor-has-content
a: (
<a
target="_blank"
rel="noopener noreferrer"
href="https://fontawesome.com/kits"
/>
),
externalLinkIcon: (
<FontAwesomeIcon
icon={faExternalLinkAlt}
style={{ marginLeft: '.5em' }}
/>
)
})}
</p>
</div>
)
}
KitConfigView.propTypes = {

View File

@ -1,50 +1,37 @@
import React, { createRef, useState, useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import Alert from './Alert'
import {
resetPendingOptions,
queryKits,
addPendingOption,
checkPreferenceConflicts,
updateApiToken,
resetOptionsFormState
} from './store/actions'
import { resetPendingOptions, queryKits, addPendingOption, checkPreferenceConflicts, updateApiToken, resetOptionsFormState } from './store/actions'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import {
faSpinner,
faSync,
faExternalLinkAlt,
faRedo,
faSkull,
faTrashAlt } from '@fortawesome/free-solid-svg-icons'
import { faSpinner, faSync, faExternalLinkAlt, faRedo, faSkull, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
import { faQuestionCircle, faCheckCircle } from '@fortawesome/free-regular-svg-icons'
import styles from './KitSelectView.module.css'
import sharedStyles from './App.module.css'
import classnames from 'classnames'
import PropTypes from 'prop-types'
import size from 'lodash/size'
import { size } from 'lodash'
import { sprintf, __ } from '@wordpress/i18n'
export default function KitSelectView({ useOption, masterSubmitButtonShowing, setMasterSubmitButtonShowing }) {
const dispatch = useDispatch()
const kitTokenActive = useSelector(state => state.options.kitToken)
const kitTokenActive = useSelector((state) => state.options.kitToken)
const kitToken = useOption('kitToken')
const [ pendingApiToken, setPendingApiToken ] = useState(null)
const [ showingRemoveApiTokenAlert, setShowRemoveApiTokenAlert ] = useState(false)
const [ showApiTokenInputForUpdate, setShowApiTokenInputForUpdate ] = useState(false)
const apiToken = useSelector(state => {
if( null !== pendingApiToken ) return pendingApiToken
const [pendingApiToken, setPendingApiToken] = useState(null)
const [showingRemoveApiTokenAlert, setShowRemoveApiTokenAlert] = useState(false)
const [showApiTokenInputForUpdate, setShowApiTokenInputForUpdate] = useState(false)
const apiToken = useSelector((state) => {
if (null !== pendingApiToken) return pendingApiToken
return state.options.apiToken
})
const kits = useSelector( state => state.kits ) || []
const hasSubmitted = useSelector(state => state.optionsFormState.hasSubmitted)
const submitSuccess = useSelector(state => state.optionsFormState.success)
const submitMessage = useSelector(state => state.optionsFormState.message)
const isSubmitting = useSelector(state => state.optionsFormState.isSubmitting)
const kits = useSelector((state) => state.kits) || []
const hasSubmitted = useSelector((state) => state.optionsFormState.hasSubmitted)
const submitSuccess = useSelector((state) => state.optionsFormState.success)
const submitMessage = useSelector((state) => state.optionsFormState.message)
const isSubmitting = useSelector((state) => state.optionsFormState.isSubmitting)
function removeApiToken() {
if( !!kitTokenActive ) {
if (!!kitTokenActive) {
setShowRemoveApiTokenAlert(true)
} else {
dispatch(updateApiToken({ apiToken: false }))
@ -59,44 +46,41 @@ export default function KitSelectView({ useOption, masterSubmitButtonShowing, se
* with registered clients.
*/
function handleKitChange({ kitToken }) {
if('' === kitToken) {
if ('' === kitToken) {
// You can't select a non-kit option. The empty option only
// appears in the selection dropdown as a placeholder before a kit is
// selected
return
}
const selectedKit = (kits || []).find(k => k.token === kitToken)
const selectedKit = (kits || []).find((k) => k.token === kitToken)
if( !selectedKit ) {
throw new Error(
sprintf(
__( 'When selecting to use kit %s, somehow the information we needed was missing. Try reloading the page.' ),
kitToken
)
)
if (!selectedKit) {
throw new Error(sprintf(__('When selecting to use kit %s, somehow the information we needed was missing. Try reloading the page.'), kitToken))
}
if( kitTokenActive === kitToken ) {
if (kitTokenActive === kitToken) {
// We're just resetting back to the state we were in
dispatch(resetPendingOptions())
} else {
dispatch(addPendingOption({
kitToken,
technology: 'svg' === selectedKit.technologySelected ? 'svg' : 'webfont',
usePro: 'pro' === selectedKit.licenseSelected,
compat: selectedKit.shimEnabled,
version: selectedKit.version,
// At the time this is being implemented, kits don't yet support
// toggling pseudoElement support for SVG, but it's implicitly supported for webfont.
pseudoElements: 'svg' !== selectedKit.technologySelected
}))
dispatch(
addPendingOption({
kitToken,
technology: 'svg' === selectedKit.technologySelected ? 'svg' : 'webfont',
usePro: 'pro' === selectedKit.licenseSelected,
compat: selectedKit.shimEnabled,
version: selectedKit.version,
// At the time this is being implemented, kits don't yet support
// toggling pseudoElement support for SVG, but it's implicitly supported for webfont.
pseudoElements: 'svg' !== selectedKit.technologySelected
})
)
}
dispatch(checkPreferenceConflicts())
}
const kitsQueryStatus = useSelector(state => state.kitsQueryStatus)
const kitsQueryStatus = useSelector((state) => state.kitsQueryStatus)
/**
* This seems like a lot of effort just to keep the focus on the API Token input
@ -111,15 +95,14 @@ export default function KitSelectView({ useOption, masterSubmitButtonShowing, se
* button, or pressing the tab key, for example.
*/
const apiTokenInputRef = createRef()
const [ apiTokenInputHasFocus, setApiTokenInputHasFocus ] = useState( false )
const [apiTokenInputHasFocus, setApiTokenInputHasFocus] = useState(false)
useEffect(() => {
if( !!apiTokenInputRef.current && apiTokenInputHasFocus ) {
if (!!apiTokenInputRef.current && apiTokenInputHasFocus) {
apiTokenInputRef.current.focus()
}
})
const hasSavedApiToken = useSelector(state => !! state.options.apiToken)
const hasSavedApiToken = useSelector((state) => !!state.options.apiToken)
function cancelApiTokenUpdate() {
setShowApiTokenInputForUpdate(false)
@ -128,78 +111,101 @@ export default function KitSelectView({ useOption, masterSubmitButtonShowing, se
}
function ApiTokenInput() {
useEffect(() => {
if ( submitSuccess && showApiTokenInputForUpdate ) {
if (submitSuccess && showApiTokenInputForUpdate) {
setShowApiTokenInputForUpdate(false)
setMasterSubmitButtonShowing(true)
}
} )
})
return <>
<div className={ classnames( styles['field-apitoken'], { [styles['api-token-update']]: showApiTokenInputForUpdate } )}>
<label htmlFor="api_token">
<FontAwesomeIcon className={ sharedStyles['icon'] } icon={ faQuestionCircle } size="lg" />
{ __( 'API Token', 'font-awesome' ) }
</label>
<div>
<input
id="api_token"
name="api_token"
type="text"
ref={ apiTokenInputRef }
value={ pendingApiToken || '' }
size="20"
onChange={ e => {
setApiTokenInputHasFocus( true )
setPendingApiToken(e.target.value)
}}
/>
return (
<>
<div className={classnames(styles['field-apitoken'], { [styles['api-token-update']]: showApiTokenInputForUpdate })}>
<label htmlFor="api_token">
<FontAwesomeIcon
className={sharedStyles['icon']}
icon={faQuestionCircle}
size="lg"
/>
{__('API Token', 'font-awesome')}
</label>
<div>
<input
id="api_token"
name="api_token"
type="text"
ref={apiTokenInputRef}
value={pendingApiToken || ''}
size="20"
onChange={(e) => {
setApiTokenInputHasFocus(true)
setPendingApiToken(e.target.value)
}}
/>
<p>
{ __( 'Grab your secure and unique API token from your Font Awesome account page and enter it here so we can securely fetch your kits.', 'font-awesome') } <a target="_blank" rel="noopener noreferrer" href="https://fontawesome.com/account#api-tokens">
{ __( 'Get your API token on fontawesome.com', 'font-awesome') } <FontAwesomeIcon icon={faExternalLinkAlt} style={{marginLeft: '.5em'}} />
</a>
</p>
<p>
{__(
'Grab your secure and unique API token from your Font Awesome account page and enter it here so we can securely fetch your kits.',
'font-awesome'
)}{' '}
<a
target="_blank"
rel="noopener noreferrer"
href="https://fontawesome.com/account#api-tokens"
>
{__('Get your API token on fontawesome.com', 'font-awesome')}{' '}
<FontAwesomeIcon
icon={faExternalLinkAlt}
style={{ marginLeft: '.5em' }}
/>
</a>
</p>
</div>
</div>
</div>
<div className="submit">
<input
type="submit"
name="submit"
id="submit"
className="button button-primary"
value={ __( 'Save API Token', 'font-awesome' ) }
disabled={ !pendingApiToken }
onMouseDown={ () => {
<div className="submit">
<input
type="submit"
name="submit"
id="submit"
className="button button-primary"
value={__('Save API Token', 'font-awesome')}
disabled={!pendingApiToken}
onMouseDown={() => {
dispatch(updateApiToken({ apiToken: pendingApiToken, runQueryKits: true }))
setPendingApiToken(null)
}
}
/>
{
(hasSubmitted && ! submitSuccess) &&
<div className={ classnames(sharedStyles['submit-status'], sharedStyles['fail']) }>
<div className={ classnames(sharedStyles['fail-icon-container']) }>
<FontAwesomeIcon className={ sharedStyles['icon'] } icon={ faSkull } />
}}
/>
{hasSubmitted && !submitSuccess && (
<div className={classnames(sharedStyles['submit-status'], sharedStyles['fail'])}>
<div className={classnames(sharedStyles['fail-icon-container'])}>
<FontAwesomeIcon
className={sharedStyles['icon']}
icon={faSkull}
/>
</div>
<div className={sharedStyles['explanation']}>{submitMessage}</div>
</div>
<div className={ sharedStyles['explanation'] }>
{ submitMessage }
</div>
</div>
}
{
isSubmitting &&
<span className={ classnames(sharedStyles['submit-status'], sharedStyles['submitting']) }>
<FontAwesomeIcon className={ sharedStyles['icon'] } icon={faSpinner} spin/>
</span>
}
{
(showApiTokenInputForUpdate && ! isSubmitting) &&
<button onClick={ () => cancelApiTokenUpdate() } className={ styles['button-dismissable'] }>{ __('Nevermind', 'font-awesome') }</button>
}
</div>
</>
)}
{isSubmitting && (
<span className={classnames(sharedStyles['submit-status'], sharedStyles['submitting'])}>
<FontAwesomeIcon
className={sharedStyles['icon']}
icon={faSpinner}
spin
/>
</span>
)}
{showApiTokenInputForUpdate && !isSubmitting && (
<button
onClick={() => cancelApiTokenUpdate()}
className={styles['button-dismissable']}
>
{__('Nevermind', 'font-awesome')}
</button>
)}
</div>
</>
)
}
function ApiTokenControl() {
@ -210,40 +216,67 @@ export default function KitSelectView({ useOption, masterSubmitButtonShowing, se
setShowRemoveApiTokenAlert(false)
}
return <div className={ styles['api-token-control-wrapper'] }>
<div className={ classnames( styles['api-token-control'], { [styles['api-token-update']]: showApiTokenInputForUpdate } )}>
{
showApiTokenInputForUpdate
? <ApiTokenInput />
: <>
<p className={ styles['token-saved'] }>
return (
<div className={styles['api-token-control-wrapper']}>
<div className={classnames(styles['api-token-control'], { [styles['api-token-update']]: showApiTokenInputForUpdate })}>
{showApiTokenInputForUpdate ? (
<ApiTokenInput />
) : (
<>
<p className={styles['token-saved']}>
<span>
<FontAwesomeIcon className={ sharedStyles['icon'] } icon={ faCheckCircle } size="lg" />
<FontAwesomeIcon
className={sharedStyles['icon']}
icon={faCheckCircle}
size="lg"
/>
</span>
{ __( 'API Token Saved', 'font-awesome' ) }
{__('API Token Saved', 'font-awesome')}
</p>
{
!!apiToken &&
<div className={ styles['button-group'] }>
<button onClick={ () => switchToApiTokenUpdate() } className={ styles['refresh'] } type="button">
<FontAwesomeIcon className={ sharedStyles['icon'] } icon={ faSync } title="update" alt="update" />
<span>{ __( 'Update token', 'font-awesome' ) }</span>
{!!apiToken && (
<div className={styles['button-group']}>
<button
onClick={() => switchToApiTokenUpdate()}
className={styles['refresh']}
type="button"
>
<FontAwesomeIcon
className={sharedStyles['icon']}
icon={faSync}
title="update"
alt="update"
/>
<span>{__('Update token', 'font-awesome')}</span>
</button>
<button
onClick={() => removeApiToken()}
className={styles['remove']}
type="button"
>
<FontAwesomeIcon
className={sharedStyles['icon']}
icon={faTrashAlt}
title="remove"
alt="remove"
/>
</button>
<button onClick={ () => removeApiToken() } className={ styles['remove'] } type="button"><FontAwesomeIcon className={ sharedStyles['icon'] } icon={ faTrashAlt } title="remove" alt="remove" /></button>
</div>
}
)}
</>
}
</div>
{
showingRemoveApiTokenAlert &&
<div className={ styles['api-token-control-alert-wrapper'] }>
<Alert title={ __( 'Whoa, whoa, whoa!', 'font-awesome' ) } type='warning'>
{ __( 'You can\'t remove your API token when "Use a Kit" is active. Switch to "Use CDN" first.', 'font-awesome' ) }
</Alert>
)}
</div>
}
</div>
{showingRemoveApiTokenAlert && (
<div className={styles['api-token-control-alert-wrapper']}>
<Alert
title={__('Whoa, whoa, whoa!', 'font-awesome')}
type="warning"
>
{__('You can\'t remove your API token when "Use a Kit" is active. Switch to "Use CDN" first.', 'font-awesome')}
</Alert>
</div>
)}
</div>
)
}
const STATUS = {
@ -257,140 +290,178 @@ export default function KitSelectView({ useOption, masterSubmitButtonShowing, se
}
function KitSelector() {
const status =
apiToken
? kitsQueryStatus.isSubmitting
? STATUS.querying
: kitsQueryStatus.hasSubmitted
? kitsQueryStatus.success
? size(kits) > 0
? STATUS.kitSelection
: STATUS.noKitsFoundAfterQuery
: STATUS.networkError
: kitTokenActive
? STATUS.showingOnlyActiveKit
: STATUS.apiTokenReadyNoKitsYet
: STATUS.noApiToken
const status = apiToken
? kitsQueryStatus.isSubmitting
? STATUS.querying
: kitsQueryStatus.hasSubmitted
? kitsQueryStatus.success
? size(kits) > 0
? STATUS.kitSelection
: STATUS.noKitsFoundAfterQuery
: STATUS.networkError
: kitTokenActive
? STATUS.showingOnlyActiveKit
: STATUS.apiTokenReadyNoKitsYet
: STATUS.noApiToken
const kitRefreshButton = <button onClick={ () => dispatch(queryKits()) } className={ styles['refresh'] }>
<FontAwesomeIcon className={ sharedStyles['icon'] } icon={ faRedo } title="refresh" alt="refresh" />
<span>
{
0 === size(kits)
? __( 'Get latest kits data', 'font-awesome' )
: __( 'Refresh kits data', 'font-awesome' )
}
</span>
</button>
const kitRefreshButton = (
<button
onClick={() => dispatch(queryKits())}
className={styles['refresh']}
>
<FontAwesomeIcon
className={sharedStyles['icon']}
icon={faRedo}
title="refresh"
alt="refresh"
/>
<span>{0 === size(kits) ? __('Get latest kits data', 'font-awesome') : __('Refresh kits data', 'font-awesome')}</span>
</button>
)
const activeKitNotice = kitTokenActive
? <div className={ styles['wrap-active-kit'] }><p className={ classnames(styles['active-kit'], styles['set']) }><FontAwesomeIcon className={ sharedStyles['icon'] } icon={ faCheckCircle } size="lg" />
{
sprintf(
__( '%s Kit is Currently Active' ),
kitTokenActive
)
}
</p></div>
: null
const activeKitNotice = kitTokenActive ? (
<div className={styles['wrap-active-kit']}>
<p className={classnames(styles['active-kit'], styles['set'])}>
<FontAwesomeIcon
className={sharedStyles['icon']}
icon={faCheckCircle}
size="lg"
/>
{sprintf(__('%s Kit is Currently Active'), kitTokenActive)}
</p>
</div>
) : null
return (
<div className={styles['kit-selector-container']}>
{activeKitNotice}
return <div className={ styles['kit-selector-container'] }>
{ activeKitNotice }
<div className={ styles['wrap-selectkit'] }>
<h3 className={ styles['title-selectkit'] }><FontAwesomeIcon className={ sharedStyles['icon'] } icon={ faQuestionCircle } size="lg" />
{ __( 'Pick a Kit to Use or Check Settings', 'font-awesome' ) }
<div className={styles['wrap-selectkit']}>
<h3 className={styles['title-selectkit']}>
<FontAwesomeIcon
className={sharedStyles['icon']}
icon={faQuestionCircle}
size="lg"
/>
{__('Pick a Kit to Use or Check Settings', 'font-awesome')}
</h3>
<div className={ styles['selectkit'] }>
<div className={styles['selectkit']}>
<p>
{
__( 'Refresh your kits data to get the latest kit settings, then select the kit you would like to use. Remember to save when you\'re ready to use it.', 'font-awesome' )
}
{__(
"Refresh your kits data to get the latest kit settings, then select the kit you would like to use. Remember to save when you're ready to use it.",
'font-awesome'
)}
</p>
{
{
noApiToken: 'noApiToken',
apiTokenReadyNoKitsYet: <>{ activeKitNotice } { kitRefreshButton }</>,
querying:
<div>
<span>
{ __( 'Loading your kits...', 'font-awesome' ) }
</span>
<span className={ classnames(sharedStyles['submit-status'], sharedStyles['submitting']) }>
<FontAwesomeIcon className={ sharedStyles['icon'] } icon={faSpinner} spin/>
</span>
</div>,
networkError:
<div className={ classnames(sharedStyles['submit-status'], sharedStyles['fail']) }>
<div className={ classnames(sharedStyles['fail-icon-container']) }>
<FontAwesomeIcon className={ sharedStyles['icon'] } icon={ faSkull } />
{
noApiToken: 'noApiToken',
apiTokenReadyNoKitsYet: (
<>
{activeKitNotice} {kitRefreshButton}
</>
),
querying: (
<div>
<span>{__('Loading your kits...', 'font-awesome')}</span>
<span className={classnames(sharedStyles['submit-status'], sharedStyles['submitting'])}>
<FontAwesomeIcon
className={sharedStyles['icon']}
icon={faSpinner}
spin
/>
</span>
</div>
<div className={ sharedStyles['explanation'] }>
{ kitsQueryStatus.message }
),
networkError: (
<div className={classnames(sharedStyles['submit-status'], sharedStyles['fail'])}>
<div className={classnames(sharedStyles['fail-icon-container'])}>
<FontAwesomeIcon
className={sharedStyles['icon']}
icon={faSkull}
/>
</div>
<div className={sharedStyles['explanation']}>{kitsQueryStatus.message}</div>
</div>
</div>,
),
noKitsFoundAfterQuery:
<>
<Alert title="Zoinks! Looks like you don't have any kits set up yet." type="info">
<p>
{ __( 'Head over to Font Awesome to create one, then come back here and refresh your kits.', 'font-awesome' ) } <a rel="noopener noreferrer" target="_blank" href="https://fontawesome.com/kits">
{ __( 'Create a kit on Font Awesome', 'font-awesome' ) } <FontAwesomeIcon icon={faExternalLinkAlt} /></a>
</p>
</Alert>
{ kitRefreshButton }
</>,
noKitsFoundAfterQuery: (
<>
<Alert
title="Zoinks! Looks like you don't have any kits set up yet."
type="info"
>
<p>
{__('Head over to Font Awesome to create one, then come back here and refresh your kits.', 'font-awesome')}{' '}
<a
rel="noopener noreferrer"
target="_blank"
href="https://fontawesome.com/kits"
>
{__('Create a kit on Font Awesome', 'font-awesome')} <FontAwesomeIcon icon={faExternalLinkAlt} />
</a>
</p>
</Alert>
{kitRefreshButton}
</>
),
kitSelection:
<>
<div className={ styles['field-kitselect'] }>
<select
className={ styles['kit-select'] }
id="kits"
name="kit"
onChange={ e => handleKitChange({ kitToken: e.target.value }) }
disabled={! masterSubmitButtonShowing }
value={ kitToken || '' }
>
<option key='empty' value=''>{ __( 'Select a kit', 'font-awesome' ) }</option>
{
kits.map((kit, index) => {
return <option key={ index } value={ kit.token }>
{ `${ kit.name } (${ kit.token })` }
</option>
})
}
</select>
{ kitRefreshButton }
</div>
</>,
kitSelection: (
<>
<div className={styles['field-kitselect']}>
<select
className={styles['kit-select']}
id="kits"
name="kit"
onChange={(e) => handleKitChange({ kitToken: e.target.value })}
disabled={!masterSubmitButtonShowing}
value={kitToken || ''}
>
<option
key="empty"
value=""
>
{__('Select a kit', 'font-awesome')}
</option>
{kits.map((kit, index) => {
return (
<option
key={index}
value={kit.token}
>
{`${kit.name} (${kit.token})`}
</option>
)
})}
</select>
{kitRefreshButton}
</div>
</>
),
showingOnlyActiveKit:
<>
{ kitRefreshButton }
</>
}[status]
}
showingOnlyActiveKit: <>{kitRefreshButton}</>
}[status]
}
</div>
</div>
</div>
}
)
}
return <div>
<div className={ styles['kit-tab-content'] }>
{
hasSavedApiToken
? <>
return (
<div>
<div className={styles['kit-tab-content']}>
{hasSavedApiToken ? (
<>
<ApiTokenControl />
<KitSelector />
</>
: <ApiTokenInput />
}
) : (
<ApiTokenInput />
)}
</div>
</div>
</div>
)
}
KitSelectView.propTypes = {

View File

@ -7,18 +7,21 @@ import { __ } from '@wordpress/i18n'
import createInterpolateElement from './createInterpolateElement'
export default function ManageFontAwesomeVersionsSection() {
return <div className={ classnames(sharedStyles['explanation'], styles['font-awesome-versions-section']) }>
<h2 className={ sharedStyles['section-title'] }>{ __( 'Versions of Font Awesome Active on Your Site', 'font-awesome' ) }</h2>
<p>
{
createInterpolateElement(
__( '<b>Registered plugins and themes</b> have opted to share information about the Font Awesome settings they are expecting, and are therefore easier to fix. For the <b>unregistered plugins and themes</b>, which are more unpredictable, we have provided options for you to block their Font Awesome source from loading and causing issues.', 'font-awesome' ),
return (
<div className={classnames(sharedStyles['explanation'], styles['font-awesome-versions-section'])}>
<h2 className={sharedStyles['section-title']}>{__('Versions of Font Awesome Active on Your Site', 'font-awesome')}</h2>
<p>
{createInterpolateElement(
__(
'<b>Registered plugins and themes</b> have opted to share information about the Font Awesome settings they are expecting, and are therefore easier to fix. For the <b>unregistered plugins and themes</b>, which are more unpredictable, we have provided options for you to block their Font Awesome source from loading and causing issues.',
'font-awesome'
),
{
b: <b />
}
)
}
</p>
<ClientPreferencesView />
</div>
)}
</p>
<ClientPreferencesView />
</div>
)
}

View File

@ -6,44 +6,34 @@ import KitConfigView from './KitConfigView'
import sharedStyles from './App.module.css'
import optionStyles from './CdnConfigView.module.css'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import {
faDotCircle,
faSpinner,
faCheck,
faSkull,
} from '@fortawesome/free-solid-svg-icons'
import { faDotCircle, faSpinner, faCheck, faSkull } from '@fortawesome/free-solid-svg-icons'
import { faCircle } from '@fortawesome/free-regular-svg-icons'
import classnames from 'classnames'
import styles from './SettingsTab.module.css'
import has from 'lodash/has'
import { has, size } from 'lodash'
import { addPendingOption, submitPendingOptions, chooseAwayFromKitConfig, chooseIntoKitConfig } from './store/actions'
import CheckingOptionStatusIndicator from './CheckingOptionsStatusIndicator'
import size from 'lodash/size'
import { __ } from '@wordpress/i18n'
export default function SettingsTab() {
const dispatch = useDispatch()
const alreadyUsingKit = useSelector( state => !!state.options.kitToken )
const alreadyUsingKit = useSelector((state) => !!state.options.kitToken)
const [useKit, setUseKit] = useState(alreadyUsingKit)
const isChecking = useSelector(state => state.preferenceConflictDetection.isChecking)
const hasSubmitted = useSelector(state => state.optionsFormState.hasSubmitted)
const submitSuccess = useSelector(state => state.optionsFormState.success)
const submitMessage = useSelector(state => state.optionsFormState.message)
const isSubmitting = useSelector(state => state.optionsFormState.isSubmitting)
const pendingOptions = useSelector(state => state.pendingOptions)
const apiToken = useSelector(state => state.options.apiToken)
const [ masterSubmitButtonShowing, setMasterSubmitButtonShowing ] = useState( true )
const isChecking = useSelector((state) => state.preferenceConflictDetection.isChecking)
const hasSubmitted = useSelector((state) => state.optionsFormState.hasSubmitted)
const submitSuccess = useSelector((state) => state.optionsFormState.success)
const submitMessage = useSelector((state) => state.optionsFormState.message)
const isSubmitting = useSelector((state) => state.optionsFormState.isSubmitting)
const pendingOptions = useSelector((state) => state.pendingOptions)
const apiToken = useSelector((state) => state.options.apiToken)
const [masterSubmitButtonShowing, setMasterSubmitButtonShowing] = useState(true)
function useOption(option) {
return useSelector(state =>
has(state.pendingOptions, option)
? state.pendingOptions[option]
: state.options[option]
)
return useSelector((state) => (has(state.pendingOptions, option) ? state.pendingOptions[option] : state.options[option]))
}
function handleSubmit(e) {
if(!!e && 'function' == typeof e.preventDefault) {
if (!!e && 'function' == typeof e.preventDefault) {
e.preventDefault()
}
@ -51,10 +41,10 @@ export default function SettingsTab() {
}
// The kitToken that may be a pendingOption
const kitToken = useOption( 'kitToken' )
const kitToken = useOption('kitToken')
// The one that's actually saved in the database already
const activeKitToken = useSelector( state => state.options.kitToken )
const activeKitToken = useSelector((state) => state.options.kitToken)
function handleOptionChange(change = {}) {
dispatch(addPendingOption(change))
@ -66,132 +56,153 @@ export default function SettingsTab() {
* that a kit selection might have put onto the form.
*/
function handleSwitchAwayFromKitConfig() {
setUseKit( false )
setUseKit(false)
dispatch( chooseAwayFromKitConfig({ activeKitToken }) )
dispatch(chooseAwayFromKitConfig({ activeKitToken }))
}
function handleSwitchToKitConfig() {
setUseKit( true )
setMasterSubmitButtonShowing( true )
setUseKit(true)
setMasterSubmitButtonShowing(true)
dispatch( chooseIntoKitConfig() )
dispatch(chooseIntoKitConfig())
}
return <div><div className={ sharedStyles['wrapper-div'] }>
<h3>{ __( 'How are you using Font Awesome?', 'font-awesome' ) }</h3>
<div className={ styles['select-config-container'] }>
<span>
<input
id="select_use_kits"
name="select_use_kits"
type="radio"
value={ useKit }
checked={ useKit }
onChange={ () => handleSwitchToKitConfig() }
className={ classnames(sharedStyles['sr-only'], sharedStyles['input-radio-custom']) }
/>
<label htmlFor="select_use_kits" className={ optionStyles['option-label'] }>
<span className={ sharedStyles['relative'] }>
<FontAwesomeIcon
icon={ faDotCircle }
className={ sharedStyles['checked-icon'] }
size="lg"
fixedWidth
return (
<div>
<div className={sharedStyles['wrapper-div']}>
<h3>{__('How are you using Font Awesome?', 'font-awesome')}</h3>
<div className={styles['select-config-container']}>
<span>
<input
id="select_use_kits"
name="select_use_kits"
type="radio"
value={useKit}
checked={useKit}
onChange={() => handleSwitchToKitConfig()}
className={classnames(sharedStyles['sr-only'], sharedStyles['input-radio-custom'])}
/>
<FontAwesomeIcon
icon={ faCircle }
className={ sharedStyles['unchecked-icon'] }
size="lg"
fixedWidth
/>
</span>
<span className={ optionStyles['option-label-text'] }>
{ __( 'Use A Kit', 'font-awesome' ) }
</span>
</label>
</span>
<span>
<input
id="select_use_cdn"
name="select_use_cdn"
type="radio"
value={ ! useKit }
checked={ ! useKit }
onChange={ () => handleSwitchAwayFromKitConfig() }
className={ classnames(sharedStyles['sr-only'], sharedStyles['input-radio-custom']) }
/>
<label htmlFor="select_use_cdn" className={ optionStyles['option-label'] }>
<span className={ sharedStyles['relative'] }>
<FontAwesomeIcon
icon={ faDotCircle }
className={ sharedStyles['checked-icon'] }
size="lg"
fixedWidth
/>
<FontAwesomeIcon
icon={ faCircle }
className={ sharedStyles['unchecked-icon'] }
size="lg"
fixedWidth
/>
</span>
<span className={ optionStyles['option-label-text'] }>
{ __( 'Use CDN', 'font-awesome' ) }
</span>
</label>
</span>
</div>
<>
{
useKit
? <>
<KitSelectView useOption={ useOption } handleOptionChange={ handleOptionChange } handleSubmit={ handleSubmit } masterSubmitButtonShowing={ masterSubmitButtonShowing } setMasterSubmitButtonShowing={ setMasterSubmitButtonShowing }/>
{ !!kitToken && <KitConfigView kitToken={ kitToken } /> }
</>
: <CdnConfigView useOption={ useOption } handleOptionChange={ handleOptionChange } handleSubmit={ handleSubmit }/>
}
</>
</div>
{
(!useKit || ( apiToken && masterSubmitButtonShowing ) ) &&
<div className={ classnames(sharedStyles['submit-wrapper'], ['submit']) }>
<input
type="submit"
name="submit"
id="submit"
className="button button-primary"
value={ __( 'Save Changes', 'font-awesome' ) }
disabled={ size(pendingOptions) === 0 }
onClick={ handleSubmit }
/>
{ hasSubmitted
? submitSuccess
? <span className={ classnames(sharedStyles['submit-status'], sharedStyles['success']) }>
<FontAwesomeIcon className={ sharedStyles['icon'] } icon={ faCheck } />
<label
htmlFor="select_use_kits"
className={optionStyles['option-label']}
>
<span className={sharedStyles['relative']}>
<FontAwesomeIcon
icon={faDotCircle}
className={sharedStyles['checked-icon']}
size="lg"
fixedWidth
/>
<FontAwesomeIcon
icon={faCircle}
className={sharedStyles['unchecked-icon']}
size="lg"
fixedWidth
/>
</span>
: <div className={ classnames(sharedStyles['submit-status'], sharedStyles['fail']) }>
<div className={ classnames(sharedStyles['fail-icon-container']) }>
<FontAwesomeIcon className={ sharedStyles['icon'] } icon={ faSkull } />
</div>
<div className={ sharedStyles['explanation'] }>
{ submitMessage }
</div>
</div>
: null
}
{
isSubmitting
? <span className={ classnames(sharedStyles['submit-status'], sharedStyles['submitting']) }>
<FontAwesomeIcon className={ sharedStyles['icon'] } icon={faSpinner} spin/>
</span>
: isChecking
? <CheckingOptionStatusIndicator/>
: size(pendingOptions) > 0
? <span className={ sharedStyles['submit-status'] }>{ __( 'you have pending changes', 'font-awesome' ) }</span>
: null
}
<span className={optionStyles['option-label-text']}>{__('Use A Kit', 'font-awesome')}</span>
</label>
</span>
<span>
<input
id="select_use_cdn"
name="select_use_cdn"
type="radio"
value={!useKit}
checked={!useKit}
onChange={() => handleSwitchAwayFromKitConfig()}
className={classnames(sharedStyles['sr-only'], sharedStyles['input-radio-custom'])}
/>
<label
htmlFor="select_use_cdn"
className={optionStyles['option-label']}
>
<span className={sharedStyles['relative']}>
<FontAwesomeIcon
icon={faDotCircle}
className={sharedStyles['checked-icon']}
size="lg"
fixedWidth
/>
<FontAwesomeIcon
icon={faCircle}
className={sharedStyles['unchecked-icon']}
size="lg"
fixedWidth
/>
</span>
<span className={optionStyles['option-label-text']}>{__('Use CDN', 'font-awesome')}</span>
</label>
</span>
</div>
<>
{useKit ? (
<>
<KitSelectView
useOption={useOption}
handleOptionChange={handleOptionChange}
handleSubmit={handleSubmit}
masterSubmitButtonShowing={masterSubmitButtonShowing}
setMasterSubmitButtonShowing={setMasterSubmitButtonShowing}
/>
{!!kitToken && <KitConfigView kitToken={kitToken} />}
</>
) : (
<CdnConfigView
useOption={useOption}
handleOptionChange={handleOptionChange}
handleSubmit={handleSubmit}
/>
)}
</>
</div>
}
</div>
{(!useKit || (apiToken && masterSubmitButtonShowing)) && (
<div className={classnames(sharedStyles['submit-wrapper'], ['submit'])}>
<input
type="submit"
name="submit"
id="submit"
className="button button-primary"
value={__('Save Changes', 'font-awesome')}
disabled={size(pendingOptions) === 0}
onClick={handleSubmit}
/>
{hasSubmitted ? (
submitSuccess ? (
<span className={classnames(sharedStyles['submit-status'], sharedStyles['success'])}>
<FontAwesomeIcon
className={sharedStyles['icon']}
icon={faCheck}
/>
</span>
) : (
<div className={classnames(sharedStyles['submit-status'], sharedStyles['fail'])}>
<div className={classnames(sharedStyles['fail-icon-container'])}>
<FontAwesomeIcon
className={sharedStyles['icon']}
icon={faSkull}
/>
</div>
<div className={sharedStyles['explanation']}>{submitMessage}</div>
</div>
)
) : null}
{isSubmitting ? (
<span className={classnames(sharedStyles['submit-status'], sharedStyles['submitting'])}>
<FontAwesomeIcon
className={sharedStyles['icon']}
icon={faSpinner}
spin
/>
</span>
) : isChecking ? (
<CheckingOptionStatusIndicator />
) : size(pendingOptions) > 0 ? (
<span className={sharedStyles['submit-status']}>{__('you have pending changes', 'font-awesome')}</span>
) : null}
</div>
)}
</div>
)
}

View File

@ -1,7 +1,6 @@
import React from 'react'
import ManageFontAwesomeVersionsSection from './ManageFontAwesomeVersionsSection'
import UnregisteredClientsView from './UnregisteredClientsView'
import V3DeprecationWarning from './V3DeprecationWarning'
import ConflictDetectionScannerSection from './ConflictDetectionScannerSection'
import sharedStyles from './App.module.css'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
@ -12,24 +11,20 @@ import {
resetPendingBlocklistSubmissionStatus,
resetUnregisteredClientsDeletionStatus
} from './store/actions'
import {
faCheck,
faSkull,
faSpinner } from '@fortawesome/free-solid-svg-icons'
import { faCheck, faSkull, faSpinner } from '@fortawesome/free-solid-svg-icons'
import classnames from 'classnames'
import size from 'lodash/size'
import { size } from 'lodash'
import { __ } from '@wordpress/i18n'
export default function TroubleshootTab() {
const dispatch = useDispatch()
const hasV3DeprecationWarning = useSelector(state => !!state.v3DeprecationWarning)
const unregisteredClients = useSelector(state => state.unregisteredClients)
const unregisteredClients = useSelector((state) => state.unregisteredClients)
const blocklistUpdateStatus = useSelector(state => state.blocklistUpdateStatus)
const unregisteredClientsDeletionStatus = useSelector(state => state.unregisteredClientsDeletionStatus)
const blocklistUpdateStatus = useSelector((state) => state.blocklistUpdateStatus)
const unregisteredClientsDeletionStatus = useSelector((state) => state.unregisteredClientsDeletionStatus)
const showSubmitButton = size( unregisteredClients ) > 0
const hasPendingChanges = null !== blocklistUpdateStatus.pending || size( unregisteredClientsDeletionStatus.pending ) > 0
const showSubmitButton = size(unregisteredClients) > 0
const hasPendingChanges = null !== blocklistUpdateStatus.pending || size(unregisteredClientsDeletionStatus.pending) > 0
const hasSubmitted = unregisteredClientsDeletionStatus.hasSubmitted || blocklistUpdateStatus.hasSubmitted
const isSubmitting = unregisteredClientsDeletionStatus.isSubmitting || blocklistUpdateStatus.isSubmitting
@ -41,68 +36,73 @@ export default function TroubleshootTab() {
function handleSubmitClick(e) {
e.preventDefault()
if ( blocklistUpdateStatus.pending ) {
if (blocklistUpdateStatus.pending) {
dispatch(submitPendingBlocklist())
} else {
dispatch(resetPendingBlocklistSubmissionStatus())
}
if ( size( unregisteredClientsDeletionStatus.pending ) > 0 ) {
if (size(unregisteredClientsDeletionStatus.pending) > 0) {
dispatch(submitPendingUnregisteredClientDeletions())
} else {
dispatch(resetUnregisteredClientsDeletionStatus())
}
}
return <>
<div className={ sharedStyles['wrapper-div'] }>
{ hasV3DeprecationWarning && <V3DeprecationWarning /> }
<ConflictDetectionScannerSection />
<ManageFontAwesomeVersionsSection />
<UnregisteredClientsView />
</div>
{
showSubmitButton &&
<div className={ classnames(sharedStyles['submit-wrapper'], ['submit']) }>
return (
<>
<div className={sharedStyles['wrapper-div']}>
<ConflictDetectionScannerSection />
<ManageFontAwesomeVersionsSection />
<UnregisteredClientsView />
</div>
{showSubmitButton && (
<div className={classnames(sharedStyles['submit-wrapper'], ['submit'])}>
<input
type="submit"
name="submit"
id="submit"
className="button button-primary"
value={ __( 'Save Changes', 'font-awesome' ) }
disabled={ !hasPendingChanges }
onClick={ handleSubmitClick }
value={__('Save Changes', 'font-awesome')}
disabled={!hasPendingChanges}
onClick={handleSubmitClick}
/>
{ hasSubmitted
? submitSuccess
? <span className={ classnames(sharedStyles['submit-status'], sharedStyles['success']) }>
<FontAwesomeIcon className={ sharedStyles['icon'] } icon={ faCheck } />
</span>
: <div className={ classnames(sharedStyles['submit-status'], sharedStyles['fail']) }>
<div className={ classnames(sharedStyles['fail-icon-container']) }>
<FontAwesomeIcon className={ sharedStyles['icon'] } icon={ faSkull } />
</div>
<div className={ sharedStyles['explanation'] }>
{
!!blocklistUpdateStatus.message && <p> { blocklistUpdateStatus.message } </p>
}
{
!!unregisteredClientsDeletionStatus.message && <p> { unregisteredClientsDeletionStatus.message } </p>
}
</div>
</div>
: null
}
{
isSubmitting
? <span className={ classnames(sharedStyles['submit-status'], sharedStyles['submitting']) }>
<FontAwesomeIcon className={ sharedStyles['icon'] } icon={faSpinner} spin/>
{hasSubmitted ? (
submitSuccess ? (
<span className={classnames(sharedStyles['submit-status'], sharedStyles['success'])}>
<FontAwesomeIcon
className={sharedStyles['icon']}
icon={faCheck}
/>
</span>
: hasPendingChanges
? <span className={ sharedStyles['submit-status'] }>{ __( 'you have pending changes', 'font-awesome' ) }</span>
: null
}
) : (
<div className={classnames(sharedStyles['submit-status'], sharedStyles['fail'])}>
<div className={classnames(sharedStyles['fail-icon-container'])}>
<FontAwesomeIcon
className={sharedStyles['icon']}
icon={faSkull}
/>
</div>
<div className={sharedStyles['explanation']}>
{!!blocklistUpdateStatus.message && <p> {blocklistUpdateStatus.message} </p>}
{!!unregisteredClientsDeletionStatus.message && <p> {unregisteredClientsDeletionStatus.message} </p>}
</div>
</div>
)
) : null}
{isSubmitting ? (
<span className={classnames(sharedStyles['submit-status'], sharedStyles['submitting'])}>
<FontAwesomeIcon
className={sharedStyles['icon']}
icon={faSpinner}
spin
/>
</span>
) : hasPendingChanges ? (
<span className={sharedStyles['submit-status']}>{__('you have pending changes', 'font-awesome')}</span>
) : null}
</div>
}
</>
)}
</>
)
}

View File

@ -1,31 +1,20 @@
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import {
updatePendingBlocklist,
updatePendingUnregisteredClientsForDeletion
} from './store/actions'
import { updatePendingBlocklist, updatePendingUnregisteredClientsForDeletion } from './store/actions'
import { blocklistSelector } from './store/reducers'
import styles from './UnregisteredClientsView.module.css'
import sharedStyles from './App.module.css'
import classnames from 'classnames'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import {
faCheckSquare,
faThumbsUp } from '@fortawesome/free-solid-svg-icons'
import {
faSquare } from '@fortawesome/free-regular-svg-icons'
import get from 'lodash/get'
import truncate from 'lodash/truncate'
import size from 'lodash/size'
import isEqual from 'lodash/isEqual'
import sortedUnique from 'lodash/sortedUniq'
import difference from 'lodash/difference'
import { faCheckSquare, faThumbsUp } from '@fortawesome/free-solid-svg-icons'
import { faSquare } from '@fortawesome/free-regular-svg-icons'
import { get, truncate, size, isEqual, sortedUnique, difference } from 'lodash'
import { __ } from '@wordpress/i18n'
import createInterpolateElement from './createInterpolateElement'
function excerpt( content ) {
if( !! content ) {
return truncate( content, { length: 100 } )
function excerpt(content) {
if (!!content) {
return truncate(content, { length: 100 })
} else {
return null
}
@ -33,232 +22,229 @@ function excerpt( content ) {
export default function UnregisteredClientsView() {
const dispatch = useDispatch()
const unregisteredClients = useSelector(state => state.unregisteredClients)
const savedBlocklist = useSelector(state => blocklistSelector(state))
const blocklist = useSelector(state => {
if( null !== state.blocklistUpdateStatus.pending ) {
const unregisteredClients = useSelector((state) => state.unregisteredClients)
const savedBlocklist = useSelector((state) => blocklistSelector(state))
const blocklist = useSelector((state) => {
if (null !== state.blocklistUpdateStatus.pending) {
return state.blocklistUpdateStatus.pending
} else {
return savedBlocklist
}
})
const deleteList = useSelector( state => state.unregisteredClientsDeletionStatus.pending)
const deleteList = useSelector((state) => state.unregisteredClientsDeletionStatus.pending)
const detectedUnregisteredClients = size(Object.keys(unregisteredClients)) > 0
const allDetectedConflictsSelectedForBlocking =
isEqual(Object.keys(unregisteredClients).sort(), [...(blocklist || [])].sort())
const allDetectedConflictsSelectedForRemoval =
isEqual(Object.keys(unregisteredClients).sort(), [...(deleteList || [])].sort())
const allDetectedConflictsSelectedForBlocking = isEqual(Object.keys(unregisteredClients).sort(), [...(blocklist || [])].sort())
const allDetectedConflictsSelectedForRemoval = isEqual(Object.keys(unregisteredClients).sort(), [...(deleteList || [])].sort())
const allDetectedConflicts = Object.keys(unregisteredClients)
function isCheckedForBlocking(md5) {
return !! blocklist.find(x => x === md5)
return !!blocklist.find((x) => x === md5)
}
function isCheckedForRemoval(md5) {
return !! deleteList.find(x => x === md5)
return !!deleteList.find((x) => x === md5)
}
function changeCheckForRemoval(md5, allDetectedConflicts) {
const newDeleteList = 'all' === md5
? allDetectedConflictsSelectedForRemoval
? [] // uncheck them all
: allDetectedConflicts // check them all
: isCheckedForRemoval(md5)
? deleteList.filter(x => x !== md5)
const newDeleteList =
'all' === md5
? allDetectedConflictsSelectedForRemoval
? [] // uncheck them all
: allDetectedConflicts // check them all
: isCheckedForRemoval(md5)
? deleteList.filter((x) => x !== md5)
: [...deleteList, md5]
dispatch(updatePendingUnregisteredClientsForDeletion(newDeleteList))
}
function changeCheckForBlocking(md5, allDetectedConflicts) {
const newBlocklist = 'all' === md5
? allDetectedConflictsSelectedForBlocking
? [] // uncheck them all
: allDetectedConflicts // check them all
: isCheckedForBlocking(md5)
? blocklist.filter(x => x !== md5)
const newBlocklist =
'all' === md5
? allDetectedConflictsSelectedForBlocking
? [] // uncheck them all
: allDetectedConflicts // check them all
: isCheckedForBlocking(md5)
? blocklist.filter((x) => x !== md5)
: [...blocklist, md5]
const orig = sortedUnique( savedBlocklist )
const updated = sortedUnique( newBlocklist )
const orig = sortedUnique(savedBlocklist)
const updated = sortedUnique(newBlocklist)
if(
orig.length === updated.length &&
0 === size( difference(orig, updated) ) &&
0 === size( difference(updated, orig) )
) {
if (orig.length === updated.length && 0 === size(difference(orig, updated)) && 0 === size(difference(updated, orig))) {
dispatch(updatePendingBlocklist(null))
} else {
dispatch(updatePendingBlocklist(newBlocklist))
}
}
return <div className={ classnames(styles['unregistered-clients'], { [styles['none-detected']]: !detectedUnregisteredClients }) }>
<h3 className={ sharedStyles['section-title'] }>{ __( 'Other themes or plugins', 'font-awesome' ) }</h3>
{detectedUnregisteredClients
? <div>
return (
<div className={classnames(styles['unregistered-clients'], { [styles['none-detected']]: !detectedUnregisteredClients })}>
<h3 className={sharedStyles['section-title']}>{__('Other themes or plugins', 'font-awesome')}</h3>
{detectedUnregisteredClients ? (
<div>
<p className={sharedStyles['explanation']}>
{
__( 'Below is the list of other versions of Font Awesome from active plugins or themes that are loading on your site. Check off any that you would like to block from loading. Normally this just blocks the conflicting version of Font Awesome and doesn\'t affect the other functions of the plugin, but you should verify your site works as expected. If you think you\'ve fixed a found conflict, you can clear it from the table.', 'font-awesome' )
}
{__(
"Below is the list of other versions of Font Awesome from active plugins or themes that are loading on your site. Check off any that you would like to block from loading. Normally this just blocks the conflicting version of Font Awesome and doesn't affect the other functions of the plugin, but you should verify your site works as expected. If you think you've fixed a found conflict, you can clear it from the table.",
'font-awesome'
)}
</p>
<table className={classnames('widefat', 'striped')}>
<thead>
<tr className={sharedStyles['table-header']}>
<th>
<div className={ styles['column-label'] }>{ __( 'Block', 'font-awesome' ) }</div>
{
size( allDetectedConflicts ) > 1 &&
<div className={ styles['block-all-container'] }>
<input
id='block_all_detected_conflicts'
name='block_all_detected_conflicts'
type="checkbox"
value='all'
checked={ allDetectedConflictsSelectedForBlocking }
onChange={ () => changeCheckForBlocking('all', allDetectedConflicts) }
className={ classnames(sharedStyles['sr-only'], sharedStyles['input-checkbox-custom']) }
/>
<label htmlFor='block_all_detected_conflicts' className={ styles['checkbox-label'] }>
<span className={ sharedStyles['relative'] }>
<FontAwesomeIcon
icon={ faCheckSquare }
className={ sharedStyles['checked-icon'] }
size="lg"
fixedWidth
/>
<FontAwesomeIcon
icon={ faSquare }
className={ sharedStyles['unchecked-icon'] }
size="lg"
fixedWidth
/>
</span>
{ __( 'All', 'font-awesome' ) }
</label>
</div>
}
</th>
<th>
<span className={ styles['column-label'] }>
{ __( 'Type', 'font-awesome' ) }
</span>
</th>
<th>
<span className={ styles['column-label'] }>
{ __( 'URL', 'font-awesome' ) }
</span>
</th>
<th>
<div className={ styles['column-label'] }>{ __( 'Clear', 'font-awesome' ) }</div>
{
size( allDetectedConflicts ) > 1 &&
<div className={ styles['remove-all-container'] }>
<input
id='remove_all_detected_conflicts'
name='remove_all_detected_conflicts'
type="checkbox"
value='all'
checked={ allDetectedConflictsSelectedForRemoval }
onChange={ () => changeCheckForRemoval('all', allDetectedConflicts) }
className={ classnames(sharedStyles['sr-only'], sharedStyles['input-checkbox-custom']) }
/>
<label htmlFor='remove_all_detected_conflicts' className={ styles['checkbox-label'] }>
<span className={ sharedStyles['relative'] }>
<FontAwesomeIcon
icon={ faCheckSquare }
className={ sharedStyles['checked-icon'] }
size="lg"
fixedWidth
/>
<FontAwesomeIcon
icon={ faSquare }
className={ sharedStyles['unchecked-icon'] }
size="lg"
fixedWidth
/>
</span>
{ __( 'All', 'font-awesome' ) }
</label>
</div>
}
</th>
</tr>
<tr className={sharedStyles['table-header']}>
<th>
<div className={styles['column-label']}>{__('Block', 'font-awesome')}</div>
{size(allDetectedConflicts) > 1 && (
<div className={styles['block-all-container']}>
<input
id="block_all_detected_conflicts"
name="block_all_detected_conflicts"
type="checkbox"
value="all"
checked={allDetectedConflictsSelectedForBlocking}
onChange={() => changeCheckForBlocking('all', allDetectedConflicts)}
className={classnames(sharedStyles['sr-only'], sharedStyles['input-checkbox-custom'])}
/>
<label
htmlFor="block_all_detected_conflicts"
className={styles['checkbox-label']}
>
<span className={sharedStyles['relative']}>
<FontAwesomeIcon
icon={faCheckSquare}
className={sharedStyles['checked-icon']}
size="lg"
fixedWidth
/>
<FontAwesomeIcon
icon={faSquare}
className={sharedStyles['unchecked-icon']}
size="lg"
fixedWidth
/>
</span>
{__('All', 'font-awesome')}
</label>
</div>
)}
</th>
<th>
<span className={styles['column-label']}>{__('Type', 'font-awesome')}</span>
</th>
<th>
<span className={styles['column-label']}>{__('URL', 'font-awesome')}</span>
</th>
<th>
<div className={styles['column-label']}>{__('Clear', 'font-awesome')}</div>
{size(allDetectedConflicts) > 1 && (
<div className={styles['remove-all-container']}>
<input
id="remove_all_detected_conflicts"
name="remove_all_detected_conflicts"
type="checkbox"
value="all"
checked={allDetectedConflictsSelectedForRemoval}
onChange={() => changeCheckForRemoval('all', allDetectedConflicts)}
className={classnames(sharedStyles['sr-only'], sharedStyles['input-checkbox-custom'])}
/>
<label
htmlFor="remove_all_detected_conflicts"
className={styles['checkbox-label']}
>
<span className={sharedStyles['relative']}>
<FontAwesomeIcon
icon={faCheckSquare}
className={sharedStyles['checked-icon']}
size="lg"
fixedWidth
/>
<FontAwesomeIcon
icon={faSquare}
className={sharedStyles['unchecked-icon']}
size="lg"
fixedWidth
/>
</span>
{__('All', 'font-awesome')}
</label>
</div>
)}
</th>
</tr>
</thead>
<tbody>
{
allDetectedConflicts.map(md5 => (
{allDetectedConflicts.map((md5) => (
<tr key={md5}>
<td>
<input
id={`block_${md5}`}
name={`block_${md5}`}
type="checkbox"
value={ md5 }
checked={ isCheckedForBlocking(md5) }
onChange={ () => changeCheckForBlocking(md5) }
className={ classnames(sharedStyles['sr-only'], sharedStyles['input-checkbox-custom']) }
value={md5}
checked={isCheckedForBlocking(md5)}
onChange={() => changeCheckForBlocking(md5)}
className={classnames(sharedStyles['sr-only'], sharedStyles['input-checkbox-custom'])}
/>
<label htmlFor={`block_${md5}`} className={ styles['checkbox-label'] }>
<span className={ sharedStyles['relative'] }>
<label
htmlFor={`block_${md5}`}
className={styles['checkbox-label']}
>
<span className={sharedStyles['relative']}>
<FontAwesomeIcon
icon={ faCheckSquare }
className={ sharedStyles['checked-icon'] }
icon={faCheckSquare}
className={sharedStyles['checked-icon']}
size="lg"
fixedWidth
/>
<FontAwesomeIcon
icon={ faSquare }
className={ sharedStyles['unchecked-icon'] }
icon={faSquare}
className={sharedStyles['unchecked-icon']}
size="lg"
fixedWidth
/>
</span>
</label>
</td>
<td>{get(unregisteredClients[md5], 'tagName', 'unknown').toLowerCase()}</td>
<td>
{get(unregisteredClients[md5], 'tagName', 'unknown').toLowerCase()}
</td>
<td>
{
unregisteredClients[md5].src
|| unregisteredClients[md5].href
|| createInterpolateElement(
__( '<em>in page source. </em><excerpt/>', 'font-awesome' ),
{
em: <em />,
excerpt: (
( content ) => content
? <>
File starts with: <code>{ content }</code>
</>
: ''
) ( excerpt( get(unregisteredClients[md5], 'innerText') ) )
}
)
}
{unregisteredClients[md5].src ||
unregisteredClients[md5].href ||
createInterpolateElement(__('<em>in page source. </em><excerpt/>', 'font-awesome'), {
em: <em />,
excerpt: ((content) =>
content ? (
<>
File starts with: <code>{content}</code>
</>
) : (
''
))(excerpt(get(unregisteredClients[md5], 'innerText')))
})}
</td>
<td>
<input
id={`remove_${md5}`}
name={`remove_${md5}`}
type="checkbox"
value={ md5 }
checked={ isCheckedForRemoval(md5) }
onChange={ () => changeCheckForRemoval(md5) }
className={ classnames(sharedStyles['sr-only'], sharedStyles['input-checkbox-custom']) }
value={md5}
checked={isCheckedForRemoval(md5)}
onChange={() => changeCheckForRemoval(md5)}
className={classnames(sharedStyles['sr-only'], sharedStyles['input-checkbox-custom'])}
/>
<label htmlFor={`remove_${md5}`} className={ styles['checkbox-label'] }>
<span className={ sharedStyles['relative'] }>
<label
htmlFor={`remove_${md5}`}
className={styles['checkbox-label']}
>
<span className={sharedStyles['relative']}>
<FontAwesomeIcon
icon={ faCheckSquare }
className={ sharedStyles['checked-icon'] }
icon={faCheckSquare}
className={sharedStyles['checked-icon']}
size="lg"
fixedWidth
/>
<FontAwesomeIcon
icon={ faSquare }
className={ sharedStyles['unchecked-icon'] }
icon={faSquare}
className={sharedStyles['unchecked-icon']}
size="lg"
fixedWidth
/>
@ -266,19 +252,21 @@ export default function UnregisteredClientsView() {
</label>
</td>
</tr>
))
}
))}
</tbody>
</table>
</div>
: <div className={ classnames(sharedStyles['explanation'], sharedStyles['flex'], sharedStyles['flex-row'] )}>
) : (
<div className={classnames(sharedStyles['explanation'], sharedStyles['flex'], sharedStyles['flex-row'])}>
<div>
<FontAwesomeIcon icon={ faThumbsUp } size='lg'/>
<FontAwesomeIcon
icon={faThumbsUp}
size="lg"
/>
</div>
<div className={ sharedStyles['space-left'] }>
{ __( 'We haven\'t detected any plugins or themes trying to load Font Awesome.', 'font-awesome' ) }
</div>
</div>
}
</div>
<div className={sharedStyles['space-left']}>{__("We haven't detected any plugins or themes trying to load Font Awesome.", 'font-awesome')}</div>
</div>
)}
</div>
)
}

View File

@ -1,91 +0,0 @@
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faClock, faSpinner, faCheck, faSkull } from '@fortawesome/free-solid-svg-icons'
import { snoozeV3DeprecationWarning } from './store/actions'
import styles from './V3DeprecationWarning.module.css'
import classnames from 'classnames'
import Alert from './Alert'
import { __, sprintf } from '@wordpress/i18n'
import createInterpolateElement from './createInterpolateElement'
export default function V3DeprecationWarning() {
const { snooze, atts, v5name, v5prefix } = useSelector(state => state.v3DeprecationWarning)
const { isSubmitting, hasSubmitted, success } = useSelector(state => state.v3DeprecationWarningStatus)
const dispatch = useDispatch()
if (snooze) return null
return <Alert
title={ __( 'Font Awesome 3 icon names are deprecated', 'font-awesome' ) }
type='warning'
>
<p>
{
createInterpolateElement(
sprintf(
__('Looks like you\'re using an old Font Awesome 3 icon name in your shortcode: <code>%s</code>. We discontinued support for Font Awesome 3 quite some time ago. Won\'t you jump into <a>the newest Font Awesome</a> with us? It\'s way better, and it\'s easy to upgrade.', 'font-awesome' ),
atts.name
),
{
code: <code />,
// eslint-disable-next-line jsx-a11y/anchor-has-content
a: <a rel="noopener noreferrer" target="_blank" href="https://fontawesome.com/" />
}
)
}
</p>
<p>
{ __('Just adjust your shortcode from this:', 'font-awesome' ) }
</p>
<blockquote><code>[icon name="{ atts.name }"]</code></blockquote>
<p>
{ __( 'to this:', 'font-awesome' ) }
</p>
<blockquote><code>[icon name="{ v5name }" prefix="{ v5prefix }"]</code></blockquote>
<p>
{
createInterpolateElement(
__( 'You\'ll need to go adjust any version 3 icon names in [icon] shortcodes in your pages, posts, widgets, templates (or wherever they\'re coming from) to the new format with prefix. You can check the icon names and prefixes in our <linkIconGallery>Icon Gallery</linkIconGallery>. But what\'s that prefix, you ask? We now support a number of different styles for each icon. <linkLearnMore>Learn more</linkLearnMore>', 'font-awesome' ),
{
// eslint-disable-next-line jsx-a11y/anchor-has-content
linkIconGallery: <a rel="noopener noreferrer" target="_blank" href="https://fontawesome.com/icons?d=gallery" />,
// eslint-disable-next-line jsx-a11y/anchor-has-content
linkLearnMore: <a rel="noopener noreferrer" target="_blank" href="https://fontawesome.com/how-to-use/on-the-web/setup/upgrading-from-version-4#changes" />
}
)
}
</p>
<p>
{
createInterpolateElement(
__( 'Once you update your icon shortcodes, this warning will disappear or you could hit snooze to hide it for a while. <strong>But we\'re gonna remove this v3-to-v5 magic soon, though, so don\'t wait forever.</strong>', 'font-awesome' ),
{
strong: <strong />
}
)
}
</p>
<p>
<button disabled={ isSubmitting } onClick={ () => dispatch(snoozeV3DeprecationWarning()) } className={ classnames( styles['snooze-button'], 'button', 'button-primary' ) }>
{
isSubmitting
? <FontAwesomeIcon icon={ faSpinner } spin className={ styles['submitting'] } />
: hasSubmitted
? success
? <FontAwesomeIcon icon={ faCheck } className={ styles['success'] }/>
: <FontAwesomeIcon icon={ faSkull } className={ styles['fail'] }/>
: <FontAwesomeIcon icon={ faClock } className={ styles['snooze'] }/>
}
<span className={ styles['label'] }>{ __( 'Snooze', 'font-awesome' ) }</span>
</button>
</p>
</Alert>
}

View File

@ -1,20 +0,0 @@
.v3-deprecation-warning {
border: 1px solid black;
background-color: #fdfdf3;
padding: 1.5em;
display: inline-block;
}
.snooze-button {
padding: .5rem;
background-color: rgba(0,0,0,0);
border-radius: 5px;
}
.snooze-button:hover {
cursor: pointer;
}
.snooze-button .label {
margin-left: 1em;
}

View File

@ -1,5 +1,5 @@
import get from 'lodash/get'
import set from 'lodash/set'
const get = require('lodash/get')
const set = require('lodash/set')
// NOTE: the Jest docs on manual mocks indicate that mocks for things under
// node_modules should be in a __mocks__ directory that is adjacent to node_modules.
@ -9,10 +9,10 @@ import set from 'lodash/set'
// the root for Jest in such a way that __mocks__ as to live under the src directory.
// See: https://github.com/facebook/create-react-app/issues/7539#issuecomment-531463603
const DEFAULT_INTERCEPTOR = thing => thing
const DEFAULT_PUT = ( url, _data, _config ) => handleRequest( { url, method: 'PUT' } )
const DEFAULT_POST = ( url, _data, _config ) => handleRequest( { url, method: 'POST' } )
const DEFAULT_DELETE = ( url, _data, _config ) => handleRequest( { url, method: 'DELETE' } )
const DEFAULT_INTERCEPTOR = (thing) => thing
const DEFAULT_PUT = (url, _data, _config) => handleRequest({ url, method: 'PUT' })
const DEFAULT_POST = (url, _data, _config) => handleRequest({ url, method: 'POST' })
const DEFAULT_DELETE = (url, _data, _config) => handleRequest({ url, method: 'DELETE' })
let responses = {}
let responseSuccessInterceptor = DEFAULT_INTERCEPTOR
let responseFailureInterceptor = DEFAULT_INTERCEPTOR
@ -33,11 +33,11 @@ const axios = {
axios.create = () => axios
export function respondWith ({ url, method = "GET", response }) {
export function respondWith({ url, method = 'GET', response }) {
responses = set(responses, [url, method.toUpperCase()], response)
}
export function resetAxiosMocks () {
export function resetAxiosMocks() {
responses = {}
axios.put = DEFAULT_PUT
axios.post = DEFAULT_POST
@ -53,28 +53,28 @@ function handleRequest(req) {
const response = get(responses, [url, method.toUpperCase()])
if ( !response ) {
if (!response) {
console.log('No prepared response for:', req) // eslint-disable-line no-console
return Promise.reject()
}
if ( response instanceof XMLHttpRequest ) {
return responseFailureInterceptor( { request: response } )
if (response instanceof XMLHttpRequest) {
return responseFailureInterceptor({ request: response })
}
if ( response instanceof Error ) {
return responseFailureInterceptor( { message: response.message } )
if (response instanceof Error) {
return responseFailureInterceptor({ message: response.message })
}
const status = get( response, 'status' )
const status = get(response, 'status')
// TODO: use axios validateStatus to determine resolve or reject, instead
// of hardcoding the default
if ( response && status && status < 300 ) {
return Promise.resolve( responseSuccessInterceptor( response ) )
if (response && status && status < 300) {
return Promise.resolve(responseSuccessInterceptor(response))
} else {
return responseFailureInterceptor( { response } )
return responseFailureInterceptor({ response })
}
}

View File

@ -1,119 +0,0 @@
import React, { useState } from 'react'
import { Modal } from '@wordpress/components'
import { FaIconChooser } from '@fortawesome/fa-icon-chooser-react'
import { __ } from '@wordpress/i18n'
import createInterpolateElement from '../createInterpolateElement'
const IconChooserModal = (props) => {
const { onSubmit, kitToken, version, pro, handleQuery, modalOpenEvent, getUrlText, settingsPageUrl } = props
const [ isOpen, setOpen ] = useState( false )
document.addEventListener(modalOpenEvent.type, () => setOpen(true))
const closeModal = () => setOpen( false )
const submitAndCloseModal = (result) => {
if('function' === typeof onSubmit) {
onSubmit(result)
}
closeModal()
}
const isProCdn = !!pro && !kitToken
return (
<>
{ isOpen && (
<Modal title="Add a Font Awesome Icon" onRequestClose={ closeModal }>
{
isProCdn &&
<div style={{ margin: '1em', backgroundColor: '#FFD200', padding: '1em', borderRadius: '.5em', fontSize: '15px'}}>
{__( 'Looking for Pro icons and styles? Youll need to use a kit. ', 'font-awesome' ) }
<a href={settingsPageUrl}>{__('Go to Font Awesome Plugin Settings', 'font-awesome')}</a>
</div>
}
<FaIconChooser
version={ version }
kitToken={ kitToken }
handleQuery={ handleQuery }
getUrlText={ getUrlText }
onFinish={ result => submitAndCloseModal(result) }
searchInputPlaceholder={__('Find icons by name, category, or keyword', 'font-awesome')}
>
<span slot='fatal-error-heading'>
{ __('Well, this is awkward...', 'font-awesome') }
</span>
<span slot='fatal-error-detail'>
{ __('Something has gone horribly wrong. Check the console for additional error information.', 'font-awesome') }
</span>
<span slot="start-view-heading">
{__( "Font Awesome is the web's most popular icon set, with tons of icons in a variety of styles.", 'font-awesome' ) }
</span>
<span slot="start-view-detail">
{
createInterpolateElement(
__( "Not sure where to start? Here are some favorites, or try a search for <strong>spinners</strong>, <strong>shopping</strong>, <strong>food</strong>, or <strong>whatever you're looking for</strong>.", 'font-awesome'),
{
strong: <strong/>
}
)
}
</span>
<span slot='search-field-label-free'>
{__('Search Font Awesome Free Icons in Version', 'font-awesome')}
</span>
<span slot='search-field-label-pro'>
{__('Search Font Awesome Pro Icons in Version', 'font-awesome')}
</span>
<span slot='searching-free'>
{__("You're searching Font Awesome Free icons in version", 'font-awesome')}
</span>
<span slot='searching-pro'>
{__("You're searching Font Awesome Pro icons in version", 'font-awesome')}
</span>
<span slot='kit-has-no-uploaded-icons'>
{__('This kit contains no uploaded icons.', 'font-awesome')}
</span>
<span slot='no-search-results-heading'>
{__("Sorry, we couldn't find anything for that.", 'font-awesome')}
</span>
<span slot='no-search-results-detail'>
{__('You might try a different search...', 'font-awesome')}
</span>
<span slot="suggest-icon-upload">
{
createInterpolateElement(
__( 'Or <a>upload your own icon</a> to a Pro kit!', 'font-awesome'),
{
// eslint-disable-next-line jsx-a11y/anchor-has-content
a: <a target="_blank" rel="noopener noreferrer" href="https://fontawesome.com/v5.15/how-to-use/on-the-web/using-kits/uploading-icons" />
}
)
}
</span>
<span slot='get-fontawesome-pro'>
{
createInterpolateElement(
__( 'Or <a>use Font Awesome Pro</a> for more icons and styles!', 'font-awesome'),
{
// eslint-disable-next-line jsx-a11y/anchor-has-content
a: <a target="_blank" rel="noopener noreferrer" href="https://fontawesome.com/" />
}
)
}
</span>
<span slot='initial-loading-view-heading'>
{__('Fetching icons', 'font-awesome') }
</span>
<span slot='initial-loading-view-detail'>
{__('When this thing gets up to 88 mph...', 'font-awesome')}
</span>
</FaIconChooser>
</Modal>
) }
</>
)
}
export default IconChooserModal

View File

@ -1,87 +0,0 @@
import IconChooserModal from './IconChooserModal'
import { buildShortCodeFromIconChooserResult } from './shortcode'
import { Fragment, Component } from '@wordpress/element'
import { SVG, Path } from '@wordpress/components'
import { __ } from '@wordpress/i18n'
import { insert, registerFormatType } from '@wordpress/rich-text'
import { RichTextToolbarButton } from '@wordpress/block-editor'
export function setupBlockEditor (params) {
// TODO: is this the right block type name for what we're doing here?
const name = 'font-awesome/icon'
const title = __('Font Awesome Icon')
const {
modalOpenEvent,
kitToken,
version,
pro,
handleQuery,
getUrlText,
settingsPageUrl
} = params
registerFormatType(name, {
name,
title: __( 'Font Awesome Icon' ),
keywords: [ __( 'icon' ), __( 'font awesome' ) ],
tagName: 'i',
className: null,
// if object is true, then the HTML rendered for this type will lack a closing tag.
object: false,
edit: class FontAwesomeIconEdit extends Component {
constructor(props) {
super( ...arguments )
this.handleFormatButtonClick = this.handleFormatButtonClick.bind(this)
this.handleSelect = this.handleSelect.bind(this)
}
handleFormatButtonClick() {
document.dispatchEvent(modalOpenEvent)
}
handleSelect(event){
const { value, onChange } = this.props
// TODO: this would indicate an invalid event. Do we want some error handling here?
if(!event.detail) return
const shortcode = buildShortCodeFromIconChooserResult(event.detail)
onChange( insert( value, shortcode ) )
}
render() {
return (
<Fragment>
<RichTextToolbarButton
icon={
<SVG
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512"
className="svg-inline--fa fa-font-awesome fa-w-14">
<Path
fill="currentColor"
d="M397.8 32H50.2C22.7 32 0 54.7 0 82.2v347.6C0 457.3 22.7 480 50.2 480h347.6c27.5 0 50.2-22.7 50.2-50.2V82.2c0-27.5-22.7-50.2-50.2-50.2zm-45.4 284.3c0 4.2-3.6 6-7.8 7.8-16.7 7.2-34.6 13.7-53.8 13.7-26.9 0-39.4-16.7-71.7-16.7-23.3 0-47.8 8.4-67.5 17.3-1.2.6-2.4.6-3.6 1.2V385c0 1.8 0 3.6-.6 4.8v1.2c-2.4 8.4-10.2 14.3-19.1 14.3-11.3 0-20.3-9-20.3-20.3V166.4c-7.8-6-13.1-15.5-13.1-26.3 0-18.5 14.9-33.5 33.5-33.5 18.5 0 33.5 14.9 33.5 33.5 0 10.8-4.8 20.3-13.1 26.3v18.5c1.8-.6 3.6-1.2 5.4-2.4 18.5-7.8 40.6-14.3 61.5-14.3 22.7 0 40.6 6 60.9 13.7 4.2 1.8 8.4 2.4 13.1 2.4 22.7 0 47.8-16.1 53.8-16.1 4.8 0 9 3.6 9 7.8v140.3z"
/>
</SVG>
}
title={ title }
onClick={ this.handleFormatButtonClick }
/>
<IconChooserModal
modalOpenEvent={ modalOpenEvent }
kitToken={ kitToken }
version={ version }
pro={ pro }
settingsPageUrl={ settingsPageUrl }
handleQuery={ handleQuery }
onSubmit={ this.handleSelect }
getUrlText={ getUrlText }
/>
</Fragment>
)
}
}
})
}

View File

@ -1,69 +0,0 @@
import React from 'react'
import ReactDOM from 'react-dom'
import IconChooserModal from './IconChooserModal'
import { buildShortCodeFromIconChooserResult } from './shortcode'
import get from 'lodash/get'
export function handleSubmit(event) {
const insert = get(window, 'wp.media.editor.insert')
insert && insert( buildShortCodeFromIconChooserResult(event.detail) )
}
export function setupClassicEditor(params) {
const {
iconChooserContainerId,
modalOpenEvent,
kitToken,
version,
pro,
handleQuery,
getUrlText,
settingsPageUrl
} = params
const container = document.querySelector(`#${iconChooserContainerId}`)
if(!container) return
if(!window.tinymce) return
let wpComponentsStyleAdded = false
if(!wpComponentsStyleAdded) {
wpComponentsStyleAdded = true
import('@wordpress/components/build-style/style.css')
.then(() => {})
.catch(err =>
console.error(
'Font Awesome Plugin failed to load styles for the Icon Chooser in the Classic Editor',
err
)
)
}
// TODO: consider how to add Font Awesome to the Tiny MCE visual pane.
// But there maybe unexpected behaviors.
/*
const editor = tinymce.activeEditor
editor.on('init', e => {
const script = editor.dom.doc.createElement('script')
script.setAttribute('src', 'https://kit.fontawesome.com/fakekit.js')
script.setAttribute('crossorigin', 'anonymous')
editor.dom.doc.head.appendChild(script)
})
*/
ReactDOM.render(
<IconChooserModal
kitToken={ kitToken }
version={ version }
pro={ pro }
modalOpenEvent={ modalOpenEvent }
handleQuery={ handleQuery }
settingsPageUrl={ settingsPageUrl }
onSubmit={ handleSubmit }
getUrlText={ getUrlText }
/>,
container
)
}

View File

@ -1,33 +0,0 @@
import apiFetch from '@wordpress/api-fetch'
const configureQueryHandler = params => async (query, variables) => {
try {
const { apiNonce, rootUrl, restApiNamespace } = params
// If apiFetch is from wp.apiFetch, it may already have RootURLMiddleware set up.
// If we're using the fallback (i.e. when running in the Classic Editor), then
// it doesn't yet have thr RootURLMiddleware.
// We want to guarantee that it's there, so we'll always add it.
// So what if it was already there? Experiment seems to have shown that this
// is idempotent. It doesn't seem to hurt to just do it again, so we will.
apiFetch.use( apiFetch.createRootURLMiddleware( rootUrl ) )
// We need the nonce to be set up because we're going to run our query through
// the API controller end point, which requires non-public authorization.
apiFetch.use( apiFetch.createNonceMiddleware( apiNonce ) )
return await apiFetch( {
path: `${restApiNamespace}/api`,
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({ query: query.replace(/\s+/g, " "), variables })
} )
} catch( error ) {
console.error('CAUGHT:', error)
throw new Error(error)
}
}
export default configureQueryHandler

View File

@ -1,51 +0,0 @@
import get from 'lodash/get'
import { setupBlockEditor } from './blockEditor'
import { setupClassicEditor } from './classicEditor'
let classicEditorSetupComplete = false
function setupClassicEditorIconChooser(initialParams) {
// We only want to do this once.
if(classicEditorSetupComplete) return
if(!window.tinymce) return
const params = {
...initialParams,
iconChooserContainerId: 'font-awesome-icon-chooser-container',
iconChooserMediaButtonClass: 'font-awesome-icon-chooser-media-button'
}
setupClassicEditor(params)
classicEditorSetupComplete = true
}
export function setupIconChooser(initialParams) {
const params = {
...initialParams,
modalOpenEvent: new Event('fontAwesomeIconChooserOpen', { "bubbles": true, "cancelable": false })
}
window['__FontAwesomeOfficialPlugin__openIconChooserModal'] = () => {
document.dispatchEvent(params.modalOpenEvent)
}
if( !! get(initialParams, 'isGutenbergPage') ) {
setupBlockEditor(params)
}
/**
* Tiny MCE loading time: In WordPress 5, it's straightforward to enqueue
* this script with a script dependency of wp-tinymce. But that's not available
* in WP 4, and there doesn't seem to be any way to ensure that the Tiny MCE
* script has been loaded before this, other than to add a script after the
* Tiny MCE scripts have been printed.
*
* So what we'll do instead is simply export this function that can be exposed
* as a global function, and in our back end PHP code, we'll add an inline script
* to invoke that global for tinyMCE setup if and when it is necessary.
*/
return {
setupClassicEditorIconChooser: () => setupClassicEditorIconChooser(params)
}
}

1
admin/src/constants.js Normal file
View File

@ -0,0 +1 @@
export const GLOBAL_KEY = '__FontAwesomeOfficialPlugin__'

View File

@ -1,9 +1,5 @@
import {
__experimentalCreateInterpolateElement,
createInterpolateElement as stableCreateInterpolateElement,
} from "@wordpress/element";
import { __experimentalCreateInterpolateElement, createInterpolateElement as stableCreateInterpolateElement } from '@wordpress/element'
const createInterpolateElement = stableCreateInterpolateElement ||
__experimentalCreateInterpolateElement;
const createInterpolateElement = stableCreateInterpolateElement || __experimentalCreateInterpolateElement
export default createInterpolateElement;
export default createInterpolateElement

View File

@ -1,9 +1,11 @@
import { createStore } from './store'
import get from 'lodash/get'
import { get, set } from 'lodash'
import { GLOBAL_KEY } from './constants'
import createInterpolateElement from './createInterpolateElement'
import { __ } from '@wordpress/i18n'
const initialData = window['__FontAwesomeOfficialPlugin__']
const initialData = window[GLOBAL_KEY]
// See: https://webpack.js.org/guides/public-path/#on-the-fly
__webpack_public_path__ = get(initialData, 'webpackPublicPath')
const CONFLICT_DETECTION_REPORT_EVENT_TYPE = 'fontAwesomeConflictDetectionReport'
/**
* This will start out as falsy, when there's a report, we'll set it with those
@ -15,8 +17,8 @@ const CONFLICT_DETECTION_REPORT_EVENT_TYPE = 'fontAwesomeConflictDetectionReport
*/
let conflictDetectionReport = null
if( get(initialData, 'showConflictDetectionReporter') ) {
const reportEvent = new Event(CONFLICT_DETECTION_REPORT_EVENT_TYPE, { "bubbles": true, "cancelable": false })
if (get(initialData, 'showConflictDetectionReporter')) {
const reportEvent = new Event(CONFLICT_DETECTION_REPORT_EVENT_TYPE, { bubbles: true, cancelable: false })
/**
* If we're doing conflict detection, we must set this up before DOMContentLoaded,
@ -24,127 +26,51 @@ if( get(initialData, 'showConflictDetectionReporter') ) {
*/
window.FontAwesomeDetection = {
...(window.FontAwesomeDetection || {}),
report: params => {
report: (params) => {
conflictDetectionReport = params
document.dispatchEvent(reportEvent)
}
}
}
/**
* First, we need to resolve whether we're using external dependencies available
* in WordPress 5 core, or whether we've loaded our WordPress 4 compatibility
* bundle. Regardless, the webpack config will use this global for settting up
* externals.
*/
if( !window.__Font_Awesome_Webpack_Externals__ ) {
window.__Font_Awesome_Webpack_Externals__ = {
React: get(window, 'React'),
ReactDOM: get(window, 'ReactDOM'),
i18n: get(window, 'wp.i18n'),
apiFetch: get(window, 'wp.apiFetch'),
components: get(window, 'wp.components'),
element: get(window, 'wp.element'),
richText: get(window, 'wp.richText'),
blockEditor: get(window, 'wp.blockEditor'),
domReady: get(window, 'wp.domReady')
}
}
const { __ } = __Font_Awesome_Webpack_Externals__.i18n
if(! initialData){
console.error( __( 'Font Awesome plugin is broken: initial state data missing.', 'font-awesome' ) )
if (!initialData) {
console.error(__('Font Awesome plugin is broken: initial state data missing.', 'font-awesome'))
}
const store = createStore(initialData)
const {
showAdmin,
showConflictDetectionReporter,
enableIconChooser,
usingCompatJs,
isGutenbergPage
} = store.getState()
set(window, [GLOBAL_KEY, 'createInterpolateElement'], createInterpolateElement)
if( showAdmin ) {
const { showAdmin, showConflictDetectionReporter } = store.getState()
if (showAdmin) {
import('./mountAdminView')
.then(({ default: mountAdminView }) => {
mountAdminView(store)
})
.catch(error => {
console.error( __( 'Font Awesome plugin error when initializing admin settings view', 'font-awesome' ), error )
})
.then(({ default: mountAdminView }) => {
mountAdminView(store)
})
.catch((error) => {
console.error(__('Font Awesome plugin error when initializing admin settings view', 'font-awesome'), error)
})
}
if( showConflictDetectionReporter ) {
Promise.all([
import('./store/actions'),
import('./mountConflictDetectionReporter')
])
.then(([{ reportDetectedConflicts }, { mountConflictDetectionReporter }]) => {
const report = params => store.dispatch(reportDetectedConflicts(params))
/**
* If the conflict detection report is already available, just use it;
* otherwise, listen for the reporting event.
*/
if( conflictDetectionReport ) {
report(conflictDetectionReport)
} else {
document.addEventListener(
CONFLICT_DETECTION_REPORT_EVENT_TYPE,
_event => report(conflictDetectionReport)
)
}
mountConflictDetectionReporter(store)
})
.catch(error => {
console.error( __( 'Font Awesome plugin error when initializing conflict detection scanner', 'font-awesome' ), error )
})
}
if ( enableIconChooser ) {
if ( usingCompatJs && isGutenbergPage ) {
console.warn( __( 'Font Awesome Plugin cannot enable the Icon Chooser on a page that includes the block editor (Gutenberg) because it is not compatible with your WordPress installation. Upgrading to at least WordPress 5.4.6 will probably resolve this.', 'font-awesome' ) )
} else {
Promise.all([
import('./chooser'),
import('./chooser/handleQuery'),
import('./chooser/getUrlText')
])
.then(([{ setupIconChooser }, { default: configureQueryHandler }, { default: getUrlText } ]) => {
const kitToken = get(initialData, 'options.kitToken')
const version = get(initialData, 'options.version')
const params = {
...initialData,
kitToken,
version,
getUrlText,
pro: get(initialData, 'options.usePro')
}
const handleQuery = configureQueryHandler(params)
const { setupClassicEditorIconChooser } = setupIconChooser({ ...params, handleQuery })
if (showConflictDetectionReporter) {
Promise.all([import('./store/actions'), import('./mountConflictDetectionReporter')])
.then(([{ reportDetectedConflicts }, { mountConflictDetectionReporter }]) => {
const report = (params) => store.dispatch(reportDetectedConflicts(params))
/**
* Tiny MCE will probably be loaded later, but since this code runs async,
* we can't guarantee the timing. So if this runs first, it will set this
* global to a function that the post-tiny-mce inline code can invoke.
* But if that code runs first, it will set this global to some truthy value,
* which tells us to invoke this setup immediately.
* If the conflict detection report is already available, just use it;
* otherwise, listen for the reporting event.
*/
if( window['__FontAwesomeOfficialPlugin__setupClassicEditorIconChooser'] ) {
setupClassicEditorIconChooser()
if (conflictDetectionReport) {
report(conflictDetectionReport)
} else {
window['__FontAwesomeOfficialPlugin__setupClassicEditorIconChooser'] = setupClassicEditorIconChooser
document.addEventListener(CONFLICT_DETECTION_REPORT_EVENT_TYPE, (_event) => report(conflictDetectionReport))
}
mountConflictDetectionReporter(store)
})
.catch(error => {
console.error( __( 'Font Awesome plugin error when initializing Icon Chooser', 'font-awesome' ), error )
.catch((error) => {
console.error(__('Font Awesome plugin error when initializing conflict detection scanner', 'font-awesome'), error)
})
}
}

View File

@ -1,19 +1,20 @@
import React from 'react'
import ReactDOM from 'react-dom'
import { createRoot } from 'react-dom/client'
import ErrorBoundary from './ErrorBoundary'
import FontAwesomeAdminView from './FontAwesomeAdminView'
import { Provider } from 'react-redux'
import domReady from '@wordpress/dom-ready'
export default function(store) {
const root = createRoot(document.getElementById('font-awesome-admin'))
export default function (store) {
domReady(() =>
ReactDOM.render(
root.render(
<ErrorBoundary>
<Provider store={ store }>
<FontAwesomeAdminView/>
<Provider store={store}>
<FontAwesomeAdminView />
</Provider>
</ErrorBoundary>,
document.getElementById('font-awesome-admin')
</ErrorBoundary>
)
)
}

View File

@ -1,5 +1,5 @@
import React from 'react'
import ReactDOM from 'react-dom'
import { createRoot } from 'react-dom/client'
import ConflictDetectionReporter from './ConflictDetectionReporter'
import { dom } from '@fortawesome/fontawesome-svg-core'
import { Provider } from 'react-redux'
@ -24,22 +24,22 @@ export function mountConflictDetectionReporter(store) {
faStyle.appendChild(cssText)
const shadowContainer = document.createElement('DIV')
const root = createRoot(shadowContainer)
shadow.appendChild(faStyle)
shadow.appendChild(shadowContainer)
ReactDOM.render(
<Provider store={ store }>
root.render(
<Provider store={store}>
<ConflictDetectionReporter />
</Provider>,
shadowContainer
</Provider>
)
})
}
export function isConflictDetectionReporterMounted() {
const shadowHost = document.getElementById(CONFLICT_DETECTION_SHADOW_HOST_ID)
if(! shadowHost ) return false
if (!shadowHost) return false
return !!shadowHost.shadowRoot
}

View File

@ -1,13 +1,13 @@
import { test as setup, expect } from '@wordpress/e2e-test-utils-playwright'
import '../support/env.js'
const authFile = 'src/playwright/.auth/state.json';
const authFile = 'src/playwright/.auth/state.json'
setup('authenticate', async ({ page }) => {
await page.goto('/wp-login.php');
await page.getByLabel('Username or Email Address').fill(process.env.WP_ADMIN_USERNAME);
await page.getByLabel('Password', { exact: true }).fill(process.env.WP_ADMIN_PASSWORD);
await page.getByRole('button', { name: 'Log In'} ).click();
await page.waitForURL('**/wp-admin/');
await page.context().storageState({ path: authFile });
await page.goto('/wp-login.php')
await page.getByLabel('Username or Email Address').fill(process.env.WP_ADMIN_USERNAME)
await page.getByLabel('Password', { exact: true }).fill(process.env.WP_ADMIN_PASSWORD)
await page.getByRole('button', { name: 'Log In' }).click()
await page.waitForURL('**/wp-admin/')
await page.context().storageState({ path: authFile })
})

View File

@ -5,8 +5,8 @@ const CONFIG_ROUTE_PATTERN = '**/font-awesome/v1/config'
const API_ROUTE_PATTERN = '**/font-awesome/v1/api*'
setup('pro kit', async ({ page }) => {
expect(process.env.API_TOKEN).toBeTruthy();
expect(process.env.KIT_TOKEN).toBeTruthy();
expect(process.env.API_TOKEN).toBeTruthy()
expect(process.env.KIT_TOKEN).toBeTruthy()
await page.goto('/wp-admin/admin.php?page=font-awesome')
await page.locator('label').filter({ hasText: 'Use A Kit' }).click()
@ -20,14 +20,13 @@ setup('pro kit', async ({ page }) => {
const kitsResponsePromise = page.waitForResponse(API_ROUTE_PATTERN)
await page.getByRole('button').filter({hasText: 'kits data'}).click()
await page.getByRole('button').filter({ hasText: 'kits data' }).click()
await kitsResponsePromise
await page.locator('select').selectOption(process.env.KIT_TOKEN)
const saveSettingsResponsePromise = page.waitForResponse(CONFIG_ROUTE_PATTERN)
await page.getByRole('button').filter({hasText: 'Save Changes'}).click()
await page.getByRole('button').filter({ hasText: 'Save Changes' }).click()
await saveSettingsResponsePromise
})

View File

@ -3,20 +3,20 @@ import mysql from 'mysql2/promise'
import { prepareRestApi } from '../support/testHelpers'
setup('reset', async ({ storageState, baseURL }) => {
const {requestUtils, requestContext} = await prepareRestApi({ storageState, baseURL })
await requestUtils.deactivatePlugin('font-awesome')
const { requestUtils, requestContext } = await prepareRestApi({ storageState, baseURL })
await requestUtils.deactivatePlugin('font-awesome')
const connection = await mysql.createConnection({
host: 'localhost',
user: process.env.WORDPRESS_DB_USER,
password: process.env.WORDPRESS_DB_PASSWORD,
database: process.env.WORDPRESS_DB_NAME,
database: process.env.WORDPRESS_DB_NAME
})
const sql = 'DELETE FROM `wp_options` WHERE `option_name` = ? LIMIT 1';
await connection.execute(sql, ['font-awesome']);
await connection.execute(sql, ['font-awesome-conflict-detection']);
await connection.execute(sql, ['font-awesome-releases']);
await requestUtils.activatePlugin('font-awesome')
await requestContext.dispose();
const sql = 'DELETE FROM `wp_options` WHERE `option_name` = ? LIMIT 1'
await connection.execute(sql, ['font-awesome'])
await connection.execute(sql, ['font-awesome-conflict-detection'])
await connection.execute(sql, ['font-awesome-releases'])
await requestUtils.activatePlugin('font-awesome')
await requestContext.dispose()
})

View File

@ -1,19 +1,18 @@
import { request } from '@playwright/test';
import { request } from '@playwright/test'
import { RequestUtils } from '@wordpress/e2e-test-utils-playwright'
export async function prepareRestApi({baseURL, storageState}) {
const requestContext = await request.newContext( {
baseURL,
} );
export async function prepareRestApi({ baseURL, storageState }) {
const requestContext = await request.newContext({
baseURL
})
const storageStatePath =
typeof storageState === 'string' ? storageState : undefined;
const storageStatePath = typeof storageState === 'string' ? storageState : undefined
const requestUtils = new RequestUtils( requestContext, {
storageStatePath,
} );
const requestUtils = new RequestUtils(requestContext, {
storageStatePath
})
await requestUtils.setupRest()
await requestUtils.setupRest()
return {requestUtils, requestContext}
return { requestUtils, requestContext }
}

View File

@ -1,7 +1,4 @@
import {
expect,
test,
} from "@wordpress/e2e-test-utils-playwright";
import { expect, test } from '@wordpress/e2e-test-utils-playwright'
import { prepareRestApi } from '../support/testHelpers'
const QUERY = 'query { search(version: "6.x", query: "coffee", first: 1) { id } }'
@ -15,23 +12,23 @@ const QUERY = 'query { search(version: "6.x", query: "coffee", first: 1) { id }
// MIME type is known to result in a 403 due to the OWASP default core ruleset
// as of OWASP 4.3.0.
test('query as plain text', async ({ storageState, baseURL }) => {
expect(process.env.ENABLE_MOD_SECURITY).toEqual("false")
expect(process.env.ENABLE_MOD_SECURITY).toEqual('false')
const {requestUtils, requestContext} = await prepareRestApi({ storageState, baseURL })
const { requestUtils, requestContext } = await prepareRestApi({ storageState, baseURL })
const url = `http://${process.env.WP_DOMAIN}/wp-json/font-awesome/v1/api?_locale=user`;
const url = `http://${process.env.WP_DOMAIN}/wp-json/font-awesome/v1/api?_locale=user`
const response = await requestUtils.request.fetch(url, {
method: 'POST',
data: QUERY,
headers: {
'X-WP-Nonce': requestUtils.storageState.nonce
}
});
const response = await requestUtils.request.fetch(url, {
method: 'POST',
data: QUERY,
headers: {
'X-WP-Nonce': requestUtils.storageState.nonce
}
})
expect(response.status()).toEqual(200);
expect(response.status()).toEqual(200)
const responseObj = await response.json();
const responseObj = await response.json()
expect(responseObj).toHaveProperty('data.search')
expect(responseObj).toHaveProperty('data.search')
})

View File

@ -1,22 +1,15 @@
import {
expect,
test,
} from "@wordpress/e2e-test-utils-playwright";
import { expect, test } from '@wordpress/e2e-test-utils-playwright'
test("change technology", async ({ page }) => {
await page.goto("/wp-admin/admin.php?page=font-awesome");
test('change technology', async ({ page }) => {
await page.goto('/wp-admin/admin.php?page=font-awesome')
const preferenceCheckResponsePromise = page.waitForResponse(
"**/font-awesome/v1/preference-check",
);
const preferenceCheckResponsePromise = page.waitForResponse('**/font-awesome/v1/preference-check')
await page.getByText('SVG').click();
await page.getByText('SVG').click()
await preferenceCheckResponsePromise;
await preferenceCheckResponsePromise
const saveChangesResponsePromise = page.waitForResponse(
"**/font-awesome/v1/config",
);
await page.getByRole("button", { name: "Save Changes" }).click();
await saveChangesResponsePromise;
});
const saveChangesResponsePromise = page.waitForResponse('**/font-awesome/v1/config')
await page.getByRole('button', { name: 'Save Changes' }).click()
await saveChangesResponsePromise
})

View File

@ -1,51 +1,43 @@
import {
Editor,
expect,
test,
} from "@wordpress/e2e-test-utils-playwright";
import { Editor, expect, test } from '@wordpress/e2e-test-utils-playwright'
test.describe('full site editor', async () => {
test.use( {
editor: async ( { page }, use ) => {
await use( new Editor( { page } ) )
},
} )
test.use({
editor: async ({ page }, use) => {
await use(new Editor({ page }))
}
})
test("insert with icon chooser", async ({ page, editor,pageUtils }) => {
const pageLoadPromise = page.waitForResponse(
'**/wp/v2/pages*'
);
test('insert with icon chooser', async ({ page, editor, pageUtils }) => {
const pageLoadPromise = page.waitForResponse('**/wp/v2/pages*')
await page.goto("/wp-admin/site-editor.php?canvas=edit");
await page.goto('/wp-admin/site-editor.php?canvas=edit')
await pageLoadPromise;
await pageLoadPromise
const getStartedCount = await page.getByRole('button', { name: 'Get started' }).count();
const getStartedCount = await page.getByRole('button', { name: 'Get started' }).count()
if (getStartedCount > 0) {
await page.getByRole('button', { name: 'Get started' }).click();
await page.getByRole('button', { name: 'Get started' }).click()
}
await editor.insertBlock( {
name: 'core/paragraph',
} );
await page.keyboard.type( 'Here comes an icon: ' )
await editor.insertBlock({
name: 'core/paragraph'
})
await page.keyboard.type('Here comes an icon: ')
await editor.clickBlockToolbarButton( 'More' )
await editor.clickBlockToolbarButton('More')
await pageUtils.pressKeys('Enter', 1)
await pageUtils.pressKeys('Enter', 1)
await page.waitForSelector( 'fa-icon-chooser input#search' );
await page.waitForSelector('fa-icon-chooser input#search')
const searchResponsePromise = page.waitForResponse(
'**/font-awesome/v1/api*'
);
const searchResponsePromise = page.waitForResponse('**/font-awesome/v1/api*')
await page.locator( 'fa-icon-chooser input#search' ).fill('coffee')
await page.locator('fa-icon-chooser input#search').fill('coffee')
await searchResponsePromise
await page.locator( 'fa-icon-chooser button.icon' ).first().click()
await page.locator('fa-icon-chooser button.icon').first().click()
let blocks = null
@ -56,7 +48,6 @@ test.describe('full site editor', async () => {
blocks = await editor.getBlocks()
expect(blocks).toHaveLength(1)
expect(blocks[0].attributes.content).toMatch(/\[icon.*?\]$/)
} catch(_e) {}
});
});
} catch (_e) {}
})
})

View File

@ -1,37 +1,35 @@
import { Editor, test, expect, login, RequestUtils } from '@wordpress/e2e-test-utils-playwright'
test.describe( 'blockEditorIconChooser', async () => {
test.beforeEach( async ( { admin } ) => {
await admin.createNewPost()
} )
test.describe('blockEditorIconChooser', async () => {
test.beforeEach(async ({ admin }) => {
await admin.createNewPost()
})
test.use( {
editor: async ( { page }, use ) => {
await use( new Editor( { page } ) )
},
} )
test.use({
editor: async ({ page }, use) => {
await use(new Editor({ page }))
}
})
test('search and select from icon chooser', async ({ editor, page, pageUtils }) => {
await editor.insertBlock( {
name: 'core/paragraph',
} );
await page.keyboard.type( 'Here comes an icon: ' )
await editor.insertBlock({
name: 'core/paragraph'
})
await page.keyboard.type('Here comes an icon: ')
await editor.clickBlockToolbarButton( 'More' )
await editor.clickBlockToolbarButton('More')
await pageUtils.pressKeys('Enter', 1)
await pageUtils.pressKeys('Enter', 1)
await page.waitForSelector( 'fa-icon-chooser input#search' );
await page.waitForSelector('fa-icon-chooser input#search')
const searchResponsePromise = page.waitForResponse(
'**/font-awesome/v1/api*'
);
const searchResponsePromise = page.waitForResponse('**/font-awesome/v1/api*')
await page.locator( 'fa-icon-chooser input#search' ).fill('coffee')
await page.locator('fa-icon-chooser input#search').fill('coffee')
await searchResponsePromise
await page.locator( 'fa-icon-chooser button.icon' ).first().click()
await page.locator('fa-icon-chooser button.icon').first().click()
let blocks = null
@ -42,13 +40,9 @@ test.describe( 'blockEditorIconChooser', async () => {
blocks = await editor.getBlocks()
expect(blocks).toHaveLength(1)
expect(blocks[0].attributes.content).toMatch(/\[icon.*?\]$/)
} catch(_e) {}
} catch (_e) {}
// The loading of the icon chooser should not have messed up globals.
// It could create problems for other plugins that depend on them.
await expect(page.evaluate(() => _.VERSION == __originalsBeforeFontAwesome._.VERSION)).toBeTruthy();
await expect(page.evaluate(() => React.version == __originalsBeforeFontAwesome.React.version)).toBeTruthy();
await expect(page.evaluate(() => ReactDOM.version == __originalsBeforeFontAwesome.ReactDOM.version)).toBeTruthy();
await expect(page.evaluate(() => moment.version == __originalsBeforeFontAwesome.moment.version)).toBeTruthy();
// The loading of the icon chooser should not have messed up the lodash globals.
await expect(page.evaluate(() => 'undefined' !== typeof _ && 'undefined' !== typeof lodash)).toBeTruthy()
})
} )
})

View File

@ -1,62 +1,41 @@
import {
expect,
test,
} from "@wordpress/e2e-test-utils-playwright";
import { expect, test } from '@wordpress/e2e-test-utils-playwright'
test.describe("conflictScanner", async () => {
test.describe('conflictScanner', async () => {
test.beforeEach(async ({ requestUtils }) => {
await requestUtils.activatePlugin("plugin-gamma");
});
await requestUtils.activatePlugin('plugin-gamma')
})
test.afterEach(async ({ requestUtils }) => {
await requestUtils.deactivatePlugin("plugin-gamma");
});
await requestUtils.deactivatePlugin('plugin-gamma')
})
test("start conflict detection scanner, detect, and block", async ({ page }) => {
await page.goto("/wp-admin/admin.php?page=font-awesome");
await page.getByRole("button", { name: "Troubleshoot" }).click();
const scannerStartResponsePromise = page.waitForResponse(
"**/font-awesome/v1/conflict-detection/until",
);
await page.getByRole("button", { name: "Enable scanner for 10 minutes" })
.click();
await scannerStartResponsePromise;
test('start conflict detection scanner, detect, and block', async ({ page }) => {
await page.goto('/wp-admin/admin.php?page=font-awesome')
await page.getByRole('button', { name: 'Troubleshoot' }).click()
const scannerStartResponsePromise = page.waitForResponse('**/font-awesome/v1/conflict-detection/until')
await page.getByRole('button', { name: 'Enable scanner for 10 minutes' }).click()
await scannerStartResponsePromise
await expect(
page.getByRole("heading", { name: "Font Awesome Conflict Scanner" }),
).toBeVisible();
await expect(page.getByRole('heading', { name: 'Font Awesome Conflict Scanner' })).toBeVisible()
await expect(
page.locator("span").filter({ hasText: "minutes left to browse" })
.first(),
).toBeVisible();
await expect(page.locator('span').filter({ hasText: 'minutes left to browse' }).first()).toBeVisible()
const scannerReportResponsePromise = page.waitForResponse(
"**/font-awesome/v1/conflict-detection/conflicts",
);
await page.goto("/");
await expect(page.getByRole("heading", { name: "Plugin Gamma" }))
.toBeVisible();
const scannerReportResponsePromise = page.waitForResponse('**/font-awesome/v1/conflict-detection/conflicts')
await page.goto('/')
await expect(page.getByRole('heading', { name: 'Plugin Gamma' })).toBeVisible()
await scannerReportResponsePromise;
await expect(
page.getByRole("heading", { name: "Font Awesome Conflict Scanner" }),
).toBeVisible();
await scannerReportResponsePromise
await expect(page.getByRole('heading', { name: 'Font Awesome Conflict Scanner' })).toBeVisible()
await expect(page.getByText("Page scan complete")).toBeVisible();
await expect(page.getByText("1 new conflicts found on this page"))
.toBeVisible();
await expect(page.getByText("1 total found")).toBeVisible();
await page.getByRole("link", { name: "manage" }).click();
await page.getByRole("row", { name: "link https://cdn.jsdelivr.net" })
.locator("svg").nth(1).click();
const saveChangesResponsePromise = page.waitForResponse(
"**/font-awesome/v1/conflict-detection/conflicts/blocklist",
);
await page.getByRole("button", { name: "Save Changes" }).click();
await saveChangesResponsePromise;
await page.goto("/");
await expect(page.getByText("0 new conflicts found on this page"))
.toBeVisible();
});
});
await expect(page.getByText('Page scan complete')).toBeVisible()
await expect(page.getByText('1 new conflicts found on this page')).toBeVisible()
await expect(page.getByText('1 total found')).toBeVisible()
await page.getByRole('link', { name: 'manage' }).click()
await page.getByRole('row', { name: 'link https://cdn.jsdelivr.net' }).locator('svg').nth(1).click()
const saveChangesResponsePromise = page.waitForResponse('**/font-awesome/v1/conflict-detection/conflicts/blocklist')
await page.getByRole('button', { name: 'Save Changes' }).click()
await saveChangesResponsePromise
await page.goto('/')
await expect(page.getByText('0 new conflicts found on this page')).toBeVisible()
})
})

View File

@ -0,0 +1,72 @@
const CACHE_KEY_PREFIX = 'wp-font-awesome-cache'
function buildPrefixedKey(key) {
return `${CACHE_KEY_PREFIX}-${key}`
}
// This removes all items in localStorage whose keys begin
// with this modules CACHE_KEY_PREFIX.
export function clearQueryCache() {
if(!window?.localStorage) return
if(localStorage.length == 0) return
for(let i=localStorage.length - 1; i>=0; i--) {
const key = localStorage.key(i)
if(key.startsWith(CACHE_KEY_PREFIX)) {
localStorage.removeItem(key)
}
}
}
export function remove(prefixedCacheKey) {
if('function' !== typeof window?.localStorage?.removeItem) return
localStorage.removeItem(prefixedCacheKey)
}
// Takes a cache that is *not* prefixed.
// Expects to be able to add its prefix, and get an item from localStorage
// which must be JSON-parseable.
//
// On success, returns the result of the cache value after JSON.parse().
// On failure, returns undefined: if there's no matching key, or an error
// in JSON parsing.
export function get(key) {
if('function' !== typeof window?.localStorage?.getItem) return
const prefixedCacheKey = buildPrefixedKey(key)
const cacheValueJson = localStorage.getItem(prefixedCacheKey)
try {
const cacheValue = JSON.parse(cacheValueJson)
if(cacheValue) {
return cacheValue
} else {
return
}
} catch {
remove(prefixedCacheKey)
return
}
}
// Takes a key that has not been prefixed, and a value that must be passable
// as an argument to JSON.stringify().
//
// Prefixes the key, and stores the JSON-stringified value in localStorage.
//
// Always returns undefined.
export function set(key, value) {
if('function' !== typeof window?.localStorage?.setItem) return
const prefixedCacheKey = buildPrefixedKey(key)
try {
const valueJson = JSON.stringify(value)
localStorage.setItem(prefixedCacheKey, valueJson)
} catch {
return
}
}

View File

@ -1,13 +1,13 @@
const reportWebVitals = onPerfEntry => {
const reportWebVitals = (onPerfEntry) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
getCLS(onPerfEntry)
getFID(onPerfEntry)
getFCP(onPerfEntry)
getLCP(onPerfEntry)
getTTFB(onPerfEntry)
})
}
};
}
export default reportWebVitals;
export default reportWebVitals

View File

@ -2,7 +2,7 @@
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
import '@testing-library/jest-dom'
import Enzyme from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'

View File

@ -1,12 +1,9 @@
import axios from 'axios'
import toPairs from 'lodash/toPairs'
import size from 'lodash/size'
import get from 'lodash/get'
import find from 'lodash/find'
import { toPairs, size, get, has, find } from 'lodash'
import reportRequestError, { redactRequestData, redactHeaders } from '../util/reportRequestError'
import { __ } from '@wordpress/i18n'
import has from 'lodash/has'
import sliceJson from '../util/sliceJson'
import { clearQueryCache } from '../queryCache'
const restApiAxios = axios.create()
@ -20,35 +17,36 @@ export const CONFLICT_DETECTION_SCANNER_DURATION_MIN = 10
// (which would just be exactly "now").
const CONFLICT_DETECTION_SCANNER_DEACTIVATION_DELTA_MS = 1
const COULD_NOT_SAVE_CHANGES_MESSAGE = __( 'Couldn\'t save those changes', 'font-awesome' )
const REJECTED_METHOD_COULD_NOT_SAVE_CHANGES_MESSAGE = __( 'Changes not saved because your WordPress server does not allow this kind of request. Look for details in the browser console.', 'font-awesome' )
const COULD_NOT_CHECK_PREFERENCES_MESSAGE = __( 'Couldn\'t check preferences', 'font-awesome' )
const NO_RESPONSE_MESSAGE = __( 'A request to your WordPress server never received a response', 'font-awesome' )
const REQUEST_FAILED_MESSAGE = __( 'A request to your WordPress server failed', 'font-awesome' )
const COULD_NOT_START_SCANNER_MESSAGE = __( 'Couldn\'t start the scanner', 'font-awesome' )
const COULD_NOT_SNOOZE_MESSAGE = __( 'Couldn\'t snooze', 'font-awesome' )
export function preprocessResponse( response ) {
const confirmed = has( response, 'headers.fontawesome-confirmation' )
const COULD_NOT_SAVE_CHANGES_MESSAGE = __("Couldn't save those changes", 'font-awesome')
const REJECTED_METHOD_COULD_NOT_SAVE_CHANGES_MESSAGE = __(
'Changes not saved because your WordPress server does not allow this kind of request. Look for details in the browser console.',
'font-awesome'
)
const COULD_NOT_CHECK_PREFERENCES_MESSAGE = __("Couldn't check preferences", 'font-awesome')
const NO_RESPONSE_MESSAGE = __('A request to your WordPress server never received a response', 'font-awesome')
const REQUEST_FAILED_MESSAGE = __('A request to your WordPress server failed', 'font-awesome')
const COULD_NOT_START_SCANNER_MESSAGE = __("Couldn't start the scanner", 'font-awesome')
const COULD_NOT_SNOOZE_MESSAGE = __("Couldn't snooze", 'font-awesome')
if ( 204 === response.status && '' !== response.data ) {
reportRequestError({ error: null, confirmed, trimmed: response.data, expectEmpty: true })
// clean it up
response.data = {}
return response
export function preprocessResponse(response) {
const confirmed = has(response, 'headers.fontawesome-confirmation')
if (204 === response.status && '' !== response.data) {
reportRequestError({ error: null, confirmed, trimmed: response.data, expectEmpty: true })
// clean it up
response.data = {}
return response
}
const data = get(response, 'data', null)
const foundUnexpectedData = 'string' === typeof data && size(data) > 0
const sliced = foundUnexpectedData
? sliceJson( data )
: {}
const sliced = foundUnexpectedData ? sliceJson(data) : {}
// Fixup the response data if garbage was fixed
if ( foundUnexpectedData) {
if ( sliced ) {
if (foundUnexpectedData) {
if (sliced) {
response.data = get(sliced, 'parsed')
}
}
@ -56,10 +54,10 @@ export function preprocessResponse( response ) {
// If we had to trim any garbage, we'll store it here
const trimmed = get(sliced, 'trimmed', '')
const errors = get( response, 'data.errors', null )
const errors = get(response, 'data.errors', null)
if ( response.status >= 400 ) {
if ( errors ) {
if (response.status >= 400) {
if (errors) {
// This is just a normal error response.
response.uiMessage = reportRequestError({ error: response.data, confirmed, trimmed })
} else {
@ -97,8 +95,8 @@ export function preprocessResponse( response ) {
* through, unless we can see that the response has been corrupted,
* in which case we'll report that first.
*/
if ( response.status < 400 && response.status >= 300 ) {
if ( !confirmed || '' !== trimmed ) {
if (response.status < 400 && response.status >= 300) {
if (!confirmed || '' !== trimmed) {
response.uiMessage = reportRequestError({ error: null, confirmed, trimmed })
}
@ -113,8 +111,8 @@ export function preprocessResponse( response ) {
* or cases where it's legitmate for the controller to return an otherwise
* successful response that also includes some error data for extra diagnostics.
*/
if ( errors ) {
/**
if (errors) {
/**
* The controller sent back _only_ error data, though the HTTP status is 2XX.
* This is a false positive.
* This can occur when other buggy code running on the WordPress server preempts
@ -126,9 +124,9 @@ export function preprocessResponse( response ) {
response.uiMessage = reportRequestError({ error: response.data, confirmed, falsePositive, trimmed })
return response
} else {
const error = get( response, 'data.error', null )
const error = get(response, 'data.error', null)
if( error ) {
if (error) {
/**
* We may receive errors back with a 200 success response, such as when
* the controller catches PreferenceRegistrationExceptions.
@ -137,7 +135,7 @@ export function preprocessResponse( response ) {
return response
}
if( !confirmed ) {
if (!confirmed) {
/**
* We have received a response that, by every indication so far, is successful.
* However, it lacks the confirmation header, which _might_ indicate a problem.
@ -149,16 +147,16 @@ export function preprocessResponse( response ) {
}
restApiAxios.interceptors.response.use(
response => preprocessResponse( response ),
error => {
if( error.response ) {
error.response = preprocessResponse( error.response )
(response) => preprocessResponse(response),
(error) => {
if (error.response) {
error.response = preprocessResponse(error.response)
error.uiMessage = get(error, 'response.uiMessage')
} else if ( error.request ) {
} else if (error.request) {
const code = 'fontawesome_request_noresponse'
const e = {
errors: {
[code]: [ NO_RESPONSE_MESSAGE ]
[code]: [NO_RESPONSE_MESSAGE]
},
error_data: {
[code]: { request: error.request }
@ -170,7 +168,7 @@ restApiAxios.interceptors.response.use(
const code = 'fontawesome_request_failed'
const e = {
errors: {
[code]: [ REQUEST_FAILED_MESSAGE ]
[code]: [REQUEST_FAILED_MESSAGE]
},
error_data: {
[code]: { failedRequestMessage: error.message }
@ -197,22 +195,22 @@ export function resetOptionsFormState() {
}
export function addPendingOption(change) {
return function(dispatch, getState) {
return function (dispatch, getState) {
const { options } = getState()
for (const [ key, val ] of toPairs(change)) {
for (const [key, val] of toPairs(change)) {
const originalValue = options[key]
// If we're changing back to an original setting
if( originalValue === val ) {
if (originalValue === val) {
dispatch({
type: 'RESET_PENDING_OPTION',
change: {[key]: val}
change: { [key]: val }
})
} else {
dispatch({
type: 'ADD_PENDING_OPTION',
change: {[key]: val}
change: { [key]: val }
})
}
}
@ -239,11 +237,11 @@ export function resetPendingBlocklistSubmissionStatus() {
}
export function submitPendingUnregisteredClientDeletions() {
return function(dispatch, getState){
return function (dispatch, getState) {
const { apiNonce, apiUrl, unregisteredClientsDeletionStatus } = getState()
const deleteList = get( unregisteredClientsDeletionStatus, 'pending', null )
const deleteList = get(unregisteredClientsDeletionStatus, 'pending', null)
if (!deleteList || size( deleteList ) === 0) return
if (!deleteList || size(deleteList) === 0) return
dispatch({ type: 'DELETE_UNREGISTERED_CLIENTS_START' })
@ -255,28 +253,28 @@ export function submitPendingUnregisteredClientDeletions() {
})
}
return restApiAxios.delete(
`${apiUrl}/conflict-detection/conflicts`,
{
return restApiAxios
.delete(`${apiUrl}/conflict-detection/conflicts`, {
data: deleteList,
headers: {
'X-WP-Nonce': apiNonce
}
}
).then(response => {
const { status, data, falsePositive } = response
})
.then((response) => {
const { status, data, falsePositive } = response
if ( falsePositive ) {
handleError(response)
} else {
dispatch({
type: 'DELETE_UNREGISTERED_CLIENTS_END',
success: true,
data: 204 === status ? null : data,
message: ''
})
}
}).catch(handleError)
if (falsePositive) {
handleError(response)
} else {
dispatch({
type: 'DELETE_UNREGISTERED_CLIENTS_END',
success: true,
data: 204 === status ? null : data,
message: ''
})
}
})
.catch(handleError)
}
}
@ -288,13 +286,13 @@ export function updatePendingBlocklist(data = []) {
}
export function submitPendingBlocklist() {
return function(dispatch, getState){
return function (dispatch, getState) {
const { apiNonce, apiUrl, blocklistUpdateStatus } = getState()
const blocklist = get( blocklistUpdateStatus, 'pending', null )
const blocklist = get(blocklistUpdateStatus, 'pending', null)
if (!blocklist) return
dispatch({type: 'BLOCKLIST_UPDATE_START'})
dispatch({ type: 'BLOCKLIST_UPDATE_START' })
const handleError = ({ uiMessage }) => {
dispatch({
@ -304,34 +302,33 @@ export function submitPendingBlocklist() {
})
}
return restApiAxios.post(
`${apiUrl}/conflict-detection/conflicts/blocklist`,
blocklist,
{
return restApiAxios
.post(`${apiUrl}/conflict-detection/conflicts/blocklist`, blocklist, {
headers: {
'X-WP-Nonce': apiNonce
}
}
).then(response => {
const { status, data, falsePositive } = response
})
.then((response) => {
const { status, data, falsePositive } = response
if ( falsePositive ) {
handleError(response)
} else {
dispatch({
type: 'BLOCKLIST_UPDATE_END',
success: true,
data: 204 === status ? null : data,
message: ''
})
}
}).catch(handleError)
if (falsePositive) {
handleError(response)
} else {
dispatch({
type: 'BLOCKLIST_UPDATE_END',
success: true,
data: 204 === status ? null : data,
message: ''
})
}
})
.catch(handleError)
}
}
export function checkPreferenceConflicts() {
return function(dispatch, getState){
dispatch({type: 'PREFERENCE_CHECK_START'})
return function (dispatch, getState) {
dispatch({ type: 'PREFERENCE_CHECK_START' })
const { apiNonce, apiUrl, options, pendingOptions } = getState()
const handleError = ({ uiMessage }) => {
@ -342,33 +339,36 @@ export function checkPreferenceConflicts() {
})
}
return restApiAxios.post(
`${apiUrl}/preference-check`,
{ ...options, ...pendingOptions },
{
headers: {
'X-WP-Nonce': apiNonce
return restApiAxios
.post(
`${apiUrl}/preference-check`,
{ ...options, ...pendingOptions },
{
headers: {
'X-WP-Nonce': apiNonce
}
}
}
).then(response => {
const { data, falsePositive } = response
)
.then((response) => {
const { data, falsePositive } = response
if( falsePositive ) {
handleError(response)
} else {
dispatch({
type: 'PREFERENCE_CHECK_END',
success: true,
message: '',
detectedConflicts: data
})
}
}).catch(handleError)
if (falsePositive) {
handleError(response)
} else {
dispatch({
type: 'PREFERENCE_CHECK_END',
success: true,
message: '',
detectedConflicts: data
})
}
})
.catch(handleError)
}
}
export function chooseAwayFromKitConfig({ activeKitToken }) {
return function(dispatch, getState) {
return function (dispatch, getState) {
const { releases } = getState()
dispatch({
@ -384,18 +384,20 @@ export function chooseIntoKitConfig() {
}
export function queryKits() {
return function(dispatch, getState) {
return function (dispatch, getState) {
const { apiNonce, apiUrl, options } = getState()
const initialKitToken = get(options, 'kitToken', null)
dispatch({ type: 'KITS_QUERY_START' })
clearQueryCache()
const handleKitsQueryError = ({ uiMessage }) => {
dispatch({
type: 'KITS_QUERY_END',
success: false,
message: uiMessage || __( 'Failed to fetch kits', 'font-awesome' )
message: uiMessage || __('Failed to fetch kits', 'font-awesome')
})
}
@ -403,153 +405,121 @@ export function queryKits() {
dispatch({
type: 'OPTIONS_FORM_SUBMIT_END',
success: false,
message: uiMessage || __( 'Couldn\'t update latest kit settings', 'font-awesome' )
message: uiMessage || __("Couldn't update latest kit settings", 'font-awesome')
})
}
return restApiAxios.post(
`${apiUrl}/api`,
'query { me { kits { name version technologySelected licenseSelected minified token shimEnabled autoAccessibilityEnabled status }}}',
{
headers: {
'X-WP-Nonce': apiNonce
}
}
).then(response => {
if ( response.falsePositive ) return handleKitsQueryError(response)
const data = get(response, 'data.data')
// We may receive errors back with a 200 response, such as when
// there PreferenceRegistrationExceptions.
if( get( data, 'me') ) {
dispatch({
type: 'KITS_QUERY_END',
data,
success: true
})
} else {
return dispatch({
type: 'KITS_QUERY_END',
success: false,
message: __( 'Failed to fetch kits. Regenerate your API Token and try again.', 'font-awesome' )
})
}
// If we didn't start out with a saved kitToken, we're done.
// Otherwise, we'll move on to update any config on that kit which
// might have changed since we saved it in WordPress.
if(! initialKitToken) return
const refreshedKits = get( data, 'me.kits', [] )
const currentKitRefreshed = find( refreshedKits, { token: initialKitToken } )
if(! currentKitRefreshed) return
const optionsUpdate = {}
// Inspect each relevant kit option for the current kit to see if it's
// been changed since our last query.
if( options.usePro && currentKitRefreshed.licenseSelected !== 'pro' ) {
optionsUpdate.usePro = false
} else if ( !options.usePro && currentKitRefreshed.licenseSelected === 'pro' ) {
optionsUpdate.usePro = true
}
if( options.technology === 'svg' && currentKitRefreshed.technologySelected !== 'svg' ) {
optionsUpdate.technology = 'webfont'
// pseudoElements must always be true for webfont
optionsUpdate.pseudoElements = true
} else if( options.technology !== 'svg' && currentKitRefreshed.technologySelected === 'svg' ) {
optionsUpdate.technology = 'svg'
// pseudoElements must always be false for svg when loaded in a kit
optionsUpdate.pseudoElements = false
}
if( options.version !== currentKitRefreshed.version) {
optionsUpdate.version = currentKitRefreshed.version
}
if( options.compat && !currentKitRefreshed.shimEnabled ) {
optionsUpdate.compat = false
} else if( !options.compat && currentKitRefreshed.shimEnabled ) {
optionsUpdate.compat = true
}
dispatch({type: 'OPTIONS_FORM_SUBMIT_START'})
return restApiAxios.post(
`${apiUrl}/config`,
{
options: {
...options, ...optionsUpdate
}
},
return restApiAxios
.post(
`${apiUrl}/api`,
'query { me { kits { name version technologySelected licenseSelected minified token shimEnabled autoAccessibilityEnabled status }}}',
{
headers: {
'X-WP-Nonce': apiNonce
}
}
).then(response => {
const { data, falsePositive } = response
)
.then((response) => {
if (response.falsePositive) return handleKitsQueryError(response)
if ( falsePositive ) return handleKitUpdateError(response)
const data = get(response, 'data.data')
dispatch({
type: 'OPTIONS_FORM_SUBMIT_END',
data,
success: true,
message: __( 'Kit changes saved', 'font-awesome' )
})
}).catch(handleKitUpdateError)
}).catch(handleKitsQueryError)
// We may receive errors back with a 200 response, such as when
// there PreferenceRegistrationExceptions.
if (get(data, 'me')) {
dispatch({
type: 'KITS_QUERY_END',
data,
success: true
})
} else {
return dispatch({
type: 'KITS_QUERY_END',
success: false,
message: __('Failed to fetch kits. Regenerate your API Token and try again.', 'font-awesome')
})
}
// If we didn't start out with a saved kitToken, we're done.
// Otherwise, we'll move on to update any config on that kit which
// might have changed since we saved it in WordPress.
if (!initialKitToken) return
const refreshedKits = get(data, 'me.kits', [])
const currentKitRefreshed = find(refreshedKits, { token: initialKitToken })
if (!currentKitRefreshed) return
const optionsUpdate = {}
// Inspect each relevant kit option for the current kit to see if it's
// been changed since our last query.
if (options.usePro && currentKitRefreshed.licenseSelected !== 'pro') {
optionsUpdate.usePro = false
} else if (!options.usePro && currentKitRefreshed.licenseSelected === 'pro') {
optionsUpdate.usePro = true
}
if (options.technology === 'svg' && currentKitRefreshed.technologySelected !== 'svg') {
optionsUpdate.technology = 'webfont'
// pseudoElements must always be true for webfont
optionsUpdate.pseudoElements = true
} else if (options.technology !== 'svg' && currentKitRefreshed.technologySelected === 'svg') {
optionsUpdate.technology = 'svg'
// pseudoElements must always be false for svg when loaded in a kit
optionsUpdate.pseudoElements = false
}
if (options.version !== currentKitRefreshed.version) {
optionsUpdate.version = currentKitRefreshed.version
}
if (options.compat && !currentKitRefreshed.shimEnabled) {
optionsUpdate.compat = false
} else if (!options.compat && currentKitRefreshed.shimEnabled) {
optionsUpdate.compat = true
}
dispatch({ type: 'OPTIONS_FORM_SUBMIT_START' })
return restApiAxios
.post(
`${apiUrl}/config`,
{
options: {
...options,
...optionsUpdate
}
},
{
headers: {
'X-WP-Nonce': apiNonce
}
}
)
.then((response) => {
const { data, falsePositive } = response
if (falsePositive) return handleKitUpdateError(response)
dispatch({
type: 'OPTIONS_FORM_SUBMIT_END',
data,
success: true,
message: __('Kit changes saved', 'font-awesome')
})
})
.catch(handleKitUpdateError)
})
.catch(handleKitsQueryError)
}
}
export function submitPendingOptions() {
return function(dispatch, getState) {
return function (dispatch, getState) {
const { apiNonce, apiUrl, options, pendingOptions } = getState()
dispatch({type: 'OPTIONS_FORM_SUBMIT_START'})
const handleError = ({ uiMessage }) => {
dispatch({
type: 'OPTIONS_FORM_SUBMIT_END',
success: false,
message: uiMessage || COULD_NOT_SAVE_CHANGES_MESSAGE
})
}
return restApiAxios.post(
`${apiUrl}/config`,
{ options: { ...options, ...pendingOptions }},
{
headers: {
'X-WP-Nonce': apiNonce
}
}
).then(response => {
const { data, falsePositive } = response
if ( falsePositive ) {
handleError(response)
} else {
dispatch({
type: 'OPTIONS_FORM_SUBMIT_END',
data,
success: true,
message: __( 'Changes saved', 'font-awesome' )
})
}
}).catch(handleError)
}
}
export function updateApiToken({ apiToken = false, runQueryKits = false }) {
return function(dispatch, getState) {
const { apiNonce, apiUrl, options } = getState()
dispatch({type: 'OPTIONS_FORM_SUBMIT_START'})
dispatch({ type: 'OPTIONS_FORM_SUBMIT_START' })
const handleError = ({ uiMessage }) => {
dispatch({
@ -559,32 +529,77 @@ export function updateApiToken({ apiToken = false, runQueryKits = false }) {
})
}
return restApiAxios.post(
`${apiUrl}/config`,
{ options: { ...options, apiToken }},
{
headers: {
'X-WP-Nonce': apiNonce
return restApiAxios
.post(
`${apiUrl}/config`,
{ options: { ...options, ...pendingOptions } },
{
headers: {
'X-WP-Nonce': apiNonce
}
}
}
).then(response => {
const { data, falsePositive } = response
)
.then((response) => {
const { data, falsePositive } = response
if ( falsePositive ) {
handleError(response)
} else {
dispatch({
type: 'OPTIONS_FORM_SUBMIT_END',
data,
success: true,
message: __( 'API Token saved', 'font-awesome' )
})
if( runQueryKits ) {
return dispatch(queryKits())
if (falsePositive) {
handleError(response)
} else {
dispatch({
type: 'OPTIONS_FORM_SUBMIT_END',
data,
success: true,
message: __('Changes saved', 'font-awesome')
})
}
}
}).catch(handleError)
})
.catch(handleError)
}
}
export function updateApiToken({ apiToken = false, runQueryKits = false }) {
return function (dispatch, getState) {
const { apiNonce, apiUrl, options } = getState()
dispatch({ type: 'OPTIONS_FORM_SUBMIT_START' })
const handleError = ({ uiMessage }) => {
dispatch({
type: 'OPTIONS_FORM_SUBMIT_END',
success: false,
message: uiMessage || COULD_NOT_SAVE_CHANGES_MESSAGE
})
}
return restApiAxios
.post(
`${apiUrl}/config`,
{ options: { ...options, apiToken } },
{
headers: {
'X-WP-Nonce': apiNonce
}
}
)
.then((response) => {
const { data, falsePositive } = response
if (falsePositive) {
handleError(response)
} else {
dispatch({
type: 'OPTIONS_FORM_SUBMIT_END',
data,
success: true,
message: __('API Token saved', 'font-awesome')
})
if (runQueryKits) {
return dispatch(queryKits())
}
}
})
.catch(handleError)
}
}
@ -603,12 +618,12 @@ export function reportDetectedConflicts({ nodesTested = {} }) {
// the current page's scan was complete and report submitted. In that case,
// we just ignore the report. Otherwise, this action would try to post results
// to a REST route that will no longer be registered and listening, resulting a 404.
if( !showConflictDetectionReporter ) {
if (!showConflictDetectionReporter) {
return
}
if( size(nodesTested.conflict) > 0 ) {
const payload = Object.keys(nodesTested.conflict).reduce(function(acc, md5){
if (size(nodesTested.conflict) > 0) {
const payload = Object.keys(nodesTested.conflict).reduce(function (acc, md5) {
acc[md5] = nodesTested.conflict[md5]
return acc
}, {})
@ -627,81 +642,37 @@ export function reportDetectedConflicts({ nodesTested = {} }) {
})
}
return restApiAxios.post(
`${apiUrl}/conflict-detection/conflicts`,
payload,
{
return restApiAxios
.post(`${apiUrl}/conflict-detection/conflicts`, payload, {
headers: {
'X-WP-Nonce': apiNonce
}
}
)
.then(response => {
const { status, data, falsePositive } = response
})
.then((response) => {
const { status, data, falsePositive } = response
if ( falsePositive ) {
handleError(response)
} else {
dispatch({
type: 'CONFLICT_DETECTION_SUBMIT_END',
success: true,
/**
* If get back no data here, that can only mean that a previous
* response with garbage in it had an erroneous HTTP 200 status
* on it, but no parseable JSON, which is equivalent to a 204.
*/
data: ( 204 === status || 0 === size(data) ) ? null : data
})
}
})
.catch(handleError)
if (falsePositive) {
handleError(response)
} else {
dispatch({
type: 'CONFLICT_DETECTION_SUBMIT_END',
success: true,
/**
* If get back no data here, that can only mean that a previous
* response with garbage in it had an erroneous HTTP 200 status
* on it, but no parseable JSON, which is equivalent to a 204.
*/
data: 204 === status || 0 === size(data) ? null : data
})
}
})
.catch(handleError)
} else {
dispatch({ type: 'CONFLICT_DETECTION_NONE_FOUND' })
}
}
}
export function snoozeV3DeprecationWarning() {
return (dispatch, getState) => {
const { apiNonce, apiUrl } = getState()
dispatch({ type: 'SNOOZE_V3DEPRECATION_WARNING_START' })
const handleError = ({ uiMessage }) => {
dispatch({
type: 'SNOOZE_V3DEPRECATION_WARNING_END',
success: false,
message: uiMessage || COULD_NOT_SNOOZE_MESSAGE
})
}
return restApiAxios.post(
`${apiUrl}/v3deprecation`,
{ snooze: true },
{
headers: {
'X-WP-Nonce': apiNonce
}
}
)
.then(response => {
const { falsePositive } = response
if ( falsePositive ) {
handleError(response)
} else {
dispatch({
type: 'SNOOZE_V3DEPRECATION_WARNING_END',
success: true,
snooze: true,
message: ''
})
}
})
.catch(handleError)
}
}
export function setActiveAdminTab(tab) {
return {
type: 'SET_ACTIVE_ADMIN_TAB',
@ -710,18 +681,14 @@ export function setActiveAdminTab(tab) {
}
export function setConflictDetectionScanner({ enable = true }) {
return function(dispatch, getState) {
return function (dispatch, getState) {
const { apiNonce, apiUrl } = getState()
const actionStartType = enable
? 'ENABLE_CONFLICT_DETECTION_SCANNER_START'
: 'DISABLE_CONFLICT_DETECTION_SCANNER_START'
const actionStartType = enable ? 'ENABLE_CONFLICT_DETECTION_SCANNER_START' : 'DISABLE_CONFLICT_DETECTION_SCANNER_START'
const actionEndType = enable
? 'ENABLE_CONFLICT_DETECTION_SCANNER_END'
: 'DISABLE_CONFLICT_DETECTION_SCANNER_END'
const actionEndType = enable ? 'ENABLE_CONFLICT_DETECTION_SCANNER_END' : 'DISABLE_CONFLICT_DETECTION_SCANNER_END'
dispatch({type: actionStartType})
dispatch({ type: actionStartType })
const handleError = ({ uiMessage }) => {
dispatch({
@ -731,28 +698,31 @@ export function setConflictDetectionScanner({ enable = true }) {
})
}
return restApiAxios.post(
`${apiUrl}/conflict-detection/until`,
enable
? Math.floor((new Date((new Date()).valueOf() + (CONFLICT_DETECTION_SCANNER_DURATION_MIN * 1000 * 60))) / 1000)
: Math.floor((new Date())/1000) - CONFLICT_DETECTION_SCANNER_DEACTIVATION_DELTA_MS,
{
headers: {
'X-WP-Nonce': apiNonce
return restApiAxios
.post(
`${apiUrl}/conflict-detection/until`,
enable
? Math.floor(new Date(new Date().valueOf() + CONFLICT_DETECTION_SCANNER_DURATION_MIN * 1000 * 60) / 1000)
: Math.floor(new Date() / 1000) - CONFLICT_DETECTION_SCANNER_DEACTIVATION_DELTA_MS,
{
headers: {
'X-WP-Nonce': apiNonce
}
}
}
).then(response => {
const { status, data, falsePositive } = response
)
.then((response) => {
const { status, data, falsePositive } = response
if ( falsePositive ) {
handleError(response)
} else {
dispatch({
type: actionEndType,
data: 204 === status ? null : data,
success: true
})
}
}).catch(handleError)
if (falsePositive) {
handleError(response)
} else {
dispatch({
type: actionEndType,
data: 204 === status ? null : data,
success: true
})
}
})
.catch(handleError)
}
}

View File

@ -30,14 +30,18 @@ describe('addPendingOption', () => {
test('when multiple pending options are adjusted together, all are updated', () => {
store.dispatch(addPendingOption({ technology: 'webfont', pseudoElements: true }))
expect(store.getActions().length).toEqual(2)
expect(store.getActions()[0]).toEqual(expect.objectContaining({
type: 'ADD_PENDING_OPTION',
change: { technology: 'webfont' }
}))
expect(store.getActions()[1]).toEqual(expect.objectContaining({
type: 'ADD_PENDING_OPTION',
change: { pseudoElements: true }
}))
expect(store.getActions()[0]).toEqual(
expect.objectContaining({
type: 'ADD_PENDING_OPTION',
change: { technology: 'webfont' }
})
)
expect(store.getActions()[1]).toEqual(
expect.objectContaining({
type: 'ADD_PENDING_OPTION',
change: { pseudoElements: true }
})
)
})
})
@ -63,7 +67,7 @@ describe('submitPendingOptions and interceptors', () => {
afterEach(() => {
resetAxiosMocks()
})
describe('when HTTP 200', () => {
describe('when confirmation header is present', () => {
describe('successful JSON response also includes error information', () => {
@ -74,11 +78,11 @@ describe('submitPendingOptions and interceptors', () => {
options: pendingOptions,
error: {
errors: {
"code1": ["message1"],
code1: ['message1']
},
error_data: {
"code1": {
"trace": 'some stack trace'
code1: {
trace: 'some stack trace'
}
}
}
@ -98,36 +102,42 @@ describe('submitPendingOptions and interceptors', () => {
})
})
test('submits successfully with successful ui message and also reports error to console', done => {
store.dispatch(submitPendingOptions()).then(() => {
expect(reportRequestError).toHaveBeenCalledTimes(1)
expect(reportRequestError).toHaveBeenCalledWith(expect.objectContaining({
error: expect.objectContaining({
errors: {
code1: expect.anything()
},
error_data: {
code1: expect.anything()
}
}),
confirmed: true,
ok: true
}))
expect(store.getActions().length).toEqual(2)
expect(store.getActions()).toEqual(expect.arrayContaining([
expect.objectContaining({
type: 'OPTIONS_FORM_SUBMIT_START'
}),
expect.objectContaining({
type: 'OPTIONS_FORM_SUBMIT_END',
success: true,
data,
message: expect.stringContaining('saved')
})
]))
done()
})
.catch(e => done(e))
test('submits successfully with successful ui message and also reports error to console', (done) => {
store
.dispatch(submitPendingOptions())
.then(() => {
expect(reportRequestError).toHaveBeenCalledTimes(1)
expect(reportRequestError).toHaveBeenCalledWith(
expect.objectContaining({
error: expect.objectContaining({
errors: {
code1: expect.anything()
},
error_data: {
code1: expect.anything()
}
}),
confirmed: true,
ok: true
})
)
expect(store.getActions().length).toEqual(2)
expect(store.getActions()).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: 'OPTIONS_FORM_SUBMIT_START'
}),
expect.objectContaining({
type: 'OPTIONS_FORM_SUBMIT_END',
success: true,
data,
message: expect.stringContaining('saved')
})
])
)
done()
})
.catch((e) => done(e))
})
})
})
@ -152,30 +162,36 @@ describe('submitPendingOptions and interceptors', () => {
})
})
test('reports warning but completes successfully', done => {
store.dispatch(submitPendingOptions()).then(() => {
expect(reportRequestError).toHaveBeenCalledTimes(1)
expect(reportRequestError).toHaveBeenCalledWith(expect.objectContaining({
error: null,
confirmed: false,
ok: true,
trimmed: INVALID_JSON_RESPONSE_DATA
}))
expect(store.getActions().length).toEqual(2)
expect(store.getActions()).toEqual(expect.arrayContaining([
expect.objectContaining({
type: 'OPTIONS_FORM_SUBMIT_START'
}),
expect.objectContaining({
type: 'OPTIONS_FORM_SUBMIT_END',
success: true,
data,
message: expect.stringContaining('saved')
})
]))
done()
})
.catch(e => done(e))
test('reports warning but completes successfully', (done) => {
store
.dispatch(submitPendingOptions())
.then(() => {
expect(reportRequestError).toHaveBeenCalledTimes(1)
expect(reportRequestError).toHaveBeenCalledWith(
expect.objectContaining({
error: null,
confirmed: false,
ok: true,
trimmed: INVALID_JSON_RESPONSE_DATA
})
)
expect(store.getActions().length).toEqual(2)
expect(store.getActions()).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: 'OPTIONS_FORM_SUBMIT_START'
}),
expect.objectContaining({
type: 'OPTIONS_FORM_SUBMIT_END',
success: true,
data,
message: expect.stringContaining('saved')
})
])
)
done()
})
.catch((e) => done(e))
})
describe('axios request', () => {
@ -185,23 +201,25 @@ describe('submitPendingOptions and interceptors', () => {
changeImpl({ name: 'post', fn: mockPost })
})
test('submits pendingOptions', done => {
store.dispatch(submitPendingOptions()).then(() => {
expect(mockPost).toHaveBeenCalledTimes(1)
expect(mockPost).toHaveBeenCalledWith(
`${apiUrl}/config`,
expect.objectContaining({
options: pendingOptions
}),
expect.objectContaining({
headers: {
'X-WP-Nonce': fakeNonce
}
})
)
done()
})
.catch(e => done(e))
test('submits pendingOptions', (done) => {
store
.dispatch(submitPendingOptions())
.then(() => {
expect(mockPost).toHaveBeenCalledTimes(1)
expect(mockPost).toHaveBeenCalledWith(
`${apiUrl}/config`,
expect.objectContaining({
options: pendingOptions
}),
expect.objectContaining({
headers: {
'X-WP-Nonce': fakeNonce
}
})
)
done()
})
.catch((e) => done(e))
})
})
})
@ -210,68 +228,74 @@ describe('submitPendingOptions and interceptors', () => {
describe('when HTTP 400', () => {
describe('when errors payload is absent', () => {
const responseData = {foo: 42}
const url = `${apiUrl}/config`
const method = 'POST'
const status = 400
const statusText = 'Bad Request'
const requestData = JSON.stringify({bar: 43})
const responseHeaders = {
'fontawesome-confirmation': 1
}
const requestHeaders = {
'Content-Type': 'application/json'
}
const responseData = { foo: 42 }
const url = `${apiUrl}/config`
const method = 'POST'
const status = 400
const statusText = 'Bad Request'
const requestData = JSON.stringify({ bar: 43 })
const responseHeaders = {
'fontawesome-confirmation': 1
}
const requestHeaders = {
'Content-Type': 'application/json'
}
beforeEach(() => {
respondWith({
url,
method,
response: {
status,
statusText,
data: responseData,
headers: responseHeaders,
config: {
method,
url,
data: requestData,
headers: requestHeaders
}
},
})
beforeEach(() => {
respondWith({
url,
method,
response: {
status,
statusText,
data: responseData,
headers: responseHeaders,
config: {
method,
url,
data: requestData,
headers: requestHeaders
}
}
})
})
test('displays default ui message and emits console message', done => {
reportRequestError.mockReturnValueOnce(null)
store.dispatch(submitPendingOptions()).then(() => {
test('displays default ui message and emits console message', (done) => {
reportRequestError.mockReturnValueOnce(null)
store
.dispatch(submitPendingOptions())
.then(() => {
expect(reportRequestError).toHaveBeenCalledTimes(1)
expect(reportRequestError).toHaveBeenCalledWith(expect.objectContaining({
confirmed: true,
requestMethod: method,
//requestData,
requestUrl: url,
// responseHeaders: expect.any(Object),
// requestHeaders: expect.any(Object),
responseStatus: status,
responseStatusText: statusText,
responseData
}))
expect(store.getActions().length).toEqual(2)
expect(store.getActions()).toEqual(expect.arrayContaining([
expect(reportRequestError).toHaveBeenCalledWith(
expect.objectContaining({
type: 'OPTIONS_FORM_SUBMIT_START'
}),
expect.objectContaining({
type: 'OPTIONS_FORM_SUBMIT_END',
success: false,
message: expect.stringContaining("Couldn't save")
confirmed: true,
requestMethod: method,
//requestData,
requestUrl: url,
// responseHeaders: expect.any(Object),
// requestHeaders: expect.any(Object),
responseStatus: status,
responseStatusText: statusText,
responseData
})
]))
)
expect(store.getActions().length).toEqual(2)
expect(store.getActions()).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: 'OPTIONS_FORM_SUBMIT_START'
}),
expect.objectContaining({
type: 'OPTIONS_FORM_SUBMIT_END',
success: false,
message: expect.stringContaining("Couldn't save")
})
])
)
done()
})
.catch(e => done(e))
})
.catch((e) => done(e))
})
})
})
@ -286,34 +310,40 @@ describe('submitPendingOptions and interceptors', () => {
reportRequestError.mockImplementation(() => MOCK_UI_MESSAGE)
})
test('failed request is reported to console and failure with uiMessage is dispatched to store', done => {
store.dispatch(submitPendingOptions()).then(() => {
expect(reportRequestError).toHaveBeenCalledTimes(1)
expect(reportRequestError).toHaveBeenCalledWith(expect.objectContaining({
error: {
errors: expect.objectContaining({
fontawesome_request_noresponse: [ expect.any(String) ]
}),
error_data: {
fontawesome_request_noresponse: {
request: expect.any(XMLHttpRequest)
test('failed request is reported to console and failure with uiMessage is dispatched to store', (done) => {
store
.dispatch(submitPendingOptions())
.then(() => {
expect(reportRequestError).toHaveBeenCalledTimes(1)
expect(reportRequestError).toHaveBeenCalledWith(
expect.objectContaining({
error: {
errors: expect.objectContaining({
fontawesome_request_noresponse: [expect.any(String)]
}),
error_data: {
fontawesome_request_noresponse: {
request: expect.any(XMLHttpRequest)
}
}
}
}
},
}))
expect(store.getActions()).toEqual(expect.arrayContaining([
expect.objectContaining({
type: 'OPTIONS_FORM_SUBMIT_START'
}),
expect.objectContaining({
type: 'OPTIONS_FORM_SUBMIT_END',
success: false,
message: MOCK_UI_MESSAGE
})
]))
done()
})
.catch(e => done(e))
})
)
expect(store.getActions()).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: 'OPTIONS_FORM_SUBMIT_START'
}),
expect.objectContaining({
type: 'OPTIONS_FORM_SUBMIT_END',
success: false,
message: MOCK_UI_MESSAGE
})
])
)
done()
})
.catch((e) => done(e))
})
})
@ -328,34 +358,40 @@ describe('submitPendingOptions and interceptors', () => {
reportRequestError.mockImplementation(() => MOCK_UI_MESSAGE)
})
test('failure is reported to console and failure with uiMessage is dispatched to store', done => {
store.dispatch(submitPendingOptions()).then(() => {
expect(reportRequestError).toHaveBeenCalledTimes(1)
expect(reportRequestError).toHaveBeenCalledWith(expect.objectContaining({
error: {
errors: expect.objectContaining({
fontawesome_request_failed: [ expect.stringContaining('server failed') ]
}),
error_data: {
fontawesome_request_failed: {
failedRequestMessage: 'some axios error'
test('failure is reported to console and failure with uiMessage is dispatched to store', (done) => {
store
.dispatch(submitPendingOptions())
.then(() => {
expect(reportRequestError).toHaveBeenCalledTimes(1)
expect(reportRequestError).toHaveBeenCalledWith(
expect.objectContaining({
error: {
errors: expect.objectContaining({
fontawesome_request_failed: [expect.stringContaining('server failed')]
}),
error_data: {
fontawesome_request_failed: {
failedRequestMessage: 'some axios error'
}
}
}
}
},
}))
expect(store.getActions()).toEqual(expect.arrayContaining([
expect.objectContaining({
type: 'OPTIONS_FORM_SUBMIT_START'
}),
expect.objectContaining({
type: 'OPTIONS_FORM_SUBMIT_END',
success: false,
message: MOCK_UI_MESSAGE
})
]))
done()
})
.catch(e => done(e))
})
)
expect(store.getActions()).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: 'OPTIONS_FORM_SUBMIT_START'
}),
expect.objectContaining({
type: 'OPTIONS_FORM_SUBMIT_END',
success: false,
message: MOCK_UI_MESSAGE
})
])
)
done()
})
.catch((e) => done(e))
})
})
})
@ -364,7 +400,6 @@ describe('some action failure cases', () => {
const STATE_TECH_CHANGE = {
options: {
technology: 'webfont'
},
pendingOptions: {
technology: 'svg'
@ -394,14 +429,14 @@ describe('some action failure cases', () => {
endAction: 'OPTIONS_FORM_SUBMIT_END',
params: {
apiToken: 'xyz456',
runQueryKits: false
runQueryKits: false
}
},
{
action: 'submitPendingBlocklist',
state: {
blocklistUpdateStatus: {
pending: [ 'abc123' ]
pending: ['abc123']
}
},
route: 'conflict-detection/conflicts/blocklist',
@ -414,7 +449,7 @@ describe('some action failure cases', () => {
action: 'submitPendingUnregisteredClientDeletions',
state: {
unregisteredClientsDeletionStatus: {
pending: [ 'abc123' ]
pending: ['abc123']
}
},
route: 'conflict-detection/conflicts',
@ -435,20 +470,11 @@ describe('some action failure cases', () => {
params: {
nodesTested: {
conflict: {
'abc123': {}
abc123: {}
}
}
}
},
{
action: 'snoozeV3DeprecationWarning',
state: {},
route: 'v3deprecation',
method: 'POST',
startAction: 'SNOOZE_V3DEPRECATION_WARNING_START',
endAction: 'SNOOZE_V3DEPRECATION_WARNING_END',
params: {}
},
{
action: 'setConflictDetectionScanner',
desc: 'when enabling',
@ -493,11 +519,11 @@ describe('some action failure cases', () => {
const data = {
errors: {
"code1": ["message1"],
code1: ['message1']
},
error_data: {
"code1": {
"trace": 'some stack trace'
code1: {
trace: 'some stack trace'
}
}
}
@ -510,8 +536,8 @@ describe('some action failure cases', () => {
resetAxiosMocks()
})
cases.map(c => {
describe(`${c.action}${ c.desc || ''}`, () => {
cases.map((c) => {
describe(`${c.action}${c.desc || ''}`, () => {
let store = null
beforeEach(() => {
@ -530,38 +556,44 @@ describe('some action failure cases', () => {
response: {
status: 200,
statusText: 'OK',
data: `${garbage}${JSON.stringify(data)}`,
data: `${garbage}${JSON.stringify(data)}`
// no confirmation header
}
})
})
test('reports warning and dispatches a failure action despite the garbage', done => {
store.dispatch(actions[c.action](c.params)).then(() => {
expect(reportRequestError).toHaveBeenCalledTimes(1)
expect(reportRequestError).toHaveBeenCalledWith(expect.objectContaining({
error: expect.objectContaining({
errors: expect.anything(),
'error_data': expect.anything()
}),
confirmed: false,
falsePositive: true,
trimmed: garbage
}))
expect(store.getActions().length).toEqual(2)
expect(store.getActions()).toEqual(expect.arrayContaining([
expect.objectContaining({
type: c.startAction
}),
expect.objectContaining({
type: c.endAction,
success: false,
message: expect.stringMatching(/[a-z]/)
})
]))
done()
})
.catch(e => done(e))
test('reports warning and dispatches a failure action despite the garbage', (done) => {
store
.dispatch(actions[c.action](c.params))
.then(() => {
expect(reportRequestError).toHaveBeenCalledTimes(1)
expect(reportRequestError).toHaveBeenCalledWith(
expect.objectContaining({
error: expect.objectContaining({
errors: expect.anything(),
error_data: expect.anything()
}),
confirmed: false,
falsePositive: true,
trimmed: garbage
})
)
expect(store.getActions().length).toEqual(2)
expect(store.getActions()).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: c.startAction
}),
expect.objectContaining({
type: c.endAction,
success: false,
message: expect.stringMatching(/[a-z]/)
})
])
)
done()
})
.catch((e) => done(e))
})
})
@ -581,32 +613,38 @@ describe('some action failure cases', () => {
})
})
test('reports ui and console error messages', done => {
test('reports ui and console error messages', (done) => {
reportRequestError.mockReturnValueOnce(null)
store.dispatch(actions[c.action](c.params)).then(() => {
expect(reportRequestError).toHaveBeenCalledTimes(1)
expect(reportRequestError).toHaveBeenCalledWith(expect.objectContaining({
error: expect.objectContaining({
errors: expect.anything(),
'error_data': expect.anything()
}),
confirmed: true,
trimmed: ''
}))
expect(store.getActions().length).toEqual(2)
expect(store.getActions()).toEqual(expect.arrayContaining([
expect.objectContaining({
type: c.startAction
}),
expect.objectContaining({
type: c.endAction,
success: false,
message: expect.stringMatching(/[a-z]/)
})
]))
done()
})
.catch(e => done(e))
store
.dispatch(actions[c.action](c.params))
.then(() => {
expect(reportRequestError).toHaveBeenCalledTimes(1)
expect(reportRequestError).toHaveBeenCalledWith(
expect.objectContaining({
error: expect.objectContaining({
errors: expect.anything(),
error_data: expect.anything()
}),
confirmed: true,
trimmed: ''
})
)
expect(store.getActions().length).toEqual(2)
expect(store.getActions()).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: c.startAction
}),
expect.objectContaining({
type: c.endAction,
success: false,
message: expect.stringMatching(/[a-z]/)
})
])
)
done()
})
.catch((e) => done(e))
})
})
})
@ -653,10 +691,6 @@ describe('reportDetectedConflicts', () => {
test.todo('success')
})
describe('snoozeV3DeprecationWarning', () => {
test.todo('success')
})
describe('setConflictDetectionScanner', () => {
test.todo('success when enabling')
test.todo('success when disabling')
@ -678,7 +712,7 @@ describe('preprocessResponse', () => {
actions.preprocessResponse(response)
expect(reportRequestError).toHaveBeenCalledWith(expect.objectContaining({confirmed: true}))
expect(reportRequestError).toHaveBeenCalledWith(expect.objectContaining({ confirmed: true }))
})
})
@ -687,7 +721,7 @@ describe('preprocessResponse', () => {
const method = 'PUT'
const status = 405
const statusText = 'Method Not Allowed'
const requestData = JSON.stringify({bar: 43})
const requestData = JSON.stringify({ bar: 43 })
const requestHeaders = {
'Content-Type': 'application/json'
}
@ -739,17 +773,19 @@ describe('preprocessResponse', () => {
actions.preprocessResponse(response)
expect(reportRequestError).toHaveBeenCalledWith(expect.objectContaining({
confirmed: false,
requestData,
requestMethod: method,
requestUrl: url,
responseStatus: status,
responseStatusText: statusText,
requestData: REDACTED_REQUEST_DATA,
responseHeaders: REDACTED_HEADERS,
requestHeaders: REDACTED_HEADERS
}))
expect(reportRequestError).toHaveBeenCalledWith(
expect.objectContaining({
confirmed: false,
requestData,
requestMethod: method,
requestUrl: url,
responseStatus: status,
responseStatusText: statusText,
requestData: REDACTED_REQUEST_DATA,
responseHeaders: REDACTED_HEADERS,
requestHeaders: REDACTED_HEADERS
})
)
})
})
@ -780,17 +816,19 @@ describe('preprocessResponse', () => {
actions.preprocessResponse(response)
expect(reportRequestError).toHaveBeenCalledWith(expect.objectContaining({
confirmed: false,
requestMethod: method,
requestUrl: url,
responseStatus: status,
responseStatusText: statusText,
responseData,
requestData: REDACTED_REQUEST_DATA,
responseHeaders: REDACTED_HEADERS,
requestHeaders: REDACTED_HEADERS
}))
expect(reportRequestError).toHaveBeenCalledWith(
expect.objectContaining({
confirmed: false,
requestMethod: method,
requestUrl: url,
responseStatus: status,
responseStatusText: statusText,
responseData,
requestData: REDACTED_REQUEST_DATA,
responseHeaders: REDACTED_HEADERS,
requestHeaders: REDACTED_HEADERS
})
)
})
})
})

View File

@ -2,21 +2,12 @@ import { createStore as reduxCreateStore, applyMiddleware, compose } from 'redux
import { thunk } from 'redux-thunk'
import rootReducer from './reducers'
const middleware = [ thunk ]
const middleware = [thunk]
const composeEnhancers = (
process.env.NODE_ENV === 'development'
&& window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
) || compose
const composeEnhancers = (process.env.NODE_ENV === 'development' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose
const enhancer = composeEnhancers(
applyMiddleware(...middleware)
)
const enhancer = composeEnhancers(applyMiddleware(...middleware))
export function createStore(initialData = {}) {
return reduxCreateStore(
rootReducer,
initialData,
enhancer
)
return reduxCreateStore(rootReducer, initialData, enhancer)
}

View File

@ -1,22 +1,20 @@
import size from 'lodash/size'
import omit from 'lodash/omit'
import get from 'lodash/get'
import { size, omit, get } from 'lodash'
import { combineReducers } from 'redux'
export const ADMIN_TAB_SETTINGS = 'ADMIN_TAB_SETTINGS'
export const ADMIN_TAB_TROUBLESHOOT = 'ADMIN_TAB_TROUBLESHOOT'
const coerceBool = val => val === true || val === "1"
const coerceBool = (val) => val === true || val === '1'
const coerceEmptyArrayToEmptyObject = val => size(val) === 0 ? {} : val
const coerceEmptyArrayToEmptyObject = (val) => (size(val) === 0 ? {} : val)
// TODO: add reducer for the clientPreferences that coerces their boolean options
export function blocklistSelector(state = {}) {
const unregisteredClients = state.unregisteredClients || {}
return Object.keys( unregisteredClients ).reduce( (acc, md5) => {
if( get( unregisteredClients, [md5, 'blocked'], false ) ) {
return Object.keys(unregisteredClients).reduce((acc, md5) => {
if (get(unregisteredClients, [md5, 'blocked'], false)) {
acc.push(md5)
}
return acc
@ -26,21 +24,13 @@ export function blocklistSelector(state = {}) {
export function options(state = {}, action = {}) {
const { type, data } = action
switch(type) {
switch (type) {
case 'OPTIONS_FORM_SUBMIT_END':
if(! get(action, 'data.options')) {
if (!get(action, 'data.options')) {
return state
} else {
const {
options: {
technology,
usePro,
compat,
pseudoElements,
version,
kitToken,
apiToken
}
options: { technology, usePro, compat, pseudoElements, version, kitToken, apiToken }
} = data
return {
@ -65,11 +55,10 @@ const OPTIONS_FORM_INITIAL_STATE = {
message: ''
}
function optionsFormState(
state = OPTIONS_FORM_INITIAL_STATE, action = {}) {
function optionsFormState(state = OPTIONS_FORM_INITIAL_STATE, action = {}) {
const { type, success, message } = action
switch(type) {
switch (type) {
case 'OPTIONS_FORM_SUBMIT_START':
return { ...state, isSubmitting: true }
case 'OPTIONS_FORM_SUBMIT_END':
@ -93,10 +82,10 @@ const INITIAL_STATE_BLOCKLIST_UPDATE_STATUS = {
message: ''
}
function blocklistUpdateStatus( state = INITIAL_STATE_BLOCKLIST_UPDATE_STATUS, action = {} ) {
function blocklistUpdateStatus(state = INITIAL_STATE_BLOCKLIST_UPDATE_STATUS, action = {}) {
const { type, success, message } = action
switch(type) {
switch (type) {
case 'BLOCKLIST_UPDATE_RESET':
return INITIAL_STATE_BLOCKLIST_UPDATE_STATUS
case 'BLOCKLIST_UPDATE_START':
@ -104,7 +93,7 @@ function blocklistUpdateStatus( state = INITIAL_STATE_BLOCKLIST_UPDATE_STATUS, a
case 'BLOCKLIST_UPDATE_END':
return { ...state, isSubmitting: false, pending: null, hasSubmitted: true, success, message }
case 'UPDATE_PENDING_BLOCKLIST':
if(Array.isArray(action.data) || null === action.data) {
if (Array.isArray(action.data) || null === action.data) {
return {
...state,
hasSubmitted: false,
@ -128,12 +117,10 @@ const INITIAL_STATE_UNREGISTERED_CLIENTS_DELETION_STATUS = {
message: ''
}
function unregisteredClientsDeletionStatus(
state = INITIAL_STATE_UNREGISTERED_CLIENTS_DELETION_STATUS,
action = {} ) {
function unregisteredClientsDeletionStatus(state = INITIAL_STATE_UNREGISTERED_CLIENTS_DELETION_STATUS, action = {}) {
const { type, success, message } = action
switch(type) {
switch (type) {
case 'DELETE_UNREGISTERED_CLIENTS_RESET':
return INITIAL_STATE_UNREGISTERED_CLIENTS_DELETION_STATUS
case 'DELETE_UNREGISTERED_CLIENTS_START':
@ -141,7 +128,7 @@ function unregisteredClientsDeletionStatus(
case 'DELETE_UNREGISTERED_CLIENTS_END':
return { ...state, isSubmitting: false, pending: [], hasSubmitted: true, success, message }
case 'UPDATE_PENDING_UNREGISTERED_CLIENTS_FOR_DELETION':
if( Array.isArray(action.data) ) {
if (Array.isArray(action.data)) {
return { ...state, hasSubmitted: false, pending: action.data, success: false, message: '' }
} else {
return state
@ -154,9 +141,9 @@ function unregisteredClientsDeletionStatus(
function pendingOptions(state = {}, action = {}) {
const { type, change, activeKitToken, concreteVersion } = action
switch(type) {
switch (type) {
case 'ADD_PENDING_OPTION':
return {...state, ...change}
return { ...state, ...change }
case 'RESET_PENDING_OPTION':
const option = Object.keys(change)[0]
return omit(state, option)
@ -173,16 +160,16 @@ function pendingOptions(state = {}, action = {}) {
function preferenceConflicts(state = {}, action = {}) {
const { type } = action
switch(type) {
switch (type) {
case 'OPTIONS_FORM_SUBMIT_END':
if ( ! action.success ) {
if (!action.success) {
return state
}
const conflicts = get(action, 'data.conflicts')
if(!! conflicts) {
if (!!conflicts) {
return coerceEmptyArrayToEmptyObject(conflicts)
} else {
return coerceEmptyArrayToEmptyObject(state)
@ -200,16 +187,16 @@ function preferenceConflictDetection(
message: ''
},
action = {}
){
) {
const { type, success, message } = action
switch(type) {
switch (type) {
case 'PREFERENCE_CHECK_START':
return { ...state, isChecking: true }
case 'PREFERENCE_CHECK_END':
return { ...state, isChecking: false, hasChecked: true, success, message }
case 'OPTIONS_FORM_SUBMIT_END':
return { ...state, isChecking: false, hasChecked: false, success: false, message: ''}
return { ...state, isChecking: false, hasChecked: false, success: false, message: '' }
default:
return state
}
@ -222,10 +209,11 @@ function kitsQueryStatus(
isSubmitting: false,
message: ''
},
action = {}) {
action = {}
) {
const { type, success, message } = action
switch(type) {
switch (type) {
case 'KITS_QUERY_START':
return { ...state, isSubmitting: true }
case 'KITS_QUERY_END':
@ -235,11 +223,11 @@ function kitsQueryStatus(
}
}
function kits( state = [], action = {} ) {
function kits(state = [], action = {}) {
const { type, data, success } = action
switch(type) {
switch (type) {
case 'KITS_QUERY_END':
if(success) {
if (success) {
return get(data, 'me.kits', [])
} else {
return state
@ -252,7 +240,7 @@ function kits( state = [], action = {} ) {
function pendingOptionConflicts(state = {}, action = {}) {
const { type, detectedConflicts = {} } = action
switch(type) {
switch (type) {
case 'PREFERENCE_CHECK_END':
return { ...detectedConflicts }
case 'OPTIONS_FORM_SUBMIT_END':
@ -264,36 +252,36 @@ function pendingOptionConflicts(state = {}, action = {}) {
}
}
function detectConflictsUntil( state = 0, action = {} ) {
function detectConflictsUntil(state = 0, action = {}) {
const { type, data } = action
const intValue = parseInt( get(data, 'detectConflictsUntil') )
const intValue = parseInt(get(data, 'detectConflictsUntil'))
switch(type) {
switch (type) {
case 'ENABLE_CONFLICT_DETECTION_SCANNER_END':
case 'DISABLE_CONFLICT_DETECTION_SCANNER_END':
if(action.success && null !== data ) {
if (action.success && null !== data) {
return isNaN(intValue) ? 0 : intValue
} else {
return state
}
default:
const initialIntValue = parseInt( state )
const initialIntValue = parseInt(state)
return isNaN(initialIntValue) ? 0 : initialIntValue
}
}
function unregisteredClients( state = {}, action = {} ) {
function unregisteredClients(state = {}, action = {}) {
const { type, data } = action
switch(type) {
switch (type) {
case 'CONFLICT_DETECTION_SUBMIT_END':
if( action.success && null !== data ) {
if (action.success && null !== data) {
return coerceEmptyArrayToEmptyObject(data)
} else {
return coerceEmptyArrayToEmptyObject(state)
}
case 'BLOCKLIST_UPDATE_END':
if(action.success && Array.isArray(data)) {
if (action.success && Array.isArray(data)) {
const updatedState = Object.keys(state).reduce(
(acc, md5) => {
acc[md5].blocked = !!~data.indexOf(md5)
@ -306,7 +294,7 @@ function unregisteredClients( state = {}, action = {} ) {
return coerceEmptyArrayToEmptyObject(state)
}
case 'DELETE_UNREGISTERED_CLIENTS_END':
if(action.success && !!data) {
if (action.success && !!data) {
return data
} else {
return coerceEmptyArrayToEmptyObject(state)
@ -327,11 +315,11 @@ function unregisteredClientDetectionStatus(
recentConflictsDetected: {},
message: ''
},
action = {}) {
action = {}
) {
const { type, success, message, unregisteredClientsBeforeDetection, recentConflictsDetected } = action
switch(type) {
switch (type) {
case 'CONFLICT_DETECTION_SUBMIT_START':
return { ...state, isSubmitting: true, unregisteredClientsBeforeDetection, recentConflictsDetected }
case 'CONFLICT_DETECTION_SUBMIT_END':
@ -358,11 +346,11 @@ function conflictDetectionScannerStatus(
success: false,
message: ''
},
action = {}) {
action = {}
) {
const { type, success, message } = action
switch(type) {
switch (type) {
case 'ENABLE_CONFLICT_DETECTION_SCANNER_START':
case 'DISABLE_CONFLICT_DETECTION_SCANNER_START':
return { ...state, hasSubmitted: false, success: false, isSubmitting: true }
@ -374,48 +362,17 @@ function conflictDetectionScannerStatus(
}
}
function v3DeprecationWarningStatus(
state = {
isSubmitting: false,
hasSubmitted: false,
success: false,
message: ''
},
action = {}) {
const { type, success, message } = action
switch(type) {
case 'SNOOZE_V3DEPRECATION_WARNING_START':
return { ...state, isSubmitting: true, hasSubmitted: true }
case 'SNOOZE_V3DEPRECATION_WARNING_END':
return { ...state, isSubmitting: false, success, message }
default:
return state
}
}
function v3DeprecationWarning(state = {}, action = {}) {
const { type, snooze = false } = action
switch(type) {
case 'SNOOZE_V3DEPRECATION_WARNING_END':
return { ...state, snooze }
default:
return state
}
}
function showConflictDetectionReporter(state = false, action = {}) {
const { type } = action
switch(type) {
switch (type) {
case 'ENABLE_CONFLICT_DETECTION_SCANNER_END':
return action.success
case 'DISABLE_CONFLICT_DETECTION_SCANNER_END':
// If we failed trying to disable the scanner, then it should remain
// visible to present the error state. If we succeeded, then we could
// stop showing it.
return ! action.success
return !action.success
case 'CONFLICT_DETECTION_TIMER_EXPIRED':
return false
default:
@ -426,7 +383,7 @@ function showConflictDetectionReporter(state = false, action = {}) {
function userAttemptedToStopScanner(state = false, action = {}) {
const { type } = action
switch(type) {
switch (type) {
case 'USER_STOP_SCANNER':
return true
case 'ENABLE_CONFLICT_DETECTION_SCANNER_START':
@ -440,7 +397,7 @@ function userAttemptedToStopScanner(state = false, action = {}) {
function activeAdminTab(state = ADMIN_TAB_SETTINGS, action = {}) {
const { type, tab } = action
switch(type) {
switch (type) {
case 'SET_ACTIVE_ADMIN_TAB':
return tab
default:
@ -448,12 +405,15 @@ function activeAdminTab(state = ADMIN_TAB_SETTINGS, action = {}) {
}
}
function simple(state = {}, _action) { return state }
function simple(state = {}, _action) {
return state
}
export default combineReducers({
activeAdminTab,
apiNonce: simple,
apiUrl: simple,
faApiUrl: simple,
blocklistUpdateStatus,
clientPreferences: coerceEmptyArrayToEmptyObject,
conflictDetectionScannerStatus,
@ -472,18 +432,13 @@ export default combineReducers({
rootUrl: simple,
mainCdnAssetUrl: simple,
mainCdnAssetIntegrity: simple,
enableIconChooser: coerceBool,
releases: simple,
settingsPageUrl: simple,
showAdmin: coerceBool,
showConflictDetectionReporter,
unregisteredClientDetectionStatus,
unregisteredClients,
unregisteredClientsDeletionStatus,
unregisteredClientsDeletionStatus,
userAttemptedToStopScanner,
v3DeprecationWarning,
v3DeprecationWarningStatus,
webpackPublicPath: simple,
isGutenbergPage: coerceBool,
usingCompatJs: coerceBool
webpackPublicPath: simple
})

View File

@ -9,7 +9,7 @@ describe('options', () => {
describe('no action match', () => {
test('return unchanged state', () => {
expect(options({ foo: 42 })).toEqual({foo: 42})
expect(options({ foo: 42 })).toEqual({ foo: 42 })
})
})
@ -21,7 +21,7 @@ describe('options', () => {
usePro: true,
compat: false,
pseudoElements: true,
version: '5.11.2',
version: '5.11.2'
}
}
@ -38,7 +38,7 @@ describe('options', () => {
type: 'OPTIONS_FORM_SUBMIT_END'
}
expect(options({ foo: 42 }, action)).toEqual({foo: 42})
expect(options({ foo: 42 }, action)).toEqual({ foo: 42 })
})
})
})

View File

@ -1,16 +1,16 @@
export async function resetOptions(page) {
return page.evaluate(() => {
const { apiUrl, apiNonce} = window.__FontAwesomeOfficialPlugin__
const { apiUrl, apiNonce } = window.__FontAwesomeOfficialPlugin__
let DEFAULT_OPTIONS = {
options:{
usePro:false,
compat:true,
technology:"webfont",
pseudoElements:true,
kitToken:null,
apiToken:true,
version:"6.0.0-beta3"
options: {
usePro: false,
compat: true,
technology: 'webfont',
pseudoElements: true,
kitToken: null,
apiToken: true,
version: '6.0.0-beta3'
}
}

View File

@ -1,6 +1,6 @@
export const MOCK_UI_MESSAGE = 'mock ui message'
const DEFAULT_REPORT_IMPL = () => MOCK_UI_MESSAGE
const mockDefaultFn = jest.fn( DEFAULT_REPORT_IMPL )
const mockDefaultFn = jest.fn(DEFAULT_REPORT_IMPL)
export const redactRequestData = jest.fn()
export const redactHeaders = jest.fn()

View File

@ -1,21 +1,40 @@
import get from 'lodash/get'
import set from 'lodash/set'
import size from 'lodash/size'
import { get, set, size } from 'lodash'
import { __ } from '@wordpress/i18n'
export const ERROR_REPORT_PREAMBLE = __( 'Font Awesome WordPress Plugin Error Report', 'font-awesome' )
const UI_MESSAGE_DEFAULT = __( 'D\'oh! That failed big time.', 'font-awesome' )
const ERROR_REPORTING_ERROR = __( 'There was an error attempting to report the error.', 'font-awesome' )
const REST_NO_ROUTE_ERROR = __( 'Oh no! Your web browser could not reach your WordPress server.', 'font-awesome' )
const REST_COOKIE_INVALID_NONCE_ERROR = __( 'It looks like your web browser session expired. Try logging out and log back in to WordPress admin.', 'font-awesome' )
const OK_ERROR_PREAMBLE = __( 'The last request was successful, but it also returned the following error(s), which might be helpful for troubleshooting.', 'font-awesome' )
const ONE_OF_MANY_ERRORS_GROUP_LABEL = __( 'Error', 'font-awesome' )
const FALSE_POSITIVE_MESSAGE = __( 'WARNING: The last request contained errors, though your WordPress server reported it as a success. This usually means there\'s a problem with your theme or one of your other plugins emitting output that is causing problems.', 'font-awesome' )
const UNCONFIRMED_RESPONSE_MESSAGE = __( 'WARNING: The last response from your WordPress server did not include the confirmation header that should be in all valid Font Awesome responses. This is a clue that some code from another theme or plugin is acting badly and causing the wrong headers to be sent.', 'font-awesome')
const CONFIRMED_RESPONSE_MESSAGE = __( 'CONFIRMED: The last response from your WordPress server included the confirmation header that is expected for all valid responses from the Font Awesome plugin\'s code running on your WordPress server.', 'font-awesome')
const TRIMMED_RESPONSE_PREAMBLE = __( 'WARNING: Invalid Data Trimmed from Server Response', 'font-awesome' )
const EXPECTED_EMPTY_MESSAGE = __( 'WARNING: We expected the last response from the server to contain no data, but it contained something unexpected.', 'font-awesome' )
const MISSING_ERROR_DATA_MESSAGE = __( 'Your WordPress server returned an error for that last request, but there was no information about the error.', 'font-awesome' )
export const ERROR_REPORT_PREAMBLE = __('Font Awesome WordPress Plugin Error Report', 'font-awesome')
const UI_MESSAGE_DEFAULT = __("D'oh! That failed big time.", 'font-awesome')
const ERROR_REPORTING_ERROR = __('There was an error attempting to report the error.', 'font-awesome')
const REST_NO_ROUTE_ERROR = __('Oh no! Your web browser could not reach your WordPress server.', 'font-awesome')
const REST_COOKIE_INVALID_NONCE_ERROR = __(
'It looks like your web browser session expired. Try logging out and log back in to WordPress admin.',
'font-awesome'
)
const OK_ERROR_PREAMBLE = __(
'The last request was successful, but it also returned the following error(s), which might be helpful for troubleshooting.',
'font-awesome'
)
const ONE_OF_MANY_ERRORS_GROUP_LABEL = __('Error', 'font-awesome')
const FALSE_POSITIVE_MESSAGE = __(
"WARNING: The last request contained errors, though your WordPress server reported it as a success. This usually means there's a problem with your theme or one of your other plugins emitting output that is causing problems.",
'font-awesome'
)
const UNCONFIRMED_RESPONSE_MESSAGE = __(
'WARNING: The last response from your WordPress server did not include the confirmation header that should be in all valid Font Awesome responses. This is a clue that some code from another theme or plugin is acting badly and causing the wrong headers to be sent.',
'font-awesome'
)
const CONFIRMED_RESPONSE_MESSAGE = __(
"CONFIRMED: The last response from your WordPress server included the confirmation header that is expected for all valid responses from the Font Awesome plugin's code running on your WordPress server.",
'font-awesome'
)
const TRIMMED_RESPONSE_PREAMBLE = __('WARNING: Invalid Data Trimmed from Server Response', 'font-awesome')
const EXPECTED_EMPTY_MESSAGE = __(
'WARNING: We expected the last response from the server to contain no data, but it contained something unexpected.',
'font-awesome'
)
const MISSING_ERROR_DATA_MESSAGE = __(
'Your WordPress server returned an error for that last request, but there was no information about the error.',
'font-awesome'
)
const REPORT_INFO_PARAM_KEYS = [
'requestMethod',
'responseStatus',
@ -31,8 +50,8 @@ const REPORT_INFO_PARAM_KEYS = [
* This both sends appropriately formatted output to the console via console.info,
* and returns a uiMessage that would be appropriate to display to an admin user.
*/
function handleSingleWpErrorOutput( wpError ) {
if( ! get(wpError, 'code') ) {
function handleSingleWpErrorOutput(wpError) {
if (!get(wpError, 'code')) {
console.info(ERROR_REPORTING_ERROR)
return UI_MESSAGE_DEFAULT
}
@ -41,16 +60,16 @@ function handleSingleWpErrorOutput( wpError ) {
let output = ''
const message = get(wpError, 'message')
if(message) {
if (message) {
output = output.concat(`message: ${message}\n`)
uiMessage = message
}
const code = get(wpError, 'code')
if(code) {
if (code) {
output = output.concat(`code: ${code}\n`)
switch(code) {
switch (code) {
case 'rest_no_route':
uiMessage = REST_NO_ROUTE_ERROR
break
@ -66,30 +85,30 @@ function handleSingleWpErrorOutput( wpError ) {
const data = get(wpError, 'data')
if ( 'string' === typeof data ) {
if ('string' === typeof data) {
output = output.concat(`data: ${data}\n`)
} else {
const status = get(wpError, 'data.status')
if(status) output = output.concat(`status: ${status}\n`)
if (status) output = output.concat(`status: ${status}\n`)
const trace = get(wpError, 'data.trace')
if(trace) output = output.concat(`trace:\n${trace}\n`)
if (trace) output = output.concat(`trace:\n${trace}\n`)
}
if( output && '' !== output ) {
if (output && '' !== output) {
console.info(output)
} else {
console.info(wpError)
}
const request = get(wpError, 'data.request')
if(request) {
if (request) {
console.info(request)
}
const failedRequestMessage = get(wpError, 'data.failedRequestMessage')
if(failedRequestMessage) {
if (failedRequestMessage) {
console.info(failedRequestMessage)
}
@ -97,7 +116,7 @@ function handleSingleWpErrorOutput( wpError ) {
}
function handleAllWpErrorOutput(errorData = {}) {
const wpErrors = Object.keys(errorData.errors || []).map(code => {
const wpErrors = Object.keys(errorData.errors || []).map((code) => {
// get the first error message available for this code
const message = get(errorData, `errors.${code}.0`)
const data = get(errorData, `error_data.${code}`)
@ -109,7 +128,7 @@ function handleAllWpErrorOutput(errorData = {}) {
}
})
if(0 === size(wpErrors)) {
if (0 === size(wpErrors)) {
wpErrors.push({
code: 'fontawesome_unknown_error',
message: ERROR_REPORTING_ERROR
@ -119,41 +138,32 @@ function handleAllWpErrorOutput(errorData = {}) {
const uiMessage = wpErrors.reduce((acc, error) => {
console.group(ONE_OF_MANY_ERRORS_GROUP_LABEL)
const msg = handleSingleWpErrorOutput( error )
const msg = handleSingleWpErrorOutput(error)
console.groupEnd()
// The uiMessage we should return will be the first error message that isn't
// from a 'previous_exception'
return (!acc && error.code !== 'previous_exception')
? msg
: acc
return !acc && error.code !== 'previous_exception' ? msg : acc
}, null)
return uiMessage
}
function report(params) {
const {
error = null,
ok = false,
falsePositive = false,
confirmed = false,
expectEmpty = false,
trimmed = ''
} = params
const { error = null, ok = false, falsePositive = false, confirmed = false, expectEmpty = false, trimmed = '' } = params
console.group(ERROR_REPORT_PREAMBLE)
if( ok ) {
if (ok) {
console.info(OK_ERROR_PREAMBLE)
}
if( falsePositive ) {
if (falsePositive) {
console.info(FALSE_POSITIVE_MESSAGE)
}
if( confirmed ) {
if (confirmed) {
console.info(CONFIRMED_RESPONSE_MESSAGE)
} else {
console.info(UNCONFIRMED_RESPONSE_MESSAGE)
@ -162,18 +172,18 @@ function report(params) {
// Strings to later join with newlines, making a report.
const info = []
for(const key of REPORT_INFO_PARAM_KEYS) {
for (const key of REPORT_INFO_PARAM_KEYS) {
const val = get(params, key)
if('undefined' !== typeof val) {
if ('undefined' !== typeof val) {
const valType = typeof val
if('string' === valType || 'number' === valType) {
if ('string' === valType || 'number' === valType) {
info.push(`${key}: ${val}`)
} else if ('object' === valType){
} else if ('object' === valType) {
info.push(`${key}:`)
for(const innerKey in val) {
for (const innerKey in val) {
info.push(`\t${innerKey}: ${val[innerKey].toString()}`)
}
} else {
@ -182,24 +192,22 @@ function report(params) {
}
}
if(size(info) > 0) {
if (size(info) > 0) {
console.info(`Extra Info:\n${info.join('\n')}`)
}
if( '' !== trimmed ) {
if ('' !== trimmed) {
console.group(TRIMMED_RESPONSE_PREAMBLE)
if( expectEmpty ) {
if (expectEmpty) {
console.info(EXPECTED_EMPTY_MESSAGE)
}
console.info(trimmed)
console.groupEnd()
}
const uiMessage = null !== error
? handleAllWpErrorOutput( error )
: null
const uiMessage = null !== error ? handleAllWpErrorOutput(error) : null
if ( error && trimmed === '' && confirmed ) {
if (error && trimmed === '' && confirmed) {
console.info(MISSING_ERROR_DATA_MESSAGE)
}
@ -215,7 +223,7 @@ export function redactRequestData(response = {}) {
let redacted = ''
if('application/json' === requestContentType) {
if ('application/json' === requestContentType) {
try {
const data = JSON.parse(requestData)
const apiTokenValue = get(data, 'options.apiToken')
@ -227,12 +235,12 @@ export function redactRequestData(response = {}) {
* that boolean. It's useful to leave it so the error report indicates whether an
* apiToken has been successfully saved.
*/
if('boolean' !== typeof apiTokenValue) {
if ('boolean' !== typeof apiTokenValue) {
set(data, 'options.apiToken', 'REDACTED')
}
redacted = JSON.stringify(data)
} catch(e) {
} catch (e) {
redacted = `ERROR while redacting request data: ${e.toString()}`
}
@ -243,10 +251,10 @@ export function redactRequestData(response = {}) {
}
export function redactHeaders(headers = {}) {
const redacted = {...headers}
const redacted = { ...headers }
for(const key in redacted) {
if('x-wp-nonce' === key.toLowerCase()) {
for (const key in redacted) {
if ('x-wp-nonce' === key.toLowerCase()) {
redacted[key] = 'REDACTED'
}
}

View File

@ -6,12 +6,13 @@ console.info = jest.fn()
const SINGLE_EXCEPTION_ERROR = {
errors: {
fontawesome_client_exception: ["Whoops, it looks like that API Token is not valid. Try another one?"]
fontawesome_client_exception: ['Whoops, it looks like that API Token is not valid. Try another one?']
},
error_data: {
fontawesome_client_exception: {
status:400,
trace:"#0 \/var\/www\/html\/wp-content\/plugins\/font-awesome\/includes\/class-fontawesome-api-settings.php(311): FortAwesome\\FontAwesome_Exception::with_wp_response(Array)\n#1 \/var\/www\/html\/wp-content\/plugins\/font-awesome\/includes\/class-fontawesome-config-controller.php(115): FortAwesome\\FontAwesome_API_Settings->request_access_token()\n#2 \/var\/www\/html\/wp-includes\/rest-api\/class-wp-rest-server.php(946): FortAwesome\\FontAwesome_Config_Controller->update_item(Object(WP_REST_Request))\n#3 \/var\/www\/html\/wp-includes\/rest-api\/class-wp-rest-server.php(329): WP_REST_Server->dispatch(Object(WP_REST_Request))\n#4 \/var\/www\/html\/wp-includes\/rest-api.php(305): WP_REST_Server->serve_request('\/font-awesome\/v...')\n#5 \/var\/www\/html\/wp-includes\/class-wp-hook.php(288): rest_api_loaded(Object(WP))\n#6 \/var\/www\/html\/wp-includes\/class-wp-hook.php(312): WP_Hook->apply_filters('', Array)\n#7 \/var\/www\/html\/wp-includes\/plugin.php(544): WP_Hook->do_action(Array)\n#8 \/var\/www\/html\/wp-includes\/class-wp.php(387): do_action_ref_array('parse_request', Array)\n#9 \/var\/www\/html\/wp-includes\/class-wp.php(729): WP->parse_request('')\n#10 \/var\/www\/html\/wp-includes\/functions.php(1255): WP->main('')\n#11 \/var\/www\/html\/wp-blog-header.php(16): wp()\n#12 \/var\/www\/html\/index.php(17): require('\/var\/www\/html\/w...')\n#13 {main}"
status: 400,
trace:
"#0 /var/www/html/wp-content/plugins/font-awesome/includes/class-fontawesome-api-settings.php(311): FortAwesome\\FontAwesome_Exception::with_wp_response(Array)\n#1 /var/www/html/wp-content/plugins/font-awesome/includes/class-fontawesome-config-controller.php(115): FortAwesome\\FontAwesome_API_Settings->request_access_token()\n#2 /var/www/html/wp-includes/rest-api/class-wp-rest-server.php(946): FortAwesome\\FontAwesome_Config_Controller->update_item(Object(WP_REST_Request))\n#3 /var/www/html/wp-includes/rest-api/class-wp-rest-server.php(329): WP_REST_Server->dispatch(Object(WP_REST_Request))\n#4 /var/www/html/wp-includes/rest-api.php(305): WP_REST_Server->serve_request('/font-awesome/v...')\n#5 /var/www/html/wp-includes/class-wp-hook.php(288): rest_api_loaded(Object(WP))\n#6 /var/www/html/wp-includes/class-wp-hook.php(312): WP_Hook->apply_filters('', Array)\n#7 /var/www/html/wp-includes/plugin.php(544): WP_Hook->do_action(Array)\n#8 /var/www/html/wp-includes/class-wp.php(387): do_action_ref_array('parse_request', Array)\n#9 /var/www/html/wp-includes/class-wp.php(729): WP->parse_request('')\n#10 /var/www/html/wp-includes/functions.php(1255): WP->main('')\n#11 /var/www/html/wp-blog-header.php(16): wp()\n#12 /var/www/html/index.php(17): require('/var/www/html/w...')\n#13 {main}"
}
}
}
@ -32,7 +33,6 @@ describe('reportRequestError', () => {
})
describe('with single fontawesome_client_exception', () => {
test('emits console report and returns uiMessage from given error', () => {
const message = reportRequestError({ error: SINGLE_EXCEPTION_ERROR })
@ -41,20 +41,12 @@ describe('reportRequestError', () => {
expect(console.group).toHaveBeenCalledTimes(2)
expect(console.info).toHaveBeenCalled()
expect(console.info).toHaveBeenCalledWith(
expect.stringMatching(/message: Whoops/),
)
expect(console.info).toHaveBeenCalledWith(expect.stringMatching(/message: Whoops/))
expect(console.info).toHaveBeenCalledWith(
expect.stringMatching(/trace:/)
)
expect(console.info).toHaveBeenCalledWith(expect.stringMatching(/trace:/))
expect(console.info).toHaveBeenCalledWith(
expect.stringMatching(/status:/),
)
expect(console.info).toHaveBeenCalledWith(
expect.stringMatching(/code: fontawesome_client_exception/)
)
expect(console.info).toHaveBeenCalledWith(expect.stringMatching(/status:/))
expect(console.info).toHaveBeenCalledWith(expect.stringMatching(/code: fontawesome_client_exception/))
expect(console.groupEnd).toHaveBeenCalledTimes(2)
})
@ -63,17 +55,19 @@ describe('reportRequestError', () => {
describe('when PreferenceRegistrationException is thrown with a previous exception', () => {
const error = {
errors: {
fontawesome_server_exception: ["A theme or plugin registered with Font Awesome threw an exception."],
previous_exception: ["epsilon-plugin throwing"]
fontawesome_server_exception: ['A theme or plugin registered with Font Awesome threw an exception.'],
previous_exception: ['epsilon-plugin throwing']
},
error_data: {
fontawesome_server_exception: {
status:500,
trace:"#0 \/var\/www\/html\/wp-content\/plugins\/font-awesome\/includes\/class-fontawesome.php(1057): FortAwesome\\FontAwesome_Exception::with_thrown(Object(Exception))\n#1 \/var\/www\/html\/wp-content\/plugins\/font-awesome\/includes\/class-fontawesome-config-controller.php(80): FortAwesome\\FontAwesome->gather_preferences()\n#2 \/var\/www\/html\/wp-content\/plugins\/font-awesome\/includes\/class-fontawesome-config-controller.php(134): FortAwesome\\FontAwesome_Config_Controller->build_item(Object(FortAwesome\\FontAwesome))\n#3 \/var\/www\/html\/wp-includes\/rest-api\/class-wp-rest-server.php(946): FortAwesome\\FontAwesome_Config_Controller->update_item(Object(WP_REST_Request))\n#4 \/var\/www\/html\/wp-includes\/rest-api\/class-wp-rest-server.php(329): WP_REST_Server->dispatch(Object(WP_REST_Request))\n#5 \/var\/www\/html\/wp-includes\/rest-api.php(305): WP_REST_Server->serve_request('\/font-awesome\/v...')\n#6 \/var\/www\/html\/wp-includes\/class-wp-hook.php(288): rest_api_loaded(Object(WP))\n#7 \/var\/www\/html\/wp-includes\/class-wp-hook.php(312): WP_Hook->apply_filters('', Array)\n#8 \/var\/www\/html\/wp-includes\/plugin.php(544): WP_Hook->do_action(Array)\n#9 \/var\/www\/html\/wp-includes\/class-wp.php(387): do_action_ref_array('parse_request', Array)\n#10 \/var\/www\/html\/wp-includes\/class-wp.php(729): WP->parse_request('')\n#11 \/var\/www\/html\/wp-includes\/functions.php(1255): WP->main('')\n#12 \/var\/www\/html\/wp-blog-header.php(16): wp()\n#13 \/var\/www\/html\/index.php(17): require('\/var\/www\/html\/w...')\n#14 {main}"
status: 500,
trace:
"#0 /var/www/html/wp-content/plugins/font-awesome/includes/class-fontawesome.php(1057): FortAwesome\\FontAwesome_Exception::with_thrown(Object(Exception))\n#1 /var/www/html/wp-content/plugins/font-awesome/includes/class-fontawesome-config-controller.php(80): FortAwesome\\FontAwesome->gather_preferences()\n#2 /var/www/html/wp-content/plugins/font-awesome/includes/class-fontawesome-config-controller.php(134): FortAwesome\\FontAwesome_Config_Controller->build_item(Object(FortAwesome\\FontAwesome))\n#3 /var/www/html/wp-includes/rest-api/class-wp-rest-server.php(946): FortAwesome\\FontAwesome_Config_Controller->update_item(Object(WP_REST_Request))\n#4 /var/www/html/wp-includes/rest-api/class-wp-rest-server.php(329): WP_REST_Server->dispatch(Object(WP_REST_Request))\n#5 /var/www/html/wp-includes/rest-api.php(305): WP_REST_Server->serve_request('/font-awesome/v...')\n#6 /var/www/html/wp-includes/class-wp-hook.php(288): rest_api_loaded(Object(WP))\n#7 /var/www/html/wp-includes/class-wp-hook.php(312): WP_Hook->apply_filters('', Array)\n#8 /var/www/html/wp-includes/plugin.php(544): WP_Hook->do_action(Array)\n#9 /var/www/html/wp-includes/class-wp.php(387): do_action_ref_array('parse_request', Array)\n#10 /var/www/html/wp-includes/class-wp.php(729): WP->parse_request('')\n#11 /var/www/html/wp-includes/functions.php(1255): WP->main('')\n#12 /var/www/html/wp-blog-header.php(16): wp()\n#13 /var/www/html/index.php(17): require('/var/www/html/w...')\n#14 {main}"
},
previous_exception: {
status:500,
trace: "#0 \/var\/www\/html\/wp-includes\/class-wp-hook.php(288): {closure}('')\n#1 \/var\/www\/html\/wp-includes\/class-wp-hook.php(312): WP_Hook->apply_filters('', Array)\n#2 \/var\/www\/html\/wp-includes\/plugin.php(478): WP_Hook->do_action(Array)\n#3 \/var\/www\/html\/wp-content\/plugins\/font-awesome\/includes\/class-fontawesome.php(1055): do_action('font_awesome_pr...')\n#4 \/var\/www\/html\/wp-content\/plugins\/font-awesome\/includes\/class-fontawesome-config-controller.php(80): FortAwesome\\FontAwesome->gather_preferences()\n#5 \/var\/www\/html\/wp-content\/plugins\/font-awesome\/includes\/class-fontawesome-config-controller.php(134): FortAwesome\\FontAwesome_Config_Controller->build_item(Object(FortAwesome\\FontAwesome))\n#6 \/var\/www\/html\/wp-includes\/rest-api\/class-wp-rest-server.php(946): FortAwesome\\FontAwesome_Config_Controller->update_item(Object(WP_REST_Request))\n#7 \/var\/www\/html\/wp-includes\/rest-api\/class-wp-rest-server.php(329): WP_REST_Server->dispatch(Object(WP_REST_Request))\n#8 \/var\/www\/html\/wp-includes\/rest-api.php(305): WP_REST_Server->serve_request('\/font-awesome\/v...')\n#9 \/var\/www\/html\/wp-includes\/class-wp-hook.php(288): rest_api_loaded(Object(WP))\n#10 \/var\/www\/html\/wp-includes\/class-wp-hook.php(312): WP_Hook->apply_filters('', Array)\n#11 \/var\/www\/html\/wp-includes\/plugin.php(544): WP_Hook->do_action(Array)\n#12 \/var\/www\/html\/wp-includes\/class-wp.php(387): do_action_ref_array('parse_request', Array)\n#13 \/var\/www\/html\/wp-includes\/class-wp.php(729): WP->parse_request('')\n#14 \/var\/www\/html\/wp-includes\/functions.php(1255): WP->main('')\n#15 \/var\/www\/html\/wp-blog-header.php(16): wp()\n#16 \/var\/www\/html\/index.php(17): require('\/var\/www\/html\/w...')\n#17 {main}"
status: 500,
trace:
"#0 /var/www/html/wp-includes/class-wp-hook.php(288): {closure}('')\n#1 /var/www/html/wp-includes/class-wp-hook.php(312): WP_Hook->apply_filters('', Array)\n#2 /var/www/html/wp-includes/plugin.php(478): WP_Hook->do_action(Array)\n#3 /var/www/html/wp-content/plugins/font-awesome/includes/class-fontawesome.php(1055): do_action('font_awesome_pr...')\n#4 /var/www/html/wp-content/plugins/font-awesome/includes/class-fontawesome-config-controller.php(80): FortAwesome\\FontAwesome->gather_preferences()\n#5 /var/www/html/wp-content/plugins/font-awesome/includes/class-fontawesome-config-controller.php(134): FortAwesome\\FontAwesome_Config_Controller->build_item(Object(FortAwesome\\FontAwesome))\n#6 /var/www/html/wp-includes/rest-api/class-wp-rest-server.php(946): FortAwesome\\FontAwesome_Config_Controller->update_item(Object(WP_REST_Request))\n#7 /var/www/html/wp-includes/rest-api/class-wp-rest-server.php(329): WP_REST_Server->dispatch(Object(WP_REST_Request))\n#8 /var/www/html/wp-includes/rest-api.php(305): WP_REST_Server->serve_request('/font-awesome/v...')\n#9 /var/www/html/wp-includes/class-wp-hook.php(288): rest_api_loaded(Object(WP))\n#10 /var/www/html/wp-includes/class-wp-hook.php(312): WP_Hook->apply_filters('', Array)\n#11 /var/www/html/wp-includes/plugin.php(544): WP_Hook->do_action(Array)\n#12 /var/www/html/wp-includes/class-wp.php(387): do_action_ref_array('parse_request', Array)\n#13 /var/www/html/wp-includes/class-wp.php(729): WP->parse_request('')\n#14 /var/www/html/wp-includes/functions.php(1255): WP->main('')\n#15 /var/www/html/wp-blog-header.php(16): wp()\n#16 /var/www/html/index.php(17): require('/var/www/html/w...')\n#17 {main}"
}
}
}
@ -84,17 +78,11 @@ describe('reportRequestError', () => {
expect(console.group).toHaveBeenCalledTimes(3)
expect(console.groupEnd).toHaveBeenCalledTimes(3)
expect(console.info).toHaveBeenCalledWith(
expect.stringMatching(/code: fontawesome_server_exception/)
)
expect(console.info).toHaveBeenCalledWith(expect.stringMatching(/code: fontawesome_server_exception/))
expect(console.info).toHaveBeenCalledWith(
expect.stringMatching(/code: previous_exception/)
)
expect(console.info).toHaveBeenCalledWith(expect.stringMatching(/code: previous_exception/))
expect(console.info).toHaveBeenCalledWith(
expect.stringMatching(/The last request was successful/)
)
expect(console.info).toHaveBeenCalledWith(expect.stringMatching(/The last request was successful/))
expect(message).toMatch(/^A theme or plugin/)
})
@ -114,48 +102,28 @@ describe('reportRequestError', () => {
expect(message).toMatch(/^Whoops/)
// The top-level group, and then one error group, then one for the trimmed content
expect(console.group).toHaveBeenCalledTimes(3)
expect(console.group).toHaveBeenCalledWith(
expect.stringMatching(/Error Report/),
)
expect(console.group).toHaveBeenCalledWith(
expect.stringMatching(/Trimmed/),
)
expect(console.group).toHaveBeenCalledWith(expect.stringMatching(/Error Report/))
expect(console.group).toHaveBeenCalledWith(expect.stringMatching(/Trimmed/))
expect(console.groupEnd).toHaveBeenCalledTimes(3)
expect(console.info).toHaveBeenCalledTimes(4)
expect(console.info).toHaveBeenCalledWith(
expect.stringMatching(/reported it as a success/),
)
expect(console.info).toHaveBeenCalledWith(expect.stringMatching(/reported it as a success/))
expect(console.info).toHaveBeenCalledWith(
expect.stringMatching(/This is a clue/),
)
expect(console.info).toHaveBeenCalledWith(expect.stringMatching(/This is a clue/))
expect(console.info).toHaveBeenCalledWith(
expect.stringMatching(/message: Whoops/),
)
expect(console.info).toHaveBeenCalledWith(expect.stringMatching(/message: Whoops/))
expect(console.info).toHaveBeenCalledWith(
expect.stringMatching(/message: Whoops/),
)
expect(console.info).toHaveBeenCalledWith(expect.stringMatching(/message: Whoops/))
expect(console.info).toHaveBeenCalledWith(
expect.stringMatching(/trace:/)
)
expect(console.info).toHaveBeenCalledWith(expect.stringMatching(/trace:/))
expect(console.info).toHaveBeenCalledWith(
expect.stringMatching(/status:/),
)
expect(console.info).toHaveBeenCalledWith(expect.stringMatching(/status:/))
expect(console.info).toHaveBeenCalledWith(
expect.stringMatching(/code: fontawesome_client_exception/)
)
expect(console.info).toHaveBeenCalledWith(expect.stringMatching(/code: fontawesome_client_exception/))
expect(console.info).toHaveBeenCalledWith(
expect.stringContaining(TRIMMED)
)
expect(console.info).toHaveBeenCalledWith(expect.stringContaining(TRIMMED))
})
})
@ -172,21 +140,15 @@ describe('reportRequestError', () => {
expect(message).toBeNull()
// The top-level group, and then one for the trimmed content
expect(console.group).toHaveBeenCalledTimes(2)
expect(console.group).toHaveBeenCalledWith(
expect.stringMatching(/Trimmed/),
)
expect(console.group).toHaveBeenCalledWith(expect.stringMatching(/Trimmed/))
expect(console.groupEnd).toHaveBeenCalledTimes(2)
expect(console.info).toHaveBeenCalled()
expect(console.info).toHaveBeenCalledWith(
expect.stringMatching(/contain no data/),
)
expect(console.info).toHaveBeenCalledWith(expect.stringMatching(/contain no data/))
expect(console.info).toHaveBeenCalledWith(
expect.stringContaining(TRIMMED),
)
expect(console.info).toHaveBeenCalledWith(expect.stringContaining(TRIMMED))
})
})
@ -203,9 +165,7 @@ describe('reportRequestError', () => {
expect(console.info).toHaveBeenCalledTimes(1)
expect(console.info).toHaveBeenCalledWith(
expect.stringMatching(/did not include the confirmation header/),
)
expect(console.info).toHaveBeenCalledWith(expect.stringMatching(/did not include the confirmation header/))
})
})
@ -214,7 +174,7 @@ describe('reportRequestError', () => {
const code = 'fontawesome_request_noresponse'
const error = {
errors: {
[code]: [ 'no response' ]
[code]: ['no response']
},
error_data: {
[code]: { request: new XMLHttpRequest() }
@ -239,7 +199,7 @@ describe('reportRequestError', () => {
const code = 'fontawesome_request_failed'
const error = {
errors: {
[code]: [ 'ui failure message' ]
[code]: ['ui failure message']
},
error_data: {
[code]: { failedRequestMessage: 'failure console message' }
@ -257,9 +217,7 @@ describe('reportRequestError', () => {
expect(console.info).toHaveBeenCalled()
expect(console.info).toHaveBeenCalledWith(
expect.stringMatching(/failure console message/)
)
expect(console.info).toHaveBeenCalledWith(expect.stringMatching(/failure console message/))
})
})
})
@ -272,11 +230,11 @@ describe('redactRequestData', () => {
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify({options: {foo: 42, apiToken: 'abc123'}})
},
data: JSON.stringify({ options: { foo: 42, apiToken: 'abc123' } })
}
}
expect(redactRequestData(response)).toEqual(JSON.stringify({options: {foo: 42, apiToken: 'REDACTED'}}))
expect(redactRequestData(response)).toEqual(JSON.stringify({ options: { foo: 42, apiToken: 'REDACTED' } }))
})
})
@ -287,11 +245,11 @@ describe('redactRequestData', () => {
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify({options: {foo: 42, apiToken: true}})
},
data: JSON.stringify({ options: { foo: 42, apiToken: true } })
}
}
expect(redactRequestData(response)).toEqual(JSON.stringify({options: {foo: 42, apiToken: true}}))
expect(redactRequestData(response)).toEqual(JSON.stringify({ options: { foo: 42, apiToken: true } }))
})
})
@ -302,25 +260,31 @@ describe('redactRequestData', () => {
headers: {
'Content-Type': 'text/plain'
},
data: JSON.stringify({options: {foo: 42, beta: 43}})
},
data: JSON.stringify({ options: { foo: 42, beta: 43 } })
}
}
expect(redactRequestData(response)).toEqual(JSON.stringify({options: {foo: 42, beta: 43}}))
expect(redactRequestData(response)).toEqual(JSON.stringify({ options: { foo: 42, beta: 43 } }))
})
})
})
describe('redactHeaders', () => {
test('when x-wp-nonce is present', () => {
expect(redactHeaders({
'X-WP-NONCE': 'abc123'
})).toEqual({'X-WP-NONCE': 'REDACTED'})
expect(redactHeaders({
'x-wp-nonce': 'abc123'
})).toEqual({'x-wp-nonce': 'REDACTED'})
expect(redactHeaders({
'X-WP-Nonce': 'abc123'
})).toEqual({'X-WP-Nonce': 'REDACTED'})
expect(
redactHeaders({
'X-WP-NONCE': 'abc123'
})
).toEqual({ 'X-WP-NONCE': 'REDACTED' })
expect(
redactHeaders({
'x-wp-nonce': 'abc123'
})
).toEqual({ 'x-wp-nonce': 'REDACTED' })
expect(
redactHeaders({
'X-WP-Nonce': 'abc123'
})
).toEqual({ 'X-WP-Nonce': 'REDACTED' })
})
})

View File

@ -1,31 +1,29 @@
function findJson( content, start = 0 ) {
function findJson(content, start = 0) {
let parsed = null
let nextStart = null
if ( 'string' !== typeof content ) return null
if ( start >= content.length ) return null
if ('string' !== typeof content) return null
if (start >= content.length) return null
try {
parsed = JSON.parse( content.slice(start) )
parsed = JSON.parse(content.slice(start))
return {
start,
parsed
}
} catch( _e ) {
} catch (_e) {
// search for the next character that would begin a JSON response
const nextLeftBracket = content.indexOf('[', start + 1)
const nextLeftBrace = content.indexOf('{', start + 1)
if( -1 === nextLeftBracket && -1 === nextLeftBrace ) {
if (-1 === nextLeftBracket && -1 === nextLeftBrace) {
// we've search to the end and found no chars that would start JSON content
return null
} else {
if ( -1 !== nextLeftBracket && -1 !== nextLeftBrace ) {
if (-1 !== nextLeftBracket && -1 !== nextLeftBrace) {
// if we found both, take the lower one
nextStart = nextLeftBracket < nextLeftBrace
? nextLeftBracket
: nextLeftBrace
} else if ( -1 !== nextLeftBrace ) {
nextStart = nextLeftBracket < nextLeftBrace ? nextLeftBracket : nextLeftBrace
} else if (-1 !== nextLeftBrace) {
nextStart = nextLeftBrace
} else {
nextStart = nextLeftBracket
@ -33,29 +31,29 @@ function findJson( content, start = 0 ) {
}
}
if ( null === nextStart ) {
if (null === nextStart) {
return null
} else {
return findJson( content, nextStart )
return findJson(content, nextStart)
}
}
/**
* Searches through the given content trying to skip over any non-JSON string
* data to find JSON data.
*
*
* Returns null if none found.
*
*
* Otherwise, returns an object indicating the starting index for the found JSON,
* the json content as an unparsed string, the non-json content trimmed from the
* beginning, and the parsed JSON.
*/
function sliceJson( content ) {
if(! content || '' === content ) return null
function sliceJson(content) {
if (!content || '' === content) return null
const result = findJson( content )
const result = findJson(content)
if ( null === result ) {
if (null === result) {
return null
} else {
const { start, parsed } = result

View File

@ -3,84 +3,94 @@ import sliceJson from './sliceJson'
describe('sliceJson', () => {
test('it works', () => {
const json = '{"alpha":42}'
const result = sliceJson( json )
const result = sliceJson(json)
expect(result).toEqual( expect.objectContaining({
start: 0,
json,
trimmed: '',
parsed: expect.objectContaining( JSON.parse( json ) )
}))
expect(result).toEqual(
expect.objectContaining({
start: 0,
json,
trimmed: '',
parsed: expect.objectContaining(JSON.parse(json))
})
)
})
test('when invalid content precedes json object', () => {
const trimmed = 'foobar '
const json = '{"alpha":42}'
const content = `${trimmed}${json}`
const result = sliceJson( content )
const result = sliceJson(content)
expect(result).toEqual( expect.objectContaining({
start: 7,
json,
trimmed,
parsed: expect.objectContaining( JSON.parse( json ) )
}))
expect(result).toEqual(
expect.objectContaining({
start: 7,
json,
trimmed,
parsed: expect.objectContaining(JSON.parse(json))
})
)
})
test('when invalid content precedes json array', () => {
const trimmed = 'foobar '
const json = '[1,2,3]'
const content = `${trimmed}${json}`
const result = sliceJson( content )
const result = sliceJson(content)
expect(result).toEqual( expect.objectContaining({
start: 7,
json,
trimmed,
parsed: expect.objectContaining( JSON.parse( json ) )
}))
expect(result).toEqual(
expect.objectContaining({
start: 7,
json,
trimmed,
parsed: expect.objectContaining(JSON.parse(json))
})
)
})
test('when invalid content with brackets and braces precedes json object', () => {
const trimmed = 'foobar[42] baz{blue}'
const json = '{"alpha":42}'
const content = `${trimmed}${json}`
const result = sliceJson( content )
const result = sliceJson(content)
expect(result).toEqual( expect.objectContaining({
start: 20,
json,
trimmed,
parsed: expect.objectContaining( JSON.parse( json ) )
}))
expect(result).toEqual(
expect.objectContaining({
start: 20,
json,
trimmed,
parsed: expect.objectContaining(JSON.parse(json))
})
)
})
test('when invalid content with brackets and braces precedes json array', () => {
const trimmed = 'foobar[42] baz{blue}'
const json = '[1,2,3]'
const content = `${trimmed}${json}`
const result = sliceJson( content )
const result = sliceJson(content)
expect(result).toEqual( expect.objectContaining({
start: 20,
json,
trimmed,
parsed: expect.objectContaining( JSON.parse( json ) )
}))
expect(result).toEqual(
expect.objectContaining({
start: 20,
json,
trimmed,
parsed: expect.objectContaining(JSON.parse(json))
})
)
})
test('when invalid content comes before and after valid json', () => {
const invalid = 'foobar[42] baz{blue}'
const json = '[1,2,3]'
const content = `${invalid}${json}${invalid}`
const result = sliceJson( content )
const result = sliceJson(content)
expect(result).toBeNull()
})
test('when no valid json is found', () => {
const invalid = 'foobar[42] baz{blue}'
const result = sliceJson( invalid )
const result = sliceJson(invalid)
expect(result).toBeNull()
})

View File

@ -1,17 +1,15 @@
const defaultConfig = require("@wordpress/scripts/config/webpack.config");
const get = require("lodash/get");
const { basename, dirname } = require("path");
const defaultConfig = require('@wordpress/scripts/config/webpack.config')
const get = require('lodash/get')
const { basename, dirname } = require('path')
/**
* There have apparently been problems with webpack chunk loading and caches.
* So we should add hashes to the chunk file names.
* See: https://wordpress.org/support/topic/plugin-settings-page-is-empty-2/#post-14855904
*/
const miniCssExtractPlugin = defaultConfig.plugins.find((p) =>
"MiniCssExtractPlugin" === p.constructor.name
);
miniCssExtractPlugin.options.filename = "[name]-[contenthash].css";
miniCssExtractPlugin.options.chunkFilename = "[name]-[chunkhash].css";
const miniCssExtractPlugin = defaultConfig.plugins.find((p) => 'MiniCssExtractPlugin' === p.constructor.name)
miniCssExtractPlugin.options.filename = '[name]-[contenthash].css'
miniCssExtractPlugin.options.chunkFilename = '[name]-[chunkhash].css'
// After updating to @wordpress/scripts wp-6.5, when using --webpack-no-externals
// there was an error:
@ -35,112 +33,13 @@ miniCssExtractPlugin.options.chunkFilename = "[name]-[chunkhash].css";
//
// Using the empty string here instead of null seems to result in that asset still
// loading as expected and styling everything as expected.
defaultConfig.optimization.splitChunks.cacheGroups.style.name = (
_,
chunks,
cacheGroupKey,
) => {
const chunkName = chunks[0].name || "";
return `${
dirname(
chunkName,
)
}/${cacheGroupKey}-${basename(chunkName)}`;
};
defaultConfig.optimization.splitChunks.cacheGroups.style.name = (_, chunks, cacheGroupKey) => {
const chunkName = chunks[0].name || ''
return `${dirname(chunkName)}/${cacheGroupKey}-${basename(chunkName)}`
}
// This causes the JavaScript chunks to include the chunk hash in the filename,
// which is important for cache busting purposes.
defaultConfig.output.chunkFilename = "[name]-[chunkhash].js";
defaultConfig.output.chunkFilename = '[name]-[chunkhash].js'
defaultConfig.externals = [
/**
* The idea here is that we want packages like react, react-dom,
* and @wordpress/element to remain as externals to all of our own app's
* chunks, and external to all our dependencies that rely on them.
*
* However, we have a compat.js output that we're building too. And in *that*
* bundle, we don't want these things to remain external--that's where we
* actually need to bundle them.
*
* Now, whether we're in WordPress 4, 5, or 6, all of our externals
* are going to be based not on the usual globals like window.React, because
* of the possibility of colliding with other plugins or themes that may
* modify those globals, but on our own globals under __Font_Awesome_Webpack_Externals__.
*
* Our app's index.js should initialize that __Font_Awesome_Webpack_Externals__
* early, before loading any modules that may depend on those externals.
*
* Under WordPress >=5, the wp_enqueue_script of the main admin JS bundle should
* declare the appropriate dependencies, like wp-element. Then our app's
* index.js should just set up those __Font_Awesome_Webpack_Externals__
* globals as copies of the modules that would be available as part of
* WordPress core, like window.wp.element.
*
* If we're running under WordPress 4, then we should be enqueueing compat.js,
* which would load a JavaScript bundle with all of the stuff that would normally
* be there for us in WordPress 5. It should set up that __Font_Awesome_Webpack_Externals__
* global accordingly.
*
* So in any case, whether we're running under WordPress 4, 5, or 6, that global
* will be set up with the appropriate externals.
*
* Then in our app's modules--as long as they are loaded after those globals
* are set up--we can just do things like:
*
* import React from 'react'
*
* or
*
* import { __ } from '@wordpress/i18n'
*
* ...and webpack will replace those imports with the right assignments
* from that global.
*/
function ({ context, request }, callback) {
switch (request) {
case "react":
return callback(null, "root __Font_Awesome_Webpack_Externals__.React");
case "react-dom":
return callback(
null,
"root __Font_Awesome_Webpack_Externals__.ReactDOM",
);
case "@wordpress/i18n":
return callback(null, "root __Font_Awesome_Webpack_Externals__.i18n");
case "@wordpress/api-fetch":
return callback(
null,
"root __Font_Awesome_Webpack_Externals__.apiFetch",
);
case "@wordpress/components":
return callback(
null,
"root __Font_Awesome_Webpack_Externals__.components",
);
case "@wordpress/element":
return callback(
null,
"root __Font_Awesome_Webpack_Externals__.element",
);
case "@wordpress/rich-text":
return callback(
null,
"root __Font_Awesome_Webpack_Externals__.richText",
);
case "@wordpress/block-editor":
return callback(
null,
"root __Font_Awesome_Webpack_Externals__.blockEditor",
);
case "@wordpress/dom-ready":
return callback(
null,
"root __Font_Awesome_Webpack_Externals__.domReady",
);
default:
return callback();
}
},
];
module.exports = defaultConfig;
module.exports = defaultConfig

View File

@ -12,6 +12,7 @@ wp_cli() {
echo "Adding WordPress debug configuration..."
wp_cli config set WP_DEBUG true \
&& wp_cli config set WP_DEBUG_LOG true \
&& wp_cli config set SCRIPT_DEBUG_LOG true \
&& wp_cli config set WP_DEBUG_DISPLAY false \
&& wp_cli config set SAVEQUERIES true \
&& wp_cli plugin install debug-bar \

View File

@ -10,15 +10,19 @@ initialize_dist_dir($dist_dir);
$rc = null;
rcp($plugin_dir . '/includes', $dist_dir . '/includes');
$admin_dist_dir = $dist_dir . '/admin';
if (! file_exists($admin_dist_dir)) mkdir($admin_dist_dir);
copy($plugin_dir . '/admin/index.php', $dist_dir . '/admin/index.php');
rcp($plugin_dir . '/admin/build', $dist_dir . '/admin/build');
rcp($plugin_dir . '/admin/views', $dist_dir . '/admin/views');
$compat_js_dist_dir = $dist_dir . '/compat-js';
if (! file_exists($compat_js_dist_dir)) mkdir($compat_js_dist_dir);
rcp($plugin_dir . '/compat-js/build', $compat_js_dist_dir . '/build');
$js_bundles = [
'admin' => ['build', 'views', 'index.php'],
'block-editor' => ['build', 'font-awesome-icon-block-init.php'],
'classic-editor' => ['build'],
'icon-chooser' => ['build']
];
foreach ( $js_bundles as $bundle_dir => $roots ) {
foreach( $roots as $root ) {
rcp($plugin_dir . "/$bundle_dir/" . $root, $dist_dir . "/$bundle_dir/" . $root);
}
}
copy($plugin_dir . '/index.php', $dist_dir . '/index.php');
copy($plugin_dir . '/defines.php', $dist_dir . '/defines.php');
@ -27,7 +31,6 @@ copy($plugin_dir . '/font-awesome-init.php', $dist_dir . '/font-awesome-init.php
copy($plugin_dir . '/uninstall.php', $dist_dir . '/uninstall.php');
copy($repo_dir . '/readme.txt', $dist_dir . '/readme.txt');
copy($repo_dir . '/LICENSE', $dist_dir . '/LICENSE');
copy($plugin_dir . '/v3shims.php', $dist_dir . '/v3shims.php');
// zip dist
$zip_filename = $repo_dir . '/font-awesome.zip';
@ -104,22 +107,38 @@ function ignore_file($file) {
return $found_ignore_pattern;
}
function copy_with_mkdir_p( $src, $dst ) {
$dst_path_info = pathinfo( $dst );
if ( ! isset($dst_path_info['dirname']) ) {
throw new Exception("invalid path: $dst");
}
if ( ! is_dir( $dst_path_info['dirname'] ) ) {
if ( ! mkdir( $dst_path_info['dirname'], 0755, true ) ) {
throw new Exception( "failed creating dir: " . $dst_path_info['dirname'] );
}
}
copy( $src, $dst );
}
// Copy recursively
// Parts borrowed from http://php.net/manual/en/function.copy.php#91010
function rcp($src, $dst){
if(is_dir($src)){
if(! file_exists($dst)) mkdir($dst);
if(! file_exists($dst)) mkdir($dst, 0755, true);
$dir = opendir($src);
while(false !== ($file = readdir($dir))){
if ( ! ignore_file($file) ) {
if( is_dir($src . '/' . $file)){
rcp($src . '/' . $file, $dst . '/' . $file);
} else {
copy($src . '/' . $file, $dst . '/' . $file);
copy_with_mkdir_p($src . '/' . $file, $dst . '/' . $file);
}
}
}
} else {
copy($src, $dst);
copy_with_mkdir_p($src, $dst);
}
}

12
block-editor/.prettierrc Normal file
View File

@ -0,0 +1,12 @@
{
"bracketSameLine": false,
"htmlWhitespaceSensitivity": "css",
"printWidth": 160,
"quoteProps": "consistent",
"semi": false,
"singleAttributePerLine": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "none",
"useTabs": false
}

View File

@ -0,0 +1,40 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "font-awesome/icon",
"version": "0.1.0",
"title": "Font Awesome Icon",
"category": "design",
"description": "Add a Font Awesome icon.",
"attributes": {
"iconLayers": {
"type": "array"
},
"justification": {
"type": "string"
}
},
"supports": {
"anchor": true,
"align": [
"left",
"center",
"right"
],
"ariaLabel": true,
"color": {
"text": false,
"background": true,
"enableConstrastChecker": true,
"gradients": true
},
"spacing": {
"margin": true,
"padding": true
},
"html": false
},
"textdomain": "font-awesome",
"editorScript": "font-awesome-block-editor",
"editorStyle": "font-awesome-block-editor"
}

View File

@ -0,0 +1 @@
<?php return array('dependencies' => array('lodash', 'react', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-rich-text'), 'version' => '1efe6b2034f2be97de84');

View File

@ -0,0 +1 @@
.wp-rich-text-font-awesome-icon:hover{border:1px solid var(--wp-components-color-accent,var(--wp-admin-theme-color,#3858e9));border-radius:2px;cursor:pointer;margin-left:-1px;margin-right:-1px}.fawp-inline-popover-wrapper{display:flex;gap:.5rem;padding:.75rem}.fawp-icon-styling-modal{--border-radius-fa:0.5rem;--border-width-fa:2px;--spacing-xs-fa:0.5rem;--spacing-sm-fa:0.75rem;--spacing-md-fa:1rem;--spacing-lg-fa:1.25rem;--spacing-xl-fa:1.5rem;--spacing-2xl-fa:2rem;--black-fa:#001e42;--gray1-fa:#183153;--gray2-fa:#515e7b;--gray3-fa:#616d89;--gray4-fa:#a5abbb;--gray5-fa:#c2c6d0;--gray6-fa:#dfe1e7;--gray7-fa:#f1f2f4;--white-fa:#fff;--red-fa:#e13333;--yellow-fa:#fab005;--teal-fa:#0da578;--blue-fa:#126ebf;--purple-fa:#9b36b5;--violet-fa:#663fd9;--input-border-color:var(--fa-navy);--input-border-width:0.15rem;border-radius:.75em;max-width:130ch;width:80vw}.fawp-button-icon{padding-right:var(--spacing-xs-fa)}.fawp-icon-modifier{display:flex;flex-wrap:wrap;gap:var(--spacing-xl-fa);position:relative}.fawp-icon-modifier-preview-container{border:2px solid var(--gray7-fa);border-radius:var(--border-radius-fa);display:flex;flex:1 0 auto;flex-direction:column;min-height:300px;padding:var(--spacing-md-fa);width:350px}.fawp-icon-modifier-preview{display:flex;font-size:var(--wp--preset--font-size--medium);margin:auto;padding:var(--spacing-md-fa);text-align:center}.fawp-icon-modifier-preview div{display:flex;margin:auto}.fawp-icon-modifier-preview-controls{flex:1 1 50%}.fawp-tab-content{margin-block-start:var(--spacing-2xl-fa)}.fawp-tab-content .fawp-options-section-heading{font-size:.8em;font-weight:500;margin-block:var(--spacing-xs-fa);text-transform:uppercase}.fawp-animation-controls,.fawp-styling-controls{align-items:stretch;display:flex;gap:var(--spacing-sm-fa)}.fawp-animation-controls .fawp-button,.fawp-styling-controls .fawp-button{background-color:var(--white-fa);border:var(--border-width-fa) solid var(--gray7-fa);border-radius:var(--border-radius-fa);cursor:pointer;flex:auto;padding:var(--spacing-xs-fa) var(--spacing-sm-fa);transition:all .2s ease-in-out}.fawp-animation-controls .fawp-button{padding:var(--spacing-md-fa) var(--spacing-lg-fa)}.fawp-animation-controls .fawp-reset,.fawp-styling-controls .fawp-reset{background-color:var(--gray7-fa);flex-grow:0}.fawp-animation-controls .fawp-button.fawp-selected,.fawp-styling-controls .fawp-button.fawp-selected{border-color:var(--black-fa)}.fawp-animation-controls .fawp-button:hover,.fawp-styling-controls .fawp-button:hover{background-color:var(--black-fa);border-color:var(--black-fa);color:var(--white-fa)}.fawp-color-settings{display:flex;gap:10px}.fawp-color-settings .fawp-color-option-wrapper{display:inline-block;height:28px;transform:scale(1);transition:transform .1s ease;vertical-align:top;width:28px;will-change:transform}.fawp-color-settings .fawp-color-option-wrapper:hover{transform:scale(1.2)}.fawp-color-settings .fawp-button{border:none;border-radius:50%;box-shadow:0 0 0 1px var(--gray5-fa);cursor:pointer;display:inline-block;height:100%;transition:box-shadow .1s ease;vertical-align:top;width:100%}.fawp-color-settings .fawp-button.fawp-selected{box-shadow:0 0 0 2px var(--white-fa),0 0 0 4px var(--black-fa)}.fawp-icon-settings-tab-panel{position:relative}.fawp-color-picker-wrapper{background:#fff;border:1px solid gray;box-shadow:0 2px 4px rgba(0,0,0,.5);padding:10px;position:absolute;right:10%;text-align:right;top:0;z-index:1000}.fawp-animation-controls{flex-wrap:wrap}.fawp-icon-animations .fawp-animation-controls .fawp-button{flex:1 0 33%}.fawp-icon-styling-rotate .fawp-input{border:var(--border-width-fa) solid var(--gray7-fa);border-radius:var(--border-radius-fa);max-width:12ch}

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More