Compare commits

...

49 Commits
4.4.0 ... main

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
Mike Wilkerson a872f419c6 Release 4.5.0 2024-06-10 11:54:37 -07:00
Mike Wilkerson 10623f82e2 update package-lock.json files for production build 2024-06-07 12:19:27 -07:00
Mike Wilkerson 485bf919ed doc fixes 2024-06-07 12:14:01 -07:00
Mike Wilkerson 389cf41a25 update readme changelog and update version numbers 2024-06-07 11:19:41 -07:00
Mike Wilkerson e4fad93844
Remove obsolete fa-icon-chooser slots (#219)
* remove obsolete content slots for fa-icon-chooser

* rebuild admin bundle
2024-06-07 11:18:44 -07:00
Mike Wilkerson 235987623d
Maintenance: updating deps, icon chooser, and accommodating security policies (#218)
* WIP: replacing redux with @wordpress/data

* switch to using POST instead of PUT for the /config route and remove whitespace in GQL query

* use the real createInterpolateElement

* update many deps

* Revert "WIP: replacing redux with @wordpress/data"

This reverts commit 3fe029ab90.

* update redux-thunk import

* fix api query REST route to use application/json content-type

* re-build with new deps and webpack config

* fix up some actions tests that now use POST

* update query handling to allow for JSON documents

* update yost/phpunit-polyfills dep

* update and test query API to accept associative array with variables

* make resetAxiosMocks do more resetting

* WIP: playwright config and setup

* cleanup the proKit setup

* finish the new playwright test for block editing with icon chooser

* remove obsolete block editor e2e test

* update test description

* formatting

* gitignore some playright artifacts

* rebuild JS bundle

* bring back the compatibility handling for createInterpolateElement

* go back to only importing the current version of createInterpolateElement

* re-enable building with --webpack-no-externals

* rebuild production admin bundle

* back to using compat form of createInterpolateElement

* add comment

* drop the react-redux dep version for compatibility

* bring back the handling of custom externals

* rebuild admin bundle

* update Dockerfile and scripts to enable mod_security with OWASP

* config for mod_security with OWASP core ruleset and conditionally allowing PUT requests

* make mod_security more configurable when starting a container

* add e2e test for conflict scanning and blocking

* Change the HTTP verb for conflict detection from PUT to POST

* rebuild admin bundle

* change HTTP verb for v3deprecation snoozer from PUT to POST

* rebuild admin bundle

* remove out of date react tests

* add test to ensure that browser globals are not overriden

* WIP: adding a container for wp 6.0

* update e2e test for WP 6.0

* wire up dev environment for WP 6.0

* update php8.2 composer deps

* update the WP 5.4 docker image build

* change e2e test reset logic to not use REST API

older WP 5.4 does not have the required REST API routes

* rework docker config for wp5.4

...and start moving away from having separate db services for each version of wp

* Revert "change e2e test reset logic to not use REST API"

This reverts commit 8d8f698943.

* refactor e2e test setup

* change default db host

* update config controller tests to change PUT to POST

* WIP: making test-enqueue more readable

* WIP: refactor and correct test-enqueue

* more refactoring and fixing of EnqueueTest

* auto-formatting

* rename file

* fix for phpcs

* auto-formatting

* re-organize e2e tests and add one for apiEndpoint compat

* update deps for fa-icon-chooser

* rebuild admin bundle

* remove docsrv and update DEVELOPMENT.md about previewing docs

* update php test workflow

* add playwright test run in CI

* rename javascript workflow

* change workflow name

* add separate playwright workflow

* Revert "rename javascript workflow"

This reverts commit 9dbadb4f6b.

* remove playwright from Jest workflow

* add composer files for php7.4

* remove playwright workflow

* update composer-php7.4.lock

* add Dockerfile for php7.4

* Change docker compose config for php7.4 and WP 4.7
- add a service that uses php4.7
- change the wp 4.7 service to just use the standard wpdb

* update composer.lock

* update composer configs for php8.3

* set up docker config for php8.3

* remove composer files for php8.2

* update composer lock for php8.1

* temporarily reduce number of php versions in test matrix

* remove docker compose config for php8.2 and update php8.1

* fix require path for match-result.php

* update composer.lock for php 8.0

* update composer lock for php7.3

* update composer lock for php7.2

* update composer lock for php7.1

* re-enable all versions of php in test matrix

* update compat-js bundle deps and config

* update docker config for wp 5.0

* migrate the changeTechnology e2e test to playwright

* add e2e test for inserting via icon chooser in full site editor

* update DEVELOPMENT.md about playwright tests and mod_security

* update fullSiteEditor e2e test to work on subsequent runs after clearing the welcome guide

* update env.js to load .env and .env.local to use override mode

* update DEVELOPMENT.md

* use env var for kit token

* update DEVELOPMENT.md about env vars for kit and api tokens
2024-06-07 10:43:26 -07:00
Mike Wilkerson b9d5e0d5a8
fix query API doc link in README.md 2024-05-24 14:17:32 -07:00
Mike Wilkerson 82c03f0424
Enable icon chooser on full site editor (#211)
* update Dockerfile-latest to use the new node 20.x installation method

* enable icon chooser on site-editor.php screen
2023-11-13 11:10:44 -08:00
Mike Wilkerson 25011279c7 remove duplicate php8.1 section from docker-compose.yml 2023-09-19 13:53:52 -07:00
Mike Wilkerson ab6600dbd1 add development note about testing against a WordPress release candidate 2023-08-03 11:19:40 -07:00
Mike Wilkerson 5894dec4b8 tested up to 6.3 2023-08-03 11:11:51 -07:00
300 changed files with 103440 additions and 112174 deletions

11
.env
View File

@ -15,3 +15,14 @@ WORDPRESS_DEBUG=true
WORDPRESS_DEBUG_LOG=true
FONTAWESOME_ENV=development
MYSQL_ROOT_PASSWORD=somewordpress
# This should be false by default.
# It could be overridden in .env.local.
# This pertains to the mod_security OWASP core rule set.
ENABLE_MOD_SECURITY=false
# ENABLE_MOD_SECURITY=true, then the following
# may be set to "true" to make exceptions to the usual mod_security rules,
# allowing any requests on the font-awesome REST API routes that would
# otherwise be blocked by the core rule set.
#
# ALLOW_ALL_REQUESTS_FOR_FONT_AWESOME=true

View File

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

View File

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

7
.gitignore vendored
View File

@ -17,3 +17,10 @@ tmp/
webpack-stats.html
webpack-stats.json
.phpunit.result.cache
admin/src/playwright/.auth/
admin/artifacts/
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"/>
<file>includes</file>
<file>tests</file>
<!-- TODO: maybe re-enable this. It's crashing phpcbf. -->
<!--
<file>font-awesome.php</file>
-->
<file>font-awesome-init.php</file>
<file>block-editor/font-awesome-icon-block-init.php</file>
<file>index.php</file>
<file>v3shims.php</file>
<file>defines.php</file>
<file>admin/index.php</file>
<file>admin/views/main.php</file>

6
.rgignore Normal file
View File

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

View File

@ -9,6 +9,7 @@
- [Optional Development Setup Steps](#optional-development-setup-steps)
- [Run tests with phpunit](#run-tests-with-phpunit)
* [Pass arguments to phpunit](#pass-arguments-to-phpunit)
- [Run end-to-end tests with playwright](#run-end-to-end-tests-with-playwright)
- [Use wp-cli within your Docker environment](#use-wp-cli-within-your-docker-environment)
- [Run anything else within your Docker environment](#run-anything-else-within-your-docker-environment)
* [Run a shell insider your Docker environment](#run-a-shell-inside-your-docker-environment)
@ -18,12 +19,14 @@
* [Main Options](#main-options)
* [Releases Metadata Transient](#releases-metadata-transient)
* [V3 Deprecation Warning](#v3-deprecation-warning)
- [Managing web security rules](#managing-web-security-rules)
- [Cut a Release](#cut-a-release)
- [Run a Local Docs Server](#run-a-local-docs-server)
- [Special Notes on plugin-sigma](#special-notes-on-plugin-sigma)
- [Remote Debugging with VSCode](#remote-debugging-with-vscode)
- [Redis Cache Setup](#redis-cache-setup)
- [Analyze Webpack Bundle](#analyze-webpack-bundle)
- [Test Against a WordPress Release Candidate](#test-against-a-wordpress-release-candidate)
<!-- tocstop -->
@ -38,7 +41,7 @@ to install any development dependencies inside the container.
The `integration` option does _not_ mount this plugin code in the container. Instead, it expects you
to install the plugin. You could do so by uploading a zip file on the Add New Plugin page in
WordPress admin: build a zip using `composer dist` or by downloading one from the [plugin's WordPress
WordPress admin: build a zip using the release steps below or by downloading one from the [plugin's WordPress
plugin directory entry](https://wordpress.org/plugins/font-awesome/). Or you could install directly
from the WordPress plugin directory by searching for plugins by author "fontawesome".
@ -453,27 +456,6 @@ brew install composer
```
</details>
<details>
<summary>The WordPress 4 compat bundle</summary>
For older versions of WordPress, we build and load this separate "compat-js" bundle. It includes some WordPress dependencies that are available at runtime on newer versions of WordPress, but which this plugin provides itself when it detects that
it's being loaded on older versions of WordPress.
This bundle will probably not change very much, so it may not be necessary to rebuild at all.
If you're doing development work only in WordPress 5, you can skip this altogether.
If you do need to update what's in this bundle, though, then you just build another
static production build like this:
```
$ cd compat-js
$ npm install
$ npm run build
```
This will create `compat-js/build/compat.js`, which the plugin looks for and
enqueues automatically when it detects that it's running under WordPress 4.
</details>
<details>
<summary>If you have an older version of Docker or one that doesn't support host.docker.internal</summary>
@ -604,10 +586,57 @@ When specifying a container with `-c`, to add additional command-line arguments,
bin/phpunit -c com.fontawesome.wordpress-php7.1-dev -- --filter EnqueueTest
```
Everything before the `--` are the options do the `bin/phpunit` script, and everything after the `--` are what get passed through
Everything before the `--` are the options do the `bin/phpunit` script, and everything after the `--` are what get passed through
to the `phpunit` command inside the container.
</details>
# Run end-to-end tests with playwright
## Add tokens to `.env.local`
```
API_TOKEN=YOUR_FA_API_TOKEN
KIT_TOKEN=YOUR_KIT_TOKEN
```
To run the end-to-end tests, you must have the WordPress environment running.
For example, from the top-level directory, run this:
```bash
bin/dev
```
Leave that running in one terminal and do the following in a separate terminal.
Playwright must be also installed when initializing a local dev environment:
```bash
cd admin
npx playwright install --with-deps
```
Then, still in the `admin` directory, run tests on the terminal:
```bash
npx playwright test
```
Or run the tests in the Playwright UI:
```bash
npx playwright test --ui
```
Or in debug mode:
```bash
npx playwright test --debug
```
See also [Playwright docs](https://playwright.dev/docs/intro).
## WordPress Version Caveat
The end-to-end tests may use features of WordPress that are not present in older versions, so their
use on older versions may be limited. But within those limits, at least some of them are useful for
running against older versions of WordPress to ensure compatibility.
# Use WP-CLI within your Docker environment
For example,
@ -730,6 +759,36 @@ Remove it:
$ bin/wp transient delete font-awesome-v3-deprecation-data
```
# Managing web security rules
For the `latest` docker image, the latest release of the [OWASP core ruleset](https://coreruleset.org/) is installed by default,
but _not_ enabled by default. This simulates what are probably common Web Application Firewall configurations for WordPress hosting providers.
By default, it merely audits. See the log in `/var/log/apache2/modsec_audit.log`.
To enable filtering--actually rejecting requests that exceed the rules' tolerances--edit your `.env.local`:
```
ENABLE_MOD_SECURITY=true
```
Note that this env var setting must be present in the environment when the docker container is created.
So if you've already started a container, you'll need to stop and remove it, then change this env var,
then start it back up.
You can watch the terminal where `apache2` is launched in the container. When `mod_security` is not enabled,
it'll look like this:
```
'apache2 -D FOREGROUND -D DEVELOPMENT'
```
When `mod_security` is enabled, it'll look like this:
```
'apache2 -D FOREGROUND -D DEVELOPMENT -D EnableModSecurity'
```
# Cut a Release
## Running composer commands for the release
@ -740,7 +799,7 @@ $ bin/wp transient delete font-awesome-v3-deprecation-data
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.
@ -765,7 +824,7 @@ $ bin/wp transient delete font-awesome-v3-deprecation-data
- `git add docs` to stage them for commit (and eventually commit them)
7. Build production admin app and WordPress distribution layout into `wp-dist`
7. Build production distribution archive.
```bash
bin/composer dist
@ -776,20 +835,7 @@ the default dev `latest` container, which you should be running via `bin/dev`.
This will cause everything to be built inside the container, which will hopefully
keep the built assets more consistent, regardless of the host environment.)
This will delete the previous build assets and produce the following:
`admin/build`: production build of the admin UI React app. This needs to be committed, so that it
can be included in the composer package (which is really just a pull of this repo)
`compat-js/build`: production build of the compatibility JS bundled. This also needs to be committed.
8. Build the zip file
```bash
bin/make-wp-dist-zip
```
This builds the following:
This will delete the previous build assets and produce:
`wp-dist/`: the contents of this directory contains everything that will be used in
subsequent steps to both build an installable zip file, and to copy into the
@ -804,13 +850,7 @@ a GitHub release.
9. Run through some manual acceptance testing
**WordPress 4.7, 4.8, 4.9**
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
```
**WordPress 6.0**
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.
@ -819,31 +859,16 @@ Run through the following, with the JavaScript console open, looking for any war
1. Load the plugin settings page.
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. 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. Expect to see a compatibility warning that the Icon Chooser is not enabled,
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. Add an icon block to a post
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**
@ -1062,7 +1087,7 @@ $ cd ..
10. Copy plugin directory assets and wp-dist layout into `wp-svn/trunk`
```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
@ -1120,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).
13. 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.
14. Create the new svn release tag
13. 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
snapshot of it for the release tag.
@ -1139,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
```
```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`
We've now got three copies of `readme.txt` that should all be updated with new tag values:
@ -1180,15 +1209,11 @@ If you want to preview the built docs with a web server, first build the docs:
bin/phpdoc
```
Then go into the `docsrv` directory and run the doc server:
Then go into the `docs` directory and run:
```
cd docsrv
npm install
node index.js
npx serve
```
Point a web browser at `http://localhost:3000`.
# Special Notes on plugin-sigma
`plugin-sigma` demonstrates how a third-party plugin developer could include this Font Awesome plugin as a composer
@ -1271,7 +1296,7 @@ wp --allow-root core update --version=5.4 /tmp/wordpress-5.4-latest.zip
# Analyze Webpack Bundle
The webpack configs for both `admin/` and `compat-js/` include the `BundleAnalyzerPlugin`,
The webpack configs for the `admin/` JavaScript bundle includes the `BundleAnalyzerPlugin`,
which produces a corresponding `webpack-stats.html` file in the corresponding
directory on each build.
@ -1289,3 +1314,13 @@ See [guide here](https://wordpress.org/support/article/create-a-network/)
```
wp --allow-root eval 'require_once "wp-content/plugins/font-awesome/includes/class-fontawesome-deactivator.php"; use FortAwesome\FontAwesome_Deactivator; define("WP_NETWORK_ADMIN", true); FontAwesome_Deactivator::uninstall();'
```
# Test Against a WordPress Release Candidate
Probably the easiest is to just use `bin/dev` to run the currently-latest release,
and then use `bin/env /bin/bash` to shell into that container and use the WP CLI
to update the release.
For example, when WordPress was at `RC3` for version 6.3, this installed as expected:
`wp --allow-root core update --version=6.3-RC3`

View File

@ -733,14 +733,14 @@ Once the site owner enables Pro, `fa()->pro()` will be `true` and your code can
then rely on the presence of Font Awesome Pro for the version indicated by
`fa()->version()`.
(See the [PHP API docs](https://fortawesome.github.io/wordpress-fontawesome/index.html) for how to resolve the symbolic
(See the [PHP API docs](https://fortawesome.github.io/wordpress-fontawesome/) for how to resolve the symbolic
`"latest"` version as a concrete version like `"5.12.0"`.)
# Query the Font Awesome GraphQL API
The Font Awesome [GraphQL API](https://fontawesome.com/docs/apis/graphql/get-started) allows you to query and search icon metadata.
See also documentation in PHP API on the [`FontAwesome::query()`](https://fortawesome.github.io/wordpress-fontawesome/classes/FortAwesome.FontAwesome.html#method_query) method.
See also documentation in PHP API on the [`FontAwesome::query()`](https://fortawesome.github.io/wordpress-fontawesome/) method.
## public scope queries on api.fontawesome.com
@ -755,7 +755,8 @@ fetch(
'https://api.fontawesome.com',
{
method: 'POST',
body: 'query { release(version:"5.12.0") { icons { id } } }'
headers: {'Content-Type': 'application/json'},
body: '{"query": "query Icons($ver: String!) { release(version:$ver) { icons { id } } }", "variables": { "ver": "6.x" } }'
}
)
.then(response => response.ok ? response.json() : null)

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,278 +0,0 @@
.Ihb3kyONjVgEWV0zB0vfxQ\=\= {
margin-right: 20px;
padding: 1rem 2rem 2rem 2rem;
background-color: #fff;
}
@media only screen and (min-width: 1024px) {
.Ihb3kyONjVgEWV0zB0vfxQ\=\= {
max-width: 1000px;
}
}
.ToMOGjAmxSPs6D\+glWGx9A\=\= {
display: flex;
align-items: center;
}
._3edKMOIt9iftk0Zz-7ia9w\=\= {
padding-left: 1em;
}
div._3edKMOIt9iftk0Zz-7ia9w\=\=.NbqVQtCU5W3ihwasrYexfA\=\= {
display: flex;
margin: 1em;
background-color: #fda09a;
border-radius: 5px;
max-width: 450px;
padding: 1em;
}
div._3edKMOIt9iftk0Zz-7ia9w\=\=.NbqVQtCU5W3ihwasrYexfA\=\= .gEl1fl\+2lk74ueiQz5cPxQ\=\= {
margin-top: auto;
margin-bottom: auto;
}
div._3edKMOIt9iftk0Zz-7ia9w\=\=.NbqVQtCU5W3ihwasrYexfA\=\= .vumGDcuTrv0Ekcc\+McKiXw\=\= {
max-width: 400px;
}
._3edKMOIt9iftk0Zz-7ia9w\=\= .vumGDcuTrv0Ekcc\+McKiXw\=\= {
padding-left: 1em;
}
._3edKMOIt9iftk0Zz-7ia9w\=\=._7IGgJmOfwN1O\+c0smUgA9Q\=\= .b1NRGX9AXkY1BfJ1MCfPTw\=\= {
color: green;
}
h2._3hBTOhCdvQjibQScAbzMIQ\=\= {
font-size: 18px;
}
h3._3hBTOhCdvQjibQScAbzMIQ\=\= {
font-size: 16px;
}
._0XZF4B-SzNg4vm8Dl0F7TA\=\= {
margin-top: 1rem;
margin-bottom: 1rem;
}
.YGhcFGkqFAeqpY9iNYq9Sw\=\= th {
font-weight: bold;
}
button.mQ\+dGgo7ePYMdFVM7UJy3Q\=\= {
border: 0;
background: none;
}
.LZqubosol4XlcTmVPXrgwA\=\= {
display: flex;
align-items: center;
}
._92G6m9T1MVtvbrtbN0ztew\=\= {
margin: 1rem;
}
button.vKD-ffoQma2PtYJ6syJLXA\=\= {
border: 1px solid #0064B1;
border-bottom: 4px solid #0064B1;
border-radius: 3px;
padding: .7em 1.5em;
background: #008DED;
font-weight: 600;
font-size: 14px;
line-height: 1.4em;
color: #fff;
cursor: pointer;
}
button.vKD-ffoQma2PtYJ6syJLXA\=\=[disabled] {
border: 1px solid #F8F9FA;
background: #F8F9FA;
color: #008DED;
cursor: default;
}
button .jRDHFr0fk9vh0tmPg3yyNA\=\= {
display: inline-block;
min-width: 3.2em;
text-align: left;
}
.BHff-dIr\+7jxh1slKud1UA\=\= {
background-color: #fdfdf3;
max-width: 600px;
padding: 1.5em;
border-radius: 5px;
border: 1px solid black;
}
._8sv48aq5xq1UY1HM-IXXWw\=\= {
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) {
.XWyrhxEjrFCimjviedIRKg\=\= {
display: flex;
}
}
.KIG-iO8JlK18PTTkxmFFfQ\=\= {
flex-direction: row;
}
.wbV6cqcB6HeXauGMWUOUJw\=\= {
flex-direction: column;
}
.PWf16KXgVsL5DasX-69r\+w\=\= {
position: relative;
}
.BFnd\+XqC\+F5AvKCZ6eMOOA\=\= .b1NRGX9AXkY1BfJ1MCfPTw\=\= {
font-size: 24px;
}
.BFnd\+XqC\+F5AvKCZ6eMOOA\=\=.x86R9\+0TG6mMWrDQqDWMxQ\=\= .b1NRGX9AXkY1BfJ1MCfPTw\=\=, ._8fwuockVscy-LVkmd5sRrg\=\= {
color: green;
}
.BFnd\+XqC\+F5AvKCZ6eMOOA\=\=._4ywMQ6iToIUtlBzG0klUZQ\=\= .b1NRGX9AXkY1BfJ1MCfPTw\=\=, .n2ieOzL8DYqXeYvR4dnlBQ\=\= {
color: red;
}
.BFnd\+XqC\+F5AvKCZ6eMOOA\=\=.BHff-dIr\+7jxh1slKud1UA\=\= .b1NRGX9AXkY1BfJ1MCfPTw\=\=, .Kc0JjWetOt7bzIP5T5F-3g\=\= {
color: #b7b700;
}
.cdItXesO30xESmpowZCWVA\=\= {
margin-left: 1rem;
}
.XrgwDjPx-AobEiR9810sug\=\= ~ label .wfGA8rTfLXMNYeed\+7P0mg\=\=, .YGOg\+3jg-Q6uUjZrsKJJBw\=\= ~ label .wfGA8rTfLXMNYeed\+7P0mg\=\= {
display:none;
opacity:0;
}
.XrgwDjPx-AobEiR9810sug\=\=:checked ~ label .wfGA8rTfLXMNYeed\+7P0mg\=\=, .YGOg\+3jg-Q6uUjZrsKJJBw\=\=:checked ~ label .wfGA8rTfLXMNYeed\+7P0mg\=\={
display:block;
opacity:1.0;
color: #228be6;
}
.XrgwDjPx-AobEiR9810sug\=\=:checked ~ label .dxKTDsQBZGG-O07iRE7TNg\=\=, .YGOg\+3jg-Q6uUjZrsKJJBw\=\=:checked ~ label .dxKTDsQBZGG-O07iRE7TNg\=\={
display:none;
opacity:0;
}
.YGOg\+3jg-Q6uUjZrsKJJBw\=\=:checked ~ label .AX07i\+p1n9K\+g4HYk3mvOg\=\= {
color: #495057;
}
.YGOg\+3jg-Q6uUjZrsKJJBw\=\=:checked ~ label .AX07i\+p1n9K\+g4HYk3mvOg\=\= a {
color: #495057;
}
.YGOg\+3jg-Q6uUjZrsKJJBw\=\=:checked ~ label .AX07i\+p1n9K\+g4HYk3mvOg\=\= a:hover,
.YGOg\+3jg-Q6uUjZrsKJJBw\=\= ~ label .AX07i\+p1n9K\+g4HYk3mvOg\=\= a:hover {
color: #228be6;
text-decoration-color: initial;
}
.d7wuKQTkcJufIbd\+gVhKnw\=\= {
max-width: 600px;
}
.F6JwARlyXPrf\+kIygaN8dQ\=\= {
padding: 1rem;
margin: 1rem 1rem 1rem 0rem;
border: 1px dotted grey;
border-radius: 5px;
}
.v2APGCcZUAaU68TnPHhvxw\=\= {
display: flex;
flex-direction: row;
flex-wrap: no-wrap;
align-items: stretch;
background-color: #E4F6FF;
color: rgb(73, 80, 87);
border-radius: 0.25rem;
margin-top: 1rem;
max-width: 800px;
}
.syPwBWS1kp-zUKz4hcgcXg\=\= {
color: #008BED;
border-top-left-radius: 0.25rem;
border-bottom-left-radius: 0.25rem;
padding: .55rem .25rem .5rem .75rem;
font-size: 1rem;
}
.ptjLX6BwJtUff-P6OkZBiA\=\= {
margin-top: 0;
margin-bottom: 0.2rem;
font-size: .8rem;
font-weight: 600;
line-height: 1.5;
}
.VAB708TLB4qhUVdnQGAxJA\=\= {
flex-grow: 1;
display: flex;
flex-direction: column;
padding: .5rem 1rem .75rem .25rem;
font-size: .8rem;
}
.VAB708TLB4qhUVdnQGAxJA\=\= p {
margin-top: 0;
margin-bottom: .5rem;
}
.VAB708TLB4qhUVdnQGAxJA\=\= svg {
font-size: .7rem;
}
.VAB708TLB4qhUVdnQGAxJA\=\= ul {
margin: 0;
}
.VAB708TLB4qhUVdnQGAxJA\=\= li {
display: inline-block;
padding-right: 1rem;
margin-bottom: 0;
}
.CIIJrcA\+PLxU-W4xIVozXw\=\= {
margin-top: 1rem;
}
.v2APGCcZUAaU68TnPHhvxw\=\= button {
color: #0073aa;
}
.v2APGCcZUAaU68TnPHhvxw\=\= button:hover {
color: #00a0d2;
}
/* type: warning */
.iAbTOYj3VuCpNr1NEwmL4g\=\= {
background: rgb(255, 249, 219);
}
.iAbTOYj3VuCpNr1NEwmL4g\=\= .syPwBWS1kp-zUKz4hcgcXg\=\= {
color: rgb(250, 176, 7);
}

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 it is too large Load Diff

View File

@ -1 +0,0 @@
(window.webpackJsonp_font_awesome_admin=window.webpackJsonp_font_awesome_admin||[]).push([[13],{289:function(n,o,w){"use strict";w.r(o)}}]);

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

View File

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

View File

@ -1 +0,0 @@
(window.webpackJsonp_font_awesome_admin=window.webpackJsonp_font_awesome_admin||[]).push([[18],{285:function(e,o,n){"use strict";n.r(o);var s=n(170),t=n.n(s);o.default=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 @@
(window.webpackJsonp_font_awesome_admin=window.webpackJsonp_font_awesome_admin||[]).push([[19],{284:function(e,a,o){"use strict";o.r(a);var r=o(157),t=o.n(r);a.default=e=>async a=>{try{const{apiNonce:o,rootUrl:r,restApiNamespace:n}=e;return t.a.use(t.a.createRootURLMiddleware(r)),t.a.use(t.a.createNonceMiddleware(o)),await t()({path:n+"/api",method:"POST",body:a})}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

@ -0,0 +1,10 @@
.iVV55iNB320NJJLspy7m{max-width:600px}.BcmdF5mOoQ3Luug6sJrn{border:1px dotted grey;border-radius:5px;margin:1rem 1rem 1rem 0;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-left-radius:.25rem;border-top-left-radius:.25rem;color:#008bed;font-size:1rem;padding:.55rem .25rem .5rem .75rem}.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 1rem .75rem .25rem}.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-right: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-left:.125em;margin-right:.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-right:.5em}._V54L7D6KbeTsNGQHyuu{font-size:.9rem;font-weight:700;width:30%}.jH1jbDhGqqv7Grbj6p5p{margin-right:.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-left:2em}.rr6y9ViQbhSy0YAp3Svc{display:flex;flex-direction:flex-row}.d104q4pTcfNp1fl3EEwj{font-weight:600;margin-left: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-right:.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-left:1em}
.xAYNgmh_FT28wOZEe4og{background-color:#fff;margin-right: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-left: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-left: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:left}.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-left: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 1rem 1rem 0}.iw1ugzHiscI8cdqPxDt5 label{font-size:.9rem;font-weight:600;width:30%}.iw1ugzHiscI8cdqPxDt5 label svg{color:#dde2e6;padding-right:.5rem}.DnA1Iv_lwCTNSAFQGrON .iw1ugzHiscI8cdqPxDt5 p{font-size:unset;font-weight:unset}.DnA1Iv_lwCTNSAFQGrON .Pnf8O2SgfIFmVM0PSv3Z p svg{color:unset;padding-right:.5rem}.DnA1Iv_lwCTNSAFQGrON{border-bottom:1px solid #dde2e6;display:flex;flex-direction:column;margin-bottom:.75rem;padding:.5rem 1rem 1rem 0}.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-right:.5rem}button.HfzrDbHUd_u1i9ndGEBR{background-color:initial;border:none;border-radius:3px;color:#999;cursor:pointer;display:inline-block;margin-left:-.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-left:2em}.V9u2jF9aJPfN0wX4PDVS{background-color:none;border:none;cursor:pointer;line-height:2.15384615;margin-left: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 1rem 1rem 0}.DFhEV9q8j6_YiAqxGCIQ{font-size:.9rem;font-weight:600;margin:0;padding:.5rem 0}.DFhEV9q8j6_YiAqxGCIQ.y9VOhnGapgfPZMY_1Bh6 svg{color:#00c346;padding-right:.5rem}.DFhEV9q8j6_YiAqxGCIQ.czVzoPvuSXhcttrzFc9L svg{color:#f8f9fa}.ngog_nfdj6MCATJVipdj{padding:.5rem 1rem 1rem 0}.S_T55Hlv5ASeRm2CyHmV{font-size:.9rem;font-weight:600;margin:0 0 1rem}.S_T55Hlv5ASeRm2CyHmV svg{color:#dde2e6;padding-right:.5rem}.fy5GsBzkO8Epk2RR04LH{margin-left:1.8rem}button.fNJ_UaCGqOZxk72kjIDA{background-color:initial;border:none;border-radius:3px;color:#228be6;cursor:pointer;display:inline-block;margin:0 0 0 .2rem;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-left:.5em}.SstVIjmK5UOaiy_ltjXE .vdGJ8TWieYuqqCakSyHG{display:inline-block;font-weight:600;height:auto;margin-right:1rem;padding:.4rem;vertical-align:middle;width:30%}.gJq7WtlHzhBcjrgjABY8{margin-left: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:left;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-right: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}

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}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,3 @@
.xAYNgmh_FT28wOZEe4og{background-color:#fff;margin-right: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-left: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-left: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:left}.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-left: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 1rem 1rem 0;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-left-radius:.25rem;border-top-left-radius:.25rem;color:#008bed;font-size:1rem;padding:.55rem .25rem .5rem .75rem}.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 1rem .75rem .25rem}.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-right: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

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}

View File

@ -1,698 +0,0 @@
.d7wuKQTkcJufIbd\+gVhKnw\=\= {
max-width: 600px;
}
.F6JwARlyXPrf\+kIygaN8dQ\=\= {
padding: 1rem;
margin: 1rem 1rem 1rem 0rem;
border: 1px dotted grey;
border-radius: 5px;
}
.v2APGCcZUAaU68TnPHhvxw\=\= {
display: flex;
flex-direction: row;
flex-wrap: no-wrap;
align-items: stretch;
background-color: #E4F6FF;
color: rgb(73, 80, 87);
border-radius: 0.25rem;
margin-top: 1rem;
max-width: 800px;
}
.syPwBWS1kp-zUKz4hcgcXg\=\= {
color: #008BED;
border-top-left-radius: 0.25rem;
border-bottom-left-radius: 0.25rem;
padding: .55rem .25rem .5rem .75rem;
font-size: 1rem;
}
.ptjLX6BwJtUff-P6OkZBiA\=\= {
margin-top: 0;
margin-bottom: 0.2rem;
font-size: .8rem;
font-weight: 600;
line-height: 1.5;
}
.VAB708TLB4qhUVdnQGAxJA\=\= {
flex-grow: 1;
display: flex;
flex-direction: column;
padding: .5rem 1rem .75rem .25rem;
font-size: .8rem;
}
.VAB708TLB4qhUVdnQGAxJA\=\= p {
margin-top: 0;
margin-bottom: .5rem;
}
.VAB708TLB4qhUVdnQGAxJA\=\= svg {
font-size: .7rem;
}
.VAB708TLB4qhUVdnQGAxJA\=\= ul {
margin: 0;
}
.VAB708TLB4qhUVdnQGAxJA\=\= li {
display: inline-block;
padding-right: 1rem;
margin-bottom: 0;
}
.CIIJrcA\+PLxU-W4xIVozXw\=\= {
margin-top: 1rem;
}
.v2APGCcZUAaU68TnPHhvxw\=\= button {
color: #0073aa;
}
.v2APGCcZUAaU68TnPHhvxw\=\= button:hover {
color: #00a0d2;
}
/* type: warning */
.iAbTOYj3VuCpNr1NEwmL4g\=\= {
background: rgb(255, 249, 219);
}
.iAbTOYj3VuCpNr1NEwmL4g\=\= .syPwBWS1kp-zUKz4hcgcXg\=\= {
color: rgb(250, 176, 7);
}
.QIGY8oMzb821jqSAA6Rn4g\=\= {
max-width: 100%;
}
.aALIlyz\+TBWS9MLVeh6wmA\=\= button {
font-size: 1.5em;
background: transparent;
margin-right: 0.125em;
margin-left: 0.125em;
border: none;
padding-bottom: 0.25em;
border-bottom: none;
}
.aALIlyz\+TBWS9MLVeh6wmA\=\= button:hover {
cursor: pointer;
}
.aALIlyz\+TBWS9MLVeh6wmA\=\= button:disabled {
cursor: default;
border-bottom: 4px solid #008DED;
color: unset;
}
._0x0RK8KiAJgDczKK031HxQ\=\= {
color: red;
}
.hyAooutsnRTCptVmz96wbg\=\= {
display: flex;
width: 100%;
}
.hyAooutsnRTCptVmz96wbg\=\= svg {
margin-right: .5em;
margin-bottom: .125em;
}
.BawvAG3StpFE9UbNiPvK3g\=\= {
width: 30%;
font-weight: bold;
font-size: .9rem;
}
.w9F7a0jctEk6w8ugAV5ABA\=\= {
width: 1rem;
margin-right: .25rem;
text-align: center;
}
.BRC\+YZNrGUxfH82gZsrdwA\=\= {
display: flex;
flex-direction: row;
}
.tuTbOAR46RTbuDXH00d2gQ\=\= {
width: 50%;
}
.BnGZOlJKEKc\+YI9KmQh\+Yg\=\= {
display: flex;
flex-direction: column;
width: 70%;
margin-top: .5rem;
}
@media only screen and (min-width: 1024px) {
.BnGZOlJKEKc\+YI9KmQh\+Yg\=\= {
margin-top: 0;
}
}
.iCLEbIAXAx8VH8RGCCbvgg\=\= {
font-style: italic;
}
.iCLEbIAXAx8VH8RGCCbvgg\=\= > ul {
list-style: none;
margin-left: 2em;
}
.QZrz215uMWhNgkyzgTmOag\=\= {
display: flex;
flex-direction: flex-row;
}
.zjk9n76WnC7PKioF3aBPNA\=\= {
margin-left: 5px;
font-weight: 600;
}
.JgyF2w3zr8u8EZtHirZ9jw\=\= {
margin-top: 1rem;
margin-bottom: 1rem;
}
.l90FgLRSjN56djI2bGkw0A\=\= {
margin-top: 1rem;
}
.l90FgLRSjN56djI2bGkw0A\=\= button {
background: none;
border: none;
text-decoration: underline;
cursor: pointer;
}
.l90FgLRSjN56djI2bGkw0A\=\= button svg {
margin-right: 0.5em;
}
.nszEb3XbyS\+sLm8WSjoWSw\=\= .tuTbOAR46RTbuDXH00d2gQ\=\= {
margin-bottom: 1.5rem;
}
.m4T9E7AFyJOV9pxI900Q3Q\=\= {
display: block;
font-weight: normal;
line-height: 1.5;
margin-top: .25rem;
color: #868e96;
}
.m4T9E7AFyJOV9pxI900Q3Q\=\= a {
color: #868e96;
}
.m4T9E7AFyJOV9pxI900Q3Q\=\= a:hover {
color: #228be6;
}
.-HAQ9msG6UCBe4TdGvJ5Gw\=\= {
padding-left: 1em;
}
.Ihb3kyONjVgEWV0zB0vfxQ\=\= {
margin-right: 20px;
padding: 1rem 2rem 2rem 2rem;
background-color: #fff;
}
@media only screen and (min-width: 1024px) {
.Ihb3kyONjVgEWV0zB0vfxQ\=\= {
max-width: 1000px;
}
}
.ToMOGjAmxSPs6D\+glWGx9A\=\= {
display: flex;
align-items: center;
}
._3edKMOIt9iftk0Zz-7ia9w\=\= {
padding-left: 1em;
}
div._3edKMOIt9iftk0Zz-7ia9w\=\=.NbqVQtCU5W3ihwasrYexfA\=\= {
display: flex;
margin: 1em;
background-color: #fda09a;
border-radius: 5px;
max-width: 450px;
padding: 1em;
}
div._3edKMOIt9iftk0Zz-7ia9w\=\=.NbqVQtCU5W3ihwasrYexfA\=\= .gEl1fl\+2lk74ueiQz5cPxQ\=\= {
margin-top: auto;
margin-bottom: auto;
}
div._3edKMOIt9iftk0Zz-7ia9w\=\=.NbqVQtCU5W3ihwasrYexfA\=\= .vumGDcuTrv0Ekcc\+McKiXw\=\= {
max-width: 400px;
}
._3edKMOIt9iftk0Zz-7ia9w\=\= .vumGDcuTrv0Ekcc\+McKiXw\=\= {
padding-left: 1em;
}
._3edKMOIt9iftk0Zz-7ia9w\=\=._7IGgJmOfwN1O\+c0smUgA9Q\=\= .b1NRGX9AXkY1BfJ1MCfPTw\=\= {
color: green;
}
h2._3hBTOhCdvQjibQScAbzMIQ\=\= {
font-size: 18px;
}
h3._3hBTOhCdvQjibQScAbzMIQ\=\= {
font-size: 16px;
}
._0XZF4B-SzNg4vm8Dl0F7TA\=\= {
margin-top: 1rem;
margin-bottom: 1rem;
}
.YGhcFGkqFAeqpY9iNYq9Sw\=\= th {
font-weight: bold;
}
button.mQ\+dGgo7ePYMdFVM7UJy3Q\=\= {
border: 0;
background: none;
}
.LZqubosol4XlcTmVPXrgwA\=\= {
display: flex;
align-items: center;
}
._92G6m9T1MVtvbrtbN0ztew\=\= {
margin: 1rem;
}
button.vKD-ffoQma2PtYJ6syJLXA\=\= {
border: 1px solid #0064B1;
border-bottom: 4px solid #0064B1;
border-radius: 3px;
padding: .7em 1.5em;
background: #008DED;
font-weight: 600;
font-size: 14px;
line-height: 1.4em;
color: #fff;
cursor: pointer;
}
button.vKD-ffoQma2PtYJ6syJLXA\=\=[disabled] {
border: 1px solid #F8F9FA;
background: #F8F9FA;
color: #008DED;
cursor: default;
}
button .jRDHFr0fk9vh0tmPg3yyNA\=\= {
display: inline-block;
min-width: 3.2em;
text-align: left;
}
.BHff-dIr\+7jxh1slKud1UA\=\= {
background-color: #fdfdf3;
max-width: 600px;
padding: 1.5em;
border-radius: 5px;
border: 1px solid black;
}
._8sv48aq5xq1UY1HM-IXXWw\=\= {
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) {
.XWyrhxEjrFCimjviedIRKg\=\= {
display: flex;
}
}
.KIG-iO8JlK18PTTkxmFFfQ\=\= {
flex-direction: row;
}
.wbV6cqcB6HeXauGMWUOUJw\=\= {
flex-direction: column;
}
.PWf16KXgVsL5DasX-69r\+w\=\= {
position: relative;
}
.BFnd\+XqC\+F5AvKCZ6eMOOA\=\= .b1NRGX9AXkY1BfJ1MCfPTw\=\= {
font-size: 24px;
}
.BFnd\+XqC\+F5AvKCZ6eMOOA\=\=.x86R9\+0TG6mMWrDQqDWMxQ\=\= .b1NRGX9AXkY1BfJ1MCfPTw\=\=, ._8fwuockVscy-LVkmd5sRrg\=\= {
color: green;
}
.BFnd\+XqC\+F5AvKCZ6eMOOA\=\=._4ywMQ6iToIUtlBzG0klUZQ\=\= .b1NRGX9AXkY1BfJ1MCfPTw\=\=, .n2ieOzL8DYqXeYvR4dnlBQ\=\= {
color: red;
}
.BFnd\+XqC\+F5AvKCZ6eMOOA\=\=.BHff-dIr\+7jxh1slKud1UA\=\= .b1NRGX9AXkY1BfJ1MCfPTw\=\=, .Kc0JjWetOt7bzIP5T5F-3g\=\= {
color: #b7b700;
}
.cdItXesO30xESmpowZCWVA\=\= {
margin-left: 1rem;
}
.XrgwDjPx-AobEiR9810sug\=\= ~ label .wfGA8rTfLXMNYeed\+7P0mg\=\=, .YGOg\+3jg-Q6uUjZrsKJJBw\=\= ~ label .wfGA8rTfLXMNYeed\+7P0mg\=\= {
display:none;
opacity:0;
}
.XrgwDjPx-AobEiR9810sug\=\=:checked ~ label .wfGA8rTfLXMNYeed\+7P0mg\=\=, .YGOg\+3jg-Q6uUjZrsKJJBw\=\=:checked ~ label .wfGA8rTfLXMNYeed\+7P0mg\=\={
display:block;
opacity:1.0;
color: #228be6;
}
.XrgwDjPx-AobEiR9810sug\=\=:checked ~ label .dxKTDsQBZGG-O07iRE7TNg\=\=, .YGOg\+3jg-Q6uUjZrsKJJBw\=\=:checked ~ label .dxKTDsQBZGG-O07iRE7TNg\=\={
display:none;
opacity:0;
}
.YGOg\+3jg-Q6uUjZrsKJJBw\=\=:checked ~ label .AX07i\+p1n9K\+g4HYk3mvOg\=\= {
color: #495057;
}
.YGOg\+3jg-Q6uUjZrsKJJBw\=\=:checked ~ label .AX07i\+p1n9K\+g4HYk3mvOg\=\= a {
color: #495057;
}
.YGOg\+3jg-Q6uUjZrsKJJBw\=\=:checked ~ label .AX07i\+p1n9K\+g4HYk3mvOg\=\= a:hover,
.YGOg\+3jg-Q6uUjZrsKJJBw\=\= ~ label .AX07i\+p1n9K\+g4HYk3mvOg\=\= a:hover {
color: #228be6;
text-decoration-color: initial;
}
/* Kit tab */
._8vGwfwQ6XO5BsPMbXdXTiA\=\= {
position: relative;
}
/* API token */
.\+8WVAaXCYDQCF\+JpeqOS\+w\=\= {
display: flex;
border-bottom: 1px solid #DDE2E6;
padding: 1rem 1rem 1rem 0;
}
.\+8WVAaXCYDQCF\+JpeqOS\+w\=\= label {
width: 30%;
font-size: .9rem;
font-weight: 600;
}
.\+8WVAaXCYDQCF\+JpeqOS\+w\=\= label svg {
padding-right: .5rem;
color: #DDE2E6;
}
.NNaEQMl0NwcAO9BQkOMVjA\=\= .\+8WVAaXCYDQCF\+JpeqOS\+w\=\= p {
font-size: unset;
font-weight: unset;
}
.NNaEQMl0NwcAO9BQkOMVjA\=\= .Uk7ZhRxwlPWH-ZTew\+MGbw\=\= p svg {
padding-right: .5rem;
color: unset;
}
.NNaEQMl0NwcAO9BQkOMVjA\=\= {
display: flex;
margin-bottom: .75rem;
padding: .5rem 1rem 1rem 0;
border-bottom: 1px solid #DDE2E6;
flex-direction: column;
}
.\+8WVAaXCYDQCF\+JpeqOS\+w\=\=.EwijJfw66yu\+IIrz6lhCfg\=\= {
border-bottom: none;
}
.Uk7ZhRxwlPWH-ZTew\+MGbw\=\= {
display: flex;
justify-content: space-between;
}
.Uk7ZhRxwlPWH-ZTew\+MGbw\=\=.EwijJfw66yu\+IIrz6lhCfg\=\= {
flex-direction: column;
}
.NNaEQMl0NwcAO9BQkOMVjA\=\= p {
margin: 0;
padding: .5rem 0;
font-size: .9rem;
font-weight: 600;
}
.Uk7ZhRxwlPWH-ZTew\+MGbw\=\= .EGcS\+kpw3Rpch27QNPDt8A\=\= span svg {
padding-right: .5rem;
color: #00C346;
}
button.IMhiWaWNyRtqLVii4y5GUw\=\= {
transition: background .1s ease-in;
transition: 0.1s ease-in;
display: inline-block;
margin-left: -.1em;
border: none;
border-radius: 3px;
background-color: transparent;
padding: .5rem 1rem;
color: #999;
cursor: pointer;
}
button.IMhiWaWNyRtqLVii4y5GUw\=\=:hover {
background-color: #DA001D;
color: #fff;
}
.oIc2sd4DIsbfgvBY8l-wPw\=\= button {
margin-left: 2em;
}
.gxUD1N\+WoBmH-EJlqniWkw\=\= {
margin-left: 1rem;
line-height: 2.15384615;
border: none;
background-color: none;
cursor: pointer;
text-decoration: underline;
text-decoration-color: rgba(0, 0, 0, 0.15);
}
.gxUD1N\+WoBmH-EJlqniWkw\=\=:hover {
text-decoration-color: black;
}
/* Active kit info */
.zPZer73GvIkmxZ7KreNJYA\=\= {
margin-bottom: .75rem;
border-bottom: 1px solid #DDE2E6;
padding: .5rem 1rem 1rem 0;
}
.J-fVpwkR2DjY\+-8RSoJYMw\=\= {
margin: 0;
padding: .5rem 0;
font-size: .9rem;
font-weight: 600;
}
.J-fVpwkR2DjY\+-8RSoJYMw\=\=.lycO-c64O\+OgDI9YkMivUw\=\= svg {
padding-right: .5rem;
color: #00C346;
}
.J-fVpwkR2DjY\+-8RSoJYMw\=\=.szbUIrRpgoLKxf9N4EyKLg\=\= svg {
color: #F8F9FA;
}
/* Select/View kit data */
.-pzVBDa-b25g\+W8geRXnxg\=\= {
padding: .5rem 1rem 1rem 0;
}
.-FLmUWzoR6pLlIRSuFBBYA\=\= {
margin: 0 0 1rem 0;
font-size: .9rem;
font-weight: 600;
}
.-FLmUWzoR6pLlIRSuFBBYA\=\= svg {
padding-right: .5rem;
color: #DDE2E6;
}
._4hIkSVrBxe2EsX5xI4X\+-w\=\= {
margin-left: 1.8rem;
}
button.PGpv2OOpBHnM9TF49ScwhQ\=\= {
transition: background .1s ease-in;
transition: 0.1s ease-in;
display: inline-block;
vertical-align: middle;
margin: 0 0 0 .2rem;
border: none;
border-radius: 3px;
background-color: transparent;
padding: .5rem;
color: #228be6;
cursor: pointer;
}
button.PGpv2OOpBHnM9TF49ScwhQ\=\=:hover {
background-color: #1c7ed6;
color: #fff;
}
button.PGpv2OOpBHnM9TF49ScwhQ\=\= span {
padding-left: .5em;
}
.TCMt1wxwCSzZQpcKueWmkQ\=\= .D-w2AnOwJY2bw5sGz5McjA\=\= {
display: inline-block;
width: 30%;
height: auto;
vertical-align: middle;
margin-right: 1rem;
padding: .4rem;
font-weight: 600;
}
/* Kit settings table */
.oJVokNJ-sIuA\+gdUpTLGJA\=\= {
margin-left: 1.8rem;
}
.EczpWfVlPxzdEaHRV3pz2A\=\= {
width: 100%;
margin: 0 0 1rem 0;
border-collapse: collapse;
font-size: .9rem;
}
.EczpWfVlPxzdEaHRV3pz2A\=\= .-qghjrb3DvBDmVsGriWvTA\=\=,
.EczpWfVlPxzdEaHRV3pz2A\=\= .YObcB6LSN4LrZ85qknFF-w\=\= {
padding: .5rem;
text-align: left;
border-top: 1px solid #DDE2E6;
border-bottom: 1px solid #DDE2E6;
vertical-align: top;
}
.EczpWfVlPxzdEaHRV3pz2A\=\= .-qghjrb3DvBDmVsGriWvTA\=\= {
font-weight: 600;
width: 30%;
}
._9FAtxV9kie\+OmOmzBfJSkA\=\= {
display: block;
margin-top: .25rem;
font-weight: normal;
line-height: 1.5;
color: #868e96;
}
._9FAtxV9kie\+OmOmzBfJSkA\=\= a {
color: #868e96;
}
._9FAtxV9kie\+OmOmzBfJSkA\=\= a:hover {
color: #228be6;
}
.c\+uCz1PFg7ovVDH7oUd5Sw\=\= {
display: flex;
margin-bottom: .5rem;
border-bottom: 2px solid #008DED;
padding-bottom: 1rem;
}
.c\+uCz1PFg7ovVDH7oUd5Sw\=\= label {
margin-right: 1rem;
}
.gRCffi6JWAwOkiNdtjZueA\=\= th.agGiNQAP3Ikmo1HKogxbiA\=\=, td.agGiNQAP3Ikmo1HKogxbiA\=\= {
background-color: #FFE2E2;
}
.B\+Bikng94RbtcQqPqBrHOA\=\= th {
vertical-align: top;
}
.B\+Bikng94RbtcQqPqBrHOA\=\= th .iRlCLvShkhGZ\+9zO5xq6uw\=\= {
font-weight: bold;
}
.B\+Bikng94RbtcQqPqBrHOA\=\= code {
font-size: 10px;
}
._9-S9B47DLrx4-SEnbiDx1Q\=\=, .b72L8oHK2yOpCV0to2lb7Q\=\= {
margin-top: .5rem;
}
.va5cD9uCSEXRsb4FfvMXgQ\=\= {
display: flex;
}
.VkiLBdIWC85apP6l5wk2TQ\=\= {
border: 1px solid black;
background-color: #fdfdf3;
padding: 1.5em;
display: inline-block;
}
.GaudPtB1hhO0HHjMCEqy4Q\=\= {
padding: .5rem;
background-color: rgba(0,0,0,0);
border-radius: 5px;
}
.GaudPtB1hhO0HHjMCEqy4Q\=\=:hover {
cursor: pointer;
}
.GaudPtB1hhO0HHjMCEqy4Q\=\= .sp3bMuQjz4\+THc5BC0vcRg\=\= {
margin-left: 1em;
}

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

@ -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

@ -1 +0,0 @@
(window.webpackJsonp_font_awesome_admin=window.webpackJsonp_font_awesome_admin||[]).push([[9],{174:function(n,r,e){"use strict";e.d(r,"a",(function(){return t})),e.d(r,"d",(function(){return c})),e.d(r,"b",(function(){return i})),e.d(r,"c",(function(){return 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"]}},258:function(n,r,e){var t=e(60),c=e(201),i=e(54),o=e(7),f=e(259),u=e(53),a=e(202),s=e(183),l=e(59),v=/\w*$/;n.exports=function(n,r){var e=30,p="...";if(o(r)){var x="separator"in r?r.separator:x;e="length"in r?s(r.length):e,p="omission"in r?t(r.omission):p}var h=(n=l(n)).length;if(i(n)){var g=a(n);h=g.length}if(e>=h)return n;var d=e-u(p);if(d<1)return p;var m=g?c(g,0,d).join(""):n.slice(0,d);if(void 0===x)return m+p;if(g&&(d+=m.length-d),f(x)){if(n.slice(d).search(x)){var z,w=m;for(x.global||(x=RegExp(x.source,l(v.exec(x))+"g")),x.lastIndex=0;z=x.exec(w);)var M=z.index;m=m.slice(0,void 0===M?d:M)}}else if(n.indexOf(t(x),d)!=d){var _=m.lastIndexOf(x);_>-1&&(m=m.slice(0,_))}return m+p}},259:function(n,r,e){var t=e(260),c=e(17),i=e(19),o=i&&i.isRegExp,f=o?c(o):t;n.exports=f},260:function(n,r,e){var t=e(4),c=e(3);n.exports=function(n){return c(n)&&"[object RegExp]"==t(n)}},263:function(n,r,e){var t=e(182);n.exports=function(n,r){return t(n,r)}},264:function(n,r,e){var t=e(265);n.exports=function(n){return n&&n.length?t(n):[]}},265:function(n,r,e){var t=e(33);n.exports=function(n,r){for(var e=-1,c=n.length,i=0,o=[];++e<c;){var f=n[e],u=r?r(f):f;if(!e||!t(u,a)){var a=u;o[i++]=0===f?0:f}}return o}},266:function(n,r,e){var t=e(267),c=e(67),i=e(273),o=e(274),f=i((function(n,r){return o(n)?t(n,c(r,1,o,!0)):[]}));n.exports=f},267:function(n,r,e){var t=e(195),c=e(268),i=e(272),o=e(32),f=e(17),u=e(196);n.exports=function(n,r,e,a){var s=-1,l=c,v=!0,p=n.length,x=[],h=r.length;if(!p)return x;e&&(r=o(r,f(e))),a?(l=i,v=!1):r.length>=200&&(l=u,v=!1,r=new t(r));n:for(;++s<p;){var g=n[s],d=null==e?g:e(g);if(g=a||0!==g?g:0,v&&d==d){for(var m=h;m--;)if(r[m]===d)continue n;x.push(g)}else l(r,d,a)||x.push(g)}return x}},268:function(n,r,e){var t=e(269);n.exports=function(n,r){return!(null==n||!n.length)&&t(n,r,0)>-1}},269:function(n,r,e){var t=e(200),c=e(270),i=e(271);n.exports=function(n,r,e){return r==r?i(n,r,e):t(n,c,e)}},270:function(n,r){n.exports=function(n){return n!=n}},271:function(n,r){n.exports=function(n,r,e){for(var t=e-1,c=n.length;++t<c;)if(n[t]===r)return t;return-1}},272:function(n,r){n.exports=function(n,r,e){for(var t=-1,c=null==n?0:n.length;++t<c;)if(e(r,n[t]))return!0;return!1}},273:function(n,r,e){var t=e(62),c=e(68),i=e(69);n.exports=function(n,r){return i(c(n,r,t),n+"")}},274:function(n,r,e){var t=e(16),c=e(3);n.exports=function(n){return c(n)&&t(n)}}}]);

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

File diff suppressed because one or more lines are too long

View File

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

59419
admin/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,27 +1,24 @@
{
"name": "font-awesome-admin",
"version": "4.4.0",
"version": "5.1.0",
"private": true,
"dependencies": {
"@fortawesome/fa-icon-chooser-react": "^0.5.0",
"@fortawesome/fontawesome-svg-core": "^6.2.0",
"@fortawesome/free-regular-svg-icons": "^6.2.0",
"@fortawesome/free-solid-svg-icons": "^6.2.0",
"@fortawesome/react-fontawesome": "^0.1.3",
"@wordpress/components": "14.1.10",
"axios": "^0.25.0",
"classnames": "^2.2.6",
"react-redux": "^7.1.1",
"react-shadow-dom-retarget-events": "^1.0.11",
"redux": "^4.0.4",
"redux-thunk": "^2.3.0",
"web-vitals": "^1.0.1",
"use-subscription": "1.5.1",
"moment": "^2.29.4",
"moment-timezone": "^0.5.35"
"axios": "^1.7.4",
"classnames": "^2.5.1",
"moment": "^2.30.1",
"moment-timezone": "^0.5.45",
"react-redux": "^8",
"react-shadow-dom-retarget-events": "^1.1.0",
"redux": "^5.0.1",
"redux-thunk": "^3.1.0",
"web-vitals": "^4.0.1"
},
"scripts": {
"build": "wp-scripts build --webpack-no-externals",
"build": "wp-scripts build",
"check-engines": "wp-scripts check-engines",
"check-licenses": "wp-scripts check-licenses",
"format": "wp-scripts format",
@ -31,8 +28,9 @@
"lint:md:js": "wp-scripts lint-md-js",
"lint:pkg-json": "wp-scripts lint-pkg-json",
"packages-update": "wp-scripts packages-update",
"start": "wp-scripts start --webpack-no-externals",
"start": "wp-scripts start",
"test:e2e": "wp-scripts test-e2e",
"test:playwright": "npx playwright test",
"test:unit": "wp-scripts test-unit-js",
"test": "npm run test:unit"
},
@ -55,30 +53,27 @@
]
},
"devDependencies": {
"@wordpress/babel-preset-default": "6.2.0",
"@wordpress/components": "14.1.10",
"@wordpress/compose": "4.1.5",
"@wordpress/data": "5.1.5",
"@wordpress/e2e-test-utils": "^5.4.10",
"@wordpress/element": "3.1.1",
"@wordpress/i18n": "^3.16.0",
"@wordpress/icons": "4.0.2",
"@wordpress/jest-preset-default": "7.0.5",
"@wordpress/primitives": "2.1.1",
"@wordpress/rich-text": "4.1.5",
"@wordpress/scripts": "16.1.4",
"babel-plugin-lodash": "^3.3.4",
"dotenv": "^14.2.0",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.0",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-test-renderer": "16.13.1",
"@wordpress/babel-preset-default": "wp-6.7",
"@wordpress/block-editor": "wp-6.7",
"@wordpress/blocks": "wp-6.7",
"@wordpress/components": "wp-6.7",
"@wordpress/compose": "wp-6.7",
"@wordpress/data": "wp-6.7",
"@wordpress/e2e-test-utils": "wp-6.7",
"@wordpress/e2e-test-utils-playwright": "^0.26.0",
"@wordpress/i18n": "wp-6.7",
"@wordpress/icons": "wp-6.7",
"@wordpress/jest-preset-default": "wp-6.7",
"@wordpress/keyboard-shortcuts": "wp-6.7",
"@wordpress/scripts": "wp-6.7",
"decode-uri-component": "^0.4.1",
"dotenv": "^14.3.2",
"eslint-config-react-app": "^7.0.1",
"lodash": "^4.17.21",
"mysql2": "^3.9.9",
"react": "18.3.1",
"react-dom": "18.3.1",
"redux-mock-store": "^1.5.4",
"rewire": "^5.0.0",
"terser": "4.8.1",
"webpack-bundle-analyzer": "^4.4.2",
"loader-utils": "^1.4.2",
"decode-uri-component": "^0.2.1"
"rewire": "^7.0.0"
}
}

View File

@ -0,0 +1,51 @@
import './src/playwright/support/env.js'
import { defineConfig, devices } from '@playwright/test'
const testDir = 'src/playwright'
const baseURL = `http://${process.env.WP_DOMAIN}`
process.env.WP_BASE_URL = baseURL
const adminStorageStatePath = 'src/playwright/.auth/state.json'
export default defineConfig({
use: {
baseURL
},
projects: [
{ name: 'auth', testDir, testMatch: 'setup/auth.js' },
{
name: 'reset',
testDir,
testMatch: 'setup/reset.js',
use: {
storageState: adminStorageStatePath
}
},
{
name: 'setupProKit',
testDir,
testMatch: 'setup/proKit.js',
use: {
storageState: adminStorageStatePath
},
dependencies: ['auth', 'reset']
},
{
name: 'with-proKit-chromium',
testMatch: 'withProKit/*.spec.js',
use: {
...devices['Desktop Chrome'],
storageState: adminStorageStatePath
},
dependencies: ['setupProKit']
},
{
name: 'withAuth-chromium',
testMatch: 'withAuth/*.spec.js',
use: {
...devices['Desktop Chrome'],
storageState: adminStorageStatePath
},
dependencies: ['auth', 'reset']
}
]
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,28 +0,0 @@
import React from 'react'
import { mount } from 'enzyme'
import { Provider } from 'react-redux'
import { createStore } from './store'
import ErrorBoundary from './ErrorBoundary'
import FontAwesomeAdminView from './FontAwesomeAdminView'
describe('FontAwesomeAdminView', () => {
test('mounts successfully', () => {
const initialData = JSON.parse(
'{"apiNonce":"deadbeef42","apiUrl":"http://localhost:8765/index.php?rest_route=/font-awesome/v1","unregisteredClients":[],"showConflictDetectionReporter":"","settingsPageUrl":"http://localhost:8765/wp-admin/options-general.php?page=font-awesome","detectConflictsUntil":0,"options":{"usePro":false,"compat":true,"technology":"webfont","pseudoElements":false,"version":"5.12.0"},"showAdmin":"1","onSettingsPage":"1","clientPreferences":{"beta-plugin":{"name":"beta-plugin","compat":true}},"releases":{"available":["5.12.0","5.11.2","5.11.1","5.11.0","5.10.2","5.10.1","5.10.0","5.9.0","5.8.2","5.8.1","5.8.0","5.7.2","5.7.1","5.7.0","5.6.3","5.6.1","5.6.0","5.5.0","5.4.2","5.4.1","5.3.1","5.2.0","5.1.1","5.1.0","5.0.13","5.0.12","5.0.10","5.0.9","5.0.8","5.0.6","5.0.4","5.0.3","5.0.2","5.0.1"],"latest_version_5":"5.12.0","latest_version_6":"6.1.1"},"pluginVersion":"4.0.0-rc13","preferenceConflicts":[],"v3DeprecationWarning":""}'
)
global['__FontAwesomeOfficialPlugin__'] = initialData
const store = createStore(initialData)
const wrapper = mount(
<ErrorBoundary>
<Provider store={ store }>
<FontAwesomeAdminView/>
</Provider>
</ErrorBoundary>
)
expect(wrapper).toBeTruthy()
})
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,119 +0,0 @@
import React from 'react'
import { mount } from 'enzyme'
import { Provider } from 'react-redux'
import { createStore } from './store'
import ErrorBoundary from './ErrorBoundary'
import TroubleshootTab from './TroubleshootTab'
import { unmountComponentAtNode } from 'react-dom'
describe('TroubleshootTab', () => {
let wrapper = null
beforeEach(() => {
const initialData = JSON.parse(
'{"apiNonce":"81245cfaf6","apiUrl":"http://localhost:8765/index.php?rest_route=/font-awesome/v1","detectConflictsUntil":"1581103894","unregisteredClients":{"3c937b6d9b50371df1e78b5d70e11512":{"type":"fontawesome-conflict","technology":"webfont","href":"https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.css","innerText":"","tagName":"LINK","blocked":true},"c604f60e1488e3c3493a19a43709b4ca":{"type":"fontawesome-conflict","technology":"webfont","href":"http://localhost:8765/wp-content/plugins/plugin-kappa/font-awesome-4.7.0/css/font-awesome.css","innerText":"","tagName":"LINK","blocked":true}},"showConflictDetectionReporter":"","settingsPageUrl":"http://localhost:8765/wp-admin/options-general.php?page=font-awesome","activeAdminTab":"ADMIN_TAB_TROUBLESHOOT","options":{"usePro":false,"compat":true,"technology":"webfont","pseudoElements":false,"version":"5.12.1"},"showAdmin":"1","onSettingsPage":"1","clientPreferences":{"beta-plugin":{"name":"beta-plugin","compat":true,"version":[["5.1.0",">="]]}},"releases":{"available":["5.12.1","5.12.0","5.11.2","5.11.1","5.11.0","5.10.2","5.10.1","5.10.0","5.9.0","5.8.2","5.8.1","5.8.0","5.7.2","5.7.1","5.7.0","5.6.3","5.6.1","5.6.0","5.5.0","5.4.2","5.4.1","5.3.1","5.2.0","5.1.1","5.1.0","5.0.13","5.0.12","5.0.10","5.0.9","5.0.8","5.0.6","5.0.4","5.0.3","5.0.2","5.0.1"],"latest_version_5":"5.12.1","latest_version_6":"6.1.1"},"pluginVersion":"4.0.0-rc13","preferenceConflicts":[],"v3DeprecationWarning":""}'
)
global['__FontAwesomeOfficialPlugin__'] = initialData
const store = createStore(initialData)
wrapper = mount(
<ErrorBoundary>
<Provider store={ store }>
<TroubleshootTab/>
</Provider>
</ErrorBoundary>
)
})
afterEach(() => {
wrapper.unmount()
wrapper = null
})
test('mounts successfully', () => {
expect(wrapper).toBeTruthy()
})
describe('when starting with all conflicts blocked', () => {
beforeEach(() => {
// Assert expectations of the state of the data
const inputs = [
'block_all_detected_conflicts',
'block_3c937b6d9b50371df1e78b5d70e11512',
'block_c604f60e1488e3c3493a19a43709b4ca'
]
inputs.forEach(id => {
expect(
wrapper
.find(`#${id}`)
.first()
.props()
.checked
).toBe(true)
})
expect(
wrapper.find('#submit').props().disabled
).toBe(true)
})
describe('when de-selecting and re-selecting an individual conflict for blocking', () => {
test('there are no pending changes shown', () => {
// change/click one of the conflicts
wrapper.find('#block_c604f60e1488e3c3493a19a43709b4ca').simulate('change')
// That one should now be unchecked
expect(
wrapper
.find('#block_c604f60e1488e3c3493a19a43709b4ca')
.first()
.props()
.checked
).toBe(false)
// The All select should no longer be checked
expect(
wrapper
.find('#block_all_detected_conflicts')
.first()
.props()
.checked
).toBe(false)
// And there should be pending changes
expect(
wrapper.find('#submit').props().disabled
).toBe(false)
// Now, just click/change that same one again
wrapper.find('#block_c604f60e1488e3c3493a19a43709b4ca').simulate('change')
// That one should now be checked
expect(
wrapper
.find('#block_c604f60e1488e3c3493a19a43709b4ca')
.first()
.props()
.checked
).toBe(true)
// The All select should be checked again
expect(
wrapper
.find('#block_all_detected_conflicts')
.first()
.props()
.checked
).toBe(true)
// And the submit button should again be disabled, for lack of pending changes
expect(
wrapper.find('#submit').props().disabled
).toBe(true)
})
})
})
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,32 @@
import { test as setup, expect } from '@wordpress/e2e-test-utils-playwright'
import '../support/env.js'
const CONFIG_ROUTE_PATTERN = '**/font-awesome/v1/config'
const API_ROUTE_PATTERN = '**/font-awesome/v1/api*'
setup('pro kit', async ({ page }) => {
expect(process.env.API_TOKEN).toBeTruthy()
expect(process.env.KIT_TOKEN).toBeTruthy()
await page.goto('/wp-admin/admin.php?page=font-awesome')
await page.locator('label').filter({ hasText: 'Use A Kit' }).click()
const allText = await page.getByRole('heading').allTextContents()
await page.locator('label').filter({ hasText: 'API Token' }).fill(process.env.API_TOKEN)
const saveAPITokenResponsePromise = page.waitForResponse(CONFIG_ROUTE_PATTERN)
await page.getByRole('button', { name: 'Save API Token' }).click()
await saveAPITokenResponsePromise
const kitsResponsePromise = page.waitForResponse(API_ROUTE_PATTERN)
await page.getByRole('button').filter({ hasText: 'kits data' }).click()
await kitsResponsePromise
await page.locator('select').selectOption(process.env.KIT_TOKEN)
const saveSettingsResponsePromise = page.waitForResponse(CONFIG_ROUTE_PATTERN)
await page.getByRole('button').filter({ hasText: 'Save Changes' }).click()
await saveSettingsResponsePromise
})

View File

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

View File

@ -0,0 +1,7 @@
import dotenv from 'dotenv'
import path from 'path'
const ROOT_DIR = path.resolve(__dirname, '../../../..')
dotenv.config({ path: path.resolve(ROOT_DIR, '.env'), override: true })
dotenv.config({ path: path.resolve(ROOT_DIR, '.env.local'), override: true })

View File

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

View File

@ -0,0 +1,34 @@
import { expect, test } from '@wordpress/e2e-test-utils-playwright'
import { prepareRestApi } from '../support/testHelpers'
const QUERY = 'query { search(version: "6.x", query: "coffee", first: 1) { id } }'
// A regression test to ensure that the former way of sending query requests
// to the plugin's /api endpoint still works, though it's no longer the recommended
// way to do it.
//
// This test can only be expected to work when mod_security is disabled,
// since making a request to the API endpoint with a default (text/plain)
// MIME type is known to result in a 403 due to the OWASP default core ruleset
// as of OWASP 4.3.0.
test('query as plain text', async ({ storageState, baseURL }) => {
expect(process.env.ENABLE_MOD_SECURITY).toEqual('false')
const { requestUtils, requestContext } = await prepareRestApi({ storageState, baseURL })
const url = `http://${process.env.WP_DOMAIN}/wp-json/font-awesome/v1/api?_locale=user`
const response = await requestUtils.request.fetch(url, {
method: 'POST',
data: QUERY,
headers: {
'X-WP-Nonce': requestUtils.storageState.nonce
}
})
expect(response.status()).toEqual(200)
const responseObj = await response.json()
expect(responseObj).toHaveProperty('data.search')
})

View File

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

View File

@ -0,0 +1,53 @@
import { Editor, expect, test } from '@wordpress/e2e-test-utils-playwright'
test.describe('full site editor', async () => {
test.use({
editor: async ({ page }, use) => {
await use(new Editor({ page }))
}
})
test('insert with icon chooser', async ({ page, editor, pageUtils }) => {
const pageLoadPromise = page.waitForResponse('**/wp/v2/pages*')
await page.goto('/wp-admin/site-editor.php?canvas=edit')
await pageLoadPromise
const getStartedCount = await page.getByRole('button', { name: 'Get started' }).count()
if (getStartedCount > 0) {
await page.getByRole('button', { name: 'Get started' }).click()
}
await editor.insertBlock({
name: 'core/paragraph'
})
await page.keyboard.type('Here comes an icon: ')
await editor.clickBlockToolbarButton('More')
await pageUtils.pressKeys('Enter', 1)
await page.waitForSelector('fa-icon-chooser input#search')
const searchResponsePromise = page.waitForResponse('**/font-awesome/v1/api*')
await page.locator('fa-icon-chooser input#search').fill('coffee')
await searchResponsePromise
await page.locator('fa-icon-chooser button.icon').first().click()
let blocks = null
try {
// On WP 6.0.8, this throws an exception, but it's a false negative.
// So, if there's a succesfully call of getBlocks(), we want to
// assert its results. But if that fails, don't fail the whole test.
blocks = await editor.getBlocks()
expect(blocks).toHaveLength(1)
expect(blocks[0].attributes.content).toMatch(/\[icon.*?\]$/)
} catch (_e) {}
})
})

View File

@ -0,0 +1,48 @@
import { Editor, test, expect, login, RequestUtils } from '@wordpress/e2e-test-utils-playwright'
test.describe('blockEditorIconChooser', async () => {
test.beforeEach(async ({ admin }) => {
await admin.createNewPost()
})
test.use({
editor: async ({ page }, use) => {
await use(new Editor({ page }))
}
})
test('search and select from icon chooser', async ({ editor, page, pageUtils }) => {
await editor.insertBlock({
name: 'core/paragraph'
})
await page.keyboard.type('Here comes an icon: ')
await editor.clickBlockToolbarButton('More')
await pageUtils.pressKeys('Enter', 1)
await page.waitForSelector('fa-icon-chooser input#search')
const searchResponsePromise = page.waitForResponse('**/font-awesome/v1/api*')
await page.locator('fa-icon-chooser input#search').fill('coffee')
await searchResponsePromise
await page.locator('fa-icon-chooser button.icon').first().click()
let blocks = null
try {
// On WP 6.0.8, this throws an exception, but it's a false negative.
// So, if there's a succesfully call of getBlocks(), we want to
// assert its results. But if that fails, don't fail the whole test.
blocks = await editor.getBlocks()
expect(blocks).toHaveLength(1)
expect(blocks[0].attributes.content).toMatch(/\[icon.*?\]$/)
} catch (_e) {}
// The loading of the icon chooser should not have messed up the lodash globals.
await expect(page.evaluate(() => 'undefined' !== typeof _ && 'undefined' !== typeof lodash)).toBeTruthy()
})
})

View File

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

View File

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

View File

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

View File

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

View File

@ -1,47 +0,0 @@
import { insertBlock, createNewPost, enablePageDialogAccept, pressKeyTimes, clickBlockToolbarButton, getAllBlocks, loginUser } from '@wordpress/e2e-test-utils'
jest.setTimeout(15000)
let config = null
describe('blockEditorIconChooser', () => {
beforeAll(async () => {
enablePageDialogAccept()
await loginUser(process.env.WP_USERNAME, process.env.WP_PASSWORD)
})
beforeEach(async () => {
await createNewPost()
config = await page.evaluate(() => window.__FontAwesomeOfficialPlugin__)
})
test('works', async () => {
await insertBlock( 'Paragraph' )
await pressKeyTimes('Space', 1)
await clickBlockToolbarButton('More')
await pressKeyTimes('Enter', 1)
const searchInput = await page.waitForFunction(() => {
const chooser = document.querySelector('fa-icon-chooser')
const shadowRoot = chooser && chooser.shadowRoot
return shadowRoot.querySelector('input#search')
})
await searchInput.focus()
await searchInput.type('coffee', { delay: 100 })
await page.waitForResponse(
`${config.apiUrl}/api?_locale=user`
);
const firstIcon = await page.waitForFunction(() => {
return document.querySelector('fa-icon-chooser').shadowRoot.querySelector('button.icon')
})
await firstIcon.click()
const blocks = await getAllBlocks()
expect(blocks).toHaveLength(1)
expect(blocks[0].attributes.content).toMatch(/\[icon.*?\]$/)
})
})

View File

@ -1,32 +0,0 @@
import { visitAdminPage } from '@wordpress/e2e-test-utils'
import { resetOptions } from '../testUtil'
describe('changeTechnology', () => {
beforeAll(async () => {
await visitAdminPage('options-general.php', 'page=font-awesome')
await resetOptions(page)
await page.reload()
})
afterAll(async () => {
await resetOptions(page)
})
test('works', async () => {
const useCdnInput = await page.$('#select_use_cdn')
const useKitInput = await page.$('#select_use_kits')
expect(await useCdnInput.evaluate( i => i.checked )).toBe(true)
expect(await useKitInput.evaluate( i => i.checked )).toBe(false)
const techSvgInput = await page.$('#code_edit_tech_svg')
expect(await techSvgInput.evaluate( i => i.checked )).toBe(false)
await techSvgInput.click()
const submitBefore = await page.waitForSelector('#submit:enabled')
await submitBefore.click()
await page.waitForSelector('#submit:disabled')
expect(await techSvgInput.evaluate( i => i.checked )).toBe(true)
})
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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