Compare commits

...

38 Commits

Author SHA1 Message Date
Mike Wilkerson d2670bfcc7 Release 5.1.0 2025-07-22 16:25:13 -07:00
Mike Wilkerson 68abe08e4a revise some of the release workflow in DEVELOPMENT.md 2025-07-22 16:24:33 -07:00
Mike Wilkerson b44e8495ab update readme.txt Changelog for 5.1.0 2025-07-22 16:11:17 -07:00
Mike Wilkerson 20152da6f4 fix multisite test expectations 2025-07-22 16:01:25 -07:00
Mike Wilkerson 3e43ea9c52 change test expectations for FA7 as initial default version 2025-07-22 15:56:19 -07:00
Mike Wilkerson d9878e7a5a rebuild admin js bundle 2025-07-22 15:35:05 -07:00
Mike Wilkerson e9a9d9e6dd use version 7 by default on plugin activation 2025-07-22 15:34:43 -07:00
Mike Wilkerson 26d1a7b78f update FA5 Pro CDN config alert 2025-07-22 15:30:21 -07:00
Mike Wilkerson 0b1fe630b3 Add CDN availability alert for FA7 2025-07-22 15:28:14 -07:00
Mike Wilkerson fa1ce8f041 handle latest_version_7 in
FontAwesome_Preference_Conflict_Detector::detect()
2025-07-22 15:21:32 -07:00
Mike Wilkerson e2d014a03c rebuild admin js bundle 2025-07-22 15:10:04 -07:00
Mike Wilkerson 24cbe3c071 fix handling of latest_version_7 in admin UI 2025-07-22 15:09:44 -07:00
Mike Wilkerson 85a9c15083 rebuild icon-chooser js bundle 2025-07-22 14:18:54 -07:00
Mike Wilkerson 12ac84d2b7 update fa-icon-chooser-react to 0.9.1 2025-07-22 14:18:36 -07:00
Mike Wilkerson d38a6298b2 rebuild php docs for 5.1.0 2025-07-22 13:14:42 -07:00
Mike Wilkerson 813a4326d1 bump version to 5.1.0 2025-07-22 13:11:53 -07:00
Mike Wilkerson d1827030ed
FA7 readiness (#256)
* update fa-icon-chooser to 0.9.0-1 for FA7 readiness

* rebuild icon-chooser bundle

* accept 7.x as a kit version

* update icon chooser to 0.9.0-2

* updates to make 7.x work, including latest_version_7

- also add assetsBaseUrlOverride and FONTAWESOME_CDN_URL_TEMPLATE
  in development to enable testing with FA7 pre-release

* front end tweaks to enable overriding the _assetsBaseUrl in
fa-icon-chooser

* adding a new define for FONTAWESOME_ASSETS_BASE_URL_OVERRIDE

based on env var

* rebuild icon-chooser bundle

* remove obsolete error message

* Use svg.css for FA7

* Fix the wire-up of assetsBaseUrlOverride

* fix getUrlText

- handle response.status correctly (fix typo)
- tell axios to keep the responseType as text,
  instead of automatically parsing the JSON payload

* rebuild icon chooser bundle

* formatting fixes

* update fa-icon-chooser to 0.9.0-3 [skip ci]

* npm update icon-chooser

* rebuild icon-chooser bundle

* npm update admin bundle

* rebuild admin js bundle

* npm update block-editor bundle

* rebuild block-editor bundle

* npm update classic-editor

* rebuild classic-editor bundle

* update graphql query response fixture

* update test expectations

* auto-formatting

* WIP: updating test expectations

* update test expectations

* keep only a few versions in releases query fixture

* fixup array indices

* update test mocks

* update test expectations

* update tests

* auto-formatting

* update to fa-icon-chooser-react 0.9.0

* re-build icon chooser bundle [skip ci]
2025-07-22 11:27:54 -07:00
Mike Wilkerson 3a1f570cb5 Release 5.0.2 2025-06-02 15:36:45 -07:00
Mike Wilkerson ec3180f86c udpate changelog for 5.0.2 2025-06-02 15:27:26 -07:00
Mike Wilkerson 39facd1161 rebuild docs 2025-06-02 15:12:38 -07:00
Mike Wilkerson 1d1a40c074 bump to version 5.0.2 2025-06-02 15:09:43 -07:00
Mike Wilkerson 1b8c4596c0
Handle restricted filesystem permissions and allow for disabling block editor support (#243)
* update theme-alpha to derive from twenty-twenty-five

* add try / catch / notify in activator

* add more custom exceptions

* Enable fallback checking for svg stylesheet using HEAD request

* enable differentiating admin warnings from fatal errors

and allow for multiline exception messages.

* check for svg styles with fallback warning notice when in WP admin

* use the warning version of fetch_svg_styles from the config controller too

* make the block editor support actions disableable

* rename function as ensure_

* on activation, initialize svg styles conditionally

* pre-release 5.0.2-1

* re-work the classic editor support

- handle null results when querying for editorId element
- use jQuery to handle binding click events to editors to avoid loss of
  click event bindings due to interaction with other plugin

* rebuild classic-editor bundle

* comment out special filters in theme-alpha/functions.php

* fix translators comment for phpcs

* make jquery dependence explicit for classic-editor

* move maybe_refresh_releases into initialize_admin()

* add default SVG styles

* maybe_refresh_releases only on the plugin's settings page

* reduce admin notices to warnings

* bump version

* wp_die with descriptive message on failed activation

* Fixes for phpcs

* update actions/cache version

* Update Dockerfile-latest to add uopz extension

* migrate SVG styles manager away from being a singleton

- make it more testable with uopz

* update multi-site activation tests to use non-singleton SVG styles
manager

* add uopz extension to CI

* add test coverage for svg styles on activation

* cleanup and auto-formatting

* cleanup and focus ActivationTest

* move svg-styles-manager tests into a separate test module

* add test coverage for filesystem permissions error on activation

* add coverage for throwing a specific exception when lacking filesystem
permissions

* add test coverage for stylesheet presence with and without filesystem permissions

* add more test coverage for loading including wp-admin

* enable uopz extension only for phpunit runs
2025-05-23 14:35:03 -07:00
Mike Wilkerson 2629a30c1c Tested up to 6.8 2025-04-28 11:29:27 -07:00
Mike Wilkerson 1b7631d4b8 Release 5.0.1 2025-02-26 11:27:48 -08:00
Mike Wilkerson b56a11afec rebuild php doc 2025-02-26 10:53:37 -08:00
Mike Wilkerson 4429576bdc bump version to 5.0.1 2025-02-26 10:53:16 -08:00
Mike Wilkerson b6d63a55f2 update readme.txt for 5.0.1 2025-02-26 10:51:05 -08:00
Mike Wilkerson 965f135474
Fetch svg styles on loading admin (#241)
* refactor SVG stylesheet presence detection

* fetch SVG stylesheet when admin page is loaded if it's not already loaded

* fixes

* auto-formatting

* remove unused vars
2025-02-26 10:36:02 -08:00
Mike Wilkerson ee475f2e2a Release 5.0.0 2025-02-24 11:58:23 -08:00
Mike Wilkerson 4484f342a5 bump version to 5.0.0 2025-02-24 11:16:30 -08:00
Mike Wilkerson d2ee8f0af6 rebuild docs 2025-02-24 11:16:12 -08:00
Mike Wilkerson 22852dd76d update theme-mu composer target branch and auto-format [skip ci] 2025-02-24 11:09:47 -08:00
Mike Wilkerson e9ccb0b239 update plugin-sigma target branch and auto-format 2025-02-24 11:08:53 -08:00
Mike Wilkerson 4131728309
Version 5: enhanced block editing and self-hosting (#224)
* refactor color setting logic

* tweak temp fawp-selected rule

* tweak temp fawp-selected style again

* wireup selected class for size buttons

* migrate flipping to use power transforms

* remove some old stuff

* rebuild block-editor bundle

* clean up some obsolete stuf

* handle fawp-selected for flips

* wire-up animation button selected state

* add temporary styling for animation button selection

* rebuild block-editor bundle

* remove obsolete CSS rules for selected-layer class

* remove obsolete consts

* WIP: refactoring

- move Colors into its own module
- factor out some constants that are shared across modules

* rebuild block-editor bundle

* WIP: IconSizer

* refactor IconSizer

* finish wiring up new IconSizer

* rebuild block-editor bundle

* remove obsolete resetSize in updateTransform()

* rebuild block-editor bundle

* add standard class names

* cleanup

* switch back to FontSizePicker

* remove unnecessary kses filter

* add the font_awesome_skip_enqueue_kit filter

* WIP: add SVG support styles manager class

* refactor skip_enqeue_kit()

* initial wire-up of size changing

* refactor size setting

* rebuild block-editor bundle

* WIP: initial wire-up of setup_selfhosting for svg support styles

* add pict model

* WIP: re-work setup for loading additional svg support styles

* complete initial happy path wire-up of loading additional svg styles

* add filters the alpha theme

* remove integrity_key condition

* migrate admin bundle to use createRoot()

* migrate conflict detection React mount to use createRoot()

* re-work conditional enqueue of kit and associated conflict detection loading

* rebuild admin bundle

* migrate classic-editor React mount to use createRoot

* rebuild classic-editor bundle

* color palette styling

* fix aria-selected for colors and import ColorPicker

* rebuild block-editor bundle

* fix color picker logic

* rebuild block-editor bundle

* fix color picker and its default

* rebuild block-editor bundle

* guard invocations of md5(): require only string arg

* remove obsolete css file

* fix popover anchor when block has only a rich text icon

* pass through context to IconModifier from richText

* rebuild block-editor bundle

* reorganize attributes state for richText icon

* fix change of icon after richText icon attributes reorg

* rebuild block-editor bundle

* move rich text toolbar button

also fix corner case where replacement was undefined

* set isActive for the toolbar button based on isFormatIconFocused

* change how rich text icons are inserted so as not to overwrite other text

* fix icon replacement: distinguishing between initial placement versus style update

* change insert/replace logic with isObjectActive

* rebuild block-editor bundle

* remove popoverAnchor fly-away hack now that we have isObjectActive

* rebuild block-editor bundle

* position custom color picker

* remove obsolete webpack config comments

* remove obsolete fa-icon-chooser-react dep from admin bundle

* remove default fontSize setting in inline style

* fix flying t-shirt size highlights by moving modal out of the popover

* include backgroundColor in context passed into the modal

* rebuild block-editor bundle

* change the rich text icon class name

* add common class to rich text wrapper element

* add common wrapper class to block icon

* rebuild block-editor bundle

* rebuild block-editor bundle

* adjust class names, cleanup, and add close to color picker

* remove redundant size header

* fixup on popover, adjust preview w background

* update rich text class

* rebuild block-editor bundle

* tweak metadata names in block registration

* fix building and loading of editorStyles

* add inline_style registration of base styles for both block and rich text icons

* 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

* change enqueue rules for svg styles

load it only when tech is webfont or we're skipping the kit load

* tweak theme alpha functions test code

* rebuild block-editor bundle

* fix derivation of fontSize attribute for re-styling sized richText icon

* rebuild block-editor bundle

* update deriveAttributes to use allow list for picking style properties

and update the corresponding transform allow list processing

* remove some "supports" from block.json

* remove obsolete commented code

* rebuild block-editor bundle

* make flip reset match rotation reset, having no selection indicator

* add alignment dropdown menu

* rebuild block-editor bundle

* add @wordpress/icons to package.json

* add justification attribute to block icon

* rebuild block-editor bundle

* adjust alignment toolbar buttons

* improve loading of CSS

* rebuild block-editor bundle

* config prettier and auto-format

* WIP: fixing linter warnings and adding i18n

* remove obsolete iconSizer

* WIP: fix linting and isAnimationSelected for No Animation

* WIP: clean up and linting

* more linting

* rebuild block-editor bundle

* fix deprecation warning by using ToolbarDropdownMenu

* rebuild block-editor bundle

* add prettierrc to admin bundle

* auto-formatting admin bundle

* rebuild admin bundle

* add prettierrc for classic-editor bundle

* auto-formatting classic-editor source

* phpcbf auto-formatting

* php auto-formatting

* move phpcs def to help with auto-formatting in neovim

* gitignore php-cs-fixer cache

* add phpactor config for neovim

* wire-up an example in JS

* rebuild block-editor bundle with example

* update block add hover preview

* rebuild block-editor bundle

* return svg styles module as a singleton to make it mockable

also, auto-formatting

* add mock to config controller test

and auto-format

* update deps in composer.json

* auto-formatting

* TEMP: disable phpcbf for font-awesome.php

* auto-formatting

* remove configurations for obsolete versions of php or WordPress

* fix loader test to run on init

* phpcs fixes

* phpcs cleanup

* use php8.2 for phpcs

* maybe exit before sourcing in other code

* add some more exits if accessed directly

* auto-formatting

* migrate to using WP_Filesystem methods for filesystem access

* ignore phpcs warning for file system function in test code

* use wp_mkdir_p for creating the svg styles subdir

* add self-hosting exceptions

* phpcs cleanup

* update dist zip-building script

* update composer.json for release build steps

* update release steps in DEVELOPMENT.md

* Add a try again message for token endpoint failure

* remove v3deprecation code from admin bundle

* remove v3deprecation code from back end

* rebuild admin bundle

* update version for conflict detection script

* update min WP and PHP requirements

* remove obsolete enableIconChooser

* rebuild admin bundle

* remove obsolete dist packing code

* remove compat-js bundle

* register js bundle scripts before enqueue

* load classic-editor bundle only when tiny_mce is being loaded

* update admin bundle version and build

* update block-editor bundle version

* update classic-editor bundle version

* update icon-chooser bundle version and build

* bump version to 5.0.0-alpha1

* rebuild docs

* update package-lock.json for each js bundle

* 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

* prepare for 5.0.0-alpha1

* run tests on push to any branc

* fix lodash dev deps for jest tests

* add eslint dep for react

* remove unused var

* rebuild admin bundle

* Release 5.0.0-alpha2

fix zip bundle

* do not add crossorigin or integrity to <link> when self-hosting

* always fetch svg-with-js.css when saving options and always load from self-hosting

* move skip_enqueue_kit() into the main module

* disable loader tests in CI

* rebuild icon-chooser bundle using local build

* use enqueue_block_assets in more recent versions of WordPress

* add missing import of __ from i18n

* rebuild block-editor bundle

* add override for SVG embed

* rebuild icon-chooser bundle

* remove function type syntax that breaks for older PHP

* fix mountAdminView to be compatible with React < 18

* rebuild admin bundle

* Update WordPress plugin page readme and screenshots (#232)

* update WordPress plugin page readme and screenshots

* resize screenshots

* revise readme for more technical aspects

---------

Co-authored-by: Mike Wilkerson <11575183+mlwilkerson@users.noreply.github.com>

* update to use fa-icon-chooser-react 0.8.0 production release

* rebuild icon-chooser bundle

* auto-formatting [skip ci]

* bump axios dep

* update @wordpress scoped packages to use wp-6.7

This resolves some of the GitHub dependabot security alerts

* rebuild admin JS bundle

* update block-editor bundle to use wp-6.7 deps

* update icon-chooser deps to use wp-6.7 dev deps

* update classic-editor bundle to use wp-6.7 dev deps

* bump version 5.0.0-alpha3 [skip ci]

* revise readme.txt [skip ci]

remove some of the details that will move over the docs.fontawesome.com

* add direct dependency on fontawesome-svg-core

* config fontawesome-svg-core not to autoAddCss or autoReplaceSvg

* auto-formatting

* rebuild block-editor bundle

* remove the extra inline css for setting height 1em

* always initialize svg stylesheet on plugin activation

* bump version to 5.0.0-alpha4 [skip ci]

* replace == with === (and auto-format)

* qualify query cache key with kit token, if relevant

* rebuild icon-chooser bundle

* rebuild admin bundle

* bump version to alpha5 [skip ci]

* remove obsolete filter from alpha theme functions.php [skip ci]

* update DEVELOPMENT doc [skip ci]

* remove version key and add 6.0-integration service

* remove boilerplate readme.txt for block-editor script

* remove obsolete version-specific composer files

* add env assets for testing in WP 5.8

* disable RichText icons on WP < 6.3

* add note about richtext requirements

* rebuild block-editor bundle

* bump to alpha6 [skip ci]

* auto-formatting

* add mock for svg_style_manager_fetch_styles

* update test to use svg_style_manager mock

* fixup multisite activation test

* throw exception try_upgrade when $options are empty

* test cleanup

* update more tests to use svg_styles mock

* auto-formatting

* bump version to alpha7 [skip ci]

* remove error log

* fix sortedUniq spelling in lodash

* rebuild admin bundle

* fix typo

* refine classic editor back end setup

including capturing the editor IDs

* WIP: refactoring classic-editor plugin to work for either tinymce or quicktags

* more refactoring

* update tags, limit to 5

* remove the tinymce plugin mechanism

* rework classic editor script

* rebuild classic-editor bundle

* fix formatting

* bump to alpha8

* fix and format plugin-sigma [skip ci]

---------

Co-authored-by: Frances Botsford <frances@switchingprotocols.com>
Co-authored-by: frances botsford <frrrances@users.noreply.github.com>
2025-02-24 11:07:41 -08:00
Mike Wilkerson 4070a2e443 update gitignore [skip ci] 2025-02-10 17:22:30 -08:00
Mike Wilkerson 393162059f use only minor version in readme.txt 2025-01-13 11:54:45 -08:00
Mike Wilkerson 291f06be8c tested up to WP 6.7.1 2025-01-13 11:50:39 -08:00
Mike Wilkerson df5e982181 tested up to 6.6 2024-07-15 14:40:42 -07:00
272 changed files with 77068 additions and 30985 deletions

View File

@ -2,13 +2,13 @@ name: Jest
on: on:
push: push:
branches: [ master ] branches:
- "**"
pull_request: pull_request:
branches: [master] branches: [master]
jobs: jobs:
jest: jest:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -16,12 +16,12 @@ jobs:
- uses: actions/setup-node@v1 - uses: actions/setup-node@v1
with: with:
node-version: '18' node-version: "18"
check-latest: true check-latest: true
- name: Cache node_module - name: Cache node_module
id: node-modules-cache id: node-modules-cache
uses: actions/cache@v2 uses: actions/cache@v4
with: with:
path: admin/node_modules path: admin/node_modules
key: ${{ runner.os }}-node-modules-${{ hashFiles('admin/package-lock.json') }} key: ${{ runner.os }}-node-modules-${{ hashFiles('admin/package-lock.json') }}

View File

@ -2,7 +2,8 @@ name: PHP Tests
on: on:
push: push:
branches: [ master ] branches:
- "**"
pull_request: pull_request:
branches: [master] branches: [master]
@ -18,7 +19,6 @@ env:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
services: services:
@ -34,12 +34,10 @@ jobs:
strategy: strategy:
matrix: matrix:
php: ['7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3'] php: ["7.4", "8.0", "8.1", "8.2", "8.3"]
wordpress: [latest] wordpress: [latest]
include: include:
- php: '5.6' - php: "8.3"
wordpress: 5.2.5
- php: '8.3'
wordpress: trunk wordpress: trunk
steps: steps:
@ -48,6 +46,7 @@ jobs:
- uses: shivammathur/setup-php@v2 - uses: shivammathur/setup-php@v2
with: with:
php-version: ${{ matrix.php }} php-version: ${{ matrix.php }}
extensions: uopz
- name: Validate composer.json and composer.lock - name: Validate composer.json and composer.lock
id: composer-lock id: composer-lock
@ -225,6 +224,8 @@ jobs:
vendor/bin/phpunit $PHP_UNIT_ARGS --group slow vendor/bin/phpunit $PHP_UNIT_ARGS --group slow
- name: Run PHPUnit Loader Tests - name: Run PHPUnit Loader Tests
# 2/13/25: temporarily disabled. Passing in local dev against WP 6.7.2, failing in CI.
if: false
run: | run: |
WP_PLUGIN_DIR="$(pwd)" \ WP_PLUGIN_DIR="$(pwd)" \
COMPOSER_VENDOR_DIR="$WP_PLUGIN_DIR/vendor" \ COMPOSER_VENDOR_DIR="$WP_PLUGIN_DIR/vendor" \
@ -267,7 +268,7 @@ jobs:
- name: Maybe run phpcs - name: Maybe run phpcs
run: | run: |
if [ ${{ matrix.php }} == '8.3' ] && [ ${{ matrix.wordpress }} == latest ]; then if [ ${{ matrix.php }} == '8.2' ] && [ ${{ matrix.wordpress }} == latest ]; then
composer phpcs composer phpcs
echo echo
echo "Skipping phpcs" echo "Skipping phpcs"

4
.gitignore vendored
View File

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

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"/> <arg name="extensions" value="php"/>
<file>includes</file> <file>includes</file>
<file>tests</file> <file>tests</file>
<!-- TODO: maybe re-enable this. It's crashing phpcbf. -->
<!--
<file>font-awesome.php</file> <file>font-awesome.php</file>
-->
<file>font-awesome-init.php</file> <file>font-awesome-init.php</file>
<file>block-editor/font-awesome-icon-block-init.php</file>
<file>index.php</file> <file>index.php</file>
<file>v3shims.php</file>
<file>defines.php</file> <file>defines.php</file>
<file>admin/index.php</file> <file>admin/index.php</file>
<file>admin/views/main.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 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 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 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". from the WordPress plugin directory by searching for plugins by author "fontawesome".
@ -456,27 +456,6 @@ brew install composer
``` ```
</details> </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> <details>
<summary>If you have an older version of Docker or one that doesn't support host.docker.internal</summary> <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` 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. 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) - `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 ```bash
bin/composer dist 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 This will cause everything to be built inside the container, which will hopefully
keep the built assets more consistent, regardless of the host environment.) keep the built assets more consistent, regardless of the host environment.)
This will delete the previous build assets and produce the following: This will delete the previous build assets and produce:
`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:
`wp-dist/`: the contents of this directory contains everything that will be used in `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 subsequent steps to both build an installable zip file, and to copy into the
@ -884,13 +850,7 @@ a GitHub release.
9. Run through some manual acceptance testing 9. Run through some manual acceptance testing
**WordPress 4.7, 4.8, 4.9** **WordPress 6.0**
For each of these 4.x environments, run and setup the corresponding integration container, like:
```
bin/integration 4.7
bin/setup -c com.fontawesome.wordpress-4.7-integration
```
Install and activate the Font Awesome plugin from the admin dashboard by uploading the `font-awesome.zip` file Install and activate the Font Awesome plugin from the admin dashboard by uploading the `font-awesome.zip` file
that was created in the previous step. that was created in the previous step.
@ -899,31 +859,16 @@ Run through the following, with the JavaScript console open, looking for any war
1. Load the plugin settings page. 1. Load the plugin settings page.
1. Change from Web Font to SVG and save. 1. Change from Web Font to SVG and save.
1. Create a new post (which will be in the Classic Editor) 1. Install the Classic Editor plugin
1. Create a new post with the Classic Editor
1. Click the "Add Font Awesome" button 1. Click the "Add Font Awesome" button
1. Search for something, and click to insert an icon from the results 1. Search for something, and click to insert an icon from the results
**WordPress 5.0**
Setup the integration environment as above, but also do the following editor
integration tests intead:
1. Install the Classic Editor plugin
1. Create a post with the Classic Editor
1. Click the "Add Font Awesome" media button
1. Search for something, and click to insert an icon from the results
1. Create a new post, switching to the Gutenberg / Block Editor 1. Create a new post, switching to the Gutenberg / Block Editor
1. Expect to see a compatibility warning that the Icon Chooser is not enabled, 1. Add an icon block to a post
but otherwise expect the Block Editor to function normally
**WordPress 5.4**
Setup the integration environment as above. Do the same tests as on 5.0, but also
expect the Icon Chooser to be enabled within the Block Editor:
1. Create a post with the Block Editor
1. Activate the Icon Chooser
1. Search for something, and click to insert an icon from the results 1. Search for something, and click to insert an icon from the results
1. style the icon
1. Add a RichText icon (inline)
1. style that too
**WordPress latest** **WordPress latest**
@ -1142,7 +1087,7 @@ $ cd ..
10. Copy plugin directory assets and wp-dist layout into `wp-svn/trunk` 10. Copy plugin directory assets and wp-dist layout into `wp-svn/trunk`
```bash ```bash
bin/dist2trunk bin/dist-to-trunk
``` ```
This script will just `rm *` anything under `wp-svn/trunk/*` and `wp-svn/assets/*` to make sure that if the new dist This script will just `rm *` anything under `wp-svn/trunk/*` and `wp-svn/assets/*` to make sure that if the new dist
@ -1200,15 +1145,7 @@ password. After the first `svn ci` caches the credentials, you probably won't ne
[See also tips on using SVN with WordPress Plugins](https://developer.wordpress.org/plugins/wordpress-org/how-to-use-subversion/#editing-existing-files). [See also tips on using SVN with WordPress Plugins](https://developer.wordpress.org/plugins/wordpress-org/how-to-use-subversion/#editing-existing-files).
13. OPTIONAL: Test Installation from WordPress Plugins Directory 13. Create the new svn release tag
Once the trunk has been committed above, this new version is available as the current "Development Version",
available for download as a ZIP file from the plugin's [Advanced View](https://wordpress.org/plugins/font-awesome/advanced/). (See the dropdown under the header: "Please select a specific version to download.")
To try it out, you could start up a clean integration environment from this repo, and then
install the plugin using that downloaded ZIP file.
14. Create the new svn release tag
First, make sure `svn stat` is clean. We want to make sure that the trunk is all committed and clean before we take a First, make sure `svn stat` is clean. We want to make sure that the trunk is all committed and clean before we take a
snapshot of it for the release tag. snapshot of it for the release tag.
@ -1219,6 +1156,18 @@ This will snapshot `trunk` as a new release tag. Replace the example tag name wi
svn cp trunk tags/42.1.2 svn cp trunk tags/42.1.2
``` ```
```bash
svn ci -m 'Tag release 42.1.2'
```
14. OPTIONAL: Test Installation from WordPress Plugins Directory
Once the trunk has been committed above, this new version is available as the current "Development Version",
available for download as a ZIP file from the plugin's [Advanced View](https://wordpress.org/plugins/font-awesome/advanced/). (See the dropdown under the header: "Please select a specific version to download.")
To try it out, you could start up a clean integration environment from this repo, and then
install the plugin using that downloaded ZIP file.
15. Update `Stable tag` and `Tested up to` tags in `readme.txt` 15. Update `Stable tag` and `Tested up to` tags in `readme.txt`
We've now got three copies of `readme.txt` that should all be updated with new tag values: We've now got three copies of `readme.txt` that should all be updated with new tag values:
@ -1347,7 +1296,7 @@ wp --allow-root core update --version=5.4 /tmp/wordpress-5.4-latest.zip
# Analyze Webpack Bundle # 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 which produces a corresponding `webpack-stats.html` file in the corresponding
directory on each build. 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} .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} .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

10
admin/build/381-rtl.css Normal file
View File

@ -0,0 +1,10 @@
.iVV55iNB320NJJLspy7m{max-width:600px}.BcmdF5mOoQ3Luug6sJrn{border:1px dotted grey;border-radius:5px;margin:1rem 0 1rem 1rem;padding:1rem}
.lX8h3LbX6kaLN7_hLhlw{align-items:stretch;background-color:#e4f6ff;border-radius:.25rem;color:#495057;display:flex;flex-direction:row;flex-wrap:no-wrap;margin-top:1rem;max-width:800px}.nx2ZqeD9AnYnPnKHAqKJ{border-bottom-right-radius:.25rem;border-top-right-radius:.25rem;color:#008bed;font-size:1rem;padding:.55rem .75rem .5rem .25rem}.ovRzytWn5jGccLKV78T9{font-size:.8rem;font-weight:600;line-height:1.5;margin-bottom:.2rem;margin-top:0}.M_C6Dj_EqhO8IuY52iA6{display:flex;flex-direction:column;flex-grow:1;font-size:.8rem;padding:.5rem .25rem .75rem 1rem}.M_C6Dj_EqhO8IuY52iA6 p{margin-bottom:.5rem;margin-top:0}.M_C6Dj_EqhO8IuY52iA6 svg{font-size:.7rem}.M_C6Dj_EqhO8IuY52iA6 ul{margin:0}.M_C6Dj_EqhO8IuY52iA6 li{display:inline-block;margin-bottom:0;padding-left:1rem}.liWjpcvKZkKaYPsJjQPA{margin-top:1rem}.lX8h3LbX6kaLN7_hLhlw button{color:#0073aa}.lX8h3LbX6kaLN7_hLhlw button:hover{color:#00a0d2}.MLwfZfK5uVZOtIHI1cdt{background:#fff9db}.MLwfZfK5uVZOtIHI1cdt .nx2ZqeD9AnYnPnKHAqKJ{color:#fab007}
.uL1wb8HtJb_0IkG3PeEN{max-width:100%}.VJ5yoahvOPmye_Iv2x78 button{background:#0000;border:none;font-size:1.5em;margin-right:.125em;margin-left:.125em;padding-bottom:.25em}.VJ5yoahvOPmye_Iv2x78 button:hover{cursor:pointer}.VJ5yoahvOPmye_Iv2x78 button:disabled{border-bottom:4px solid #008ded;color:unset;cursor:default}
.GfbntzmAC3JXYwdY57Hz{color:red}.nkCRdVrm4cTGk23nWmwZ{display:flex;width:100%}.nkCRdVrm4cTGk23nWmwZ svg{margin-bottom:.125em;margin-left:.5em}._V54L7D6KbeTsNGQHyuu{font-size:.9rem;font-weight:700;width:30%}.jH1jbDhGqqv7Grbj6p5p{margin-left:.25rem;text-align:center;width:1rem}.KYI6mFOoLc2LXE3_WOAV{display:flex;flex-direction:row}.uMUQXnIQhpFojiorojGR{width:50%}.Q3PqTXZI2xQbQuon9Brf{display:flex;flex-direction:column;margin-top:.5rem;width:70%}@media only screen and (min-width:1024px){.Q3PqTXZI2xQbQuon9Brf{margin-top:0}}.p0FxJ9gY4EvqdrEJ5pDs{font-style:italic}.p0FxJ9gY4EvqdrEJ5pDs>ul{list-style:none;margin-right:2em}.rr6y9ViQbhSy0YAp3Svc{display:flex;flex-direction:flex-row}.d104q4pTcfNp1fl3EEwj{font-weight:600;margin-right:5px}.pFNsNzsa6QtxNtsHMsos{margin-bottom:1rem;margin-top:1rem}.x21UupVYDcbhJEXG_vuB{margin-top:1rem}.x21UupVYDcbhJEXG_vuB button{background:none;border:none;cursor:pointer;text-decoration:underline}.x21UupVYDcbhJEXG_vuB button svg{margin-left:.5em}.elGAHNDAzrtnF0LZNYl_ .uMUQXnIQhpFojiorojGR{margin-bottom:1.5rem}.FHOPD8z6_efjfjjQHK9B{color:#868e96;display:block;font-weight:400;line-height:1.5;margin-top:.25rem}.FHOPD8z6_efjfjjQHK9B a{color:#868e96}.FHOPD8z6_efjfjjQHK9B a:hover{color:#228be6}.kP55KzmQ_7zoJ5baS76k{padding-right:1em}
.xAYNgmh_FT28wOZEe4og{background-color:#fff;margin-left:20px;padding:1rem 2rem 2rem}@media only screen and (min-width:1024px){.xAYNgmh_FT28wOZEe4og{max-width:1000px}}.W3wz4Liah2EvWxtTBXN8{align-items:center;display:flex}.OpLLWfmNs6BXGmnmuinK{padding-right:1em}div.OpLLWfmNs6BXGmnmuinK.q0fCXPnTi1vRhNmP0IEt{background-color:#fda09a;border-radius:5px;display:flex;margin:1em;max-width:450px;padding:1em}div.OpLLWfmNs6BXGmnmuinK.q0fCXPnTi1vRhNmP0IEt .A2dLn8oZtVzuXngZMDRp{margin-bottom:auto;margin-top:auto}div.OpLLWfmNs6BXGmnmuinK.q0fCXPnTi1vRhNmP0IEt .xFoMk9Jc8Ir4n5Olcce1{max-width:400px}.OpLLWfmNs6BXGmnmuinK .xFoMk9Jc8Ir4n5Olcce1{padding-right:1em}.OpLLWfmNs6BXGmnmuinK.fQeEY3YNz4yh6R7vdi7J .JPBgwk6PxfiitLxJLE54{color:green}h2.VklefjWwawC59yrOPe3e{font-size:18px}h3.VklefjWwawC59yrOPe3e{font-size:16px}.e8Vu3y2YBkuW8N9IhY2m{margin-bottom:1rem;margin-top:1rem}.gNYVG50hxMZs8Gqbj_T0 th{font-weight:700}button.dpYyb_l0GWlAiVkOmmYt{background:none;border:0}.WJl_9YHKGkhUvtVwgVco{align-items:center;display:flex}.HBCEbIhIET1XISEYneSA{margin:1rem}button.ZXe2iyFqFThwx_UF4CBf{background:#008ded;border:solid #0064b1;border-radius:3px;border-width:1px 1px 4px;color:#fff;cursor:pointer;font-size:14px;font-weight:600;line-height:1.4em;padding:.7em 1.5em}button.ZXe2iyFqFThwx_UF4CBf[disabled]{background:#f8f9fa;border:1px solid #f8f9fa;color:#008ded;cursor:default}button .HgLyUkphZYd8YsLSMJAZ{display:inline-block;min-width:3.2em;text-align:right}.Gu2u4ZSZT25Yqm8zSogj{background-color:#fdfdf3;border:1px solid #000;border-radius:5px;max-width:600px;padding:1.5em}.WOV9bdVrpJVdQWzhBnHZ{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}@media only screen and (min-width:1024px){.QN_KH8sqi5QFBDqaH1rI{display:flex}}.bBMVcUUJf1GW7veG1Zic{flex-direction:row}.pIa2BGO1ABMtYZY185Bf{flex-direction:column}.h0koIl1zvME7toM3jUk9{position:relative}.kWqY7l_wn27HmdUNz7ZY .JPBgwk6PxfiitLxJLE54{font-size:24px}.kWqY7l_wn27HmdUNz7ZY.q3No9l7YMUEH1xvYTNfI .JPBgwk6PxfiitLxJLE54,.Y7M4JHzDp7jtCt6MonbK{color:green}.a6qTuZmDiKS_FHgMZawo,.kWqY7l_wn27HmdUNz7ZY.PwCQsIQEdGz9b0cOj3iA .JPBgwk6PxfiitLxJLE54{color:red}.kWqY7l_wn27HmdUNz7ZY.Gu2u4ZSZT25Yqm8zSogj .JPBgwk6PxfiitLxJLE54,.rw5FUVRrrdM17WyxcRZ9{color:#b7b700}.QEoklKhbCbwOUBs0cspa{margin-right:1rem}.oWHnpotXuoOIlJoqkkgw~label .NzRaF0U8aKPVtS6JIaK8,.gIUwcNcpOHhTKG4sTlfg~label .NzRaF0U8aKPVtS6JIaK8{display:none;opacity:0}.oWHnpotXuoOIlJoqkkgw:checked~label .NzRaF0U8aKPVtS6JIaK8,.gIUwcNcpOHhTKG4sTlfg:checked~label .NzRaF0U8aKPVtS6JIaK8{color:#228be6;display:block;opacity:1}.oWHnpotXuoOIlJoqkkgw:checked~label .iemYJRvB4tzF1xnuGiAw,.gIUwcNcpOHhTKG4sTlfg:checked~label .iemYJRvB4tzF1xnuGiAw{display:none;opacity:0}.oWHnpotXuoOIlJoqkkgw:checked~label .BFR5diS8tiViycbuTDVS,.oWHnpotXuoOIlJoqkkgw:checked~label .BFR5diS8tiViycbuTDVS a{color:#495057}.oWHnpotXuoOIlJoqkkgw:checked~label .BFR5diS8tiViycbuTDVS a:hover,.oWHnpotXuoOIlJoqkkgw~label .BFR5diS8tiViycbuTDVS a:hover{color:#228be6;text-decoration-color:initial}
.kaJ7ZfPW8LYXB3jcG1xM{position:relative}.iw1ugzHiscI8cdqPxDt5{border-bottom:1px solid #dde2e6;display:flex;padding:1rem 0 1rem 1rem}.iw1ugzHiscI8cdqPxDt5 label{font-size:.9rem;font-weight:600;width:30%}.iw1ugzHiscI8cdqPxDt5 label svg{color:#dde2e6;padding-left:.5rem}.DnA1Iv_lwCTNSAFQGrON .iw1ugzHiscI8cdqPxDt5 p{font-size:unset;font-weight:unset}.DnA1Iv_lwCTNSAFQGrON .Pnf8O2SgfIFmVM0PSv3Z p svg{color:unset;padding-left:.5rem}.DnA1Iv_lwCTNSAFQGrON{border-bottom:1px solid #dde2e6;display:flex;flex-direction:column;margin-bottom:.75rem;padding:.5rem 0 1rem 1rem}.iw1ugzHiscI8cdqPxDt5.kjodIeFA7B16RQcru0GW{border-bottom:none}.Pnf8O2SgfIFmVM0PSv3Z{display:flex;justify-content:space-between}.Pnf8O2SgfIFmVM0PSv3Z.kjodIeFA7B16RQcru0GW{flex-direction:column}.DnA1Iv_lwCTNSAFQGrON p{font-size:.9rem;font-weight:600;margin:0;padding:.5rem 0}.Pnf8O2SgfIFmVM0PSv3Z .A0_oAmpyVJ9wdtADndGQ span svg{color:#00c346;padding-left:.5rem}button.HfzrDbHUd_u1i9ndGEBR{background-color:initial;border:none;border-radius:3px;color:#999;cursor:pointer;display:inline-block;margin-right:-.1em;padding:.5rem 1rem;transition:background .1s ease-in;transition:.1s ease-in}button.HfzrDbHUd_u1i9ndGEBR:hover{background-color:#da001d;color:#fff}.elgzg717O9Crp2uzkrTD button{margin-right:2em}.V9u2jF9aJPfN0wX4PDVS{background-color:none;border:none;cursor:pointer;line-height:2.15384615;margin-right:1rem;text-decoration:underline;text-decoration-color:#00000026}.V9u2jF9aJPfN0wX4PDVS:hover{text-decoration-color:#000}.wziOBkVmZ17vJu2Sz35G{border-bottom:1px solid #dde2e6;margin-bottom:.75rem;padding:.5rem 0 1rem 1rem}.DFhEV9q8j6_YiAqxGCIQ{font-size:.9rem;font-weight:600;margin:0;padding:.5rem 0}.DFhEV9q8j6_YiAqxGCIQ.y9VOhnGapgfPZMY_1Bh6 svg{color:#00c346;padding-left:.5rem}.DFhEV9q8j6_YiAqxGCIQ.czVzoPvuSXhcttrzFc9L svg{color:#f8f9fa}.ngog_nfdj6MCATJVipdj{padding:.5rem 0 1rem 1rem}.S_T55Hlv5ASeRm2CyHmV{font-size:.9rem;font-weight:600;margin:0 0 1rem}.S_T55Hlv5ASeRm2CyHmV svg{color:#dde2e6;padding-left:.5rem}.fy5GsBzkO8Epk2RR04LH{margin-right:1.8rem}button.fNJ_UaCGqOZxk72kjIDA{background-color:initial;border:none;border-radius:3px;color:#228be6;cursor:pointer;display:inline-block;margin:0 .2rem 0 0;padding:.5rem;transition:background .1s ease-in;transition:.1s ease-in;vertical-align:middle}button.fNJ_UaCGqOZxk72kjIDA:hover{background-color:#1c7ed6;color:#fff}button.fNJ_UaCGqOZxk72kjIDA span{padding-right:.5em}.SstVIjmK5UOaiy_ltjXE .vdGJ8TWieYuqqCakSyHG{display:inline-block;font-weight:600;height:auto;margin-left:1rem;padding:.4rem;vertical-align:middle;width:30%}.gJq7WtlHzhBcjrgjABY8{margin-right:1.8rem}.o1IZC2E8wgWEslXswxvX{border-collapse:collapse;font-size:.9rem;margin:0 0 1rem;width:100%}.o1IZC2E8wgWEslXswxvX .W3xNRcvtnX8v6hHiakrQ,.o1IZC2E8wgWEslXswxvX .HS7POK_5ddxGDSTzJHun{border-bottom:1px solid #dde2e6;border-top:1px solid #dde2e6;padding:.5rem;text-align:right;vertical-align:top}.o1IZC2E8wgWEslXswxvX .W3xNRcvtnX8v6hHiakrQ{font-weight:600;width:30%}.co95Sqvd5n7VgX7SM5jt{display:block;font-weight:400;line-height:1.5;margin-top:.25rem}.co95Sqvd5n7VgX7SM5jt,.co95Sqvd5n7VgX7SM5jt a{color:#868e96}.co95Sqvd5n7VgX7SM5jt a:hover{color:#228be6}
.wVJC_TuxmtpxI03Tbdkt{border-bottom:2px solid #008ded;display:flex;margin-bottom:.5rem;padding-bottom:1rem}.wVJC_TuxmtpxI03Tbdkt label{margin-left:1rem}
.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}

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

3
admin/build/587-rtl.css Normal file
View File

@ -0,0 +1,3 @@
.xAYNgmh_FT28wOZEe4og{background-color:#fff;margin-left:20px;padding:1rem 2rem 2rem}@media only screen and (min-width:1024px){.xAYNgmh_FT28wOZEe4og{max-width:1000px}}.W3wz4Liah2EvWxtTBXN8{align-items:center;display:flex}.OpLLWfmNs6BXGmnmuinK{padding-right:1em}div.OpLLWfmNs6BXGmnmuinK.q0fCXPnTi1vRhNmP0IEt{background-color:#fda09a;border-radius:5px;display:flex;margin:1em;max-width:450px;padding:1em}div.OpLLWfmNs6BXGmnmuinK.q0fCXPnTi1vRhNmP0IEt .A2dLn8oZtVzuXngZMDRp{margin-bottom:auto;margin-top:auto}div.OpLLWfmNs6BXGmnmuinK.q0fCXPnTi1vRhNmP0IEt .xFoMk9Jc8Ir4n5Olcce1{max-width:400px}.OpLLWfmNs6BXGmnmuinK .xFoMk9Jc8Ir4n5Olcce1{padding-right:1em}.OpLLWfmNs6BXGmnmuinK.fQeEY3YNz4yh6R7vdi7J .JPBgwk6PxfiitLxJLE54{color:green}h2.VklefjWwawC59yrOPe3e{font-size:18px}h3.VklefjWwawC59yrOPe3e{font-size:16px}.e8Vu3y2YBkuW8N9IhY2m{margin-bottom:1rem;margin-top:1rem}.gNYVG50hxMZs8Gqbj_T0 th{font-weight:700}button.dpYyb_l0GWlAiVkOmmYt{background:none;border:0}.WJl_9YHKGkhUvtVwgVco{align-items:center;display:flex}.HBCEbIhIET1XISEYneSA{margin:1rem}button.ZXe2iyFqFThwx_UF4CBf{background:#008ded;border:solid #0064b1;border-radius:3px;border-width:1px 1px 4px;color:#fff;cursor:pointer;font-size:14px;font-weight:600;line-height:1.4em;padding:.7em 1.5em}button.ZXe2iyFqFThwx_UF4CBf[disabled]{background:#f8f9fa;border:1px solid #f8f9fa;color:#008ded;cursor:default}button .HgLyUkphZYd8YsLSMJAZ{display:inline-block;min-width:3.2em;text-align:right}.Gu2u4ZSZT25Yqm8zSogj{background-color:#fdfdf3;border:1px solid #000;border-radius:5px;max-width:600px;padding:1.5em}.WOV9bdVrpJVdQWzhBnHZ{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}@media only screen and (min-width:1024px){.QN_KH8sqi5QFBDqaH1rI{display:flex}}.bBMVcUUJf1GW7veG1Zic{flex-direction:row}.pIa2BGO1ABMtYZY185Bf{flex-direction:column}.h0koIl1zvME7toM3jUk9{position:relative}.kWqY7l_wn27HmdUNz7ZY .JPBgwk6PxfiitLxJLE54{font-size:24px}.kWqY7l_wn27HmdUNz7ZY.q3No9l7YMUEH1xvYTNfI .JPBgwk6PxfiitLxJLE54,.Y7M4JHzDp7jtCt6MonbK{color:green}.a6qTuZmDiKS_FHgMZawo,.kWqY7l_wn27HmdUNz7ZY.PwCQsIQEdGz9b0cOj3iA .JPBgwk6PxfiitLxJLE54{color:red}.kWqY7l_wn27HmdUNz7ZY.Gu2u4ZSZT25Yqm8zSogj .JPBgwk6PxfiitLxJLE54,.rw5FUVRrrdM17WyxcRZ9{color:#b7b700}.QEoklKhbCbwOUBs0cspa{margin-right:1rem}.oWHnpotXuoOIlJoqkkgw~label .NzRaF0U8aKPVtS6JIaK8,.gIUwcNcpOHhTKG4sTlfg~label .NzRaF0U8aKPVtS6JIaK8{display:none;opacity:0}.oWHnpotXuoOIlJoqkkgw:checked~label .NzRaF0U8aKPVtS6JIaK8,.gIUwcNcpOHhTKG4sTlfg:checked~label .NzRaF0U8aKPVtS6JIaK8{color:#228be6;display:block;opacity:1}.oWHnpotXuoOIlJoqkkgw:checked~label .iemYJRvB4tzF1xnuGiAw,.gIUwcNcpOHhTKG4sTlfg:checked~label .iemYJRvB4tzF1xnuGiAw{display:none;opacity:0}.oWHnpotXuoOIlJoqkkgw:checked~label .BFR5diS8tiViycbuTDVS,.oWHnpotXuoOIlJoqkkgw:checked~label .BFR5diS8tiViycbuTDVS a{color:#495057}.oWHnpotXuoOIlJoqkkgw:checked~label .BFR5diS8tiViycbuTDVS a:hover,.oWHnpotXuoOIlJoqkkgw~label .BFR5diS8tiViycbuTDVS a:hover{color:#228be6;text-decoration-color:initial}
.iVV55iNB320NJJLspy7m{max-width:600px}.BcmdF5mOoQ3Luug6sJrn{border:1px dotted grey;border-radius:5px;margin:1rem 0 1rem 1rem;padding:1rem}
.lX8h3LbX6kaLN7_hLhlw{align-items:stretch;background-color:#e4f6ff;border-radius:.25rem;color:#495057;display:flex;flex-direction:row;flex-wrap:no-wrap;margin-top:1rem;max-width:800px}.nx2ZqeD9AnYnPnKHAqKJ{border-bottom-right-radius:.25rem;border-top-right-radius:.25rem;color:#008bed;font-size:1rem;padding:.55rem .75rem .5rem .25rem}.ovRzytWn5jGccLKV78T9{font-size:.8rem;font-weight:600;line-height:1.5;margin-bottom:.2rem;margin-top:0}.M_C6Dj_EqhO8IuY52iA6{display:flex;flex-direction:column;flex-grow:1;font-size:.8rem;padding:.5rem .25rem .75rem 1rem}.M_C6Dj_EqhO8IuY52iA6 p{margin-bottom:.5rem;margin-top:0}.M_C6Dj_EqhO8IuY52iA6 svg{font-size:.7rem}.M_C6Dj_EqhO8IuY52iA6 ul{margin:0}.M_C6Dj_EqhO8IuY52iA6 li{display:inline-block;margin-bottom:0;padding-left:1rem}.liWjpcvKZkKaYPsJjQPA{margin-top:1rem}.lX8h3LbX6kaLN7_hLhlw button{color:#0073aa}.lX8h3LbX6kaLN7_hLhlw button:hover{color:#00a0d2}.MLwfZfK5uVZOtIHI1cdt{background:#fff9db}.MLwfZfK5uVZOtIHI1cdt .nx2ZqeD9AnYnPnKHAqKJ{color:#fab007}

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:()=>f,Nfw:()=>l,SGM:()=>i,wRm:()=>n});const 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"]},l={prefix:"far",iconName:"square",icon:[448,512,[9632,9723,9724,61590],"f0c8","M384 80c8.8 0 16 7.2 16 16l0 320c0 8.8-7.2 16-16 16L64 432c-8.8 0-16-7.2-16-16L48 96c0-8.8 7.2-16 16-16l320 0zM64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l320 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64L64 32z"]},f={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"]},n={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.3l58.3 0c34.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-24l0-13.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.1l-58.3 0c-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', 'react-jsx-runtime', 'wp-dom-ready', 'wp-element', 'wp-i18n'), 'version' => 'c533036ab18a17f3f9bc');

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

@ -5,4 +5,4 @@ process.env.WP_BASE_URL = `http://${process.env.WP_DOMAIN}`
process.env.WP_USERNAME = process.env.WP_ADMIN_USERNAME process.env.WP_USERNAME = process.env.WP_ADMIN_USERNAME
process.env.WP_PASSWORD = process.env.WP_ADMIN_PASSWORD process.env.WP_PASSWORD = process.env.WP_ADMIN_PASSWORD
module.exports = defaultConfig; module.exports = defaultConfig

14768
admin/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,13 @@
{ {
"name": "font-awesome-admin", "name": "font-awesome-admin",
"version": "4.5.0", "version": "5.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@fortawesome/fa-icon-chooser-react": "^0.7.0",
"@fortawesome/fontawesome-svg-core": "^6.2.0", "@fortawesome/fontawesome-svg-core": "^6.2.0",
"@fortawesome/free-regular-svg-icons": "^6.2.0", "@fortawesome/free-regular-svg-icons": "^6.2.0",
"@fortawesome/free-solid-svg-icons": "^6.2.0", "@fortawesome/free-solid-svg-icons": "^6.2.0",
"@fortawesome/react-fontawesome": "^0.1.3", "@fortawesome/react-fontawesome": "^0.1.3",
"axios": "^1.7.2", "axios": "^1.7.4",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"moment": "^2.30.1", "moment": "^2.30.1",
"moment-timezone": "^0.5.45", "moment-timezone": "^0.5.45",
@ -19,7 +18,7 @@
"web-vitals": "^4.0.1" "web-vitals": "^4.0.1"
}, },
"scripts": { "scripts": {
"build": "wp-scripts build --webpack-no-externals", "build": "wp-scripts build",
"check-engines": "wp-scripts check-engines", "check-engines": "wp-scripts check-engines",
"check-licenses": "wp-scripts check-licenses", "check-licenses": "wp-scripts check-licenses",
"format": "wp-scripts format", "format": "wp-scripts format",
@ -29,7 +28,7 @@
"lint:md:js": "wp-scripts lint-md-js", "lint:md:js": "wp-scripts lint-md-js",
"lint:pkg-json": "wp-scripts lint-pkg-json", "lint:pkg-json": "wp-scripts lint-pkg-json",
"packages-update": "wp-scripts packages-update", "packages-update": "wp-scripts packages-update",
"start": "wp-scripts start --webpack-no-externals", "start": "wp-scripts start",
"test:e2e": "wp-scripts test-e2e", "test:e2e": "wp-scripts test-e2e",
"test:playwright": "npx playwright test", "test:playwright": "npx playwright test",
"test:unit": "wp-scripts test-unit-js", "test:unit": "wp-scripts test-unit-js",
@ -54,21 +53,23 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@wordpress/babel-preset-default": "wp-6.5", "@wordpress/babel-preset-default": "wp-6.7",
"@wordpress/block-editor": "wp-6.5", "@wordpress/block-editor": "wp-6.7",
"@wordpress/blocks": "wp-6.5", "@wordpress/blocks": "wp-6.7",
"@wordpress/components": "wp-6.5", "@wordpress/components": "wp-6.7",
"@wordpress/compose": "wp-6.5", "@wordpress/compose": "wp-6.7",
"@wordpress/data": "wp-6.5", "@wordpress/data": "wp-6.7",
"@wordpress/e2e-test-utils": "wp-6.5", "@wordpress/e2e-test-utils": "wp-6.7",
"@wordpress/e2e-test-utils-playwright": "^0.26.0", "@wordpress/e2e-test-utils-playwright": "^0.26.0",
"@wordpress/i18n": "wp-6.5", "@wordpress/i18n": "wp-6.7",
"@wordpress/icons": "wp-6.5", "@wordpress/icons": "wp-6.7",
"@wordpress/jest-preset-default": "wp-6.5", "@wordpress/jest-preset-default": "wp-6.7",
"@wordpress/keyboard-shortcuts": "wp-6.5", "@wordpress/keyboard-shortcuts": "wp-6.7",
"@wordpress/scripts": "wp-6.5", "@wordpress/scripts": "wp-6.7",
"decode-uri-component": "^0.4.1", "decode-uri-component": "^0.4.1",
"dotenv": "^14.3.2", "dotenv": "^14.3.2",
"eslint-config-react-app": "^7.0.1",
"lodash": "^4.17.21",
"mysql2": "^3.9.9", "mysql2": "^3.9.9",
"react": "18.3.1", "react": "18.3.1",
"react-dom": "18.3.1", "react-dom": "18.3.1",

View File

@ -1,5 +1,5 @@
import './src/playwright/support/env.js' import './src/playwright/support/env.js'
import { defineConfig, devices } from '@playwright/test'; import { defineConfig, devices } from '@playwright/test'
const testDir = 'src/playwright' const testDir = 'src/playwright'
const baseURL = `http://${process.env.WP_DOMAIN}` const baseURL = `http://${process.env.WP_DOMAIN}`
@ -8,7 +8,7 @@ const adminStorageStatePath = 'src/playwright/.auth/state.json'
export default defineConfig({ export default defineConfig({
use: { use: {
baseURL, baseURL
}, },
projects: [ projects: [
{ name: 'auth', testDir, testMatch: 'setup/auth.js' }, { name: 'auth', testDir, testMatch: 'setup/auth.js' },
@ -18,7 +18,7 @@ export default defineConfig({
testMatch: 'setup/reset.js', testMatch: 'setup/reset.js',
use: { use: {
storageState: adminStorageStatePath storageState: adminStorageStatePath
}, }
}, },
{ {
name: 'setupProKit', name: 'setupProKit',
@ -27,10 +27,7 @@ export default defineConfig({
use: { use: {
storageState: adminStorageStatePath storageState: adminStorageStatePath
}, },
dependencies: [ dependencies: ['auth', 'reset']
'auth',
'reset'
]
}, },
{ {
name: 'with-proKit-chromium', name: 'with-proKit-chromium',
@ -39,7 +36,7 @@ export default defineConfig({
...devices['Desktop Chrome'], ...devices['Desktop Chrome'],
storageState: adminStorageStatePath storageState: adminStorageStatePath
}, },
dependencies: ['setupProKit'], dependencies: ['setupProKit']
}, },
{ {
name: 'withAuth-chromium', name: 'withAuth-chromium',
@ -48,8 +45,7 @@ export default defineConfig({
...devices['Desktop Chrome'], ...devices['Desktop Chrome'],
storageState: adminStorageStatePath 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 styles from './Alert.module.css'
import classnames from 'classnames' import classnames from 'classnames'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { import { faInfoCircle, faThumbsUp, faSpinner, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'
faInfoCircle,
faThumbsUp,
faSpinner,
faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'
function getIcon(props = {}) { function getIcon(props = {}) {
switch (props.type) { switch (props.type) {
case 'info': case 'info':
return <FontAwesomeIcon icon={ faInfoCircle } title='info' fixedWidth /> return (
<FontAwesomeIcon
icon={faInfoCircle}
title="info"
fixedWidth
/>
)
case 'warning': case 'warning':
return <FontAwesomeIcon icon={ faExclamationTriangle } title='warning' fixedWidth /> return (
<FontAwesomeIcon
icon={faExclamationTriangle}
title="warning"
fixedWidth
/>
)
case 'pending': case 'pending':
return <FontAwesomeIcon icon={ faSpinner } title='pending' spin fixedWidth /> return (
<FontAwesomeIcon
icon={faSpinner}
title="pending"
spin
fixedWidth
/>
)
case 'success': case 'success':
return <FontAwesomeIcon icon={ faThumbsUp } title='success' fixedWidth /> return (
<FontAwesomeIcon
icon={faThumbsUp}
title="success"
fixedWidth
/>
)
default: default:
return <FontAwesomeIcon icon={ faExclamationTriangle } title='warning' fixedWidth /> return (
<FontAwesomeIcon
icon={faExclamationTriangle}
title="warning"
fixedWidth
/>
)
} }
} }
function Alert(props = {}) { function Alert(props = {}) {
return <div className={ classnames(styles['alert'], styles[`alert-${ props.type }`]) } role="alert"> return (
<div className={ styles['alert-icon'] }> <div
{ getIcon(props) } className={classnames(styles['alert'], styles[`alert-${props.type}`])}
</div> role="alert"
>
<div className={styles['alert-icon']}>{getIcon(props)}</div>
<div className={styles['alert-message']}> <div className={styles['alert-message']}>
<h2 className={ styles['alert-title'] }> <h2 className={styles['alert-title']}>{props.title}</h2>
{ props.title } <div className={styles['alert-copy']}>{props.children}</div>
</h2>
<div className={ styles['alert-copy'] }>
{ props.children }
</div>
</div> </div>
</div> </div>
)
} }
Alert.propTypes = { Alert.propTypes = {
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
type: PropTypes.oneOf(['info', 'warning', 'success', 'pending']), type: PropTypes.oneOf(['info', 'warning', 'success', 'pending']),
children: PropTypes.oneOfType([ children: PropTypes.oneOfType([PropTypes.object, PropTypes.string, PropTypes.arrayOf(PropTypes.element)]).isRequired
PropTypes.object,
PropTypes.string,
PropTypes.arrayOf(PropTypes.element)
]).isRequired
} }
export default Alert export default Alert

View File

@ -1,23 +1,15 @@
import React from 'react' import React from 'react'
import { useSelector, useDispatch } from 'react-redux' import { useSelector, useDispatch } from 'react-redux'
import { import { addPendingOption, checkPreferenceConflicts } from './store/actions'
addPendingOption,
checkPreferenceConflicts
} from './store/actions'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { import { faDotCircle, faCheckSquare, faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons'
faDotCircle,
faCheckSquare,
faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons'
import { faCircle, faSquare } from '@fortawesome/free-regular-svg-icons' import { faCircle, faSquare } from '@fortawesome/free-regular-svg-icons'
import styles from './CdnConfigView.module.css' import styles from './CdnConfigView.module.css'
import sharedStyles from './App.module.css' import sharedStyles from './App.module.css'
import classnames from 'classnames' import classnames from 'classnames'
import has from 'lodash/has' import { has, size, get } from 'lodash'
import size from 'lodash/size'
import Alert from './Alert' import Alert from './Alert'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import get from 'lodash/get'
import { __ } from '@wordpress/i18n' import { __ } from '@wordpress/i18n'
const UNSPECIFIED = '' const UNSPECIFIED = ''
@ -29,20 +21,26 @@ export default function CdnConfigView({ useOption, handleSubmit }) {
const compat = useOption('compat') const compat = useOption('compat')
const pseudoElements = useOption('pseudoElements') const pseudoElements = useOption('pseudoElements')
const isVersion6 = !!version.match(/^6\./) const isVersion6 = !!version.match(/^6\./)
const isVersion7 = !!version.match(/^7\./)
const isVersion5 = !isVersion6 && !isVersion7
const pendingOptions = useSelector(state => state.pendingOptions) const pendingOptions = useSelector((state) => state.pendingOptions)
const pendingOptionConflicts = useSelector(state => state.pendingOptionConflicts) const pendingOptionConflicts = useSelector((state) => state.pendingOptionConflicts)
const hasChecked = useSelector(state => state.preferenceConflictDetection.hasChecked) const hasChecked = useSelector((state) => state.preferenceConflictDetection.hasChecked)
const preferenceCheckSuccess = useSelector(state => state.preferenceConflictDetection.success) const preferenceCheckSuccess = useSelector((state) => state.preferenceConflictDetection.success)
const preferenceCheckMessage = useSelector(state => state.preferenceConflictDetection.message) const preferenceCheckMessage = useSelector((state) => state.preferenceConflictDetection.message)
const versionOptions = useSelector(state => { const versionOptions = useSelector((state) => {
const { releases: { available, latest_version_5, latest_version_6 } } = state const {
releases: { available, latest_version_5, latest_version_6, latest_version_7 }
} = state
return available.reduce((acc, version) => { return available.reduce((acc, version) => {
if (latest_version_5 === version) { if (latest_version_5 === version) {
acc[version] = `${version} (latest 5.x)` acc[version] = `${version} (latest 5.x)`
} else if (latest_version_6 === version) { } else if (latest_version_6 === version) {
acc[version] = `${version} (latest 6.x)`
} else if (latest_version_7 === version) {
acc[version] = `${version} (latest)` acc[version] = `${version} (latest)`
} else { } else {
acc[version] = version acc[version] = version
@ -69,21 +67,31 @@ export default function CdnConfigView({ useOption, handleSubmit }) {
function getDetectionStatusForOption(option) { function getDetectionStatusForOption(option) {
if (has(pendingOptions, option)) { if (has(pendingOptions, option)) {
if (hasChecked && !preferenceCheckSuccess) { if (hasChecked && !preferenceCheckSuccess) {
return <Alert title={ __( 'Error checking preferences', 'font-awesome' ) } type='warning'> return (
<Alert
title={__('Error checking preferences', 'font-awesome')}
type="warning"
>
<p>{preferenceCheckMessage}</p> <p>{preferenceCheckMessage}</p>
</Alert> </Alert>
)
} else if (has(pendingOptionConflicts, option)) { } else if (has(pendingOptionConflicts, option)) {
return <Alert title={ __( 'Preference Conflict', 'font-awesome' ) } type='warning'> return (
{ <Alert
size(pendingOptionConflicts[option]) > 1 title={__('Preference Conflict', 'font-awesome')}
? <div> type="warning"
>
{size(pendingOptionConflicts[option]) > 1 ? (
<div>
{__('This change might cause problems for these themes or plugins', 'font-awesome')}: {pendingOptionConflicts[option].join(', ')}. {__('This change might cause problems for these themes or plugins', 'font-awesome')}: {pendingOptionConflicts[option].join(', ')}.
</div> </div>
: <div> ) : (
<div>
{__('This change might cause problems for the theme or plugin', 'font-awesome')}: {pendingOptionConflicts[option][0]}. {__('This change might cause problems for the theme or plugin', 'font-awesome')}: {pendingOptionConflicts[option][0]}.
</div> </div>
} )}
</Alert> </Alert>
)
} else { } else {
return null return null
} }
@ -92,8 +100,9 @@ export default function CdnConfigView({ useOption, handleSubmit }) {
} }
} }
return <div className={ classnames(styles['options-setter']) }> return (
<form onSubmit={ e => e.preventDefault() }> <div className={classnames(styles['options-setter'])}>
<form onSubmit={(e) => e.preventDefault()}>
<div className={classnames(sharedStyles['flex'], sharedStyles['flex-row'])}> <div className={classnames(sharedStyles['flex'], sharedStyles['flex-row'])}>
<div className={styles['option-header']}>Icons</div> <div className={styles['option-header']}>Icons</div>
<div className={styles['option-choice-container']}> <div className={styles['option-choice-container']}>
@ -107,7 +116,10 @@ export default function CdnConfigView({ useOption, handleSubmit }) {
onChange={() => handleOptionChange({ usePro: true })} onChange={() => handleOptionChange({ usePro: true })}
className={classnames(sharedStyles['sr-only'], sharedStyles['input-radio-custom'])} className={classnames(sharedStyles['sr-only'], sharedStyles['input-radio-custom'])}
/> />
<label htmlFor="code_edit_icons_pro" className={ styles['option-label'] }> <label
htmlFor="code_edit_icons_pro"
className={styles['option-label']}
>
<span className={sharedStyles['relative']}> <span className={sharedStyles['relative']}>
<FontAwesomeIcon <FontAwesomeIcon
icon={faDotCircle} icon={faDotCircle}
@ -122,9 +134,7 @@ export default function CdnConfigView({ useOption, handleSubmit }) {
fixedWidth fixedWidth
/> />
</span> </span>
<span className={ styles['option-label-text'] }> <span className={styles['option-label-text']}>Pro</span>
Pro
</span>
</label> </label>
</div> </div>
<div className={styles['option-choice']}> <div className={styles['option-choice']}>
@ -136,7 +146,10 @@ export default function CdnConfigView({ useOption, handleSubmit }) {
onChange={() => handleOptionChange({ usePro: false })} onChange={() => handleOptionChange({ usePro: false })}
className={classnames(sharedStyles['sr-only'], sharedStyles['input-radio-custom'])} className={classnames(sharedStyles['sr-only'], sharedStyles['input-radio-custom'])}
/> />
<label htmlFor="code_edit_icons_free" className={ styles['option-label'] }> <label
htmlFor="code_edit_icons_free"
className={styles['option-label']}
>
<span className={sharedStyles['relative']}> <span className={sharedStyles['relative']}>
<FontAwesomeIcon <FontAwesomeIcon
icon={faDotCircle} icon={faDotCircle}
@ -151,32 +164,57 @@ export default function CdnConfigView({ useOption, handleSubmit }) {
className={sharedStyles['unchecked-icon']} className={sharedStyles['unchecked-icon']}
/> />
</span> </span>
<span className={ styles['option-label-text'] }> <span className={styles['option-label-text']}>Free</span>
Free
</span>
</label> </label>
</div> </div>
</div> </div>
{ usePro && {usePro && (isVersion6 || isVersion7) && (
isVersion6 && <Alert
<Alert title={ __( 'Heads up! Pro Version 6 is not available from CDN', 'font-awesome' ) } type='warning'> title={isVersion6 ? __('Heads up! Pro Version 6 is not available from CDN', 'font-awesome') : __('Heads up! Pro Version 7 is not available from CDN', 'font-awesome')}
<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> 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> </Alert>
} )}
{ usePro && {usePro && isVersion5 && (
!isVersion6 && <Alert
<Alert title={ __( 'Heads up! Pro requires a Font Awesome subscription', 'font-awesome' ) } type='info'> 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> <p>And you need to add your WordPress site to the allowed domains for your CDN.</p>
<ul> <ul>
<li> <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>
<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> </li>
</ul> </ul>
</Alert> </Alert>
} )}
{getDetectionStatusForOption('usePro')} {getDetectionStatusForOption('usePro')}
</div> </div>
</div> </div>
@ -194,7 +232,10 @@ export default function CdnConfigView({ useOption, handleSubmit }) {
onChange={() => handleOptionChange({ technology: 'svg' })} onChange={() => handleOptionChange({ technology: 'svg' })}
className={classnames(sharedStyles['sr-only'], sharedStyles['input-radio-custom'])} className={classnames(sharedStyles['sr-only'], sharedStyles['input-radio-custom'])}
/> />
<label htmlFor="code_edit_tech_svg" className={ styles['option-label'] }> <label
htmlFor="code_edit_tech_svg"
className={styles['option-label']}
>
<span className={sharedStyles['relative']}> <span className={sharedStyles['relative']}>
<FontAwesomeIcon <FontAwesomeIcon
icon={faDotCircle} icon={faDotCircle}
@ -209,9 +250,7 @@ export default function CdnConfigView({ useOption, handleSubmit }) {
fixedWidth fixedWidth
/> />
</span> </span>
<span className={ styles['option-label-text'] }> <span className={styles['option-label-text']}>{__('SVG', 'font-awesome')}</span>
{ __( 'SVG', 'font-awesome' ) }
</span>
</label> </label>
</div> </div>
<div className={styles['option-choice']}> <div className={styles['option-choice']}>
@ -220,13 +259,18 @@ export default function CdnConfigView({ useOption, handleSubmit }) {
name="code_edit_tech" name="code_edit_tech"
type="radio" type="radio"
checked={technology === 'webfont'} checked={technology === 'webfont'}
onChange={ () => handleOptionChange({ onChange={() =>
handleOptionChange({
technology: 'webfont', technology: 'webfont',
pseudoElements: false pseudoElements: false
}) } })
}
className={classnames(sharedStyles['sr-only'], sharedStyles['input-radio-custom'])} className={classnames(sharedStyles['sr-only'], sharedStyles['input-radio-custom'])}
/> />
<label htmlFor="code_edit_tech_webfont" className={ styles['option-label'] }> <label
htmlFor="code_edit_tech_webfont"
className={styles['option-label']}
>
<span className={sharedStyles['relative']}> <span className={sharedStyles['relative']}>
<FontAwesomeIcon <FontAwesomeIcon
icon={faDotCircle} icon={faDotCircle}
@ -243,12 +287,11 @@ export default function CdnConfigView({ useOption, handleSubmit }) {
</span> </span>
<span className={styles['option-label-text']}> <span className={styles['option-label-text']}>
{__('Web Font', 'font-awesome')} {__('Web Font', 'font-awesome')}
{ {technology === 'webfont' && (
technology === 'webfont' &&
<span className={styles['option-label-explanation']}> <span className={styles['option-label-explanation']}>
{__('CSS Pseudo-elements are enabled by default with Web Font', 'font-awesome')} {__('CSS Pseudo-elements are enabled by default with Web Font', 'font-awesome')}
</span> </span>
} )}
</span> </span>
</label> </label>
</div> </div>
@ -258,8 +301,11 @@ export default function CdnConfigView({ useOption, handleSubmit }) {
</div> </div>
<div className={classnames(sharedStyles['flex'], sharedStyles['flex-row'])}> <div className={classnames(sharedStyles['flex'], sharedStyles['flex-row'])}>
<div className={styles['option-header']}></div> <div className={styles['option-header']}></div>
<div className={ styles['option-choice-container'] } style={{marginTop: '1em'}}> <div
{ technology === 'svg' && className={styles['option-choice-container']}
style={{ marginTop: '1em' }}
>
{technology === 'svg' && (
<> <>
<input <input
id="code_edit_features_pseudo_elements" id="code_edit_features_pseudo_elements"
@ -269,7 +315,10 @@ export default function CdnConfigView({ useOption, handleSubmit }) {
onChange={() => handleOptionChange({ pseudoElements: !pseudoElements })} onChange={() => handleOptionChange({ pseudoElements: !pseudoElements })}
className={classnames(sharedStyles['sr-only'], sharedStyles['input-checkbox-custom'])} 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']}> <span className={sharedStyles['relative']}>
<FontAwesomeIcon <FontAwesomeIcon
icon={faCheckSquare} icon={faCheckSquare}
@ -287,15 +336,25 @@ export default function CdnConfigView({ useOption, handleSubmit }) {
<span className={styles['option-label-text']}> <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']}> <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"> {__('May cause performance issues.', 'font-awesome')}{' '}
{ __( 'Learn more', 'font-awesome' ) } <FontAwesomeIcon icon={faExternalLinkAlt} style={{marginLeft: '.5em'}} /> <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> </a>
</span> </span>
</span> </span>
</label> </label>
{getDetectionStatusForOption('pseudoElements')} {getDetectionStatusForOption('pseudoElements')}
</> </>
} )}
</div> </div>
</div> </div>
<hr className={styles['option-divider']} /> <hr className={styles['option-divider']} />
@ -306,16 +365,19 @@ export default function CdnConfigView({ useOption, handleSubmit }) {
<select <select
className={styles['version-select']} className={styles['version-select']}
name="version" name="version"
onChange={ e => handleOptionChange({ version: e.target.value }) } onChange={(e) => handleOptionChange({ version: e.target.value })}
value={version}
>
{Object.keys(versionOptions).map((version, index) => {
return (
<option
key={index}
value={version} value={version}
> >
{
Object.keys(versionOptions).map((version, index) => {
return <option key={ index } value={ version }>
{version === UNSPECIFIED ? '-' : versionOptions[version]} {version === UNSPECIFIED ? '-' : versionOptions[version]}
</option> </option>
}) )
} })}
</select> </select>
</div> </div>
{getDetectionStatusForOption('version')} {getDetectionStatusForOption('version')}
@ -336,7 +398,10 @@ export default function CdnConfigView({ useOption, handleSubmit }) {
onChange={() => handleOptionChange({ compat: !compat })} onChange={() => handleOptionChange({ compat: !compat })}
className={classnames(sharedStyles['sr-only'], sharedStyles['input-radio-custom'])} className={classnames(sharedStyles['sr-only'], sharedStyles['input-radio-custom'])}
/> />
<label htmlFor="code_edit_compat_on" className={ styles['option-label'] }> <label
htmlFor="code_edit_compat_on"
className={styles['option-label']}
>
<span className={sharedStyles['relative']}> <span className={sharedStyles['relative']}>
<FontAwesomeIcon <FontAwesomeIcon
icon={faDotCircle} icon={faDotCircle}
@ -351,9 +416,7 @@ export default function CdnConfigView({ useOption, handleSubmit }) {
fixedWidth fixedWidth
/> />
</span> </span>
<span className={ styles['option-label-text'] }> <span className={styles['option-label-text']}>{__('On', 'font-awesome')}</span>
{ __( 'On', 'font-awesome' ) }
</span>
</label> </label>
</div> </div>
<div className={styles['option-choice']}> <div className={styles['option-choice']}>
@ -366,7 +429,10 @@ export default function CdnConfigView({ useOption, handleSubmit }) {
onChange={() => handleOptionChange({ compat: !compat })} onChange={() => handleOptionChange({ compat: !compat })}
className={classnames(sharedStyles['sr-only'], sharedStyles['input-radio-custom'])} className={classnames(sharedStyles['sr-only'], sharedStyles['input-radio-custom'])}
/> />
<label htmlFor="code_edit_v4_compat_off" className={ styles['option-label'] }> <label
htmlFor="code_edit_v4_compat_off"
className={styles['option-label']}
>
<span className={sharedStyles['relative']}> <span className={sharedStyles['relative']}>
<FontAwesomeIcon <FontAwesomeIcon
icon={faDotCircle} icon={faDotCircle}
@ -381,9 +447,7 @@ export default function CdnConfigView({ useOption, handleSubmit }) {
className={sharedStyles['unchecked-icon']} className={sharedStyles['unchecked-icon']}
/> />
</span> </span>
<span className={ styles['option-label-text'] }> <span className={styles['option-label-text']}>{__('Off', 'font-awesome')}</span>
{ __( 'Off', 'font-awesome' ) }
</span>
</label> </label>
</div> </div>
</div> </div>
@ -392,6 +456,7 @@ export default function CdnConfigView({ useOption, handleSubmit }) {
</div> </div>
</form> </form>
</div> </div>
)
} }
CdnConfigView.propTypes = { CdnConfigView.propTypes = {

View File

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

View File

@ -2,9 +2,7 @@ import React from 'react'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import styles from './ClientPreferencesView.module.css' import styles from './ClientPreferencesView.module.css'
import sharedStyles from './App.module.css' import sharedStyles from './App.module.css'
import find from 'lodash/find' import { find, has, size } from 'lodash'
import has from 'lodash/has'
import size from 'lodash/size'
import classnames from 'classnames' import classnames from 'classnames'
import { __, sprintf } from '@wordpress/i18n' import { __, sprintf } from '@wordpress/i18n'
@ -12,7 +10,7 @@ const UNSPECIFIED_INDICATOR = '-'
function formatVersionPreference(versionPreference = []) { function formatVersionPreference(versionPreference = []) {
return versionPreference return versionPreference
.map(pref => `${pref[1]}${pref[0]}`) .map((pref) => `${pref[1]}${pref[0]}`)
.join( .join(
sprintf( sprintf(
/* translators: 1: space */ /* translators: 1: space */
@ -23,31 +21,31 @@ function formatVersionPreference(versionPreference = []) {
} }
export default function ClientPreferencesView() { export default function ClientPreferencesView() {
const clientPreferences = useSelector(state => state.clientPreferences) const clientPreferences = useSelector((state) => state.clientPreferences)
const conflicts = useSelector(state => state.preferenceConflicts) const conflicts = useSelector((state) => state.preferenceConflicts)
const hasAdditionalClients = size(clientPreferences) const hasAdditionalClients = size(clientPreferences)
const hasConflicts = size(conflicts) const hasConflicts = size(conflicts)
return <div className={ styles['client-requirements'] }> return (
<div className={styles['client-requirements']}>
<h3 className={sharedStyles['section-title']}>{__('Registered themes or plugins', 'font-awesome')}</h3> <h3 className={sharedStyles['section-title']}>{__('Registered themes or plugins', 'font-awesome')}</h3>
{ {hasAdditionalClients ? (
hasAdditionalClients
?
<div> <div>
<p className={sharedStyles['explanation']}> <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.', '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' 'font-awesome'
) )}
}
{ hasConflicts {hasConflicts ? (
? <span className={sharedStyles['explanation']}> <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' ) } {__(
'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> </span>
: null ) : null}
}</p> </p>
<table className={classnames('widefat', 'striped')}> <table className={classnames('widefat', 'striped')}>
<thead> <thead>
<tr className={sharedStyles['table-header']}> <tr className={sharedStyles['table-header']}>
@ -60,59 +58,36 @@ export default function ClientPreferencesView() {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{ {Object.values(clientPreferences).map((client, index) => {
Object.values(clientPreferences).map((client, index) => { const clientHasConflict = (optionName) => !!find(conflicts[optionName], (c) => c === client.name)
const clientHasConflict = optionName => !!find(conflicts[optionName], c => c === client.name)
return <tr key={ index }> return (
<tr key={index}>
<td>{client.name}</td> <td>{client.name}</td>
<td <td className={classnames({ [styles.conflicted]: clientHasConflict('usePro') })}>
className={ {has(client, 'usePro') ? (client.usePro ? 'Pro' : 'Free') : UNSPECIFIED_INDICATOR}
classnames({ [styles.conflicted]: clientHasConflict('usePro') })
}>
{ has(client, 'usePro')
? client.usePro ? 'Pro' : 'Free'
: UNSPECIFIED_INDICATOR
}
</td> </td>
<td <td className={classnames({ [styles.conflicted]: clientHasConflict('technology') })}>
className={ classnames({ [styles.conflicted]: clientHasConflict('technology') }) }> {has(client, 'technology') ? client.technology : UNSPECIFIED_INDICATOR}
{ has(client, 'technology')
? client.technology
: UNSPECIFIED_INDICATOR
}
</td> </td>
<td <td className={classnames({ [styles.conflicted]: clientHasConflict('version') })}>
className={ classnames({ [styles.conflicted]: clientHasConflict('version') }) }> {has(client, 'version') ? formatVersionPreference(client.version) : UNSPECIFIED_INDICATOR}
{ has(client, 'version')
? formatVersionPreference(client.version)
: UNSPECIFIED_INDICATOR
}
</td> </td>
<td <td className={classnames({ [styles.conflicted]: clientHasConflict('compat') })}>
className={ classnames({ [styles.conflicted]: clientHasConflict('compat') }) }> {has(client, 'compat') ? (client.compat ? 'true' : 'false') : UNSPECIFIED_INDICATOR}
{ has(client, 'compat')
? client.compat ? 'true' : 'false'
: UNSPECIFIED_INDICATOR
}
</td> </td>
<td <td className={classnames({ [styles.conflicted]: clientHasConflict('pseudoElements') })}>
className={ classnames({ [styles.conflicted]: clientHasConflict('pseudoElements') }) }> {has(client, 'pseudoElements') ? (client.pseudoElements ? 'true' : 'false') : UNSPECIFIED_INDICATOR}
{ has(client, 'pseudoElements')
? client.pseudoElements ? 'true' : 'false'
: UNSPECIFIED_INDICATOR
}
</td> </td>
</tr> </tr>
}) )
} })}
</tbody> </tbody>
</table> </table>
</div> </div>
: ) : (
<p className={ sharedStyles['explanation'] }> <p className={sharedStyles['explanation']}>{__('No active themes or plugins have requested preferences for Font Awesome.', 'font-awesome')}</p>
{ __( 'No active themes or plugins have requested preferences for Font Awesome.', 'font-awesome' ) } )}
</p>
}
</div> </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 { faCheckCircle, faCog, faExclamationTriangle, faGrin, faSkull, faThumbsUp, faTimesCircle } from '@fortawesome/free-solid-svg-icons'
import { ADMIN_TAB_TROUBLESHOOT } from './store/reducers' import { ADMIN_TAB_TROUBLESHOOT } from './store/reducers'
import ConflictDetectionTimer from './ConflictDetectionTimer' import ConflictDetectionTimer from './ConflictDetectionTimer'
import size from 'lodash/size' import { has, size } from 'lodash'
import has from 'lodash/has'
import { __ } from '@wordpress/i18n' import { __ } from '@wordpress/i18n'
import ErrorBoundary from './ErrorBoundary' import ErrorBoundary from './ErrorBoundary'
@ -147,67 +146,53 @@ const STYLES = {
function withErrorBoundary(Component) { function withErrorBoundary(Component) {
return class extends ErrorBoundary { return class extends ErrorBoundary {
render() { render() {
return <div style={ STYLES.container }> return (
{ <div style={STYLES.container}>
!!this.state.error {!!this.state.error ? (
? <div style={ STYLES.badness }> <div style={STYLES.badness}>
<FontAwesomeIcon icon={faExclamationTriangle} /> <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' ) ' Whoops, this is embarrassing! Some unexpected error has occurred. There might be some additional diagnostic information in the JavaScript console.',
} 'font-awesome'
)}
</div> </div>
: <Component /> ) : (
} <Component />
)}
</div> </div>
)
} }
} }
} }
function ConflictDetectionReporter() { function ConflictDetectionReporter() {
const dispatch = useDispatch() const dispatch = useDispatch()
const settingsPageUrl = useSelector(state => state.settingsPageUrl) const settingsPageUrl = useSelector((state) => state.settingsPageUrl)
const troubleshootTabUrl = `${settingsPageUrl}&tab=ts` 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 currentlyOnPluginAdminPage = window.location.href.startsWith(settingsPageUrl)
const currentlyOnTroubleshootTab = currentlyOnPluginAdminPage && activeAdminTab === ADMIN_TAB_TROUBLESHOOT const currentlyOnTroubleshootTab = currentlyOnPluginAdminPage && activeAdminTab === ADMIN_TAB_TROUBLESHOOT
const userAttemptedToStopScanner = useSelector(state => state.userAttemptedToStopScanner) const userAttemptedToStopScanner = useSelector((state) => state.userAttemptedToStopScanner)
const unregisteredClients = useSelector( const unregisteredClients = useSelector((state) => state.unregisteredClients)
state => state.unregisteredClients
)
const unregisteredClientsBeforeDetection = useSelector( const unregisteredClientsBeforeDetection = useSelector((state) => state.unregisteredClientDetectionStatus.unregisteredClientsBeforeDetection)
state => state.unregisteredClientDetectionStatus.unregisteredClientsBeforeDetection
)
const recentConflictsDetected = useSelector( const recentConflictsDetected = useSelector((state) => state.unregisteredClientDetectionStatus.recentConflictsDetected)
state => state.unregisteredClientDetectionStatus.recentConflictsDetected
)
const expired = useSelector( const expired = useSelector((state) => !state.showConflictDetectionReporter)
state => !state.showConflictDetectionReporter
)
const restarting = useSelector( const restarting = useSelector((state) => expired && state.conflictDetectionScannerStatus.isSubmitting)
state => expired && state.conflictDetectionScannerStatus.isSubmitting
)
const scannerReady = useSelector( const scannerReady = useSelector((state) => state.conflictDetectionScannerStatus.hasSubmitted && state.conflictDetectionScannerStatus.success)
state => state.conflictDetectionScannerStatus.hasSubmitted && state.conflictDetectionScannerStatus.success
)
const scannerIsStopping = useSelector( const scannerIsStopping = useSelector((state) => userAttemptedToStopScanner && !state.conflictDetectionScannerStatus.hasSubmitted)
state => userAttemptedToStopScanner
&& !state.conflictDetectionScannerStatus.hasSubmitted
)
const userStoppedScannerSuccessfully = useSelector( const userStoppedScannerSuccessfully = useSelector(
state => userAttemptedToStopScanner (state) => userAttemptedToStopScanner && !scannerIsStopping && state.conflictDetectionScannerStatus.success
&& !scannerIsStopping
&& state.conflictDetectionScannerStatus.success
) )
const runStatus = useSelector(state => { const runStatus = useSelector((state) => {
const { isSubmitting, hasSubmitted, success } = state.unregisteredClientDetectionStatus const { isSubmitting, hasSubmitted, success } = state.unregisteredClientDetectionStatus
if (userAttemptedToStopScanner) { if (userAttemptedToStopScanner) {
if (scannerIsStopping) { if (scannerIsStopping) {
@ -238,37 +223,50 @@ function ConflictDetectionReporter() {
} }
}) })
const errorMessage = useSelector( const errorMessage = useSelector((state) => state.unregisteredClientDetectionStatus.message)
state => state.unregisteredClientDetectionStatus.message
)
function stopScanner() { function stopScanner() {
dispatch(userAttemptToStopScanner()) dispatch(userAttemptToStopScanner())
dispatch(setConflictDetectionScanner({ enable: false })) dispatch(setConflictDetectionScanner({ enable: false }))
} }
const expiredOrStoppedDiv = const expiredOrStoppedDiv = (
<div> <div>
<h2 style={ STYLES.tally }><span>{ size( unregisteredClients ) }</span> <span>&nbsp;{ __( 'Results to Review', 'font-awesome' ) }</span></h2> <h2 style={STYLES.tally}>
<span>{size(unregisteredClients)}</span> <span>&nbsp;{__('Results to Review', 'font-awesome')}</span>
</h2>
<p style={STYLES.p}> <p style={STYLES.p}>
{ {currentlyOnTroubleshootTab ? (
currentlyOnTroubleshootTab __('Manage results or restart the scanner here on the Troubleshoot tab.', 'font-awesome')
? __( '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')}{' '}
__( 'Manage results or restart the scanner on the Troubleshoot tab.', 'font-awesome' ) <a
} <a href={ troubleshootTabUrl } style={ STYLES.link }>{ __('Go', 'font-awesome' ) }</a> href={troubleshootTabUrl}
style={STYLES.link}
>
{__('Go', 'font-awesome')}
</a>
</> </>
} )}
</p> </p>
</div> </div>
)
const stoppingOrSubmittingDiv = const stoppingOrSubmittingDiv = (
<div> <div>
<div style={STYLES.status}> <div style={STYLES.status}>
<h2 style={ STYLES.h2 }><FontAwesomeIcon icon={ faCog } size="sm" spin /> <span>{ runStatus.display }</span></h2> <h2 style={STYLES.h2}>
<FontAwesomeIcon
icon={faCog}
size="sm"
spin
/>{' '}
<span>{runStatus.display}</span>
</h2>
</div> </div>
</div> </div>
)
return ( return (
<> <>
@ -277,68 +275,125 @@ function ConflictDetectionReporter() {
<p style={STYLES.adminEyesOnly}>{__('only admins can see this box', 'font-awesome')}</p> <p style={STYLES.adminEyesOnly}>{__('only admins can see this box', 'font-awesome')}</p>
</div> </div>
<div style={STYLES.content}> <div style={STYLES.content}>
{ {
{ {
None: None: (
<div> <div>
<div style={STYLES.status}> <div style={STYLES.status}>
<h2 style={ STYLES.h2 }><FontAwesomeIcon icon={ faGrin } size="sm" /> <span>{ __( 'All clear!', 'font-awesome' ) }</span></h2> <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> <p style={STYLES.p}>{__('No new conflicts found on this page.', 'font-awesome')}</p>
</div> </div>
</div>, </div>
Running: ),
Running: (
<div> <div>
<div style={STYLES.status}> <div style={STYLES.status}>
<h2 style={ STYLES.h2 }><FontAwesomeIcon icon={ faCog } size="sm" spin /> <span>{ __( 'Scanning', 'font-awesome' ) }...</span></h2> <h2 style={STYLES.h2}>
<FontAwesomeIcon
icon={faCog}
size="sm"
spin
/>{' '}
<span>{__('Scanning', 'font-awesome')}...</span>
</h2>
</div> </div>
</div>, </div>
Restarting: ),
Restarting: (
<div> <div>
<div style={STYLES.status}> <div style={STYLES.status}>
<h2 style={ STYLES.h2 }><FontAwesomeIcon icon={ faCog } size="sm" spin /> <span>{ __( 'Restarting', 'font-awesome' ) }...</span></h2> <h2 style={STYLES.h2}>
<FontAwesomeIcon
icon={faCog}
size="sm"
spin
/>{' '}
<span>{__('Restarting', 'font-awesome')}...</span>
</h2>
</div> </div>
</div>, </div>
Ready: ),
Ready: (
<div> <div>
<div> <div>
<h2 style={ STYLES.h2 }><FontAwesomeIcon icon={ faThumbsUp } size="sm" /> { __( 'Proton pack charged!', 'font-awesome' ) }</h2> <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> <p style={STYLES.p}>{__('Wander through the pages of your web site and this scanner will track progress.', 'font-awesome')}</p>
</div> </div>
</div>, </div>
),
Submitting: stoppingOrSubmittingDiv, Submitting: stoppingOrSubmittingDiv,
Stopping: stoppingOrSubmittingDiv, Stopping: stoppingOrSubmittingDiv,
Done: Done: (
<div> <div>
<div style={STYLES.status}> <div style={STYLES.status}>
<h2 style={ STYLES.h2 }><FontAwesomeIcon icon={ faCheckCircle } size="sm" /> <span>{ __( 'Page scan complete', 'font-awesome' ) }</span></h2> <h2 style={STYLES.h2}>
<FontAwesomeIcon
icon={faCheckCircle}
size="sm"
/>{' '}
<span>{__('Page scan complete', 'font-awesome')}</span>
</h2>
</div> </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}>
<p style={ STYLES.tally }><span style={ STYLES.count }>{ size( unregisteredClients ) }</span> <span>total found</span> <span style={STYLES.count}>{size(Object.keys(recentConflictsDetected).filter((k) => !has(unregisteredClientsBeforeDetection, k)))}</span>{' '}
{ <span>{__('new conflicts found on this page', 'font-awesome')}</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> </p>
</div>, <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, Expired: expiredOrStoppedDiv,
Stopped: expiredOrStoppedDiv, Stopped: expiredOrStoppedDiv,
Error: Error: (
<div> <div>
<h2 style={ STYLES.h2 }><FontAwesomeIcon icon={ faSkull } /> <span>{ __( 'Don\'t cross the streams! It would be bad.', 'font-awesome' ) }</span></h2> <h2 style={STYLES.h2}>
<p style={ STYLES.p }> <FontAwesomeIcon icon={faSkull} /> <span>{__("Don't cross the streams! It would be bad.", 'font-awesome')}</span>
{ errorMessage } </h2>
</p> <p style={STYLES.p}>{errorMessage}</p>
</div> </div>
)
}[runStatus.code] }[runStatus.code]
} }
</div> </div>
<div style={STYLES.timerRow}> <div style={STYLES.timerRow}>
<span> <span>
<ConflictDetectionTimer addDescription> <ConflictDetectionTimer addDescription>
<button style={ STYLES.button } title={ __( 'Stop timer', 'font-awesome' ) } onClick={() => stopScanner()}> <button
<FontAwesomeIcon icon={ faTimesCircle } size="lg" /> style={STYLES.button}
title={__('Stop timer', 'font-awesome')}
onClick={() => stopScanner()}
>
<FontAwesomeIcon
icon={faTimesCircle}
size="lg"
/>
</button> </button>
</ConflictDetectionTimer> </ConflictDetectionTimer>
</span> </span>

View File

@ -11,11 +11,11 @@ import createInterpolateElement from './createInterpolateElement'
export default function ConflictDetectionScannerSection() { export default function ConflictDetectionScannerSection() {
const dispatch = useDispatch() const dispatch = useDispatch()
const detectConflictsUntil = useSelector(state => state.detectConflictsUntil) const detectConflictsUntil = useSelector((state) => state.detectConflictsUntil)
const nowMs = (new Date()).valueOf() const nowMs = new Date().valueOf()
const detectingConflicts = (new Date(detectConflictsUntil * 1000)) > nowMs const detectingConflicts = new Date(detectConflictsUntil * 1000) > nowMs
const { isSubmitting, hasSubmitted, message, success } = useSelector(state => state.conflictDetectionScannerStatus) const { isSubmitting, hasSubmitted, message, success } = useSelector((state) => state.conflictDetectionScannerStatus)
const showConflictDetectionReporter = useSelector(state => state.showConflictDetectionReporter) const showConflictDetectionReporter = useSelector((state) => state.showConflictDetectionReporter)
const store = useStore() const store = useStore()
useEffect(() => { useEffect(() => {
@ -31,51 +31,64 @@ export default function ConflictDetectionScannerSection() {
} }
}, [showConflictDetectionReporter, store]) }, [showConflictDetectionReporter, store])
return <div> return (
<div>
<h2 className={sharedStyles['section-title']}>{__('Detect Conflicts with Other Versions of Font Awesome', 'font-awesome')}</h2> <h2 className={sharedStyles['section-title']}>{__('Detect Conflicts with Other Versions of Font Awesome', 'font-awesome')}</h2>
<div className={sharedStyles['explanation']}> <div className={sharedStyles['explanation']}>
<p> <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' ) } {__(
'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>
<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'
),
{ {
createInterpolateElement( noWrap: <span style={{ whiteSpace: 'nowrap' }} />
__( '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" }} />
}
)
} }
)}
</p> </p>
</div> </div>
<div className={sharedStyles['scanner-actions']}> <div className={sharedStyles['scanner-actions']}>
{ {detectingConflicts ? (
detectingConflicts <button
? <button className={sharedStyles['faPrimary']} disabled > className={sharedStyles['faPrimary']}
disabled
>
{__('Scanner running', 'font-awesome')}: <ConflictDetectionTimer /> {__('Scanner running', 'font-awesome')}: <ConflictDetectionTimer />
</button> </button>
: <button className="button button-primary" disabled={ isSubmitting } onClick={() => dispatch(setConflictDetectionScanner({ enable: true }))}> ) : (
{ <button
sprintf( className="button button-primary"
__( 'Enable scanner for %d minutes', 'font-awesome' ), disabled={isSubmitting}
CONFLICT_DETECTION_SCANNER_DURATION_MIN onClick={() => dispatch(setConflictDetectionScanner({ enable: true }))}
) >
} {sprintf(__('Enable scanner for %d minutes', 'font-awesome'), CONFLICT_DETECTION_SCANNER_DURATION_MIN)}
</button> </button>
} )}
<div className={sharedStyles['scanner-runstatus']}> <div className={sharedStyles['scanner-runstatus']}>
{ {isSubmitting ? (
isSubmitting <FontAwesomeIcon
? <FontAwesomeIcon icon={ faSpinner } spin /> icon={faSpinner}
: hasSubmitted spin
? success />
? <FontAwesomeIcon icon={ faCheck } /> ) : hasSubmitted ? (
: <><FontAwesomeIcon icon={ faSkull } /> <span>{ message }</span></> success ? (
: null <FontAwesomeIcon icon={faCheck} />
} ) : (
<>
<FontAwesomeIcon icon={faSkull} /> <span>{message}</span>
</>
)
) : null}
</div> </div>
</div> </div>
<hr className={sharedStyles['section-divider']} /> <hr className={sharedStyles['section-divider']} />
</div> </div>
)
} }

View File

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

View File

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

View File

@ -9,7 +9,7 @@ import { setActiveAdminTab } from './store/actions'
import { __ } from '@wordpress/i18n' import { __ } from '@wordpress/i18n'
export default function FontAwesomeAdminView() { 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() const dispatch = useDispatch()
return ( return (

View File

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

View File

@ -1,47 +1,34 @@
import React, { createRef, useState, useEffect } from 'react' import React, { createRef, useState, useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux' import { useSelector, useDispatch } from 'react-redux'
import Alert from './Alert' import Alert from './Alert'
import { import { resetPendingOptions, queryKits, addPendingOption, checkPreferenceConflicts, updateApiToken, resetOptionsFormState } from './store/actions'
resetPendingOptions,
queryKits,
addPendingOption,
checkPreferenceConflicts,
updateApiToken,
resetOptionsFormState
} from './store/actions'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { import { faSpinner, faSync, faExternalLinkAlt, faRedo, faSkull, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
faSpinner,
faSync,
faExternalLinkAlt,
faRedo,
faSkull,
faTrashAlt } from '@fortawesome/free-solid-svg-icons'
import { faQuestionCircle, faCheckCircle } from '@fortawesome/free-regular-svg-icons' import { faQuestionCircle, faCheckCircle } from '@fortawesome/free-regular-svg-icons'
import styles from './KitSelectView.module.css' import styles from './KitSelectView.module.css'
import sharedStyles from './App.module.css' import sharedStyles from './App.module.css'
import classnames from 'classnames' import classnames from 'classnames'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import size from 'lodash/size' import { size } from 'lodash'
import { sprintf, __ } from '@wordpress/i18n' import { sprintf, __ } from '@wordpress/i18n'
export default function KitSelectView({ useOption, masterSubmitButtonShowing, setMasterSubmitButtonShowing }) { export default function KitSelectView({ useOption, masterSubmitButtonShowing, setMasterSubmitButtonShowing }) {
const dispatch = useDispatch() const dispatch = useDispatch()
const kitTokenActive = useSelector(state => state.options.kitToken) const kitTokenActive = useSelector((state) => state.options.kitToken)
const kitToken = useOption('kitToken') const kitToken = useOption('kitToken')
const [pendingApiToken, setPendingApiToken] = useState(null) const [pendingApiToken, setPendingApiToken] = useState(null)
const [showingRemoveApiTokenAlert, setShowRemoveApiTokenAlert] = useState(false) const [showingRemoveApiTokenAlert, setShowRemoveApiTokenAlert] = useState(false)
const [showApiTokenInputForUpdate, setShowApiTokenInputForUpdate] = useState(false) const [showApiTokenInputForUpdate, setShowApiTokenInputForUpdate] = useState(false)
const apiToken = useSelector(state => { const apiToken = useSelector((state) => {
if (null !== pendingApiToken) return pendingApiToken if (null !== pendingApiToken) return pendingApiToken
return state.options.apiToken return state.options.apiToken
}) })
const kits = useSelector( state => state.kits ) || [] const kits = useSelector((state) => state.kits) || []
const hasSubmitted = useSelector(state => state.optionsFormState.hasSubmitted) const hasSubmitted = useSelector((state) => state.optionsFormState.hasSubmitted)
const submitSuccess = useSelector(state => state.optionsFormState.success) const submitSuccess = useSelector((state) => state.optionsFormState.success)
const submitMessage = useSelector(state => state.optionsFormState.message) const submitMessage = useSelector((state) => state.optionsFormState.message)
const isSubmitting = useSelector(state => state.optionsFormState.isSubmitting) const isSubmitting = useSelector((state) => state.optionsFormState.isSubmitting)
function removeApiToken() { function removeApiToken() {
if (!!kitTokenActive) { if (!!kitTokenActive) {
@ -66,22 +53,18 @@ export default function KitSelectView({ useOption, masterSubmitButtonShowing, se
return return
} }
const selectedKit = (kits || []).find(k => k.token === kitToken) const selectedKit = (kits || []).find((k) => k.token === kitToken)
if (!selectedKit) { if (!selectedKit) {
throw new Error( throw new Error(sprintf(__('When selecting to use kit %s, somehow the information we needed was missing. Try reloading the page.'), kitToken))
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 // We're just resetting back to the state we were in
dispatch(resetPendingOptions()) dispatch(resetPendingOptions())
} else { } else {
dispatch(addPendingOption({ dispatch(
addPendingOption({
kitToken, kitToken,
technology: 'svg' === selectedKit.technologySelected ? 'svg' : 'webfont', technology: 'svg' === selectedKit.technologySelected ? 'svg' : 'webfont',
usePro: 'pro' === selectedKit.licenseSelected, usePro: 'pro' === selectedKit.licenseSelected,
@ -90,13 +73,14 @@ export default function KitSelectView({ useOption, masterSubmitButtonShowing, se
// At the time this is being implemented, kits don't yet support // At the time this is being implemented, kits don't yet support
// toggling pseudoElement support for SVG, but it's implicitly supported for webfont. // toggling pseudoElement support for SVG, but it's implicitly supported for webfont.
pseudoElements: 'svg' !== selectedKit.technologySelected pseudoElements: 'svg' !== selectedKit.technologySelected
})) })
)
} }
dispatch(checkPreferenceConflicts()) 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 * This seems like a lot of effort just to keep the focus on the API Token input
@ -118,8 +102,7 @@ export default function KitSelectView({ useOption, masterSubmitButtonShowing, se
} }
}) })
const hasSavedApiToken = useSelector((state) => !!state.options.apiToken)
const hasSavedApiToken = useSelector(state => !! state.options.apiToken)
function cancelApiTokenUpdate() { function cancelApiTokenUpdate() {
setShowApiTokenInputForUpdate(false) setShowApiTokenInputForUpdate(false)
@ -128,7 +111,6 @@ export default function KitSelectView({ useOption, masterSubmitButtonShowing, se
} }
function ApiTokenInput() { function ApiTokenInput() {
useEffect(() => { useEffect(() => {
if (submitSuccess && showApiTokenInputForUpdate) { if (submitSuccess && showApiTokenInputForUpdate) {
setShowApiTokenInputForUpdate(false) setShowApiTokenInputForUpdate(false)
@ -136,10 +118,15 @@ export default function KitSelectView({ useOption, masterSubmitButtonShowing, se
} }
}) })
return <> return (
<>
<div className={classnames(styles['field-apitoken'], { [styles['api-token-update']]: showApiTokenInputForUpdate })}> <div className={classnames(styles['field-apitoken'], { [styles['api-token-update']]: showApiTokenInputForUpdate })}>
<label htmlFor="api_token"> <label htmlFor="api_token">
<FontAwesomeIcon className={ sharedStyles['icon'] } icon={ faQuestionCircle } size="lg" /> <FontAwesomeIcon
className={sharedStyles['icon']}
icon={faQuestionCircle}
size="lg"
/>
{__('API Token', 'font-awesome')} {__('API Token', 'font-awesome')}
</label> </label>
<div> <div>
@ -150,15 +137,27 @@ export default function KitSelectView({ useOption, masterSubmitButtonShowing, se
ref={apiTokenInputRef} ref={apiTokenInputRef}
value={pendingApiToken || ''} value={pendingApiToken || ''}
size="20" size="20"
onChange={ e => { onChange={(e) => {
setApiTokenInputHasFocus(true) setApiTokenInputHasFocus(true)
setPendingApiToken(e.target.value) setPendingApiToken(e.target.value)
}} }}
/> />
<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'}} /> '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> </a>
</p> </p>
</div> </div>
@ -174,32 +173,39 @@ export default function KitSelectView({ useOption, masterSubmitButtonShowing, se
onMouseDown={() => { onMouseDown={() => {
dispatch(updateApiToken({ apiToken: pendingApiToken, runQueryKits: true })) dispatch(updateApiToken({ apiToken: pendingApiToken, runQueryKits: true }))
setPendingApiToken(null) setPendingApiToken(null)
} }}
}
/> />
{ {hasSubmitted && !submitSuccess && (
(hasSubmitted && ! submitSuccess) &&
<div className={classnames(sharedStyles['submit-status'], sharedStyles['fail'])}> <div className={classnames(sharedStyles['submit-status'], sharedStyles['fail'])}>
<div className={classnames(sharedStyles['fail-icon-container'])}> <div className={classnames(sharedStyles['fail-icon-container'])}>
<FontAwesomeIcon className={ sharedStyles['icon'] } icon={ faSkull } /> <FontAwesomeIcon
className={sharedStyles['icon']}
icon={faSkull}
/>
</div> </div>
<div className={ sharedStyles['explanation'] }> <div className={sharedStyles['explanation']}>{submitMessage}</div>
{ submitMessage }
</div> </div>
</div> )}
} {isSubmitting && (
{
isSubmitting &&
<span className={classnames(sharedStyles['submit-status'], sharedStyles['submitting'])}> <span className={classnames(sharedStyles['submit-status'], sharedStyles['submitting'])}>
<FontAwesomeIcon className={ sharedStyles['icon'] } icon={faSpinner} spin/> <FontAwesomeIcon
className={sharedStyles['icon']}
icon={faSpinner}
spin
/>
</span> </span>
} )}
{ {showApiTokenInputForUpdate && !isSubmitting && (
(showApiTokenInputForUpdate && ! isSubmitting) && <button
<button onClick={ () => cancelApiTokenUpdate() } className={ styles['button-dismissable'] }>{ __('Nevermind', 'font-awesome') }</button> onClick={() => cancelApiTokenUpdate()}
} className={styles['button-dismissable']}
>
{__('Nevermind', 'font-awesome')}
</button>
)}
</div> </div>
</> </>
)
} }
function ApiTokenControl() { function ApiTokenControl() {
@ -210,40 +216,67 @@ export default function KitSelectView({ useOption, masterSubmitButtonShowing, se
setShowRemoveApiTokenAlert(false) setShowRemoveApiTokenAlert(false)
} }
return <div className={ styles['api-token-control-wrapper'] }> return (
<div className={styles['api-token-control-wrapper']}>
<div className={classnames(styles['api-token-control'], { [styles['api-token-update']]: showApiTokenInputForUpdate })}> <div className={classnames(styles['api-token-control'], { [styles['api-token-update']]: showApiTokenInputForUpdate })}>
{ {showApiTokenInputForUpdate ? (
showApiTokenInputForUpdate <ApiTokenInput />
? <ApiTokenInput /> ) : (
: <> <>
<p className={styles['token-saved']}> <p className={styles['token-saved']}>
<span> <span>
<FontAwesomeIcon className={ sharedStyles['icon'] } icon={ faCheckCircle } size="lg" /> <FontAwesomeIcon
className={sharedStyles['icon']}
icon={faCheckCircle}
size="lg"
/>
</span> </span>
{__('API Token Saved', 'font-awesome')} {__('API Token Saved', 'font-awesome')}
</p> </p>
{ {!!apiToken && (
!!apiToken &&
<div className={styles['button-group']}> <div className={styles['button-group']}>
<button onClick={ () => switchToApiTokenUpdate() } className={ styles['refresh'] } type="button"> <button
<FontAwesomeIcon className={ sharedStyles['icon'] } icon={ faSync } title="update" alt="update" /> onClick={() => switchToApiTokenUpdate()}
className={styles['refresh']}
type="button"
>
<FontAwesomeIcon
className={sharedStyles['icon']}
icon={faSync}
title="update"
alt="update"
/>
<span>{__('Update token', 'font-awesome')}</span> <span>{__('Update token', 'font-awesome')}</span>
</button> </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>
} )}
</> </>
} )}
</div> </div>
{ {showingRemoveApiTokenAlert && (
showingRemoveApiTokenAlert &&
<div className={styles['api-token-control-alert-wrapper']}> <div className={styles['api-token-control-alert-wrapper']}>
<Alert title={ __( 'Whoa, whoa, whoa!', 'font-awesome' ) } type='warning'> <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')} {__('You can\'t remove your API token when "Use a Kit" is active. Switch to "Use CDN" first.', 'font-awesome')}
</Alert> </Alert>
</div> </div>
} )}
</div> </div>
)
} }
const STATUS = { const STATUS = {
@ -257,8 +290,7 @@ export default function KitSelectView({ useOption, masterSubmitButtonShowing, se
} }
function KitSelector() { function KitSelector() {
const status = const status = apiToken
apiToken
? kitsQueryStatus.isSubmitting ? kitsQueryStatus.isSubmitting
? STATUS.querying ? STATUS.querying
: kitsQueryStatus.hasSubmitted : kitsQueryStatus.hasSubmitted
@ -272,125 +304,164 @@ export default function KitSelectView({ useOption, masterSubmitButtonShowing, se
: STATUS.apiTokenReadyNoKitsYet : STATUS.apiTokenReadyNoKitsYet
: STATUS.noApiToken : STATUS.noApiToken
const kitRefreshButton = <button onClick={ () => dispatch(queryKits()) } className={ styles['refresh'] }> const kitRefreshButton = (
<FontAwesomeIcon className={ sharedStyles['icon'] } icon={ faRedo } title="refresh" alt="refresh" /> <button
<span> onClick={() => dispatch(queryKits())}
{ className={styles['refresh']}
0 === size(kits) >
? __( 'Get latest kits data', 'font-awesome' ) <FontAwesomeIcon
: __( 'Refresh kits data', 'font-awesome' ) className={sharedStyles['icon']}
} icon={faRedo}
</span> title="refresh"
alt="refresh"
/>
<span>{0 === size(kits) ? __('Get latest kits data', 'font-awesome') : __('Refresh kits data', 'font-awesome')}</span>
</button> </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'] }> return (
<div className={styles['kit-selector-container']}>
{activeKitNotice} {activeKitNotice}
<div className={styles['wrap-selectkit']}> <div className={styles['wrap-selectkit']}>
<h3 className={ styles['title-selectkit'] }><FontAwesomeIcon className={ sharedStyles['icon'] } icon={ faQuestionCircle } size="lg" /> <h3 className={styles['title-selectkit']}>
<FontAwesomeIcon
className={sharedStyles['icon']}
icon={faQuestionCircle}
size="lg"
/>
{__('Pick a Kit to Use or Check Settings', 'font-awesome')} {__('Pick a Kit to Use or Check Settings', 'font-awesome')}
</h3> </h3>
<div className={styles['selectkit']}> <div className={styles['selectkit']}>
<p> <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> </p>
{ {
{ {
noApiToken: 'noApiToken', noApiToken: 'noApiToken',
apiTokenReadyNoKitsYet: <>{ activeKitNotice } { kitRefreshButton }</>, apiTokenReadyNoKitsYet: (
querying: <>
{activeKitNotice} {kitRefreshButton}
</>
),
querying: (
<div> <div>
<span> <span>{__('Loading your kits...', 'font-awesome')}</span>
{ __( 'Loading your kits...', 'font-awesome' ) }
</span>
<span className={classnames(sharedStyles['submit-status'], sharedStyles['submitting'])}> <span className={classnames(sharedStyles['submit-status'], sharedStyles['submitting'])}>
<FontAwesomeIcon className={ sharedStyles['icon'] } icon={faSpinner} spin/> <FontAwesomeIcon
className={sharedStyles['icon']}
icon={faSpinner}
spin
/>
</span> </span>
</div>, </div>
),
networkError: networkError: (
<div className={classnames(sharedStyles['submit-status'], sharedStyles['fail'])}> <div className={classnames(sharedStyles['submit-status'], sharedStyles['fail'])}>
<div className={classnames(sharedStyles['fail-icon-container'])}> <div className={classnames(sharedStyles['fail-icon-container'])}>
<FontAwesomeIcon className={ sharedStyles['icon'] } icon={ faSkull } /> <FontAwesomeIcon
className={sharedStyles['icon']}
icon={faSkull}
/>
</div> </div>
<div className={ sharedStyles['explanation'] }> <div className={sharedStyles['explanation']}>{kitsQueryStatus.message}</div>
{ kitsQueryStatus.message }
</div> </div>
</div>, ),
noKitsFoundAfterQuery: noKitsFoundAfterQuery: (
<> <>
<Alert title="Zoinks! Looks like you don't have any kits set up yet." type="info"> <Alert
title="Zoinks! Looks like you don't have any kits set up yet."
type="info"
>
<p> <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"> {__('Head over to Font Awesome to create one, then come back here and refresh your kits.', 'font-awesome')}{' '}
{ __( 'Create a kit on Font Awesome', 'font-awesome' ) } <FontAwesomeIcon icon={faExternalLinkAlt} /></a> <a
rel="noopener noreferrer"
target="_blank"
href="https://fontawesome.com/kits"
>
{__('Create a kit on Font Awesome', 'font-awesome')} <FontAwesomeIcon icon={faExternalLinkAlt} />
</a>
</p> </p>
</Alert> </Alert>
{kitRefreshButton} {kitRefreshButton}
</>, </>
),
kitSelection: kitSelection: (
<> <>
<div className={styles['field-kitselect']}> <div className={styles['field-kitselect']}>
<select <select
className={styles['kit-select']} className={styles['kit-select']}
id="kits" id="kits"
name="kit" name="kit"
onChange={ e => handleKitChange({ kitToken: e.target.value }) } onChange={(e) => handleKitChange({ kitToken: e.target.value })}
disabled={!masterSubmitButtonShowing} disabled={!masterSubmitButtonShowing}
value={kitToken || ''} value={kitToken || ''}
> >
<option key='empty' value=''>{ __( 'Select a kit', 'font-awesome' ) }</option> <option
{ key="empty"
kits.map((kit, index) => { value=""
return <option key={ index } value={ kit.token }> >
{__('Select a kit', 'font-awesome')}
</option>
{kits.map((kit, index) => {
return (
<option
key={index}
value={kit.token}
>
{`${kit.name} (${kit.token})`} {`${kit.name} (${kit.token})`}
</option> </option>
}) )
} })}
</select> </select>
{kitRefreshButton} {kitRefreshButton}
</div> </div>
</>,
showingOnlyActiveKit:
<>
{ kitRefreshButton }
</> </>
),
showingOnlyActiveKit: <>{kitRefreshButton}</>
}[status] }[status]
} }
</div> </div>
</div> </div>
</div> </div>
)
} }
return <div> return (
<div>
<div className={styles['kit-tab-content']}> <div className={styles['kit-tab-content']}>
{ {hasSavedApiToken ? (
hasSavedApiToken <>
? <>
<ApiTokenControl /> <ApiTokenControl />
<KitSelector /> <KitSelector />
</> </>
: <ApiTokenInput /> ) : (
} <ApiTokenInput />
)}
</div> </div>
</div> </div>
)
} }
KitSelectView.propTypes = { KitSelectView.propTypes = {

View File

@ -7,18 +7,21 @@ import { __ } from '@wordpress/i18n'
import createInterpolateElement from './createInterpolateElement' import createInterpolateElement from './createInterpolateElement'
export default function ManageFontAwesomeVersionsSection() { export default function ManageFontAwesomeVersionsSection() {
return <div className={ classnames(sharedStyles['explanation'], styles['font-awesome-versions-section']) }> 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> <h2 className={sharedStyles['section-title']}>{__('Versions of Font Awesome Active on Your Site', 'font-awesome')}</h2>
<p> <p>
{ {createInterpolateElement(
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>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 /> b: <b />
} }
) )}
}
</p> </p>
<ClientPreferencesView /> <ClientPreferencesView />
</div> </div>
)
} }

View File

@ -6,40 +6,30 @@ import KitConfigView from './KitConfigView'
import sharedStyles from './App.module.css' import sharedStyles from './App.module.css'
import optionStyles from './CdnConfigView.module.css' import optionStyles from './CdnConfigView.module.css'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { import { faDotCircle, faSpinner, faCheck, faSkull } from '@fortawesome/free-solid-svg-icons'
faDotCircle,
faSpinner,
faCheck,
faSkull,
} from '@fortawesome/free-solid-svg-icons'
import { faCircle } from '@fortawesome/free-regular-svg-icons' import { faCircle } from '@fortawesome/free-regular-svg-icons'
import classnames from 'classnames' import classnames from 'classnames'
import styles from './SettingsTab.module.css' 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 { addPendingOption, submitPendingOptions, chooseAwayFromKitConfig, chooseIntoKitConfig } from './store/actions'
import CheckingOptionStatusIndicator from './CheckingOptionsStatusIndicator' import CheckingOptionStatusIndicator from './CheckingOptionsStatusIndicator'
import size from 'lodash/size'
import { __ } from '@wordpress/i18n' import { __ } from '@wordpress/i18n'
export default function SettingsTab() { export default function SettingsTab() {
const dispatch = useDispatch() const dispatch = useDispatch()
const alreadyUsingKit = useSelector( state => !!state.options.kitToken ) const alreadyUsingKit = useSelector((state) => !!state.options.kitToken)
const [useKit, setUseKit] = useState(alreadyUsingKit) const [useKit, setUseKit] = useState(alreadyUsingKit)
const isChecking = useSelector(state => state.preferenceConflictDetection.isChecking) const isChecking = useSelector((state) => state.preferenceConflictDetection.isChecking)
const hasSubmitted = useSelector(state => state.optionsFormState.hasSubmitted) const hasSubmitted = useSelector((state) => state.optionsFormState.hasSubmitted)
const submitSuccess = useSelector(state => state.optionsFormState.success) const submitSuccess = useSelector((state) => state.optionsFormState.success)
const submitMessage = useSelector(state => state.optionsFormState.message) const submitMessage = useSelector((state) => state.optionsFormState.message)
const isSubmitting = useSelector(state => state.optionsFormState.isSubmitting) const isSubmitting = useSelector((state) => state.optionsFormState.isSubmitting)
const pendingOptions = useSelector(state => state.pendingOptions) const pendingOptions = useSelector((state) => state.pendingOptions)
const apiToken = useSelector(state => state.options.apiToken) const apiToken = useSelector((state) => state.options.apiToken)
const [masterSubmitButtonShowing, setMasterSubmitButtonShowing] = useState(true) const [masterSubmitButtonShowing, setMasterSubmitButtonShowing] = useState(true)
function useOption(option) { function useOption(option) {
return useSelector(state => return useSelector((state) => (has(state.pendingOptions, option) ? state.pendingOptions[option] : state.options[option]))
has(state.pendingOptions, option)
? state.pendingOptions[option]
: state.options[option]
)
} }
function handleSubmit(e) { function handleSubmit(e) {
@ -54,7 +44,7 @@ export default function SettingsTab() {
const kitToken = useOption('kitToken') const kitToken = useOption('kitToken')
// The one that's actually saved in the database already // 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 = {}) { function handleOptionChange(change = {}) {
dispatch(addPendingOption(change)) dispatch(addPendingOption(change))
@ -78,7 +68,9 @@ export default function SettingsTab() {
dispatch(chooseIntoKitConfig()) dispatch(chooseIntoKitConfig())
} }
return <div><div className={ sharedStyles['wrapper-div'] }> return (
<div>
<div className={sharedStyles['wrapper-div']}>
<h3>{__('How are you using Font Awesome?', 'font-awesome')}</h3> <h3>{__('How are you using Font Awesome?', 'font-awesome')}</h3>
<div className={styles['select-config-container']}> <div className={styles['select-config-container']}>
<span> <span>
@ -91,7 +83,10 @@ export default function SettingsTab() {
onChange={() => handleSwitchToKitConfig()} onChange={() => handleSwitchToKitConfig()}
className={classnames(sharedStyles['sr-only'], sharedStyles['input-radio-custom'])} className={classnames(sharedStyles['sr-only'], sharedStyles['input-radio-custom'])}
/> />
<label htmlFor="select_use_kits" className={ optionStyles['option-label'] }> <label
htmlFor="select_use_kits"
className={optionStyles['option-label']}
>
<span className={sharedStyles['relative']}> <span className={sharedStyles['relative']}>
<FontAwesomeIcon <FontAwesomeIcon
icon={faDotCircle} icon={faDotCircle}
@ -106,9 +101,7 @@ export default function SettingsTab() {
fixedWidth fixedWidth
/> />
</span> </span>
<span className={ optionStyles['option-label-text'] }> <span className={optionStyles['option-label-text']}>{__('Use A Kit', 'font-awesome')}</span>
{ __( 'Use A Kit', 'font-awesome' ) }
</span>
</label> </label>
</span> </span>
<span> <span>
@ -121,7 +114,10 @@ export default function SettingsTab() {
onChange={() => handleSwitchAwayFromKitConfig()} onChange={() => handleSwitchAwayFromKitConfig()}
className={classnames(sharedStyles['sr-only'], sharedStyles['input-radio-custom'])} className={classnames(sharedStyles['sr-only'], sharedStyles['input-radio-custom'])}
/> />
<label htmlFor="select_use_cdn" className={ optionStyles['option-label'] }> <label
htmlFor="select_use_cdn"
className={optionStyles['option-label']}
>
<span className={sharedStyles['relative']}> <span className={sharedStyles['relative']}>
<FontAwesomeIcon <FontAwesomeIcon
icon={faDotCircle} icon={faDotCircle}
@ -136,25 +132,32 @@ export default function SettingsTab() {
fixedWidth fixedWidth
/> />
</span> </span>
<span className={ optionStyles['option-label-text'] }> <span className={optionStyles['option-label-text']}>{__('Use CDN', 'font-awesome')}</span>
{ __( 'Use CDN', 'font-awesome' ) }
</span>
</label> </label>
</span> </span>
</div> </div>
<> <>
{ {useKit ? (
useKit <>
? <> <KitSelectView
<KitSelectView useOption={ useOption } handleOptionChange={ handleOptionChange } handleSubmit={ handleSubmit } masterSubmitButtonShowing={ masterSubmitButtonShowing } setMasterSubmitButtonShowing={ setMasterSubmitButtonShowing }/> useOption={useOption}
handleOptionChange={handleOptionChange}
handleSubmit={handleSubmit}
masterSubmitButtonShowing={masterSubmitButtonShowing}
setMasterSubmitButtonShowing={setMasterSubmitButtonShowing}
/>
{!!kitToken && <KitConfigView kitToken={kitToken} />} {!!kitToken && <KitConfigView kitToken={kitToken} />}
</> </>
: <CdnConfigView useOption={ useOption } handleOptionChange={ handleOptionChange } handleSubmit={ handleSubmit }/> ) : (
} <CdnConfigView
useOption={useOption}
handleOptionChange={handleOptionChange}
handleSubmit={handleSubmit}
/>
)}
</> </>
</div> </div>
{ {(!useKit || (apiToken && masterSubmitButtonShowing)) && (
(!useKit || ( apiToken && masterSubmitButtonShowing ) ) &&
<div className={classnames(sharedStyles['submit-wrapper'], ['submit'])}> <div className={classnames(sharedStyles['submit-wrapper'], ['submit'])}>
<input <input
type="submit" type="submit"
@ -165,33 +168,41 @@ export default function SettingsTab() {
disabled={size(pendingOptions) === 0} disabled={size(pendingOptions) === 0}
onClick={handleSubmit} onClick={handleSubmit}
/> />
{ hasSubmitted {hasSubmitted ? (
? submitSuccess submitSuccess ? (
? <span className={ classnames(sharedStyles['submit-status'], sharedStyles['success']) }> <span className={classnames(sharedStyles['submit-status'], sharedStyles['success'])}>
<FontAwesomeIcon className={ sharedStyles['icon'] } icon={ faCheck } /> <FontAwesomeIcon
className={sharedStyles['icon']}
icon={faCheck}
/>
</span> </span>
: <div className={ classnames(sharedStyles['submit-status'], sharedStyles['fail']) }> ) : (
<div className={classnames(sharedStyles['submit-status'], sharedStyles['fail'])}>
<div className={classnames(sharedStyles['fail-icon-container'])}> <div className={classnames(sharedStyles['fail-icon-container'])}>
<FontAwesomeIcon className={ sharedStyles['icon'] } icon={ faSkull } /> <FontAwesomeIcon
className={sharedStyles['icon']}
icon={faSkull}
/>
</div> </div>
<div className={ sharedStyles['explanation'] }> <div className={sharedStyles['explanation']}>{submitMessage}</div>
{ submitMessage }
</div> </div>
</div> )
: null ) : null}
} {isSubmitting ? (
{ <span className={classnames(sharedStyles['submit-status'], sharedStyles['submitting'])}>
isSubmitting <FontAwesomeIcon
? <span className={ classnames(sharedStyles['submit-status'], sharedStyles['submitting']) }> className={sharedStyles['icon']}
<FontAwesomeIcon className={ sharedStyles['icon'] } icon={faSpinner} spin/> icon={faSpinner}
spin
/>
</span> </span>
: isChecking ) : isChecking ? (
? <CheckingOptionStatusIndicator/> <CheckingOptionStatusIndicator />
: size(pendingOptions) > 0 ) : size(pendingOptions) > 0 ? (
? <span className={ sharedStyles['submit-status'] }>{ __( 'you have pending changes', 'font-awesome' ) }</span> <span className={sharedStyles['submit-status']}>{__('you have pending changes', 'font-awesome')}</span>
: null ) : null}
}
</div> </div>
} )}
</div> </div>
)
} }

View File

@ -1,7 +1,6 @@
import React from 'react' import React from 'react'
import ManageFontAwesomeVersionsSection from './ManageFontAwesomeVersionsSection' import ManageFontAwesomeVersionsSection from './ManageFontAwesomeVersionsSection'
import UnregisteredClientsView from './UnregisteredClientsView' import UnregisteredClientsView from './UnregisteredClientsView'
import V3DeprecationWarning from './V3DeprecationWarning'
import ConflictDetectionScannerSection from './ConflictDetectionScannerSection' import ConflictDetectionScannerSection from './ConflictDetectionScannerSection'
import sharedStyles from './App.module.css' import sharedStyles from './App.module.css'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
@ -12,21 +11,17 @@ import {
resetPendingBlocklistSubmissionStatus, resetPendingBlocklistSubmissionStatus,
resetUnregisteredClientsDeletionStatus resetUnregisteredClientsDeletionStatus
} from './store/actions' } from './store/actions'
import { import { faCheck, faSkull, faSpinner } from '@fortawesome/free-solid-svg-icons'
faCheck,
faSkull,
faSpinner } from '@fortawesome/free-solid-svg-icons'
import classnames from 'classnames' import classnames from 'classnames'
import size from 'lodash/size' import { size } from 'lodash'
import { __ } from '@wordpress/i18n' import { __ } from '@wordpress/i18n'
export default function TroubleshootTab() { export default function TroubleshootTab() {
const dispatch = useDispatch() 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 blocklistUpdateStatus = useSelector((state) => state.blocklistUpdateStatus)
const unregisteredClientsDeletionStatus = useSelector(state => state.unregisteredClientsDeletionStatus) const unregisteredClientsDeletionStatus = useSelector((state) => state.unregisteredClientsDeletionStatus)
const showSubmitButton = size(unregisteredClients) > 0 const showSubmitButton = size(unregisteredClients) > 0
const hasPendingChanges = null !== blocklistUpdateStatus.pending || size(unregisteredClientsDeletionStatus.pending) > 0 const hasPendingChanges = null !== blocklistUpdateStatus.pending || size(unregisteredClientsDeletionStatus.pending) > 0
@ -54,15 +49,14 @@ export default function TroubleshootTab() {
} }
} }
return <> return (
<>
<div className={sharedStyles['wrapper-div']}> <div className={sharedStyles['wrapper-div']}>
{ hasV3DeprecationWarning && <V3DeprecationWarning /> }
<ConflictDetectionScannerSection /> <ConflictDetectionScannerSection />
<ManageFontAwesomeVersionsSection /> <ManageFontAwesomeVersionsSection />
<UnregisteredClientsView /> <UnregisteredClientsView />
</div> </div>
{ {showSubmitButton && (
showSubmitButton &&
<div className={classnames(sharedStyles['submit-wrapper'], ['submit'])}> <div className={classnames(sharedStyles['submit-wrapper'], ['submit'])}>
<input <input
type="submit" type="submit"
@ -73,36 +67,42 @@ export default function TroubleshootTab() {
disabled={!hasPendingChanges} disabled={!hasPendingChanges}
onClick={handleSubmitClick} onClick={handleSubmitClick}
/> />
{ hasSubmitted {hasSubmitted ? (
? submitSuccess submitSuccess ? (
? <span className={ classnames(sharedStyles['submit-status'], sharedStyles['success']) }> <span className={classnames(sharedStyles['submit-status'], sharedStyles['success'])}>
<FontAwesomeIcon className={ sharedStyles['icon'] } icon={ faCheck } /> <FontAwesomeIcon
className={sharedStyles['icon']}
icon={faCheck}
/>
</span> </span>
: <div className={ classnames(sharedStyles['submit-status'], sharedStyles['fail']) }> ) : (
<div className={classnames(sharedStyles['submit-status'], sharedStyles['fail'])}>
<div className={classnames(sharedStyles['fail-icon-container'])}> <div className={classnames(sharedStyles['fail-icon-container'])}>
<FontAwesomeIcon className={ sharedStyles['icon'] } icon={ faSkull } /> <FontAwesomeIcon
className={sharedStyles['icon']}
icon={faSkull}
/>
</div> </div>
<div className={sharedStyles['explanation']}> <div className={sharedStyles['explanation']}>
{ {!!blocklistUpdateStatus.message && <p> {blocklistUpdateStatus.message} </p>}
!!blocklistUpdateStatus.message && <p> { blocklistUpdateStatus.message } </p> {!!unregisteredClientsDeletionStatus.message && <p> {unregisteredClientsDeletionStatus.message} </p>}
}
{
!!unregisteredClientsDeletionStatus.message && <p> { unregisteredClientsDeletionStatus.message } </p>
}
</div> </div>
</div> </div>
: null )
} ) : null}
{ {isSubmitting ? (
isSubmitting <span className={classnames(sharedStyles['submit-status'], sharedStyles['submitting'])}>
? <span className={ classnames(sharedStyles['submit-status'], sharedStyles['submitting']) }> <FontAwesomeIcon
<FontAwesomeIcon className={ sharedStyles['icon'] } icon={faSpinner} spin/> className={sharedStyles['icon']}
icon={faSpinner}
spin
/>
</span> </span>
: hasPendingChanges ) : hasPendingChanges ? (
? <span className={ sharedStyles['submit-status'] }>{ __( 'you have pending changes', 'font-awesome' ) }</span> <span className={sharedStyles['submit-status']}>{__('you have pending changes', 'font-awesome')}</span>
: null ) : null}
}
</div> </div>
} )}
</> </>
)
} }

View File

@ -1,25 +1,14 @@
import React from 'react' import React from 'react'
import { useSelector, useDispatch } from 'react-redux' import { useSelector, useDispatch } from 'react-redux'
import { import { updatePendingBlocklist, updatePendingUnregisteredClientsForDeletion } from './store/actions'
updatePendingBlocklist,
updatePendingUnregisteredClientsForDeletion
} from './store/actions'
import { blocklistSelector } from './store/reducers' import { blocklistSelector } from './store/reducers'
import styles from './UnregisteredClientsView.module.css' import styles from './UnregisteredClientsView.module.css'
import sharedStyles from './App.module.css' import sharedStyles from './App.module.css'
import classnames from 'classnames' import classnames from 'classnames'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { import { faCheckSquare, faThumbsUp } from '@fortawesome/free-solid-svg-icons'
faCheckSquare, import { faSquare } from '@fortawesome/free-regular-svg-icons'
faThumbsUp } from '@fortawesome/free-solid-svg-icons' import { get, truncate, size, isEqual, sortedUniq, difference } from 'lodash'
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 { __ } from '@wordpress/i18n' import { __ } from '@wordpress/i18n'
import createInterpolateElement from './createInterpolateElement' import createInterpolateElement from './createInterpolateElement'
@ -33,93 +22,93 @@ function excerpt( content ) {
export default function UnregisteredClientsView() { export default function UnregisteredClientsView() {
const dispatch = useDispatch() const dispatch = useDispatch()
const unregisteredClients = useSelector(state => state.unregisteredClients) const unregisteredClients = useSelector((state) => state.unregisteredClients)
const savedBlocklist = useSelector(state => blocklistSelector(state)) const savedBlocklist = useSelector((state) => blocklistSelector(state))
const blocklist = useSelector(state => { const blocklist = useSelector((state) => {
if (null !== state.blocklistUpdateStatus.pending) { if (null !== state.blocklistUpdateStatus.pending) {
return state.blocklistUpdateStatus.pending return state.blocklistUpdateStatus.pending
} else { } else {
return savedBlocklist return savedBlocklist
} }
}) })
const deleteList = useSelector( state => state.unregisteredClientsDeletionStatus.pending) const deleteList = useSelector((state) => state.unregisteredClientsDeletionStatus.pending)
const detectedUnregisteredClients = size(Object.keys(unregisteredClients)) > 0 const detectedUnregisteredClients = size(Object.keys(unregisteredClients)) > 0
const allDetectedConflictsSelectedForBlocking = const allDetectedConflictsSelectedForBlocking = isEqual(Object.keys(unregisteredClients).sort(), [...(blocklist || [])].sort())
isEqual(Object.keys(unregisteredClients).sort(), [...(blocklist || [])].sort()) const allDetectedConflictsSelectedForRemoval = isEqual(Object.keys(unregisteredClients).sort(), [...(deleteList || [])].sort())
const allDetectedConflictsSelectedForRemoval =
isEqual(Object.keys(unregisteredClients).sort(), [...(deleteList || [])].sort())
const allDetectedConflicts = Object.keys(unregisteredClients) const allDetectedConflicts = Object.keys(unregisteredClients)
function isCheckedForBlocking(md5) { function isCheckedForBlocking(md5) {
return !! blocklist.find(x => x === md5) return !!blocklist.find((x) => x === md5)
} }
function isCheckedForRemoval(md5) { function isCheckedForRemoval(md5) {
return !! deleteList.find(x => x === md5) return !!deleteList.find((x) => x === md5)
} }
function changeCheckForRemoval(md5, allDetectedConflicts) { function changeCheckForRemoval(md5, allDetectedConflicts) {
const newDeleteList = 'all' === md5 const newDeleteList =
'all' === md5
? allDetectedConflictsSelectedForRemoval ? allDetectedConflictsSelectedForRemoval
? [] // uncheck them all ? [] // uncheck them all
: allDetectedConflicts // check them all : allDetectedConflicts // check them all
: isCheckedForRemoval(md5) : isCheckedForRemoval(md5)
? deleteList.filter(x => x !== md5) ? deleteList.filter((x) => x !== md5)
: [...deleteList, md5] : [...deleteList, md5]
dispatch(updatePendingUnregisteredClientsForDeletion(newDeleteList)) dispatch(updatePendingUnregisteredClientsForDeletion(newDeleteList))
} }
function changeCheckForBlocking(md5, allDetectedConflicts) { function changeCheckForBlocking(md5, allDetectedConflicts) {
const newBlocklist = 'all' === md5 const newBlocklist =
'all' === md5
? allDetectedConflictsSelectedForBlocking ? allDetectedConflictsSelectedForBlocking
? [] // uncheck them all ? [] // uncheck them all
: allDetectedConflicts // check them all : allDetectedConflicts // check them all
: isCheckedForBlocking(md5) : isCheckedForBlocking(md5)
? blocklist.filter(x => x !== md5) ? blocklist.filter((x) => x !== md5)
: [...blocklist, md5] : [...blocklist, md5]
const orig = sortedUnique( savedBlocklist ) const orig = sortedUniq(savedBlocklist)
const updated = sortedUnique( newBlocklist ) const updated = sortedUniq(newBlocklist)
if( if (orig.length === updated.length && 0 === size(difference(orig, updated)) && 0 === size(difference(updated, orig))) {
orig.length === updated.length &&
0 === size( difference(orig, updated) ) &&
0 === size( difference(updated, orig) )
) {
dispatch(updatePendingBlocklist(null)) dispatch(updatePendingBlocklist(null))
} else { } else {
dispatch(updatePendingBlocklist(newBlocklist)) dispatch(updatePendingBlocklist(newBlocklist))
} }
} }
return <div className={ classnames(styles['unregistered-clients'], { [styles['none-detected']]: !detectedUnregisteredClients }) }> return (
<div className={classnames(styles['unregistered-clients'], { [styles['none-detected']]: !detectedUnregisteredClients })}>
<h3 className={sharedStyles['section-title']}>{__('Other themes or plugins', 'font-awesome')}</h3> <h3 className={sharedStyles['section-title']}>{__('Other themes or plugins', 'font-awesome')}</h3>
{detectedUnregisteredClients {detectedUnregisteredClients ? (
? <div> <div>
<p className={sharedStyles['explanation']}> <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> </p>
<table className={classnames('widefat', 'striped')}> <table className={classnames('widefat', 'striped')}>
<thead> <thead>
<tr className={sharedStyles['table-header']}> <tr className={sharedStyles['table-header']}>
<th> <th>
<div className={styles['column-label']}>{__('Block', 'font-awesome')}</div> <div className={styles['column-label']}>{__('Block', 'font-awesome')}</div>
{ {size(allDetectedConflicts) > 1 && (
size( allDetectedConflicts ) > 1 &&
<div className={styles['block-all-container']}> <div className={styles['block-all-container']}>
<input <input
id='block_all_detected_conflicts' id="block_all_detected_conflicts"
name='block_all_detected_conflicts' name="block_all_detected_conflicts"
type="checkbox" type="checkbox"
value='all' value="all"
checked={allDetectedConflictsSelectedForBlocking} checked={allDetectedConflictsSelectedForBlocking}
onChange={() => changeCheckForBlocking('all', allDetectedConflicts)} onChange={() => changeCheckForBlocking('all', allDetectedConflicts)}
className={classnames(sharedStyles['sr-only'], sharedStyles['input-checkbox-custom'])} className={classnames(sharedStyles['sr-only'], sharedStyles['input-checkbox-custom'])}
/> />
<label htmlFor='block_all_detected_conflicts' className={ styles['checkbox-label'] }> <label
htmlFor="block_all_detected_conflicts"
className={styles['checkbox-label']}
>
<span className={sharedStyles['relative']}> <span className={sharedStyles['relative']}>
<FontAwesomeIcon <FontAwesomeIcon
icon={faCheckSquare} icon={faCheckSquare}
@ -137,33 +126,31 @@ export default function UnregisteredClientsView() {
{__('All', 'font-awesome')} {__('All', 'font-awesome')}
</label> </label>
</div> </div>
} )}
</th> </th>
<th> <th>
<span className={ styles['column-label'] }> <span className={styles['column-label']}>{__('Type', 'font-awesome')}</span>
{ __( 'Type', 'font-awesome' ) }
</span>
</th> </th>
<th> <th>
<span className={ styles['column-label'] }> <span className={styles['column-label']}>{__('URL', 'font-awesome')}</span>
{ __( 'URL', 'font-awesome' ) }
</span>
</th> </th>
<th> <th>
<div className={styles['column-label']}>{__('Clear', 'font-awesome')}</div> <div className={styles['column-label']}>{__('Clear', 'font-awesome')}</div>
{ {size(allDetectedConflicts) > 1 && (
size( allDetectedConflicts ) > 1 &&
<div className={styles['remove-all-container']}> <div className={styles['remove-all-container']}>
<input <input
id='remove_all_detected_conflicts' id="remove_all_detected_conflicts"
name='remove_all_detected_conflicts' name="remove_all_detected_conflicts"
type="checkbox" type="checkbox"
value='all' value="all"
checked={allDetectedConflictsSelectedForRemoval} checked={allDetectedConflictsSelectedForRemoval}
onChange={() => changeCheckForRemoval('all', allDetectedConflicts)} onChange={() => changeCheckForRemoval('all', allDetectedConflicts)}
className={classnames(sharedStyles['sr-only'], sharedStyles['input-checkbox-custom'])} className={classnames(sharedStyles['sr-only'], sharedStyles['input-checkbox-custom'])}
/> />
<label htmlFor='remove_all_detected_conflicts' className={ styles['checkbox-label'] }> <label
htmlFor="remove_all_detected_conflicts"
className={styles['checkbox-label']}
>
<span className={sharedStyles['relative']}> <span className={sharedStyles['relative']}>
<FontAwesomeIcon <FontAwesomeIcon
icon={faCheckSquare} icon={faCheckSquare}
@ -181,13 +168,12 @@ export default function UnregisteredClientsView() {
{__('All', 'font-awesome')} {__('All', 'font-awesome')}
</label> </label>
</div> </div>
} )}
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{ {allDetectedConflicts.map((md5) => (
allDetectedConflicts.map(md5 => (
<tr key={md5}> <tr key={md5}>
<td> <td>
<input <input
@ -199,7 +185,10 @@ export default function UnregisteredClientsView() {
onChange={() => changeCheckForBlocking(md5)} onChange={() => changeCheckForBlocking(md5)}
className={classnames(sharedStyles['sr-only'], sharedStyles['input-checkbox-custom'])} className={classnames(sharedStyles['sr-only'], sharedStyles['input-checkbox-custom'])}
/> />
<label htmlFor={`block_${md5}`} className={ styles['checkbox-label'] }> <label
htmlFor={`block_${md5}`}
className={styles['checkbox-label']}
>
<span className={sharedStyles['relative']}> <span className={sharedStyles['relative']}>
<FontAwesomeIcon <FontAwesomeIcon
icon={faCheckSquare} icon={faCheckSquare}
@ -216,27 +205,21 @@ export default function UnregisteredClientsView() {
</span> </span>
</label> </label>
</td> </td>
<td>{get(unregisteredClients[md5], 'tagName', 'unknown').toLowerCase()}</td>
<td> <td>
{get(unregisteredClients[md5], 'tagName', 'unknown').toLowerCase()} {unregisteredClients[md5].src ||
</td> unregisteredClients[md5].href ||
<td> createInterpolateElement(__('<em>in page source. </em><excerpt/>', 'font-awesome'), {
{
unregisteredClients[md5].src
|| unregisteredClients[md5].href
|| createInterpolateElement(
__( '<em>in page source. </em><excerpt/>', 'font-awesome' ),
{
em: <em />, em: <em />,
excerpt: ( excerpt: ((content) =>
( content ) => content content ? (
? <> <>
File starts with: <code>{content}</code> File starts with: <code>{content}</code>
</> </>
: '' ) : (
) ( excerpt( get(unregisteredClients[md5], 'innerText') ) ) ''
} ))(excerpt(get(unregisteredClients[md5], 'innerText')))
) })}
}
</td> </td>
<td> <td>
<input <input
@ -248,7 +231,10 @@ export default function UnregisteredClientsView() {
onChange={() => changeCheckForRemoval(md5)} onChange={() => changeCheckForRemoval(md5)}
className={classnames(sharedStyles['sr-only'], sharedStyles['input-checkbox-custom'])} className={classnames(sharedStyles['sr-only'], sharedStyles['input-checkbox-custom'])}
/> />
<label htmlFor={`remove_${md5}`} className={ styles['checkbox-label'] }> <label
htmlFor={`remove_${md5}`}
className={styles['checkbox-label']}
>
<span className={sharedStyles['relative']}> <span className={sharedStyles['relative']}>
<FontAwesomeIcon <FontAwesomeIcon
icon={faCheckSquare} icon={faCheckSquare}
@ -266,19 +252,21 @@ export default function UnregisteredClientsView() {
</label> </label>
</td> </td>
</tr> </tr>
)) ))}
}
</tbody> </tbody>
</table> </table>
</div> </div>
: <div className={ classnames(sharedStyles['explanation'], sharedStyles['flex'], sharedStyles['flex-row'] )}> ) : (
<div className={classnames(sharedStyles['explanation'], sharedStyles['flex'], sharedStyles['flex-row'])}>
<div> <div>
<FontAwesomeIcon icon={ faThumbsUp } size='lg'/> <FontAwesomeIcon
icon={faThumbsUp}
size="lg"
/>
</div> </div>
<div className={ sharedStyles['space-left'] }> <div className={sharedStyles['space-left']}>{__("We haven't detected any plugins or themes trying to load Font Awesome.", 'font-awesome')}</div>
{ __( 'We haven\'t detected any plugins or themes trying to load Font Awesome.', 'font-awesome' ) }
</div> </div>
)}
</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' const get = require('lodash/get')
import set from 'lodash/set' const set = require('lodash/set')
// NOTE: the Jest docs on manual mocks indicate that mocks for things under // 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. // node_modules should be in a __mocks__ directory that is adjacent to node_modules.
@ -9,7 +9,7 @@ import set from 'lodash/set'
// the root for Jest in such a way that __mocks__ as to live under the src directory. // 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 // See: https://github.com/facebook/create-react-app/issues/7539#issuecomment-531463603
const DEFAULT_INTERCEPTOR = thing => thing const DEFAULT_INTERCEPTOR = (thing) => thing
const DEFAULT_PUT = (url, _data, _config) => handleRequest({ url, method: 'PUT' }) const DEFAULT_PUT = (url, _data, _config) => handleRequest({ url, method: 'PUT' })
const DEFAULT_POST = (url, _data, _config) => handleRequest({ url, method: 'POST' }) const DEFAULT_POST = (url, _data, _config) => handleRequest({ url, method: 'POST' })
const DEFAULT_DELETE = (url, _data, _config) => handleRequest({ url, method: 'DELETE' }) const DEFAULT_DELETE = (url, _data, _config) => handleRequest({ url, method: 'DELETE' })
@ -33,7 +33,7 @@ const axios = {
axios.create = () => 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) responses = set(responses, [url, method.toUpperCase()], 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 { import { __experimentalCreateInterpolateElement, createInterpolateElement as stableCreateInterpolateElement } from '@wordpress/element'
__experimentalCreateInterpolateElement,
createInterpolateElement as stableCreateInterpolateElement,
} from "@wordpress/element";
const createInterpolateElement = stableCreateInterpolateElement || const createInterpolateElement = stableCreateInterpolateElement || __experimentalCreateInterpolateElement
__experimentalCreateInterpolateElement;
export default createInterpolateElement; export default createInterpolateElement

View File

@ -1,9 +1,11 @@
import { createStore } from './store' 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 // See: https://webpack.js.org/guides/public-path/#on-the-fly
__webpack_public_path__ = get(initialData, 'webpackPublicPath')
const CONFLICT_DETECTION_REPORT_EVENT_TYPE = 'fontAwesomeConflictDetectionReport' const CONFLICT_DETECTION_REPORT_EVENT_TYPE = 'fontAwesomeConflictDetectionReport'
/** /**
* This will start out as falsy, when there's a report, we'll set it with those * This will start out as falsy, when there's a report, we'll set it with those
@ -16,7 +18,7 @@ const CONFLICT_DETECTION_REPORT_EVENT_TYPE = 'fontAwesomeConflictDetectionReport
let conflictDetectionReport = null let conflictDetectionReport = null
if (get(initialData, 'showConflictDetectionReporter')) { if (get(initialData, 'showConflictDetectionReporter')) {
const reportEvent = new Event(CONFLICT_DETECTION_REPORT_EVENT_TYPE, { "bubbles": true, "cancelable": false }) 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, * If we're doing conflict detection, we must set this up before DOMContentLoaded,
@ -24,66 +26,37 @@ if( get(initialData, 'showConflictDetectionReporter') ) {
*/ */
window.FontAwesomeDetection = { window.FontAwesomeDetection = {
...(window.FontAwesomeDetection || {}), ...(window.FontAwesomeDetection || {}),
report: params => { report: (params) => {
conflictDetectionReport = params conflictDetectionReport = params
document.dispatchEvent(reportEvent) 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) { if (!initialData) {
console.error(__('Font Awesome plugin is broken: initial state data missing.', 'font-awesome')) console.error(__('Font Awesome plugin is broken: initial state data missing.', 'font-awesome'))
} }
const store = createStore(initialData) const store = createStore(initialData)
const { set(window, [GLOBAL_KEY, 'createInterpolateElement'], createInterpolateElement)
showAdmin,
showConflictDetectionReporter, const { showAdmin, showConflictDetectionReporter } = store.getState()
enableIconChooser,
usingCompatJs,
isGutenbergPage
} = store.getState()
if (showAdmin) { if (showAdmin) {
import('./mountAdminView') import('./mountAdminView')
.then(({ default: mountAdminView }) => { .then(({ default: mountAdminView }) => {
mountAdminView(store) mountAdminView(store)
}) })
.catch(error => { .catch((error) => {
console.error(__('Font Awesome plugin error when initializing admin settings view', 'font-awesome'), error) console.error(__('Font Awesome plugin error when initializing admin settings view', 'font-awesome'), error)
}) })
} }
if (showConflictDetectionReporter) { if (showConflictDetectionReporter) {
Promise.all([ Promise.all([import('./store/actions'), import('./mountConflictDetectionReporter')])
import('./store/actions'),
import('./mountConflictDetectionReporter')
])
.then(([{ reportDetectedConflicts }, { mountConflictDetectionReporter }]) => { .then(([{ reportDetectedConflicts }, { mountConflictDetectionReporter }]) => {
const report = params => store.dispatch(reportDetectedConflicts(params)) const report = (params) => store.dispatch(reportDetectedConflicts(params))
/** /**
* If the conflict detection report is already available, just use it; * If the conflict detection report is already available, just use it;
@ -92,59 +65,12 @@ if( showConflictDetectionReporter ) {
if (conflictDetectionReport) { if (conflictDetectionReport) {
report(conflictDetectionReport) report(conflictDetectionReport)
} else { } else {
document.addEventListener( document.addEventListener(CONFLICT_DETECTION_REPORT_EVENT_TYPE, (_event) => report(conflictDetectionReport))
CONFLICT_DETECTION_REPORT_EVENT_TYPE,
_event => report(conflictDetectionReport)
)
} }
mountConflictDetectionReporter(store) mountConflictDetectionReporter(store)
}) })
.catch(error => { .catch((error) => {
console.error(__('Font Awesome plugin error when initializing conflict detection scanner', 'font-awesome'), 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 })
/**
* 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( window['__FontAwesomeOfficialPlugin__setupClassicEditorIconChooser'] ) {
setupClassicEditorIconChooser()
} else {
window['__FontAwesomeOfficialPlugin__setupClassicEditorIconChooser'] = setupClassicEditorIconChooser
}
})
.catch(error => {
console.error( __( 'Font Awesome plugin error when initializing Icon Chooser', 'font-awesome' ), error )
})
}
}

View File

@ -1,19 +1,29 @@
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import { createRoot } from 'react-dom/client'
import ErrorBoundary from './ErrorBoundary' import ErrorBoundary from './ErrorBoundary'
import FontAwesomeAdminView from './FontAwesomeAdminView' import FontAwesomeAdminView from './FontAwesomeAdminView'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import domReady from '@wordpress/dom-ready' import domReady from '@wordpress/dom-ready'
const isAtLeastReact18 = React.version.split('.')[0] >= 18
export default function (store) { export default function (store) {
domReady(() => const container = document.getElementById('font-awesome-admin')
ReactDOM.render(
domReady(() => {
const app = (
<ErrorBoundary> <ErrorBoundary>
<Provider store={store}> <Provider store={store}>
<FontAwesomeAdminView /> <FontAwesomeAdminView />
</Provider> </Provider>
</ErrorBoundary>, </ErrorBoundary>
document.getElementById('font-awesome-admin')
)
) )
if (isAtLeastReact18) {
createRoot(container).render(app)
} else {
ReactDOM.render(app, container)
}
})
} }

View File

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

View File

@ -1,13 +1,13 @@
import { test as setup, expect } from '@wordpress/e2e-test-utils-playwright' import { test as setup, expect } from '@wordpress/e2e-test-utils-playwright'
import '../support/env.js' import '../support/env.js'
const authFile = 'src/playwright/.auth/state.json'; const authFile = 'src/playwright/.auth/state.json'
setup('authenticate', async ({ page }) => { setup('authenticate', async ({ page }) => {
await page.goto('/wp-login.php'); await page.goto('/wp-login.php')
await page.getByLabel('Username or Email Address').fill(process.env.WP_ADMIN_USERNAME); 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.getByLabel('Password', { exact: true }).fill(process.env.WP_ADMIN_PASSWORD)
await page.getByRole('button', { name: 'Log In'} ).click(); await page.getByRole('button', { name: 'Log In' }).click()
await page.waitForURL('**/wp-admin/'); await page.waitForURL('**/wp-admin/')
await page.context().storageState({ path: authFile }); 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*' const API_ROUTE_PATTERN = '**/font-awesome/v1/api*'
setup('pro kit', async ({ page }) => { setup('pro kit', async ({ page }) => {
expect(process.env.API_TOKEN).toBeTruthy(); expect(process.env.API_TOKEN).toBeTruthy()
expect(process.env.KIT_TOKEN).toBeTruthy(); expect(process.env.KIT_TOKEN).toBeTruthy()
await page.goto('/wp-admin/admin.php?page=font-awesome') await page.goto('/wp-admin/admin.php?page=font-awesome')
await page.locator('label').filter({ hasText: 'Use A Kit' }).click() await page.locator('label').filter({ hasText: 'Use A Kit' }).click()
@ -23,7 +23,6 @@ setup('pro kit', async ({ page }) => {
await page.getByRole('button').filter({ hasText: 'kits data' }).click() await page.getByRole('button').filter({ hasText: 'kits data' }).click()
await kitsResponsePromise await kitsResponsePromise
await page.locator('select').selectOption(process.env.KIT_TOKEN) await page.locator('select').selectOption(process.env.KIT_TOKEN)
const saveSettingsResponsePromise = page.waitForResponse(CONFIG_ROUTE_PATTERN) const saveSettingsResponsePromise = page.waitForResponse(CONFIG_ROUTE_PATTERN)

View File

@ -10,13 +10,13 @@ setup('reset', async ({ storageState, baseURL }) => {
host: 'localhost', host: 'localhost',
user: process.env.WORDPRESS_DB_USER, user: process.env.WORDPRESS_DB_USER,
password: process.env.WORDPRESS_DB_PASSWORD, 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'; const sql = 'DELETE FROM `wp_options` WHERE `option_name` = ? LIMIT 1'
await connection.execute(sql, ['font-awesome']); await connection.execute(sql, ['font-awesome'])
await connection.execute(sql, ['font-awesome-conflict-detection']); await connection.execute(sql, ['font-awesome-conflict-detection'])
await connection.execute(sql, ['font-awesome-releases']); await connection.execute(sql, ['font-awesome-releases'])
await requestUtils.activatePlugin('font-awesome') await requestUtils.activatePlugin('font-awesome')
await requestContext.dispose(); await requestContext.dispose()
}) })

View File

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

View File

@ -1,7 +1,4 @@
import { import { expect, test } from '@wordpress/e2e-test-utils-playwright'
expect,
test,
} from "@wordpress/e2e-test-utils-playwright";
import { prepareRestApi } from '../support/testHelpers' import { prepareRestApi } from '../support/testHelpers'
const QUERY = 'query { search(version: "6.x", query: "coffee", first: 1) { id } }' const QUERY = 'query { search(version: "6.x", query: "coffee", first: 1) { id } }'
@ -15,11 +12,11 @@ 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 // MIME type is known to result in a 403 due to the OWASP default core ruleset
// as of OWASP 4.3.0. // as of OWASP 4.3.0.
test('query as plain text', async ({ storageState, baseURL }) => { 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, { const response = await requestUtils.request.fetch(url, {
method: 'POST', method: 'POST',
@ -27,11 +24,11 @@ test('query as plain text', async ({ storageState, baseURL }) => {
headers: { headers: {
'X-WP-Nonce': requestUtils.storageState.nonce '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 { import { expect, test } from '@wordpress/e2e-test-utils-playwright'
expect,
test,
} from "@wordpress/e2e-test-utils-playwright";
test("change technology", async ({ page }) => { test('change technology', async ({ page }) => {
await page.goto("/wp-admin/admin.php?page=font-awesome"); await page.goto('/wp-admin/admin.php?page=font-awesome')
const preferenceCheckResponsePromise = page.waitForResponse( const preferenceCheckResponsePromise = page.waitForResponse('**/font-awesome/v1/preference-check')
"**/font-awesome/v1/preference-check",
);
await page.getByText('SVG').click(); await page.getByText('SVG').click()
await preferenceCheckResponsePromise; await preferenceCheckResponsePromise
const saveChangesResponsePromise = page.waitForResponse( const saveChangesResponsePromise = page.waitForResponse('**/font-awesome/v1/config')
"**/font-awesome/v1/config", await page.getByRole('button', { name: 'Save Changes' }).click()
); await saveChangesResponsePromise
await page.getByRole("button", { name: "Save Changes" }).click(); })
await saveChangesResponsePromise;
});

View File

@ -1,45 +1,37 @@
import { import { Editor, expect, test } from '@wordpress/e2e-test-utils-playwright'
Editor,
expect,
test,
} from "@wordpress/e2e-test-utils-playwright";
test.describe('full site editor', async () => { test.describe('full site editor', async () => {
test.use({ test.use({
editor: async ({ page }, use) => { editor: async ({ page }, use) => {
await use(new Editor({ page })) await use(new Editor({ page }))
}, }
}) })
test("insert with icon chooser", async ({ page, editor,pageUtils }) => { test('insert with icon chooser', async ({ page, editor, pageUtils }) => {
const pageLoadPromise = page.waitForResponse( const pageLoadPromise = page.waitForResponse('**/wp/v2/pages*')
'**/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) { if (getStartedCount > 0) {
await page.getByRole('button', { name: 'Get started' }).click(); await page.getByRole('button', { name: 'Get started' }).click()
} }
await editor.insertBlock({ await editor.insertBlock({
name: 'core/paragraph', name: 'core/paragraph'
} ); })
await page.keyboard.type('Here comes an icon: ') 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( const searchResponsePromise = page.waitForResponse('**/font-awesome/v1/api*')
'**/font-awesome/v1/api*'
);
await page.locator('fa-icon-chooser input#search').fill('coffee') await page.locator('fa-icon-chooser input#search').fill('coffee')
@ -57,6 +49,5 @@ test.describe('full site editor', async () => {
expect(blocks).toHaveLength(1) expect(blocks).toHaveLength(1)
expect(blocks[0].attributes.content).toMatch(/\[icon.*?\]$/) expect(blocks[0].attributes.content).toMatch(/\[icon.*?\]$/)
} catch (_e) {} } catch (_e) {}
}); })
}); })

View File

@ -8,24 +8,22 @@ test.describe( 'blockEditorIconChooser', async () => {
test.use({ test.use({
editor: async ({ page }, use) => { editor: async ({ page }, use) => {
await use(new Editor({ page })) await use(new Editor({ page }))
}, }
}) })
test('search and select from icon chooser', async ({ editor, page, pageUtils }) => { test('search and select from icon chooser', async ({ editor, page, pageUtils }) => {
await editor.insertBlock({ await editor.insertBlock({
name: 'core/paragraph', name: 'core/paragraph'
} ); })
await page.keyboard.type('Here comes an icon: ') 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( const searchResponsePromise = page.waitForResponse('**/font-awesome/v1/api*')
'**/font-awesome/v1/api*'
);
await page.locator('fa-icon-chooser input#search').fill('coffee') await page.locator('fa-icon-chooser input#search').fill('coffee')
@ -44,11 +42,7 @@ test.describe( 'blockEditorIconChooser', async () => {
expect(blocks[0].attributes.content).toMatch(/\[icon.*?\]$/) expect(blocks[0].attributes.content).toMatch(/\[icon.*?\]$/)
} catch (_e) {} } catch (_e) {}
// The loading of the icon chooser should not have messed up globals. // The loading of the icon chooser should not have messed up the lodash globals.
// It could create problems for other plugins that depend on them. await expect(page.evaluate(() => 'undefined' !== typeof _ && 'undefined' !== typeof lodash)).toBeTruthy()
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();
}) })
}) })

View File

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

View File

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

View File

@ -1,12 +1,9 @@
import axios from 'axios' import axios from 'axios'
import toPairs from 'lodash/toPairs' import { toPairs, size, get, has, find } from 'lodash'
import size from 'lodash/size'
import get from 'lodash/get'
import find from 'lodash/find'
import reportRequestError, { redactRequestData, redactHeaders } from '../util/reportRequestError' import reportRequestError, { redactRequestData, redactHeaders } from '../util/reportRequestError'
import { __ } from '@wordpress/i18n' import { __ } from '@wordpress/i18n'
import has from 'lodash/has'
import sliceJson from '../util/sliceJson' import sliceJson from '../util/sliceJson'
import { clearQueryCache } from '../queryCache'
const restApiAxios = axios.create() const restApiAxios = axios.create()
@ -20,13 +17,16 @@ export const CONFLICT_DETECTION_SCANNER_DURATION_MIN = 10
// (which would just be exactly "now"). // (which would just be exactly "now").
const CONFLICT_DETECTION_SCANNER_DEACTIVATION_DELTA_MS = 1 const CONFLICT_DETECTION_SCANNER_DEACTIVATION_DELTA_MS = 1
const COULD_NOT_SAVE_CHANGES_MESSAGE = __( 'Couldn\'t save those changes', 'font-awesome' ) 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 REJECTED_METHOD_COULD_NOT_SAVE_CHANGES_MESSAGE = __(
const COULD_NOT_CHECK_PREFERENCES_MESSAGE = __( 'Couldn\'t check preferences', 'font-awesome' ) '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 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 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_START_SCANNER_MESSAGE = __("Couldn't start the scanner", 'font-awesome')
const COULD_NOT_SNOOZE_MESSAGE = __( 'Couldn\'t snooze', 'font-awesome' ) const COULD_NOT_SNOOZE_MESSAGE = __("Couldn't snooze", 'font-awesome')
export function preprocessResponse(response) { export function preprocessResponse(response) {
const confirmed = has(response, 'headers.fontawesome-confirmation') const confirmed = has(response, 'headers.fontawesome-confirmation')
@ -42,9 +42,7 @@ export function preprocessResponse( response ) {
const foundUnexpectedData = 'string' === typeof data && size(data) > 0 const foundUnexpectedData = 'string' === typeof data && size(data) > 0
const sliced = foundUnexpectedData const sliced = foundUnexpectedData ? sliceJson(data) : {}
? sliceJson( data )
: {}
// Fixup the response data if garbage was fixed // Fixup the response data if garbage was fixed
if (foundUnexpectedData) { if (foundUnexpectedData) {
@ -149,8 +147,8 @@ export function preprocessResponse( response ) {
} }
restApiAxios.interceptors.response.use( restApiAxios.interceptors.response.use(
response => preprocessResponse( response ), (response) => preprocessResponse(response),
error => { (error) => {
if (error.response) { if (error.response) {
error.response = preprocessResponse(error.response) error.response = preprocessResponse(error.response)
error.uiMessage = get(error, 'response.uiMessage') error.uiMessage = get(error, 'response.uiMessage')
@ -255,15 +253,14 @@ export function submitPendingUnregisteredClientDeletions() {
}) })
} }
return restApiAxios.delete( return restApiAxios
`${apiUrl}/conflict-detection/conflicts`, .delete(`${apiUrl}/conflict-detection/conflicts`, {
{
data: deleteList, data: deleteList,
headers: { headers: {
'X-WP-Nonce': apiNonce 'X-WP-Nonce': apiNonce
} }
} })
).then(response => { .then((response) => {
const { status, data, falsePositive } = response const { status, data, falsePositive } = response
if (falsePositive) { if (falsePositive) {
@ -276,7 +273,8 @@ export function submitPendingUnregisteredClientDeletions() {
message: '' message: ''
}) })
} }
}).catch(handleError) })
.catch(handleError)
} }
} }
@ -304,15 +302,13 @@ export function submitPendingBlocklist() {
}) })
} }
return restApiAxios.post( return restApiAxios
`${apiUrl}/conflict-detection/conflicts/blocklist`, .post(`${apiUrl}/conflict-detection/conflicts/blocklist`, blocklist, {
blocklist,
{
headers: { headers: {
'X-WP-Nonce': apiNonce 'X-WP-Nonce': apiNonce
} }
} })
).then(response => { .then((response) => {
const { status, data, falsePositive } = response const { status, data, falsePositive } = response
if (falsePositive) { if (falsePositive) {
@ -325,7 +321,8 @@ export function submitPendingBlocklist() {
message: '' message: ''
}) })
} }
}).catch(handleError) })
.catch(handleError)
} }
} }
@ -342,7 +339,8 @@ export function checkPreferenceConflicts() {
}) })
} }
return restApiAxios.post( return restApiAxios
.post(
`${apiUrl}/preference-check`, `${apiUrl}/preference-check`,
{ ...options, ...pendingOptions }, { ...options, ...pendingOptions },
{ {
@ -350,7 +348,8 @@ export function checkPreferenceConflicts() {
'X-WP-Nonce': apiNonce 'X-WP-Nonce': apiNonce
} }
} }
).then(response => { )
.then((response) => {
const { data, falsePositive } = response const { data, falsePositive } = response
if (falsePositive) { if (falsePositive) {
@ -363,7 +362,8 @@ export function checkPreferenceConflicts() {
detectedConflicts: data detectedConflicts: data
}) })
} }
}).catch(handleError) })
.catch(handleError)
} }
} }
@ -374,7 +374,7 @@ export function chooseAwayFromKitConfig({ activeKitToken }) {
dispatch({ dispatch({
type: 'CHOOSE_AWAY_FROM_KIT_CONFIG', type: 'CHOOSE_AWAY_FROM_KIT_CONFIG',
activeKitToken, activeKitToken,
concreteVersion: get(releases, 'latest_version_6') concreteVersion: get(releases, 'latest_version_7')
}) })
} }
} }
@ -391,6 +391,8 @@ export function queryKits() {
dispatch({ type: 'KITS_QUERY_START' }) dispatch({ type: 'KITS_QUERY_START' })
clearQueryCache()
const handleKitsQueryError = ({ uiMessage }) => { const handleKitsQueryError = ({ uiMessage }) => {
dispatch({ dispatch({
type: 'KITS_QUERY_END', type: 'KITS_QUERY_END',
@ -403,11 +405,12 @@ export function queryKits() {
dispatch({ dispatch({
type: 'OPTIONS_FORM_SUBMIT_END', type: 'OPTIONS_FORM_SUBMIT_END',
success: false, 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( return restApiAxios
.post(
`${apiUrl}/api`, `${apiUrl}/api`,
'query { me { kits { name version technologySelected licenseSelected minified token shimEnabled autoAccessibilityEnabled status }}}', 'query { me { kits { name version technologySelected licenseSelected minified token shimEnabled autoAccessibilityEnabled status }}}',
{ {
@ -415,7 +418,8 @@ export function queryKits() {
'X-WP-Nonce': apiNonce 'X-WP-Nonce': apiNonce
} }
} }
).then(response => { )
.then((response) => {
if (response.falsePositive) return handleKitsQueryError(response) if (response.falsePositive) return handleKitsQueryError(response)
const data = get(response, 'data.data') const data = get(response, 'data.data')
@ -478,11 +482,13 @@ export function queryKits() {
dispatch({ type: 'OPTIONS_FORM_SUBMIT_START' }) dispatch({ type: 'OPTIONS_FORM_SUBMIT_START' })
return restApiAxios.post( return restApiAxios
.post(
`${apiUrl}/config`, `${apiUrl}/config`,
{ {
options: { options: {
...options, ...optionsUpdate ...options,
...optionsUpdate
} }
}, },
{ {
@ -490,7 +496,8 @@ export function queryKits() {
'X-WP-Nonce': apiNonce 'X-WP-Nonce': apiNonce
} }
} }
).then(response => { )
.then((response) => {
const { data, falsePositive } = response const { data, falsePositive } = response
if (falsePositive) return handleKitUpdateError(response) if (falsePositive) return handleKitUpdateError(response)
@ -501,8 +508,10 @@ export function queryKits() {
success: true, success: true,
message: __('Kit changes saved', 'font-awesome') message: __('Kit changes saved', 'font-awesome')
}) })
}).catch(handleKitUpdateError) })
}).catch(handleKitsQueryError) .catch(handleKitUpdateError)
})
.catch(handleKitsQueryError)
} }
} }
@ -520,7 +529,8 @@ export function submitPendingOptions() {
}) })
} }
return restApiAxios.post( return restApiAxios
.post(
`${apiUrl}/config`, `${apiUrl}/config`,
{ options: { ...options, ...pendingOptions } }, { options: { ...options, ...pendingOptions } },
{ {
@ -528,7 +538,8 @@ export function submitPendingOptions() {
'X-WP-Nonce': apiNonce 'X-WP-Nonce': apiNonce
} }
} }
).then(response => { )
.then((response) => {
const { data, falsePositive } = response const { data, falsePositive } = response
if (falsePositive) { if (falsePositive) {
@ -541,7 +552,8 @@ export function submitPendingOptions() {
message: __('Changes saved', 'font-awesome') message: __('Changes saved', 'font-awesome')
}) })
} }
}).catch(handleError) })
.catch(handleError)
} }
} }
@ -559,7 +571,8 @@ export function updateApiToken({ apiToken = false, runQueryKits = false }) {
}) })
} }
return restApiAxios.post( return restApiAxios
.post(
`${apiUrl}/config`, `${apiUrl}/config`,
{ options: { ...options, apiToken } }, { options: { ...options, apiToken } },
{ {
@ -567,7 +580,8 @@ export function updateApiToken({ apiToken = false, runQueryKits = false }) {
'X-WP-Nonce': apiNonce 'X-WP-Nonce': apiNonce
} }
} }
).then(response => { )
.then((response) => {
const { data, falsePositive } = response const { data, falsePositive } = response
if (falsePositive) { if (falsePositive) {
@ -584,7 +598,8 @@ export function updateApiToken({ apiToken = false, runQueryKits = false }) {
return dispatch(queryKits()) return dispatch(queryKits())
} }
} }
}).catch(handleError) })
.catch(handleError)
} }
} }
@ -627,16 +642,13 @@ export function reportDetectedConflicts({ nodesTested = {} }) {
}) })
} }
return restApiAxios.post( return restApiAxios
`${apiUrl}/conflict-detection/conflicts`, .post(`${apiUrl}/conflict-detection/conflicts`, payload, {
payload,
{
headers: { headers: {
'X-WP-Nonce': apiNonce 'X-WP-Nonce': apiNonce
} }
} })
) .then((response) => {
.then(response => {
const { status, data, falsePositive } = response const { status, data, falsePositive } = response
if (falsePositive) { if (falsePositive) {
@ -650,7 +662,7 @@ export function reportDetectedConflicts({ nodesTested = {} }) {
* response with garbage in it had an erroneous HTTP 200 status * response with garbage in it had an erroneous HTTP 200 status
* on it, but no parseable JSON, which is equivalent to a 204. * on it, but no parseable JSON, which is equivalent to a 204.
*/ */
data: ( 204 === status || 0 === size(data) ) ? null : data data: 204 === status || 0 === size(data) ? null : data
}) })
} }
}) })
@ -661,47 +673,6 @@ export function reportDetectedConflicts({ nodesTested = {} }) {
} }
} }
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) { export function setActiveAdminTab(tab) {
return { return {
type: 'SET_ACTIVE_ADMIN_TAB', type: 'SET_ACTIVE_ADMIN_TAB',
@ -713,13 +684,9 @@ export function setConflictDetectionScanner({ enable = true }) {
return function (dispatch, getState) { return function (dispatch, getState) {
const { apiNonce, apiUrl } = getState() const { apiNonce, apiUrl } = getState()
const actionStartType = enable const actionStartType = enable ? 'ENABLE_CONFLICT_DETECTION_SCANNER_START' : 'DISABLE_CONFLICT_DETECTION_SCANNER_START'
? 'ENABLE_CONFLICT_DETECTION_SCANNER_START'
: 'DISABLE_CONFLICT_DETECTION_SCANNER_START'
const actionEndType = enable const actionEndType = enable ? 'ENABLE_CONFLICT_DETECTION_SCANNER_END' : 'DISABLE_CONFLICT_DETECTION_SCANNER_END'
? 'ENABLE_CONFLICT_DETECTION_SCANNER_END'
: 'DISABLE_CONFLICT_DETECTION_SCANNER_END'
dispatch({ type: actionStartType }) dispatch({ type: actionStartType })
@ -731,17 +698,19 @@ export function setConflictDetectionScanner({ enable = true }) {
}) })
} }
return restApiAxios.post( return restApiAxios
.post(
`${apiUrl}/conflict-detection/until`, `${apiUrl}/conflict-detection/until`,
enable enable
? Math.floor((new Date((new Date()).valueOf() + (CONFLICT_DETECTION_SCANNER_DURATION_MIN * 1000 * 60))) / 1000) ? 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, : Math.floor(new Date() / 1000) - CONFLICT_DETECTION_SCANNER_DEACTIVATION_DELTA_MS,
{ {
headers: { headers: {
'X-WP-Nonce': apiNonce 'X-WP-Nonce': apiNonce
} }
} }
).then(response => { )
.then((response) => {
const { status, data, falsePositive } = response const { status, data, falsePositive } = response
if (falsePositive) { if (falsePositive) {
@ -753,6 +722,7 @@ export function setConflictDetectionScanner({ enable = true }) {
success: true success: true
}) })
} }
}).catch(handleError) })
.catch(handleError)
} }
} }

View File

@ -30,14 +30,18 @@ describe('addPendingOption', () => {
test('when multiple pending options are adjusted together, all are updated', () => { test('when multiple pending options are adjusted together, all are updated', () => {
store.dispatch(addPendingOption({ technology: 'webfont', pseudoElements: true })) store.dispatch(addPendingOption({ technology: 'webfont', pseudoElements: true }))
expect(store.getActions().length).toEqual(2) expect(store.getActions().length).toEqual(2)
expect(store.getActions()[0]).toEqual(expect.objectContaining({ expect(store.getActions()[0]).toEqual(
expect.objectContaining({
type: 'ADD_PENDING_OPTION', type: 'ADD_PENDING_OPTION',
change: { technology: 'webfont' } change: { technology: 'webfont' }
})) })
expect(store.getActions()[1]).toEqual(expect.objectContaining({ )
expect(store.getActions()[1]).toEqual(
expect.objectContaining({
type: 'ADD_PENDING_OPTION', type: 'ADD_PENDING_OPTION',
change: { pseudoElements: true } change: { pseudoElements: true }
})) })
)
}) })
}) })
@ -74,11 +78,11 @@ describe('submitPendingOptions and interceptors', () => {
options: pendingOptions, options: pendingOptions,
error: { error: {
errors: { errors: {
"code1": ["message1"], code1: ['message1']
}, },
error_data: { error_data: {
"code1": { code1: {
"trace": 'some stack trace' trace: 'some stack trace'
} }
} }
} }
@ -98,10 +102,13 @@ describe('submitPendingOptions and interceptors', () => {
}) })
}) })
test('submits successfully with successful ui message and also reports error to console', done => { test('submits successfully with successful ui message and also reports error to console', (done) => {
store.dispatch(submitPendingOptions()).then(() => { store
.dispatch(submitPendingOptions())
.then(() => {
expect(reportRequestError).toHaveBeenCalledTimes(1) expect(reportRequestError).toHaveBeenCalledTimes(1)
expect(reportRequestError).toHaveBeenCalledWith(expect.objectContaining({ expect(reportRequestError).toHaveBeenCalledWith(
expect.objectContaining({
error: expect.objectContaining({ error: expect.objectContaining({
errors: { errors: {
code1: expect.anything() code1: expect.anything()
@ -112,9 +119,11 @@ describe('submitPendingOptions and interceptors', () => {
}), }),
confirmed: true, confirmed: true,
ok: true ok: true
})) })
)
expect(store.getActions().length).toEqual(2) expect(store.getActions().length).toEqual(2)
expect(store.getActions()).toEqual(expect.arrayContaining([ expect(store.getActions()).toEqual(
expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
type: 'OPTIONS_FORM_SUBMIT_START' type: 'OPTIONS_FORM_SUBMIT_START'
}), }),
@ -124,10 +133,11 @@ describe('submitPendingOptions and interceptors', () => {
data, data,
message: expect.stringContaining('saved') message: expect.stringContaining('saved')
}) })
])) ])
)
done() done()
}) })
.catch(e => done(e)) .catch((e) => done(e))
}) })
}) })
}) })
@ -152,17 +162,22 @@ describe('submitPendingOptions and interceptors', () => {
}) })
}) })
test('reports warning but completes successfully', done => { test('reports warning but completes successfully', (done) => {
store.dispatch(submitPendingOptions()).then(() => { store
.dispatch(submitPendingOptions())
.then(() => {
expect(reportRequestError).toHaveBeenCalledTimes(1) expect(reportRequestError).toHaveBeenCalledTimes(1)
expect(reportRequestError).toHaveBeenCalledWith(expect.objectContaining({ expect(reportRequestError).toHaveBeenCalledWith(
expect.objectContaining({
error: null, error: null,
confirmed: false, confirmed: false,
ok: true, ok: true,
trimmed: INVALID_JSON_RESPONSE_DATA trimmed: INVALID_JSON_RESPONSE_DATA
})) })
)
expect(store.getActions().length).toEqual(2) expect(store.getActions().length).toEqual(2)
expect(store.getActions()).toEqual(expect.arrayContaining([ expect(store.getActions()).toEqual(
expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
type: 'OPTIONS_FORM_SUBMIT_START' type: 'OPTIONS_FORM_SUBMIT_START'
}), }),
@ -172,10 +187,11 @@ describe('submitPendingOptions and interceptors', () => {
data, data,
message: expect.stringContaining('saved') message: expect.stringContaining('saved')
}) })
])) ])
)
done() done()
}) })
.catch(e => done(e)) .catch((e) => done(e))
}) })
describe('axios request', () => { describe('axios request', () => {
@ -185,8 +201,10 @@ describe('submitPendingOptions and interceptors', () => {
changeImpl({ name: 'post', fn: mockPost }) changeImpl({ name: 'post', fn: mockPost })
}) })
test('submits pendingOptions', done => { test('submits pendingOptions', (done) => {
store.dispatch(submitPendingOptions()).then(() => { store
.dispatch(submitPendingOptions())
.then(() => {
expect(mockPost).toHaveBeenCalledTimes(1) expect(mockPost).toHaveBeenCalledTimes(1)
expect(mockPost).toHaveBeenCalledWith( expect(mockPost).toHaveBeenCalledWith(
`${apiUrl}/config`, `${apiUrl}/config`,
@ -201,7 +219,7 @@ describe('submitPendingOptions and interceptors', () => {
) )
done() done()
}) })
.catch(e => done(e)) .catch((e) => done(e))
}) })
}) })
}) })
@ -238,15 +256,18 @@ describe('submitPendingOptions and interceptors', () => {
data: requestData, data: requestData,
headers: requestHeaders headers: requestHeaders
} }
}, }
}) })
}) })
test('displays default ui message and emits console message', done => { test('displays default ui message and emits console message', (done) => {
reportRequestError.mockReturnValueOnce(null) reportRequestError.mockReturnValueOnce(null)
store.dispatch(submitPendingOptions()).then(() => { store
.dispatch(submitPendingOptions())
.then(() => {
expect(reportRequestError).toHaveBeenCalledTimes(1) expect(reportRequestError).toHaveBeenCalledTimes(1)
expect(reportRequestError).toHaveBeenCalledWith(expect.objectContaining({ expect(reportRequestError).toHaveBeenCalledWith(
expect.objectContaining({
confirmed: true, confirmed: true,
requestMethod: method, requestMethod: method,
//requestData, //requestData,
@ -256,9 +277,11 @@ describe('submitPendingOptions and interceptors', () => {
responseStatus: status, responseStatus: status,
responseStatusText: statusText, responseStatusText: statusText,
responseData responseData
})) })
)
expect(store.getActions().length).toEqual(2) expect(store.getActions().length).toEqual(2)
expect(store.getActions()).toEqual(expect.arrayContaining([ expect(store.getActions()).toEqual(
expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
type: 'OPTIONS_FORM_SUBMIT_START' type: 'OPTIONS_FORM_SUBMIT_START'
}), }),
@ -267,10 +290,11 @@ describe('submitPendingOptions and interceptors', () => {
success: false, success: false,
message: expect.stringContaining("Couldn't save") message: expect.stringContaining("Couldn't save")
}) })
])) ])
)
done() done()
}) })
.catch(e => done(e)) .catch((e) => done(e))
}) })
}) })
}) })
@ -286,10 +310,13 @@ describe('submitPendingOptions and interceptors', () => {
reportRequestError.mockImplementation(() => MOCK_UI_MESSAGE) reportRequestError.mockImplementation(() => MOCK_UI_MESSAGE)
}) })
test('failed request is reported to console and failure with uiMessage is dispatched to store', done => { test('failed request is reported to console and failure with uiMessage is dispatched to store', (done) => {
store.dispatch(submitPendingOptions()).then(() => { store
.dispatch(submitPendingOptions())
.then(() => {
expect(reportRequestError).toHaveBeenCalledTimes(1) expect(reportRequestError).toHaveBeenCalledTimes(1)
expect(reportRequestError).toHaveBeenCalledWith(expect.objectContaining({ expect(reportRequestError).toHaveBeenCalledWith(
expect.objectContaining({
error: { error: {
errors: expect.objectContaining({ errors: expect.objectContaining({
fontawesome_request_noresponse: [expect.any(String)] fontawesome_request_noresponse: [expect.any(String)]
@ -299,9 +326,11 @@ describe('submitPendingOptions and interceptors', () => {
request: expect.any(XMLHttpRequest) request: expect.any(XMLHttpRequest)
} }
} }
}, }
})) })
expect(store.getActions()).toEqual(expect.arrayContaining([ )
expect(store.getActions()).toEqual(
expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
type: 'OPTIONS_FORM_SUBMIT_START' type: 'OPTIONS_FORM_SUBMIT_START'
}), }),
@ -310,10 +339,11 @@ describe('submitPendingOptions and interceptors', () => {
success: false, success: false,
message: MOCK_UI_MESSAGE message: MOCK_UI_MESSAGE
}) })
])) ])
)
done() done()
}) })
.catch(e => done(e)) .catch((e) => done(e))
}) })
}) })
@ -328,10 +358,13 @@ describe('submitPendingOptions and interceptors', () => {
reportRequestError.mockImplementation(() => MOCK_UI_MESSAGE) reportRequestError.mockImplementation(() => MOCK_UI_MESSAGE)
}) })
test('failure is reported to console and failure with uiMessage is dispatched to store', done => { test('failure is reported to console and failure with uiMessage is dispatched to store', (done) => {
store.dispatch(submitPendingOptions()).then(() => { store
.dispatch(submitPendingOptions())
.then(() => {
expect(reportRequestError).toHaveBeenCalledTimes(1) expect(reportRequestError).toHaveBeenCalledTimes(1)
expect(reportRequestError).toHaveBeenCalledWith(expect.objectContaining({ expect(reportRequestError).toHaveBeenCalledWith(
expect.objectContaining({
error: { error: {
errors: expect.objectContaining({ errors: expect.objectContaining({
fontawesome_request_failed: [expect.stringContaining('server failed')] fontawesome_request_failed: [expect.stringContaining('server failed')]
@ -341,9 +374,11 @@ describe('submitPendingOptions and interceptors', () => {
failedRequestMessage: 'some axios error' failedRequestMessage: 'some axios error'
} }
} }
}, }
})) })
expect(store.getActions()).toEqual(expect.arrayContaining([ )
expect(store.getActions()).toEqual(
expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
type: 'OPTIONS_FORM_SUBMIT_START' type: 'OPTIONS_FORM_SUBMIT_START'
}), }),
@ -352,10 +387,11 @@ describe('submitPendingOptions and interceptors', () => {
success: false, success: false,
message: MOCK_UI_MESSAGE message: MOCK_UI_MESSAGE
}) })
])) ])
)
done() done()
}) })
.catch(e => done(e)) .catch((e) => done(e))
}) })
}) })
}) })
@ -364,7 +400,6 @@ describe('some action failure cases', () => {
const STATE_TECH_CHANGE = { const STATE_TECH_CHANGE = {
options: { options: {
technology: 'webfont' technology: 'webfont'
}, },
pendingOptions: { pendingOptions: {
technology: 'svg' technology: 'svg'
@ -435,20 +470,11 @@ describe('some action failure cases', () => {
params: { params: {
nodesTested: { nodesTested: {
conflict: { conflict: {
'abc123': {} abc123: {}
} }
} }
} }
}, },
{
action: 'snoozeV3DeprecationWarning',
state: {},
route: 'v3deprecation',
method: 'POST',
startAction: 'SNOOZE_V3DEPRECATION_WARNING_START',
endAction: 'SNOOZE_V3DEPRECATION_WARNING_END',
params: {}
},
{ {
action: 'setConflictDetectionScanner', action: 'setConflictDetectionScanner',
desc: 'when enabling', desc: 'when enabling',
@ -493,11 +519,11 @@ describe('some action failure cases', () => {
const data = { const data = {
errors: { errors: {
"code1": ["message1"], code1: ['message1']
}, },
error_data: { error_data: {
"code1": { code1: {
"trace": 'some stack trace' trace: 'some stack trace'
} }
} }
} }
@ -510,7 +536,7 @@ describe('some action failure cases', () => {
resetAxiosMocks() resetAxiosMocks()
}) })
cases.map(c => { cases.map((c) => {
describe(`${c.action}${c.desc || ''}`, () => { describe(`${c.action}${c.desc || ''}`, () => {
let store = null let store = null
@ -530,26 +556,31 @@ describe('some action failure cases', () => {
response: { response: {
status: 200, status: 200,
statusText: 'OK', statusText: 'OK',
data: `${garbage}${JSON.stringify(data)}`, data: `${garbage}${JSON.stringify(data)}`
// no confirmation header // no confirmation header
} }
}) })
}) })
test('reports warning and dispatches a failure action despite the garbage', done => { test('reports warning and dispatches a failure action despite the garbage', (done) => {
store.dispatch(actions[c.action](c.params)).then(() => { store
.dispatch(actions[c.action](c.params))
.then(() => {
expect(reportRequestError).toHaveBeenCalledTimes(1) expect(reportRequestError).toHaveBeenCalledTimes(1)
expect(reportRequestError).toHaveBeenCalledWith(expect.objectContaining({ expect(reportRequestError).toHaveBeenCalledWith(
expect.objectContaining({
error: expect.objectContaining({ error: expect.objectContaining({
errors: expect.anything(), errors: expect.anything(),
'error_data': expect.anything() error_data: expect.anything()
}), }),
confirmed: false, confirmed: false,
falsePositive: true, falsePositive: true,
trimmed: garbage trimmed: garbage
})) })
)
expect(store.getActions().length).toEqual(2) expect(store.getActions().length).toEqual(2)
expect(store.getActions()).toEqual(expect.arrayContaining([ expect(store.getActions()).toEqual(
expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
type: c.startAction type: c.startAction
}), }),
@ -558,10 +589,11 @@ describe('some action failure cases', () => {
success: false, success: false,
message: expect.stringMatching(/[a-z]/) message: expect.stringMatching(/[a-z]/)
}) })
])) ])
)
done() done()
}) })
.catch(e => done(e)) .catch((e) => done(e))
}) })
}) })
@ -581,20 +613,25 @@ describe('some action failure cases', () => {
}) })
}) })
test('reports ui and console error messages', done => { test('reports ui and console error messages', (done) => {
reportRequestError.mockReturnValueOnce(null) reportRequestError.mockReturnValueOnce(null)
store.dispatch(actions[c.action](c.params)).then(() => { store
.dispatch(actions[c.action](c.params))
.then(() => {
expect(reportRequestError).toHaveBeenCalledTimes(1) expect(reportRequestError).toHaveBeenCalledTimes(1)
expect(reportRequestError).toHaveBeenCalledWith(expect.objectContaining({ expect(reportRequestError).toHaveBeenCalledWith(
expect.objectContaining({
error: expect.objectContaining({ error: expect.objectContaining({
errors: expect.anything(), errors: expect.anything(),
'error_data': expect.anything() error_data: expect.anything()
}), }),
confirmed: true, confirmed: true,
trimmed: '' trimmed: ''
})) })
)
expect(store.getActions().length).toEqual(2) expect(store.getActions().length).toEqual(2)
expect(store.getActions()).toEqual(expect.arrayContaining([ expect(store.getActions()).toEqual(
expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
type: c.startAction type: c.startAction
}), }),
@ -603,10 +640,11 @@ describe('some action failure cases', () => {
success: false, success: false,
message: expect.stringMatching(/[a-z]/) message: expect.stringMatching(/[a-z]/)
}) })
])) ])
)
done() done()
}) })
.catch(e => done(e)) .catch((e) => done(e))
}) })
}) })
}) })
@ -653,10 +691,6 @@ describe('reportDetectedConflicts', () => {
test.todo('success') test.todo('success')
}) })
describe('snoozeV3DeprecationWarning', () => {
test.todo('success')
})
describe('setConflictDetectionScanner', () => { describe('setConflictDetectionScanner', () => {
test.todo('success when enabling') test.todo('success when enabling')
test.todo('success when disabling') test.todo('success when disabling')
@ -739,7 +773,8 @@ describe('preprocessResponse', () => {
actions.preprocessResponse(response) actions.preprocessResponse(response)
expect(reportRequestError).toHaveBeenCalledWith(expect.objectContaining({ expect(reportRequestError).toHaveBeenCalledWith(
expect.objectContaining({
confirmed: false, confirmed: false,
requestData, requestData,
requestMethod: method, requestMethod: method,
@ -749,7 +784,8 @@ describe('preprocessResponse', () => {
requestData: REDACTED_REQUEST_DATA, requestData: REDACTED_REQUEST_DATA,
responseHeaders: REDACTED_HEADERS, responseHeaders: REDACTED_HEADERS,
requestHeaders: REDACTED_HEADERS requestHeaders: REDACTED_HEADERS
})) })
)
}) })
}) })
@ -780,7 +816,8 @@ describe('preprocessResponse', () => {
actions.preprocessResponse(response) actions.preprocessResponse(response)
expect(reportRequestError).toHaveBeenCalledWith(expect.objectContaining({ expect(reportRequestError).toHaveBeenCalledWith(
expect.objectContaining({
confirmed: false, confirmed: false,
requestMethod: method, requestMethod: method,
requestUrl: url, requestUrl: url,
@ -790,7 +827,8 @@ describe('preprocessResponse', () => {
requestData: REDACTED_REQUEST_DATA, requestData: REDACTED_REQUEST_DATA,
responseHeaders: REDACTED_HEADERS, responseHeaders: REDACTED_HEADERS,
requestHeaders: REDACTED_HEADERS requestHeaders: REDACTED_HEADERS
})) })
)
}) })
}) })
}) })

View File

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

View File

@ -1,14 +1,12 @@
import size from 'lodash/size' import { size, omit, get } from 'lodash'
import omit from 'lodash/omit'
import get from 'lodash/get'
import { combineReducers } from 'redux' import { combineReducers } from 'redux'
export const ADMIN_TAB_SETTINGS = 'ADMIN_TAB_SETTINGS' export const ADMIN_TAB_SETTINGS = 'ADMIN_TAB_SETTINGS'
export const ADMIN_TAB_TROUBLESHOOT = 'ADMIN_TAB_TROUBLESHOOT' 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 // TODO: add reducer for the clientPreferences that coerces their boolean options
@ -32,15 +30,7 @@ export function options(state = {}, action = {}) {
return state return state
} else { } else {
const { const {
options: { options: { technology, usePro, compat, pseudoElements, version, kitToken, apiToken }
technology,
usePro,
compat,
pseudoElements,
version,
kitToken,
apiToken
}
} = data } = data
return { return {
@ -65,8 +55,7 @@ const OPTIONS_FORM_INITIAL_STATE = {
message: '' message: ''
} }
function optionsFormState( function optionsFormState(state = OPTIONS_FORM_INITIAL_STATE, action = {}) {
state = OPTIONS_FORM_INITIAL_STATE, action = {}) {
const { type, success, message } = action const { type, success, message } = action
switch (type) { switch (type) {
@ -128,9 +117,7 @@ const INITIAL_STATE_UNREGISTERED_CLIENTS_DELETION_STATUS = {
message: '' message: ''
} }
function unregisteredClientsDeletionStatus( function unregisteredClientsDeletionStatus(state = INITIAL_STATE_UNREGISTERED_CLIENTS_DELETION_STATUS, action = {}) {
state = INITIAL_STATE_UNREGISTERED_CLIENTS_DELETION_STATUS,
action = {} ) {
const { type, success, message } = action const { type, success, message } = action
switch (type) { switch (type) {
@ -222,7 +209,8 @@ function kitsQueryStatus(
isSubmitting: false, isSubmitting: false,
message: '' message: ''
}, },
action = {}) { action = {}
) {
const { type, success, message } = action const { type, success, message } = action
switch (type) { switch (type) {
@ -327,8 +315,8 @@ function unregisteredClientDetectionStatus(
recentConflictsDetected: {}, recentConflictsDetected: {},
message: '' message: ''
}, },
action = {}) { action = {}
) {
const { type, success, message, unregisteredClientsBeforeDetection, recentConflictsDetected } = action const { type, success, message, unregisteredClientsBeforeDetection, recentConflictsDetected } = action
switch (type) { switch (type) {
@ -358,8 +346,8 @@ function conflictDetectionScannerStatus(
success: false, success: false,
message: '' message: ''
}, },
action = {}) { action = {}
) {
const { type, success, message } = action const { type, success, message } = action
switch (type) { switch (type) {
@ -374,37 +362,6 @@ 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 = {}) { function showConflictDetectionReporter(state = false, action = {}) {
const { type } = action const { type } = action
@ -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({ export default combineReducers({
activeAdminTab, activeAdminTab,
apiNonce: simple, apiNonce: simple,
apiUrl: simple, apiUrl: simple,
faApiUrl: simple,
blocklistUpdateStatus, blocklistUpdateStatus,
clientPreferences: coerceEmptyArrayToEmptyObject, clientPreferences: coerceEmptyArrayToEmptyObject,
conflictDetectionScannerStatus, conflictDetectionScannerStatus,
@ -472,7 +432,6 @@ export default combineReducers({
rootUrl: simple, rootUrl: simple,
mainCdnAssetUrl: simple, mainCdnAssetUrl: simple,
mainCdnAssetIntegrity: simple, mainCdnAssetIntegrity: simple,
enableIconChooser: coerceBool,
releases: simple, releases: simple,
settingsPageUrl: simple, settingsPageUrl: simple,
showAdmin: coerceBool, showAdmin: coerceBool,
@ -481,9 +440,5 @@ export default combineReducers({
unregisteredClients, unregisteredClients,
unregisteredClientsDeletionStatus, unregisteredClientsDeletionStatus,
userAttemptedToStopScanner, userAttemptedToStopScanner,
v3DeprecationWarning, webpackPublicPath: simple
v3DeprecationWarningStatus,
webpackPublicPath: simple,
isGutenbergPage: coerceBool,
usingCompatJs: coerceBool
}) })

View File

@ -21,7 +21,7 @@ describe('options', () => {
usePro: true, usePro: true,
compat: false, compat: false,
pseudoElements: true, pseudoElements: true,
version: '5.11.2', version: '5.11.2'
} }
} }

View File

@ -6,11 +6,11 @@ export async function resetOptions(page) {
options: { options: {
usePro: false, usePro: false,
compat: true, compat: true,
technology:"webfont", technology: 'webfont',
pseudoElements: true, pseudoElements: true,
kitToken: null, kitToken: null,
apiToken: true, apiToken: true,
version:"6.0.0-beta3" version: '6.0.0-beta3'
} }
} }

View File

@ -1,21 +1,40 @@
import get from 'lodash/get' import { get, set, size } from 'lodash'
import set from 'lodash/set'
import size from 'lodash/size'
import { __ } from '@wordpress/i18n' import { __ } from '@wordpress/i18n'
export const ERROR_REPORT_PREAMBLE = __('Font Awesome WordPress Plugin Error Report', '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 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 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_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 REST_COOKIE_INVALID_NONCE_ERROR = __(
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' ) '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 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 FALSE_POSITIVE_MESSAGE = __(
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') "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.",
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') '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 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 EXPECTED_EMPTY_MESSAGE = __(
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' ) '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 = [ const REPORT_INFO_PARAM_KEYS = [
'requestMethod', 'requestMethod',
'responseStatus', 'responseStatus',
@ -97,7 +116,7 @@ function handleSingleWpErrorOutput( wpError ) {
} }
function handleAllWpErrorOutput(errorData = {}) { 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 // get the first error message available for this code
const message = get(errorData, `errors.${code}.0`) const message = get(errorData, `errors.${code}.0`)
const data = get(errorData, `error_data.${code}`) const data = get(errorData, `error_data.${code}`)
@ -125,23 +144,14 @@ function handleAllWpErrorOutput(errorData = {}) {
// The uiMessage we should return will be the first error message that isn't // The uiMessage we should return will be the first error message that isn't
// from a 'previous_exception' // from a 'previous_exception'
return (!acc && error.code !== 'previous_exception') return !acc && error.code !== 'previous_exception' ? msg : acc
? msg
: acc
}, null) }, null)
return uiMessage return uiMessage
} }
function report(params) { function report(params) {
const { const { error = null, ok = false, falsePositive = false, confirmed = false, expectEmpty = false, trimmed = '' } = params
error = null,
ok = false,
falsePositive = false,
confirmed = false,
expectEmpty = false,
trimmed = ''
} = params
console.group(ERROR_REPORT_PREAMBLE) console.group(ERROR_REPORT_PREAMBLE)
@ -195,9 +205,7 @@ function report(params) {
console.groupEnd() console.groupEnd()
} }
const uiMessage = null !== error const uiMessage = null !== error ? handleAllWpErrorOutput(error) : null
? handleAllWpErrorOutput( error )
: null
if (error && trimmed === '' && confirmed) { if (error && trimmed === '' && confirmed) {
console.info(MISSING_ERROR_DATA_MESSAGE) console.info(MISSING_ERROR_DATA_MESSAGE)

View File

@ -6,12 +6,13 @@ console.info = jest.fn()
const SINGLE_EXCEPTION_ERROR = { const SINGLE_EXCEPTION_ERROR = {
errors: { 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: { error_data: {
fontawesome_client_exception: { fontawesome_client_exception: {
status: 400, 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}" 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', () => { describe('with single fontawesome_client_exception', () => {
test('emits console report and returns uiMessage from given error', () => { test('emits console report and returns uiMessage from given error', () => {
const message = reportRequestError({ error: SINGLE_EXCEPTION_ERROR }) const message = reportRequestError({ error: SINGLE_EXCEPTION_ERROR })
@ -41,20 +41,12 @@ describe('reportRequestError', () => {
expect(console.group).toHaveBeenCalledTimes(2) expect(console.group).toHaveBeenCalledTimes(2)
expect(console.info).toHaveBeenCalled() expect(console.info).toHaveBeenCalled()
expect(console.info).toHaveBeenCalledWith( expect(console.info).toHaveBeenCalledWith(expect.stringMatching(/message: Whoops/))
expect.stringMatching(/message: Whoops/),
)
expect(console.info).toHaveBeenCalledWith( expect(console.info).toHaveBeenCalledWith(expect.stringMatching(/trace:/))
expect.stringMatching(/trace:/)
)
expect(console.info).toHaveBeenCalledWith( expect(console.info).toHaveBeenCalledWith(expect.stringMatching(/status:/))
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.groupEnd).toHaveBeenCalledTimes(2) expect(console.groupEnd).toHaveBeenCalledTimes(2)
}) })
@ -63,17 +55,19 @@ describe('reportRequestError', () => {
describe('when PreferenceRegistrationException is thrown with a previous exception', () => { describe('when PreferenceRegistrationException is thrown with a previous exception', () => {
const error = { const error = {
errors: { errors: {
fontawesome_server_exception: ["A theme or plugin registered with Font Awesome threw an exception."], fontawesome_server_exception: ['A theme or plugin registered with Font Awesome threw an exception.'],
previous_exception: ["epsilon-plugin throwing"] previous_exception: ['epsilon-plugin throwing']
}, },
error_data: { error_data: {
fontawesome_server_exception: { fontawesome_server_exception: {
status: 500, 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}" 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: { previous_exception: {
status: 500, 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}" 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.group).toHaveBeenCalledTimes(3)
expect(console.groupEnd).toHaveBeenCalledTimes(3) expect(console.groupEnd).toHaveBeenCalledTimes(3)
expect(console.info).toHaveBeenCalledWith( expect(console.info).toHaveBeenCalledWith(expect.stringMatching(/code: fontawesome_server_exception/))
expect.stringMatching(/code: fontawesome_server_exception/)
)
expect(console.info).toHaveBeenCalledWith( expect(console.info).toHaveBeenCalledWith(expect.stringMatching(/code: previous_exception/))
expect.stringMatching(/code: previous_exception/)
)
expect(console.info).toHaveBeenCalledWith( expect(console.info).toHaveBeenCalledWith(expect.stringMatching(/The last request was successful/))
expect.stringMatching(/The last request was successful/)
)
expect(message).toMatch(/^A theme or plugin/) expect(message).toMatch(/^A theme or plugin/)
}) })
@ -114,48 +102,28 @@ describe('reportRequestError', () => {
expect(message).toMatch(/^Whoops/) expect(message).toMatch(/^Whoops/)
// The top-level group, and then one error group, then one for the trimmed content // The top-level group, and then one error group, then one for the trimmed content
expect(console.group).toHaveBeenCalledTimes(3) expect(console.group).toHaveBeenCalledTimes(3)
expect(console.group).toHaveBeenCalledWith( expect(console.group).toHaveBeenCalledWith(expect.stringMatching(/Error Report/))
expect.stringMatching(/Error Report/), expect(console.group).toHaveBeenCalledWith(expect.stringMatching(/Trimmed/))
)
expect(console.group).toHaveBeenCalledWith(
expect.stringMatching(/Trimmed/),
)
expect(console.groupEnd).toHaveBeenCalledTimes(3) expect(console.groupEnd).toHaveBeenCalledTimes(3)
expect(console.info).toHaveBeenCalledTimes(4) expect(console.info).toHaveBeenCalledTimes(4)
expect(console.info).toHaveBeenCalledWith( expect(console.info).toHaveBeenCalledWith(expect.stringMatching(/reported it as a success/))
expect.stringMatching(/reported it as a success/),
)
expect(console.info).toHaveBeenCalledWith( expect(console.info).toHaveBeenCalledWith(expect.stringMatching(/This is a clue/))
expect.stringMatching(/This is a clue/),
)
expect(console.info).toHaveBeenCalledWith( expect(console.info).toHaveBeenCalledWith(expect.stringMatching(/message: Whoops/))
expect.stringMatching(/message: Whoops/),
)
expect(console.info).toHaveBeenCalledWith( expect(console.info).toHaveBeenCalledWith(expect.stringMatching(/message: Whoops/))
expect.stringMatching(/message: Whoops/),
)
expect(console.info).toHaveBeenCalledWith( expect(console.info).toHaveBeenCalledWith(expect.stringMatching(/trace:/))
expect.stringMatching(/trace:/)
)
expect(console.info).toHaveBeenCalledWith( expect(console.info).toHaveBeenCalledWith(expect.stringMatching(/status:/))
expect.stringMatching(/status:/),
)
expect(console.info).toHaveBeenCalledWith( expect(console.info).toHaveBeenCalledWith(expect.stringMatching(/code: fontawesome_client_exception/))
expect.stringMatching(/code: fontawesome_client_exception/)
)
expect(console.info).toHaveBeenCalledWith( expect(console.info).toHaveBeenCalledWith(expect.stringContaining(TRIMMED))
expect.stringContaining(TRIMMED)
)
}) })
}) })
@ -172,21 +140,15 @@ describe('reportRequestError', () => {
expect(message).toBeNull() expect(message).toBeNull()
// The top-level group, and then one for the trimmed content // The top-level group, and then one for the trimmed content
expect(console.group).toHaveBeenCalledTimes(2) expect(console.group).toHaveBeenCalledTimes(2)
expect(console.group).toHaveBeenCalledWith( expect(console.group).toHaveBeenCalledWith(expect.stringMatching(/Trimmed/))
expect.stringMatching(/Trimmed/),
)
expect(console.groupEnd).toHaveBeenCalledTimes(2) expect(console.groupEnd).toHaveBeenCalledTimes(2)
expect(console.info).toHaveBeenCalled() expect(console.info).toHaveBeenCalled()
expect(console.info).toHaveBeenCalledWith( expect(console.info).toHaveBeenCalledWith(expect.stringMatching(/contain no data/))
expect.stringMatching(/contain no data/),
)
expect(console.info).toHaveBeenCalledWith( expect(console.info).toHaveBeenCalledWith(expect.stringContaining(TRIMMED))
expect.stringContaining(TRIMMED),
)
}) })
}) })
@ -203,9 +165,7 @@ describe('reportRequestError', () => {
expect(console.info).toHaveBeenCalledTimes(1) expect(console.info).toHaveBeenCalledTimes(1)
expect(console.info).toHaveBeenCalledWith( expect(console.info).toHaveBeenCalledWith(expect.stringMatching(/did not include the confirmation header/))
expect.stringMatching(/did not include the confirmation header/),
)
}) })
}) })
@ -257,9 +217,7 @@ describe('reportRequestError', () => {
expect(console.info).toHaveBeenCalled() expect(console.info).toHaveBeenCalled()
expect(console.info).toHaveBeenCalledWith( expect(console.info).toHaveBeenCalledWith(expect.stringMatching(/failure console message/))
expect.stringMatching(/failure console message/)
)
}) })
}) })
}) })
@ -273,7 +231,7 @@ describe('redactRequestData', () => {
'Content-Type': 'application/json' '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' } }))
@ -288,7 +246,7 @@ describe('redactRequestData', () => {
'Content-Type': 'application/json' '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 } }))
@ -303,7 +261,7 @@ describe('redactRequestData', () => {
'Content-Type': 'text/plain' '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 } }))
@ -313,14 +271,20 @@ describe('redactRequestData', () => {
describe('redactHeaders', () => { describe('redactHeaders', () => {
test('when x-wp-nonce is present', () => { test('when x-wp-nonce is present', () => {
expect(redactHeaders({ expect(
redactHeaders({
'X-WP-NONCE': 'abc123' 'X-WP-NONCE': 'abc123'
})).toEqual({'X-WP-NONCE': 'REDACTED'}) })
expect(redactHeaders({ ).toEqual({ 'X-WP-NONCE': 'REDACTED' })
expect(
redactHeaders({
'x-wp-nonce': 'abc123' 'x-wp-nonce': 'abc123'
})).toEqual({'x-wp-nonce': 'REDACTED'}) })
expect(redactHeaders({ ).toEqual({ 'x-wp-nonce': 'REDACTED' })
expect(
redactHeaders({
'X-WP-Nonce': 'abc123' 'X-WP-Nonce': 'abc123'
})).toEqual({'X-WP-Nonce': 'REDACTED'}) })
).toEqual({ 'X-WP-Nonce': 'REDACTED' })
}) })
}) })

View File

@ -22,9 +22,7 @@ function findJson( content, start = 0 ) {
} else { } else {
if (-1 !== nextLeftBracket && -1 !== nextLeftBrace) { if (-1 !== nextLeftBracket && -1 !== nextLeftBrace) {
// if we found both, take the lower one // if we found both, take the lower one
nextStart = nextLeftBracket < nextLeftBrace nextStart = nextLeftBracket < nextLeftBrace ? nextLeftBracket : nextLeftBrace
? nextLeftBracket
: nextLeftBrace
} else if (-1 !== nextLeftBrace) { } else if (-1 !== nextLeftBrace) {
nextStart = nextLeftBrace nextStart = nextLeftBrace
} else { } else {

View File

@ -5,12 +5,14 @@ describe('sliceJson', () => {
const json = '{"alpha":42}' const json = '{"alpha":42}'
const result = sliceJson(json) const result = sliceJson(json)
expect(result).toEqual( expect.objectContaining({ expect(result).toEqual(
expect.objectContaining({
start: 0, start: 0,
json, json,
trimmed: '', trimmed: '',
parsed: expect.objectContaining(JSON.parse(json)) parsed: expect.objectContaining(JSON.parse(json))
})) })
)
}) })
test('when invalid content precedes json object', () => { test('when invalid content precedes json object', () => {
@ -19,12 +21,14 @@ describe('sliceJson', () => {
const content = `${trimmed}${json}` const content = `${trimmed}${json}`
const result = sliceJson(content) const result = sliceJson(content)
expect(result).toEqual( expect.objectContaining({ expect(result).toEqual(
expect.objectContaining({
start: 7, start: 7,
json, json,
trimmed, trimmed,
parsed: expect.objectContaining(JSON.parse(json)) parsed: expect.objectContaining(JSON.parse(json))
})) })
)
}) })
test('when invalid content precedes json array', () => { test('when invalid content precedes json array', () => {
@ -33,12 +37,14 @@ describe('sliceJson', () => {
const content = `${trimmed}${json}` const content = `${trimmed}${json}`
const result = sliceJson(content) const result = sliceJson(content)
expect(result).toEqual( expect.objectContaining({ expect(result).toEqual(
expect.objectContaining({
start: 7, start: 7,
json, json,
trimmed, trimmed,
parsed: expect.objectContaining(JSON.parse(json)) parsed: expect.objectContaining(JSON.parse(json))
})) })
)
}) })
test('when invalid content with brackets and braces precedes json object', () => { test('when invalid content with brackets and braces precedes json object', () => {
@ -47,12 +53,14 @@ describe('sliceJson', () => {
const content = `${trimmed}${json}` const content = `${trimmed}${json}`
const result = sliceJson(content) const result = sliceJson(content)
expect(result).toEqual( expect.objectContaining({ expect(result).toEqual(
expect.objectContaining({
start: 20, start: 20,
json, json,
trimmed, trimmed,
parsed: expect.objectContaining(JSON.parse(json)) parsed: expect.objectContaining(JSON.parse(json))
})) })
)
}) })
test('when invalid content with brackets and braces precedes json array', () => { test('when invalid content with brackets and braces precedes json array', () => {
@ -61,12 +69,14 @@ describe('sliceJson', () => {
const content = `${trimmed}${json}` const content = `${trimmed}${json}`
const result = sliceJson(content) const result = sliceJson(content)
expect(result).toEqual( expect.objectContaining({ expect(result).toEqual(
expect.objectContaining({
start: 20, start: 20,
json, json,
trimmed, trimmed,
parsed: expect.objectContaining(JSON.parse(json)) parsed: expect.objectContaining(JSON.parse(json))
})) })
)
}) })
test('when invalid content comes before and after valid json', () => { test('when invalid content comes before and after valid json', () => {

View File

@ -1,17 +1,15 @@
const defaultConfig = require("@wordpress/scripts/config/webpack.config"); const defaultConfig = require('@wordpress/scripts/config/webpack.config')
const get = require("lodash/get"); const get = require('lodash/get')
const { basename, dirname } = require("path"); const { basename, dirname } = require('path')
/** /**
* There have apparently been problems with webpack chunk loading and caches. * There have apparently been problems with webpack chunk loading and caches.
* So we should add hashes to the chunk file names. * So we should add hashes to the chunk file names.
* See: https://wordpress.org/support/topic/plugin-settings-page-is-empty-2/#post-14855904 * See: https://wordpress.org/support/topic/plugin-settings-page-is-empty-2/#post-14855904
*/ */
const miniCssExtractPlugin = defaultConfig.plugins.find((p) => const miniCssExtractPlugin = defaultConfig.plugins.find((p) => 'MiniCssExtractPlugin' === p.constructor.name)
"MiniCssExtractPlugin" === p.constructor.name miniCssExtractPlugin.options.filename = '[name]-[contenthash].css'
); miniCssExtractPlugin.options.chunkFilename = '[name]-[chunkhash].css'
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 // After updating to @wordpress/scripts wp-6.5, when using --webpack-no-externals
// there was an error: // 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 // Using the empty string here instead of null seems to result in that asset still
// loading as expected and styling everything as expected. // loading as expected and styling everything as expected.
defaultConfig.optimization.splitChunks.cacheGroups.style.name = ( defaultConfig.optimization.splitChunks.cacheGroups.style.name = (_, chunks, cacheGroupKey) => {
_, const chunkName = chunks[0].name || ''
chunks, return `${dirname(chunkName)}/${cacheGroupKey}-${basename(chunkName)}`
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, // This causes the JavaScript chunks to include the chunk hash in the filename,
// which is important for cache busting purposes. // which is important for cache busting purposes.
defaultConfig.output.chunkFilename = "[name]-[chunkhash].js"; defaultConfig.output.chunkFilename = '[name]-[chunkhash].js'
defaultConfig.externals = [ module.exports = defaultConfig
/**
* 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;

BIN
assets/screenshot-1.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

BIN
assets/screenshot-2.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 995 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 139 KiB

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