From eb3dd15da109cb3e052b1909912bb3e63129faa1 Mon Sep 17 00:00:00 2001 From: Dimitris Spachos Date: Wed, 10 Apr 2024 16:20:03 +0300 Subject: [PATCH 01/82] feat(slb-146): silverback autosave and preview module --- .lagoon/Dockerfile | 2 +- .vscode/settings.json | 4 +- apps/cms/composer.json | 3 + apps/cms/config/sync/core.extension.yml | 1 + .../sync/silverback_autosave.messages.yml | 3 + .../sync/silverback_autosave.settings.yml | 15 + apps/cms/config/sync/system.performance.yml | 4 +- apps/cms/patches/fetch-entity-patch | 40 ++ .../drupal/silverback_autosave/LICENSE.txt | 339 ++++++++++++ .../drupal/silverback_autosave/composer.json | 6 + .../install/silverback_autosave.messages.yml | 1 + .../install/silverback_autosave.settings.yml | 10 + .../schema/silverback_autosave.schema.yml | 56 ++ .../css/silverback_autosave.css | 15 + .../js/silverback_autosave.js | 512 ++++++++++++++++++ .../silverback_autosave.info.yml | 8 + .../silverback_autosave.install | 111 ++++ .../silverback_autosave.libraries.yml | 16 + .../silverback_autosave.links.menu.yml | 6 + .../silverback_autosave.module | 147 +++++ .../silverback_autosave.routing.yml | 7 + .../silverback_autosave.services.yml | 52 ++ .../src/Ajax/OpenAutosaveDisabledDialog.php | 21 + .../Extension/ModuleHandlerEmptyAlter.php | 22 + .../Theme/ThemeManagerEmptyAlter.php | 22 + .../src/EventSubscriber/ConfigSubscriber.php | 130 +++++ .../src/Form/AutosaveButtonClickedTrait.php | 72 +++ .../src/Form/AutosaveEntityFormHandler.php | 327 +++++++++++ .../AutosaveEntityFormHandlerInterface.php | 25 + .../src/Form/AutosaveFormAlterTrait.php | 281 ++++++++++ .../src/Form/AutosaveFormBuilder.php | 254 +++++++++ .../src/Form/AutosaveFormErrorHandler.php | 43 ++ .../src/Form/AutosaveFormInterface.php | 111 ++++ .../src/Form/AutosaveFormSettingsForm.php | 213 ++++++++ .../src/Form/AutosaveFormValidator.php | 80 +++ .../AutosaveEntityFormDatabaseStorage.php | 310 +++++++++++ .../AutosaveEntityFormStorageInterface.php | 203 +++++++ .../Storage/AutosaveFormBackendInterface.php | 83 +++ packages/ui/src/utils/operation.ts | 5 +- 39 files changed, 3553 insertions(+), 7 deletions(-) create mode 100644 apps/cms/config/sync/silverback_autosave.messages.yml create mode 100644 apps/cms/config/sync/silverback_autosave.settings.yml create mode 100644 apps/cms/patches/fetch-entity-patch create mode 100644 packages/drupal/silverback_autosave/LICENSE.txt create mode 100644 packages/drupal/silverback_autosave/composer.json create mode 100644 packages/drupal/silverback_autosave/config/install/silverback_autosave.messages.yml create mode 100644 packages/drupal/silverback_autosave/config/install/silverback_autosave.settings.yml create mode 100644 packages/drupal/silverback_autosave/config/schema/silverback_autosave.schema.yml create mode 100644 packages/drupal/silverback_autosave/css/silverback_autosave.css create mode 100644 packages/drupal/silverback_autosave/js/silverback_autosave.js create mode 100644 packages/drupal/silverback_autosave/silverback_autosave.info.yml create mode 100644 packages/drupal/silverback_autosave/silverback_autosave.install create mode 100644 packages/drupal/silverback_autosave/silverback_autosave.libraries.yml create mode 100644 packages/drupal/silverback_autosave/silverback_autosave.links.menu.yml create mode 100644 packages/drupal/silverback_autosave/silverback_autosave.module create mode 100644 packages/drupal/silverback_autosave/silverback_autosave.routing.yml create mode 100644 packages/drupal/silverback_autosave/silverback_autosave.services.yml create mode 100644 packages/drupal/silverback_autosave/src/Ajax/OpenAutosaveDisabledDialog.php create mode 100644 packages/drupal/silverback_autosave/src/EmptyAlter/Extension/ModuleHandlerEmptyAlter.php create mode 100644 packages/drupal/silverback_autosave/src/EmptyAlter/Theme/ThemeManagerEmptyAlter.php create mode 100644 packages/drupal/silverback_autosave/src/EventSubscriber/ConfigSubscriber.php create mode 100644 packages/drupal/silverback_autosave/src/Form/AutosaveButtonClickedTrait.php create mode 100644 packages/drupal/silverback_autosave/src/Form/AutosaveEntityFormHandler.php create mode 100644 packages/drupal/silverback_autosave/src/Form/AutosaveEntityFormHandlerInterface.php create mode 100644 packages/drupal/silverback_autosave/src/Form/AutosaveFormAlterTrait.php create mode 100644 packages/drupal/silverback_autosave/src/Form/AutosaveFormBuilder.php create mode 100644 packages/drupal/silverback_autosave/src/Form/AutosaveFormErrorHandler.php create mode 100644 packages/drupal/silverback_autosave/src/Form/AutosaveFormInterface.php create mode 100644 packages/drupal/silverback_autosave/src/Form/AutosaveFormSettingsForm.php create mode 100644 packages/drupal/silverback_autosave/src/Form/AutosaveFormValidator.php create mode 100644 packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormDatabaseStorage.php create mode 100644 packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormStorageInterface.php create mode 100644 packages/drupal/silverback_autosave/src/Storage/AutosaveFormBackendInterface.php diff --git a/.lagoon/Dockerfile b/.lagoon/Dockerfile index f81105156..333bcd2cb 100644 --- a/.lagoon/Dockerfile +++ b/.lagoon/Dockerfile @@ -15,7 +15,7 @@ RUN npm install -g pnpm@8.6.12 && pnpm config set store-dir /tmp/cache/pnpm # Copy pnpm lockfile and install dependencies. COPY pnpm-lock.yaml .npmrc /app/ -#COPY patches /app/patches +COPY patches /app/patches RUN --mount=type=cache,target=/tmp/cache pnpm fetch && \ # There is a bug in pnpm: `pnpm fetch` creates _some_ node_modules folders # with _some_ packages. This can lead to an incomplete package installation. diff --git a/.vscode/settings.json b/.vscode/settings.json index 4929af1d2..df21dac26 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -31,5 +31,7 @@ "*.profile": "php" }, "graphql-config.load.filePath": ".graphqlrc.json", - "graphql-config.load.rootDir": "packages/schema" + "graphql-config.load.rootDir": "packages/schema", + "phpsab.executablePathCBF": "./apps/cms/vendor/bin/phpcbf", + "phpsab.executablePathCS": "./apps/cms/vendor/bin/phpcs" } diff --git a/apps/cms/composer.json b/apps/cms/composer.json index d7ca377fd..6235b4cf4 100644 --- a/apps/cms/composer.json +++ b/apps/cms/composer.json @@ -96,6 +96,9 @@ "patches": { "drupal/userprotect": { "Fix site install": "https://www.drupal.org/files/issues/2023-07-28/3349663-8.patch" + }, + "amazeelabs/silverback_gatsby": { + "Autosave preview": "./patches/fetch-entity-patch" } }, "patchLevel": { diff --git a/apps/cms/config/sync/core.extension.yml b/apps/cms/config/sync/core.extension.yml index 42ce257f4..c32cf22ef 100644 --- a/apps/cms/config/sync/core.extension.yml +++ b/apps/cms/config/sync/core.extension.yml @@ -59,6 +59,7 @@ module: role_delegation: 0 serialization: 0 shortcut: 0 + silverback_autosave: 0 silverback_campaign_urls: 0 silverback_cloudinary: 0 silverback_external_preview: 0 diff --git a/apps/cms/config/sync/silverback_autosave.messages.yml b/apps/cms/config/sync/silverback_autosave.messages.yml new file mode 100644 index 000000000..4b81262fb --- /dev/null +++ b/apps/cms/config/sync/silverback_autosave.messages.yml @@ -0,0 +1,3 @@ +_core: + default_config_hash: In3MY-CuVp64RxCeU5X6rsEhR6jR4Si8OG6BX18DALg +entity_saved_in_background_alert_message: 'The content has been modified elsewhere. As a result, autosaving has been disabled and autosaved states for the current page have been deleted. Autosave will be reactivated after reloading the page.' diff --git a/apps/cms/config/sync/silverback_autosave.settings.yml b/apps/cms/config/sync/silverback_autosave.settings.yml new file mode 100644 index 000000000..3bd7d1556 --- /dev/null +++ b/apps/cms/config/sync/silverback_autosave.settings.yml @@ -0,0 +1,15 @@ +_core: + default_config_hash: HaTNbtQe6_A0WF-ZElHNWcv0VK_aqOj5lBEFhAgY85s +interval: 10000 +only_on_form_change: false +active_on: + content_entity_forms: true + config_entity_forms: false +notification: + active: true + message: 'Updating preview...' + delay: 1000 +allowed_content_entity_types: + node: + bundles: + page: page diff --git a/apps/cms/config/sync/system.performance.yml b/apps/cms/config/sync/system.performance.yml index d8dbe9a10..bf0902d1c 100644 --- a/apps/cms/config/sync/system.performance.yml +++ b/apps/cms/config/sync/system.performance.yml @@ -4,7 +4,7 @@ cache: page: max_age: 0 css: - preprocess: true + preprocess: false gzip: true fast_404: enabled: true @@ -12,5 +12,5 @@ fast_404: exclude_paths: '/\/(?:styles|imagecache)\//' html: '404 Not Found

Not Found

The requested URL "@path" was not found on this server.

' js: - preprocess: true + preprocess: false gzip: true diff --git a/apps/cms/patches/fetch-entity-patch b/apps/cms/patches/fetch-entity-patch new file mode 100644 index 000000000..b3a5aeaba --- /dev/null +++ b/apps/cms/patches/fetch-entity-patch @@ -0,0 +1,40 @@ +diff --git a/src/Plugin/GraphQL/DataProducer/FetchEntity.php b/src/Plugin/GraphQL/DataProducer/FetchEntity.php +index ffef6a8..7c84154 100644 +--- a/src/Plugin/GraphQL/DataProducer/FetchEntity.php ++++ b/src/Plugin/GraphQL/DataProducer/FetchEntity.php +@@ -250,6 +250,35 @@ class FetchEntity extends DataProducerPluginBase implements ContainerFactoryPlug + } + } + ++ // Autosave: get autosaved values. ++ if (\Drupal::service('module_handler')->moduleExists('silverback_autosave')) { ++ $context->mergeCacheMaxAge(0); ++ // @todo Add DI to both. ++ $service = \Drupal::service('silverback_autosave.entity_form_storage'); ++ /** ++ * Quick and dirty because this causes leaked metadata error. ++ * $entityForm = \Drupal::service('entity.form_builder')->getForm($entity, 'edit'); ++ * $form_id = $entityForm['form_id']['#value']; ++ */ ++ $form_id = "{$entity->getEntityTypeId()}_{$entity->bundle()}_edit_form"; ++ $autosaved_state = $service->getEntityAndFormState($form_id, $entity->getEntityTypeId(), $entity->id(), $entity->language()->getId(), \Drupal::currentUser()->id()); ++ /** @var \Drupal\Core\Entity\EntityInterface $autosaved_entity */ ++ $autosaved_entity = $autosaved_state['entity'] ?? NULL; ++ /** @var \Drupal\Core\Form\FormStateInterface $autosaved_form_state */ ++ $autosaved_form_state = $autosaved_state['form_state'] ?? []; ++ if ($autosaved_entity && !empty($autosaved_form_state)) { ++ $current_user_input = $autosaved_form_state->getUserInput(); ++ foreach ($autosaved_entity->getFields() as $name => $field) { ++ if (in_array($name, ['title', 'body']) || str_starts_with($name, 'field_')) { ++ if (isset($current_user_input[$name])) { ++ $field->setValue($current_user_input[$name]); ++ } ++ } ++ } ++ return $autosaved_entity; ++ } ++ } ++ + return $entity; + }); + } diff --git a/packages/drupal/silverback_autosave/LICENSE.txt b/packages/drupal/silverback_autosave/LICENSE.txt new file mode 100644 index 000000000..d159169d1 --- /dev/null +++ b/packages/drupal/silverback_autosave/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/packages/drupal/silverback_autosave/composer.json b/packages/drupal/silverback_autosave/composer.json new file mode 100644 index 000000000..d1359e6b8 --- /dev/null +++ b/packages/drupal/silverback_autosave/composer.json @@ -0,0 +1,6 @@ +{ + "name": "drupal/silverback_autosave", + "type": "drupal-module", + "description": "Adds autosave feature on forms.", + "license": "GPL-2.0-or-later" +} diff --git a/packages/drupal/silverback_autosave/config/install/silverback_autosave.messages.yml b/packages/drupal/silverback_autosave/config/install/silverback_autosave.messages.yml new file mode 100644 index 000000000..338d94046 --- /dev/null +++ b/packages/drupal/silverback_autosave/config/install/silverback_autosave.messages.yml @@ -0,0 +1 @@ +entity_saved_in_background_alert_message: 'The content has been modified elsewhere. As a result, autosaving has been disabled and autosaved states for the current page have been deleted. Autosave will be reactivated after reloading the page.' \ No newline at end of file diff --git a/packages/drupal/silverback_autosave/config/install/silverback_autosave.settings.yml b/packages/drupal/silverback_autosave/config/install/silverback_autosave.settings.yml new file mode 100644 index 000000000..c5a0c78aa --- /dev/null +++ b/packages/drupal/silverback_autosave/config/install/silverback_autosave.settings.yml @@ -0,0 +1,10 @@ +interval: 10000 +only_on_form_change: FALSE +active_on: + content_entity_forms: TRUE + config_entity_forms: FALSE +notification: + active: TRUE + message: "Updating preview..." + delay: 1000 +allowed_content_entity_types: { } diff --git a/packages/drupal/silverback_autosave/config/schema/silverback_autosave.schema.yml b/packages/drupal/silverback_autosave/config/schema/silverback_autosave.schema.yml new file mode 100644 index 000000000..86bef3549 --- /dev/null +++ b/packages/drupal/silverback_autosave/config/schema/silverback_autosave.schema.yml @@ -0,0 +1,56 @@ +# Schema for the configuration files of the Autosave Form module. + +silverback_autosave.settings: + type: config_object + label: 'Autosave Form Settings' + mapping: + interval: + type: integer + label: 'The interval used to trigger autosave.' + only_on_form_change: + type: boolean + label: 'Run autosave submission within the specified interval only if there was a form change.' + active_on: + type: mapping + label: 'Active on' + mapping: + content_entity_forms: + type: boolean + label: 'Content entity forms' + config_entity_forms: + type: boolean + label: 'Config entity forms' + notification: + type: mapping + label: 'Notification' + mapping: + active: + type: boolean + label: 'Show notification each time autosave is triggered' + message: + type: text + label: 'Notification message' + delay: + type: integer + label: 'Notification duration' + allowed_content_entity_types: + type: sequence + label: 'Allowed content entity types' + sequence: + type: mapping + label: 'Entity type' + mapping: + bundles: + type: sequence + label: 'Allowed bundles of the entity type' + sequence: + type: string + label: 'Bundle' + +silverback_autosave.messages: + type: config_object + label: 'Autosave Form Messages' + mapping: + entity_saved_in_background_alert_message: + type: string + label: 'The alert message to show to the user if the entity has been saved meanwhile in the background.' diff --git a/packages/drupal/silverback_autosave/css/silverback_autosave.css b/packages/drupal/silverback_autosave/css/silverback_autosave.css new file mode 100644 index 000000000..85d069e50 --- /dev/null +++ b/packages/drupal/silverback_autosave/css/silverback_autosave.css @@ -0,0 +1,15 @@ +#autosave-notification { + display: none; + position: fixed; + bottom: 5px; + right: 5px; + z-index: 9999; + background-color: black; + color: white; + padding: 0.5em 1em; +} + +#autosave-notification:before { + content: "\2714"; + padding-right: 10px; +} diff --git a/packages/drupal/silverback_autosave/js/silverback_autosave.js b/packages/drupal/silverback_autosave/js/silverback_autosave.js new file mode 100644 index 000000000..be91b640e --- /dev/null +++ b/packages/drupal/silverback_autosave/js/silverback_autosave.js @@ -0,0 +1,512 @@ +/** + * @file + * Defines Javascript behaviors for the silverback_autosave module. + */ + +(function ($, Drupal, drupalSettings, once) { + 'use strict'; + + /** + * Define defaults. + */ + Drupal.autosaveForm = { + timer: null, + interval: null, + onlyOnFormChange: false, + autosaveFormRunning: false, + autosaveFormInstances: {}, + initialized: false, + formHasErrors: false, + message: '', + dialog_options: [], + autosave_submit_class: 'autosave-form-save', + autosave_restore_class: 'autosave-form-restore', + autosave_reject_class: 'autosave-form-reject', + notification: { + active: true, + message: Drupal.t('Updating preview...'), + delay: 1000, + }, + form: null, + }; + + /** + * Add a variable which determines if the window is being unloaded. + */ + Drupal.autosaveForm.beforeUnloadCalled = false; + $(window).on('beforeunload pagehide', function () { + Drupal.autosaveForm.beforeUnloadCalled = true; + }); + + /** + * Default dialog options. + */ + Drupal.autosaveForm.defaultDialogOptions = { + open: function () { + $(this).siblings('.ui-dialog-titlebar').remove(); + }, + modal: true, + zIndex: 10000, + position: { my: 'top', at: 'top+25%' }, + autoOpen: true, + width: 'auto', + resizable: false, + closeOnEscape: false, + }; + + /** + * Behaviors the autosave form feature. + * + * @type {Drupal~behavior} + * + * @prop {Drupal~behaviorAttach} attach + * Attaches the autosave behavior. + */ + Drupal.behaviors.autosaveForm = { + attach: function (context, settings) { + var $context = $(context); + var autosave_submit = $context.find( + 'form .' + Drupal.autosaveForm.autosave_submit_class, + ); + + // Disable autosave when the form is submitted in order to prevent race + // conditions and creating further autosave entries after the entity is + // saved. + if (autosave_submit.length > 0) { + Drupal.autosaveForm.form = $(autosave_submit[0]).parents( + 'form.autosave-form', + ); + + Drupal.autosaveForm.form.submit(function () { + if (Drupal.autosaveForm.autosaveFormRunning) { + Drupal.autosaveForm.autosaveFormRunning = false; + clearInterval(Drupal.autosaveForm.timer); + Drupal.autosaveForm.timer = null; + } + }); + } + + if (settings.hasOwnProperty('autosaveForm')) { + $.extend(Drupal.autosaveForm, settings.autosaveForm); + } + + if (Drupal.autosaveForm.initialized) { + // If requested so turn off ajax submission. + if ( + !Drupal.autosaveForm.autosaveFormRunning && + Drupal.autosaveForm.timer + ) { + clearInterval(Drupal.autosaveForm.timer); + Drupal.autosaveForm.timer = null; + } else { + return; + } + } + + // Continue to show the dialog or activate autosave functionality only in + // case the context contains the autosave submit and it is not disabled. + if ( + autosave_submit.length === 0 || + autosave_submit.is(':disabled') || + autosave_submit.hasClass('is-disabled') + ) { + return; + } + Drupal.autosaveForm.autosaveSubmit = autosave_submit; + + if ( + !Drupal.autosaveForm.initialized && + !Drupal.autosaveForm.autosaveFormRunning + ) { + Drupal.autosaveForm.initialized = true; + + $('
') + .appendTo('body') + .append(Drupal.autosaveForm.notification.message); + + // Show the resume/discard confirmation message only if the form does + // not contain any errors, otherwise continue with normal autosave + // submissions. This is for the use case where a form is submitted but + // returned to the user with validation errors in which case we should + // not show the resume/discard message but continue the autosave + // submissions. + if (Drupal.autosaveForm.message && !Drupal.autosaveForm.formHasErrors) { + var dialogOptions = { + buttons: { + button_confirm: { + text: Drupal.t('Resume editing'), + class: 'autosave-form-resume-button', + click: function () { + // Non ajax buttons are bound to click. + // autosave-form-restore + $('.' + Drupal.autosaveForm.autosave_restore_class).trigger( + 'click', + ); + }, + }, + button_reject: { + text: Drupal.t('Discard'), + class: 'autosave-form-reject-button', + click: function () { + triggerAjaxSubmitWithoutProgressIndication( + Drupal.autosaveForm.autosave_reject_class, + true, + ); + $(this).dialog('close'); + }, + primary: true, + }, + }, + close: function (event, ui) { + $(this).remove(); + $(context) + .find('.' + Drupal.autosaveForm.autosave_restore_class) + .remove(); + $(context) + .find('.' + Drupal.autosaveForm.autosave_reject_class) + .remove(); + $(context).find('.autosave-form-restore-discard').remove(); + autosavePeriodic(); + }, + }; + + $.extend( + true, + dialogOptions, + Drupal.autosaveForm.defaultDialogOptions, + Drupal.autosaveForm.dialog_options, + ); + + $('
') + .appendTo('body') + .html('
' + Drupal.autosaveForm.message + '
') + .dialog(dialogOptions); + + // Temp + const btn = $('.autosave-form-restore'); + $('.ui-dialog-buttonset').append(btn[0]); + // .... + } else { + autosavePeriodic(); + } + } + + /** + * Returns the ajax instance corresponding to an element. + * + * @param {string} class_name + * The element class name for which to return its ajax instance. + * + * @return {Drupal.Ajax | null} + * The ajax instance if found, otherwise null. + */ + function findAjaxInstance(class_name) { + if ( + !Drupal.autosaveForm.autosaveFormInstances.hasOwnProperty(class_name) + ) { + var element = document.getElementsByClassName(class_name)[0]; + var ajax = null; + var selector = '#' + element.id; + for (var index in Drupal.ajax.instances) { + if (Drupal.ajax.instances.hasOwnProperty(index)) { + var ajaxInstance = Drupal.ajax.instances[index]; + if (ajaxInstance && ajaxInstance.selector === selector) { + ajax = ajaxInstance; + break; + } + } + } + Drupal.autosaveForm.autosaveFormInstances[class_name] = ajax; + } + return Drupal.autosaveForm.autosaveFormInstances[class_name]; + } + + /** + * Triggers an ajax submit based on the class of the ajax element. + * + * @param {string} ajax_class + * The class of the ajax element. + * @param {boolean} skip_checks + * Skip checks. + */ + function triggerAjaxSubmitWithoutProgressIndication( + ajax_class, + skip_checks = false, + ) { + // If the autosave button suddenly gets the 'is-disabled' class then + // autosave submission should not run until the class is removed. + if ( + Drupal.autosaveForm.autosaveSubmit.is(':disabled') || + Drupal.autosaveForm.autosaveSubmit.hasClass('is-disabled') + ) { + return; + } + + // If configured so run only in the specified interval only if there + // was a form change. + if ( + !skip_checks && + Drupal.autosaveForm.onlyOnFormChange && + !Drupal.autosaveForm.form.data('autosave-form-changed') + ) { + return; + } + + // If there are gutenberg fields, we need to make sure the textarea value + // is updated before autosaving. + const $gutenbergFields = $('.field--gutenberg textarea').first(); + if ($gutenbergFields.length) { + const { data } = window.wp; + $gutenbergFields.each(function (i, element) { + // Update editor textarea with gutenberg content. + $(element).val(data.select('core/editor').getEditedPostContent()); + // We need to update the 'editor-value-is-changed' flag otherwise + // the content won't be updated. + $(element).data({ 'editor-value-is-changed': true }); + $(element).attr('data-editor-value-is-changed', true); + }); + } + + var ajax = findAjaxInstance(ajax_class); + if (ajax) { + if (Drupal.autosaveForm.notification.active) { + $('#autosave-notification') + .fadeIn() + .delay(Drupal.autosaveForm.notification.delay) + .fadeOut(); + } + ajax.success = function (response, status) { + // @todo: On success send a post request to update the preview + + // If interval submission is configured to happen only on form + // change, then reset the changed flag on successful autosave. + if ( + Drupal.autosaveForm.onlyOnFormChange && + Drupal.autosaveForm.form.data('autosave-form-changed') + ) { + Drupal.autosaveForm.form.data('autosave-form-changed', false); + } + + // Call original method with main functionality. + Drupal.Ajax.prototype.success.call(this, response, status); + }; + ajax.options.error = function ( + xmlhttprequest, + text_status, + error_thrown, + ) { + if (xmlhttprequest.status === 0 || xmlhttprequest.status >= 400) { + Drupal.autosaveForm.autosaveFormRunning = false; + clearInterval(Drupal.autosaveForm.timer); + Drupal.autosaveForm.timer = null; + + if (!Drupal.autosaveForm.beforeUnloadCalled) { + var dialogOptions = { + buttons: { + button_confirm: { + text: Drupal.t('Ok'), + primary: true, + click: function () { + $(this).dialog('close'); + }, + }, + }, + }; + $.extend( + true, + dialogOptions, + Drupal.autosaveForm.defaultDialogOptions, + ); + + $('
') + .appendTo('body') + .html( + '
' + + Drupal.t( + 'A server error occurred during autosaving the current page. As a result autosave is disabled. To activate it please revisit the page and continue the editing from the last autosaved state of the current page.', + ) + + '
', + ) + .dialog(dialogOptions); + } + } + }; + // Disable progress indication. + ajax.progress = false; + $(ajax.element).trigger( + ajax.element_settings + ? ajax.element_settings.event + : ajax.elementSettings.event, + ); + } + } + + /** + * Starts periodic autosave submission. + */ + function autosavePeriodic() { + if (Drupal.autosaveForm.interval) { + Drupal.autosaveForm.autosaveFormRunning = true; + + // Run the autosave submission at the beginning to capture the user + // input and compare it later for changes, however wait for sometime + // until triggering the submission in order to let all the Drupal + // behaviors be executed and probably alter the page before doing the + // first submission, otherwise we might capture not the correct user + // input and on the second submission detect changes even if there + // aren't actually any changes. + // @todo Remove this and let autosave attach itself instead as the + // last behavior as soon as the following issues are fixed: + // @see https://www.drupal.org/node/2367655 + // @see https://www.drupal.org/node/2474019 + if (Drupal.autosaveForm.interval > 500) { + setTimeout(function () { + triggerAjaxSubmitWithoutProgressIndication( + Drupal.autosaveForm.autosave_submit_class, + true, + ); + }, 500); + } + + Drupal.autosaveForm.timer = setInterval(function () { + if (!Drupal.ajax.instances.some(isAjaxing)) { + triggerAjaxSubmitWithoutProgressIndication( + Drupal.autosaveForm.autosave_submit_class, + ); + } + }, Drupal.autosaveForm.interval); + } + } + + /** + * Checks if an ajax instance is currently running a submission. + * + * @param {Drupal.Ajax} instance + * The ajax instance. + * + * @return {boolean} + * TRUE if the ajax instance is in a state of submitting. + */ + function isAjaxing(instance) { + return ( + instance && + instance.hasOwnProperty('ajaxing') && + instance.ajaxing === true + ); + } + }, + }; + + /** + * Command to open a dialog for notifying that autosave has been disabled. + * + * We have to use a dedicated command, as otherwise there is no way to define + * the "click" on the button and close the dialog when the button is clicked. + * + * @param {Drupal.Ajax} ajax + * The Drupal Ajax object. + * @param {object} response + * Object holding the server response. + * @param {number} [status] + * The HTTP status code. + */ + Drupal.AjaxCommands.prototype.openAutosaveDisabledDialog = function ( + ajax, + response, + status, + ) { + response.dialogOptions.buttons = { + button_confirm: { + text: Drupal.t('Ok'), + click: function () { + $(this).dialog('close'); + }, + }, + }; + // Remove the "x" button and force confirmation through the "Ok" button. + response.dialogOptions.open = function () { + $(this) + .siblings('.ui-dialog-titlebar') + .find('.ui-dialog-titlebar-close') + .remove(); + }; + + Drupal.AjaxCommands.prototype.openDialog(ajax, response, status); + }; + + /** + * Attach behaviors to monitor changes on entity forms. + * + * This is a modified version of the monitoring functionality provided by + * https://www.drupal.org/project/entity_form_monitor. + * + * @type {Drupal~behavior} + * + * @prop {Drupal~behaviorAttach} attach + * Attaches triggers. + */ + Drupal.behaviors.autosaveFormMonitor = { + attach: function (context, settings) { + var $context = $(context); + var $form = $context.find('form.autosave-form'); + if ($form.length === 0) { + var $form = $context.parents('form.autosave-form'); + } + + // Disable autosave when the form is submitted in order to prevent race + // conditions and creating further autosave entries after the entity is + // saved. + if ($form.length > 0) { + // Detect new elements added through field widgets. + if ($context.find('.ajax-new-content').length > 0) { + $form.data('autosave-form-changed', true); + } + + // Add a change handler that will help us determine if any inputs + // inside the entity forms have changed values. + var inputs = $form + .find(':input, [contenteditable="true"]') + // Filter out buttons + .not('button, input[type="button"]'); + + $(once('autosave-form-input-monitor', inputs)) + .on('change textInput input', function (event) { + var $form = $(event.target).parents('.autosave-form').first(); + if ($form.length) { + var val = $(this).val(); + if (val != $(this).attr('autosave-old-val')) { + $(this).attr('autosave-old-val', val); + $form.data('autosave-form-changed', true); + } + } + }) + // Detect Ajax changes e.g. removing an element. + .on('mousedown', function (event) { + if (event.target.type === 'submit') { + $form.data('autosave-form-changed', true); + } + }); + + // Add change handlers to any CKEditor instances. + if (typeof CKEDITOR !== 'undefined') { + CKEDITOR.on('instanceCreated', function (event) { + event.editor.on('change', function (event) { + // Handle CKEditor change events. + if ( + typeof event.editor !== 'undefined' && + typeof event.target === 'undefined' + ) { + event.target = event.editor.element.$; + } + + var $form = $(event.target).parents('.autosave-form').first(); + if ($form.length) { + $form.data('autosave-form-changed', true); + } + }); + }); + } + } + }, + }; +})(jQuery, Drupal, drupalSettings, once); diff --git a/packages/drupal/silverback_autosave/silverback_autosave.info.yml b/packages/drupal/silverback_autosave/silverback_autosave.info.yml new file mode 100644 index 000000000..6608a328d --- /dev/null +++ b/packages/drupal/silverback_autosave/silverback_autosave.info.yml @@ -0,0 +1,8 @@ +name: Silverback autosave +type: module +description: 'Adds autosave & preview feature on gutenberg nodes.' +package: Content +core_version_requirement: ^9.5 || ^10 +configure: silverback_autosave.admin_settings +dependencies: + - drupal:system diff --git a/packages/drupal/silverback_autosave/silverback_autosave.install b/packages/drupal/silverback_autosave/silverback_autosave.install new file mode 100644 index 000000000..79270393c --- /dev/null +++ b/packages/drupal/silverback_autosave/silverback_autosave.install @@ -0,0 +1,111 @@ + 'Saves the form state of partially filled content entity form for restoration by the silverback_autosave module.', + 'fields' => [ + 'form_id' => [ + 'type' => 'varchar_ascii', + 'length' => AutosaveEntityFormStorageInterface::silverback_autosave_FORM_ID_LENGTH, + 'not null' => TRUE, + ], + // We need the form session id as it is possible that the user opens the + // same form in two tabs and concurrently edits it. Therefore we have to + // assign each form session to an unique auto save session. + // We use the form build id for this and add an extra length to cover any + // case. + 'form_session_id' => [ + 'type' => 'varchar_ascii', + 'length' => AutosaveEntityFormStorageInterface::silverback_autosave_FORM_ID_LENGTH, + 'not null' => TRUE, + ], + 'entity_type_id' => [ + 'type' => 'varchar_ascii', + 'length' => EntityTypeInterface::ID_MAX_LENGTH, + 'not null' => TRUE, + ], + 'entity_id' => [ + 'type' => 'varchar_ascii', + 'length' => EntityTypeInterface::ID_MAX_LENGTH, + 'not null' => TRUE, + ], + 'langcode' => [ + 'type' => 'varchar_ascii', + 'length' => 12, + 'not null' => TRUE, + ], + 'uid' => [ + 'type' => 'int', + 'not null' => TRUE, + ], + 'timestamp' => [ + 'type' => 'int', + 'not null' => TRUE, + ], + 'entity' => [ + 'type' => 'blob', + 'not null' => TRUE, + 'size' => 'big', + ], + 'form_state' => [ + 'type' => 'blob', + 'not null' => TRUE, + 'size' => 'big', + ], + ], + 'primary key' => ['form_id', 'form_session_id', 'entity_type_id', 'entity_id', 'langcode', 'uid', 'timestamp'], + ]; + + return $schema; +} + +/** + * Implements hook_install(). + */ +function silverback_autosave_install() { + // For some reasons it might happen that the autosave form library is not + // loaded or the handler is missing because on module install those both + // cache entries have not been invalidated, therefore we do it manually here. + Cache::invalidateTags(['entity_types', 'library_info']); +} + +/** + * Add the settings for the notification to the settings config. + */ +function silverback_autosave_update_8001() { + $config = \Drupal::configFactory()->getEditable('silverback_autosave.settings'); + if ($config->get('notification') === NULL) { + $config->set('notification', [ + 'active' => TRUE, + 'message' => 'Saving draft...', + 'delay' => 1000, + ]); + $config->save(TRUE); + } +} + +/** + * Populates the setting "only_on_form_change". + */ +function silverback_autosave_update_8002() { + $config = \Drupal::configFactory()->getEditable('silverback_autosave.settings'); + if ($config->get('only_on_form_change') === NULL) { + $config->set('only_on_form_change', FALSE); + $config->save(TRUE); + } +} diff --git a/packages/drupal/silverback_autosave/silverback_autosave.libraries.yml b/packages/drupal/silverback_autosave/silverback_autosave.libraries.yml new file mode 100644 index 000000000..aa5489ad3 --- /dev/null +++ b/packages/drupal/silverback_autosave/silverback_autosave.libraries.yml @@ -0,0 +1,16 @@ +drupal.silverback_autosave: + version: VERSION + css: + theme: + css/silverback_autosave.css: {} + js: + js/silverback_autosave.js: {} + dependencies: + - core/jquery + - core/drupal + - core/drupalSettings + - core/drupal.dialog + - core/drupal.dialog.ajax + - core/drupal.ajax + - core/once + - gutenberg/edit-node diff --git a/packages/drupal/silverback_autosave/silverback_autosave.links.menu.yml b/packages/drupal/silverback_autosave/silverback_autosave.links.menu.yml new file mode 100644 index 000000000..cb94aa739 --- /dev/null +++ b/packages/drupal/silverback_autosave/silverback_autosave.links.menu.yml @@ -0,0 +1,6 @@ +silverback_autosave.admin_settings: + title: 'Silverback autosave and preview settings' + parent: system.admin_config_content + description: 'Change interval and forms on which autosave is active.' + route_name: silverback_autosave.admin_settings + weight: -20 diff --git a/packages/drupal/silverback_autosave/silverback_autosave.module b/packages/drupal/silverback_autosave/silverback_autosave.module new file mode 100644 index 000000000..2f3dabfdb --- /dev/null +++ b/packages/drupal/silverback_autosave/silverback_autosave.module @@ -0,0 +1,147 @@ +hasHandlerClass('silverback_autosave')) { + $entity_type->setHandlerClass('silverback_autosave', AutosaveEntityFormHandler::class); + } + } +} + +/** + * Implements hook_form_alter(). + */ +function silverback_autosave_form_alter(&$form, FormStateInterface $form_state, $form_id) { + + $active_on = \Drupal::configFactory()->get('silverback_autosave.settings')->get('active_on'); + $form_object = $form_state->getFormObject(); + + if ($form_object instanceof ContentEntityFormInterface) { + if (!$active_on['content_entity_forms']) { + return; + } + + $entity = $form_object->getEntity(); + $entity_type_id = $entity->getEntityTypeId(); + $allowed_content_entity_types = \Drupal::configFactory() + ->get('silverback_autosave.settings') + ->get('allowed_content_entity_types'); + + // Autosave is enabled if either no restriction has been made or the current + // entity type is part of the restriction and as well either no restriction + // on bundle level has been made or the current bundle is part of that + // restriction. + if (!empty($allowed_content_entity_types)) { + if (!isset($allowed_content_entity_types[$entity_type_id]) || + (!empty($allowed_content_entity_types[$entity_type_id]['bundles']) && !isset($allowed_content_entity_types[$entity_type_id]['bundles'][$entity->bundle()]))) { + return; + } + } + } + elseif ($form_object instanceof EntityFormInterface) { + if (!$active_on['config_entity_forms']) { + return; + } + } + else { + return; + } + + // Allow autosave only for entity form routes, as forms might be included in + // blocks and other places and it is impossible to determine to which URL we + // have to post the autosave submit to. Also we don't support embedded forms + // as e.g. it might be surprising for the user getting autosave on the entity + // view, because e.g. a block is using an entity form. + $route = \Drupal::routeMatch()->getRouteObject(); + if ($route && ($route_defaults = $route->getDefaults()) && isset($route_defaults['_entity_form'])) { + [$entity_type_id, $form_op] = explode('.', $route_defaults['_entity_form']); + $entity = $form_object->getEntity(); + if (($entity->getEntityTypeId() != $entity_type_id) || ($form_object->getOperation() != $form_op)) { + return; + } + } + else { + return; + } + + $entity_type_manager = \Drupal::entityTypeManager(); + if ($entity_type_manager->hasHandler($entity->getEntityTypeId(), 'silverback_autosave')) { + $silverback_autosave_handler = $entity_type_manager->getHandler($entity->getEntityTypeId(), 'silverback_autosave'); + if ($silverback_autosave_handler instanceof AutosaveEntityFormHandlerInterface) { + $silverback_autosave_handler->formAlter($form, $form_state); + } + } +} + +/** + * Implements hook_entity_update(). + */ +function silverback_autosave_entity_update(EntityInterface $entity) { + static $conflict_enabled; + static $autosave_entity_form_storage; + + if (!isset($autosave_entity_form_storage)) { + /** @var \Drupal\silverback_autosave\Storage\AutosaveEntityFormStorageInterface $autosave_entity_form_storage */ + $autosave_entity_form_storage = \Drupal::service('silverback_autosave.entity_form_storage'); + } + + if (($conflict_enabled === FALSE) || (is_null($conflict_enabled) && !($conflict_enabled = \Drupal::moduleHandler()->moduleExists('conflict')))) { + // If conflict management is not available the autosaved entity states have + // to be removed when the entity is saved. + $autosave_entity_form_storage->purgeAutosavedEntityState($entity->getEntityTypeId(), $entity->id()); + } + else { + $entity_type = $entity->getEntityType(); + if ($entity_type->hasHandlerClass('silverback_autosave') && ($class = $entity_type->getHandlerClass('silverback_autosave'))) { + // If conflict is enabled and the entity is saved then delete only the + // current autosave session of the current user. + if ($autosave_session_id = $class::getAutosaveSessionID($entity)) { + $autosave_entity_form_storage->purgeAutosavedEntityState($entity->getEntityTypeId(), $entity->id(), $autosave_session_id); + } + } + } +} + +/** + * Implements hook_entity_update() for the user entity type. + * + * If the permissions of a user are changed, then we delete the autosave states + * belonging to that user. It is possible that new permissions the user is + * loosing the ability to access certain fields, but the autosave states contain + * data for them. + */ +function silverback_autosave_user_update(EntityInterface $user) { + /** @var \Drupal\user\UserInterface $user */ + $current_roles = $user->getRoles(); + $original_roles = $user->original->getRoles(); + sort($current_roles); + sort($original_roles); + + if ($current_roles !== $original_roles) { + /** @var \Drupal\silverback_autosave\Storage\AutosaveEntityFormStorageInterface $autosave_entity_form_storage */ + $autosave_entity_form_storage = \Drupal::service('silverback_autosave.entity_form_storage'); + $autosave_entity_form_storage->purgeAutosavedEntitiesStates(NULL, NULL, $user->id()); + } +} diff --git a/packages/drupal/silverback_autosave/silverback_autosave.routing.yml b/packages/drupal/silverback_autosave/silverback_autosave.routing.yml new file mode 100644 index 000000000..ad468d60a --- /dev/null +++ b/packages/drupal/silverback_autosave/silverback_autosave.routing.yml @@ -0,0 +1,7 @@ +silverback_autosave.admin_settings: + path: '/admin/config/content/silverback_autosave' + defaults: + _form: '\Drupal\silverback_autosave\Form\AutosaveFormSettingsForm' + _title: 'Silverback autosave settings' + requirements: + _permission: 'administer site configuration' diff --git a/packages/drupal/silverback_autosave/silverback_autosave.services.yml b/packages/drupal/silverback_autosave/silverback_autosave.services.yml new file mode 100644 index 000000000..8a25c019c --- /dev/null +++ b/packages/drupal/silverback_autosave/silverback_autosave.services.yml @@ -0,0 +1,52 @@ +services: + silverback_autosave.entity_form_storage: + class: \Drupal\silverback_autosave\Storage\AutosaveEntityFormDatabaseStorage + arguments: ['@database', '@serialization.phpserialize'] + + form_validator.silverback_autosave: + public: false + class: \Drupal\silverback_autosave\Form\AutosaveFormValidator + decorates: form_validator + arguments: + [ + '@form_validator.silverback_autosave.inner', + '@request_stack', + '@string_translation', + '@csrf_token', + '@logger.channel.form', + '@form_error_handler', + ] + form_builder.silverback_autosave: + public: false + class: \Drupal\silverback_autosave\Form\AutosaveFormBuilder + decorates: form_builder + # The decorated autosave form builder should be the first applied as it + # will not call further decorators, but directly override the core form + # builder by extending from it and just calling the parent methods. + decoration_priority: 10000 + arguments: + [ + '@form_builder.silverback_autosave.inner', + '@form_validator', + '@form_submitter', + '@form_cache', + '@module_handler', + '@event_dispatcher', + '@request_stack', + '@class_resolver', + '@element_info', + '@theme.manager', + '@?csrf_token', + '@silverback_autosave.entity_form_storage', + ] + form_error_handler.silverback_autosave: + public: false + class: Drupal\silverback_autosave\Form\AutosaveFormErrorHandler + decorates: form_error_handler + arguments: ['@form_error_handler.silverback_autosave.inner'] + + silverback_autosave.config_subscriber: + class: Drupal\silverback_autosave\EventSubscriber\ConfigSubscriber + tags: + - { name: event_subscriber } + arguments: ['@silverback_autosave.entity_form_storage'] diff --git a/packages/drupal/silverback_autosave/src/Ajax/OpenAutosaveDisabledDialog.php b/packages/drupal/silverback_autosave/src/Ajax/OpenAutosaveDisabledDialog.php new file mode 100644 index 000000000..7fad3b04f --- /dev/null +++ b/packages/drupal/silverback_autosave/src/Ajax/OpenAutosaveDisabledDialog.php @@ -0,0 +1,21 @@ + 'openAutosaveDisabledDialog'] + parent::render(); + } + +} diff --git a/packages/drupal/silverback_autosave/src/EmptyAlter/Extension/ModuleHandlerEmptyAlter.php b/packages/drupal/silverback_autosave/src/EmptyAlter/Extension/ModuleHandlerEmptyAlter.php new file mode 100644 index 000000000..ce2975c10 --- /dev/null +++ b/packages/drupal/silverback_autosave/src/EmptyAlter/Extension/ModuleHandlerEmptyAlter.php @@ -0,0 +1,22 @@ +autosaveEntityFormStorage = $autosave_entity_form_storage; + } + + /** + * Purges all autosave states in case a form-related config is saved. + * + * Deletes all autosaved states if an important property of a form related + * config is changed. We cannot know if some kind of an inline reference has + * been used and its display has been changed, which is why we purge the whole + * autosave storage. Otherwise a much more complex detection mechanism will + * be needed. We though try to prevent the purging in case a change on an + * property is made, which doesn't influence the form functionality - e.g. if + * the label of a field is changed then we don't have to purge the autosave + * states. + * + * @param \Drupal\Core\Config\ConfigCrudEvent $event + * The configuration event. + */ + public function onConfigSave(ConfigCrudEvent $event) { + $saved_config = $event->getConfig(); + $name = $saved_config->getName(); + + // We try to prevent purging autosave states unnecessarily by defining + // properties, which are allowed to change and don't have an influence on + // the form functionality. + $allowed_changes = []; + $purge_autosave_states = TRUE; + if (strpos($name, 'field.field.') === 0) { + $allowed_changes = [ + 'dependencies', + 'label', + 'description', + 'required', + 'default_value', + 'default_value_callback', + ]; + } + elseif (strpos($name, 'field.storage.') === 0) { + $allowed_changes = [ + 'dependencies', + 'module', + 'indexes', + 'persist_with_no_fields', + 'custom_storage', + ]; + } + elseif (strpos($name, 'core.entity_form_display.') === 0) { + $allowed_changes = [ + 'hidden', + ]; + } + elseif (strpos($name, 'user.role.') === 0) { + $allowed_changes = [ + 'label', + 'weight', + ]; + } + else { + $purge_autosave_states = FALSE; + } + + if ($purge_autosave_states) { + $property_names = array_diff(array_keys($saved_config->getRawData()), $allowed_changes); + foreach ($property_names as $property_name) { + if ($event->isChanged($property_name)) { + $this->autosaveEntityFormStorage->purgeAutosavedEntitiesStates(); + break; + } + } + } + } + + /** + * Purges all autosave states in case a form-related config is deleted. + * + * @param \Drupal\Core\Config\ConfigCrudEvent $event + * The configuration event. + */ + public function onConfigDelete(ConfigCrudEvent $event) { + $deleted_config = $event->getConfig(); + $name = $deleted_config->getName(); + + $purge_autosave_states = (strpos($name, 'field.field.') === 0) || + (strpos($name, 'field.storage.') === 0) || + (strpos($name, 'core.entity_form_display.') === 0) || + (strpos($name, 'user.role.') === 0); + + if ($purge_autosave_states) { + $this->autosaveEntityFormStorage->purgeAutosavedEntitiesStates(); + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events[ConfigEvents::SAVE][] = ['onConfigSave']; + $events[ConfigEvents::DELETE][] = ['onConfigDelete']; + return $events; + } + +} diff --git a/packages/drupal/silverback_autosave/src/Form/AutosaveButtonClickedTrait.php b/packages/drupal/silverback_autosave/src/Form/AutosaveButtonClickedTrait.php new file mode 100644 index 000000000..a51d17f7b --- /dev/null +++ b/packages/drupal/silverback_autosave/src/Form/AutosaveButtonClickedTrait.php @@ -0,0 +1,72 @@ +getTriggeringElement(); + if (is_null($triggering_element)) { + $user_input = $form_state->getUserInput(); + $autosave = isset($user_input['_triggering_element_name']) && ($user_input['_triggering_element_name'] == AutosaveFormInterface::AUTOSAVE_ELEMENT_NAME); + } + else { + $autosave = $triggering_element && !empty($triggering_element['#silverback_autosave']); + } + return $autosave; + } + + /** + * Checks if autosave restore has been triggered. + * + * @param array $form + * The form array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return bool + * TRUE if autosave restore has been triggered, FALSE otherwise. + */ + protected function isRestoreTriggered($form, FormStateInterface $form_state) { + $input = $form_state->getUserInput(); + // The restore submit is a non-ajax element and therefore its name will be + // contained in the user input as a key. + $triggered = isset($input[AutosaveFormInterface::AUTOSAVE_RESTORE_ELEMENT_NAME]); + return $triggered; + } + + /** + * Checks if autosave restore has been triggered. + * + * @param array $form + * The form array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return bool + * TRUE if autosave restore has been triggered, FALSE otherwise. + */ + protected function isRejectTriggered($form, FormStateInterface $form_state) { + $user_input = $form_state->getUserInput(); + // The reject submit is an ajax element and therefore its name will not be + // contained in the user input as a key, but will be contained as a value + // under the key "_triggering_element_name". + $triggered = isset($user_input['_triggering_element_name']) && ($user_input['_triggering_element_name'] == AutosaveFormInterface::AUTOSAVE_REJECT_ELEMENT_NAME); + return $triggered; + } + +} diff --git a/packages/drupal/silverback_autosave/src/Form/AutosaveEntityFormHandler.php b/packages/drupal/silverback_autosave/src/Form/AutosaveEntityFormHandler.php new file mode 100644 index 000000000..562f1b3a2 --- /dev/null +++ b/packages/drupal/silverback_autosave/src/Form/AutosaveEntityFormHandler.php @@ -0,0 +1,327 @@ +entityTypeId = $entity_type->id(); + $this->entityType = $entity_type; + $this->entityStorage = $entity_type_manager->getStorage($entity_type->id()); + $this->currentUser = $current_user; + $this->autosaveEntityFormStorage = $autosave_entity_form_storage; + $this->time = $time; + $this->dateFormatter = $date_formatter; + $this->configFactory = $config_factory; + $this->keyValueExpirableFactory = $key_value_expirable_factory; + $this->conflictEnabled = $module_handler->moduleExists('conflict'); + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { + return new static( + $entity_type, + $container->get('entity_type.manager'), + $container->get('current_user'), + $container->get('silverback_autosave.entity_form_storage'), + $container->get('datetime.time'), + $container->get('date.formatter'), + $container->get('config.factory'), + $container->get('keyvalue.expirable'), + $container->get('module_handler') + ); + } + + /** + * Performs the needed alterations to the entity form. + * + * @param array $form + * The entity form to be altered to provide the autosave functionality. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + public function formAlter(array &$form, FormStateInterface $form_state) { + $this->traitFormAlter($form, $form_state); + // Flag the entity with the current autosave session ID. + $form['#entity_builders'][] = [static::class, 'entityFormEntityBuild']; + } + + /** + * Ajax callback for autosaving content entity forms. + */ + public function autosaveFormAjax($form, FormStateInterface $form_state) { + $response = $this->traitAutosaveFormAjax($form, $form_state); + $timestamp = $form_state->getTemporaryValue('silverback_autosave_last_autosave_timestamp'); + if ($timestamp == 'entity_saved_meanwhile') { + $input = $form_state->getUserInput(); + // Autosave has run already or this is the first run. + if (is_numeric($input['silverback_autosave_last_autosave_timestamp']) || empty($input['silverback_autosave_last_autosave_timestamp'])) { + // Alert the user about turning off autosave and deleting all the + // autosaved states. + $message = $this->configFactory->get('silverback_autosave.messages') + ->get('entity_saved_in_background_alert_message'); + + $options = [ + 'width' => 'auto', + 'closeOnEscape' => FALSE, + ]; + $response->addCommand(new OpenAutosaveDisabledDialog($this->t('Autosave has been disabled'), $message, $options)); + + // Reset the last autosaved timestamp on the page. + $response->addCommand(new InvokeCommand('input[name="silverback_autosave_last_autosave_timestamp"]', 'attr', ['value', (string) $this->t('Autosave is turned off.')])); + // Stop the autosave submissions. + $attachments = []; + $attachments['drupalSettings']['autosaveForm']['autosaveFormRunning'] = FALSE; + $response->addAttachments($attachments); + } + } + + return $response; + } + + /** + * Entity builder method. + * + * Flags the entity with the autosave form session ID. + * + * @param string $entity_type + * The type of the entity. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity whose form is being built. + */ + public static function entityFormEntityBuild($entity_type, EntityInterface $entity, array $form, FormStateInterface $form_state) { + if ($silverback_autosave_session_id = static::getAutosaveFormSessionID($form_state)) { + $entity->{static::AUTOSAVE_SESSION_ID} = $silverback_autosave_session_id; + } + } + + /** + * {@inheritdoc} + */ + public function isAutosaveSubmitValid(FormStateInterface $form_state) { + [$form_id, $entity] = $this->getFormIDandEntity($form_state); + + // Check that the entity is still valid. + if ($entity instanceof EntityChangedInterface) { + $changed_time = $entity->getChangedTime(); + + $input = $form_state->getUserInput(); + $changed_form_value = $input['changed'] ?? NULL; + $entity->setChangedTime($changed_form_value ?: $changed_time); + + if (!$this->conflictEnabled && ($unchanged = $this->entityStorage->loadUnchanged($entity->id())) && ($unchanged->getChangedTimeAcrossTranslations() > $entity->getChangedTimeAcrossTranslations())) { + $form_state->setTemporaryValue('silverback_autosave_last_autosave_timestamp', 'entity_saved_meanwhile'); + return FALSE; + } + else { + $entity->setChangedTime($changed_time); + } + } + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function getLastAutosavedFormState(FormStateInterface $form_state, $silverback_autosave_session_id, $uid) { + [$form_id, $entity] = $this->getFormIDandEntity($form_state); + return $this->autosaveEntityFormStorage->getFormState($form_id, $entity->getEntityTypeId(), $entity->id(), $entity->language()->getId(), $uid, $silverback_autosave_session_id); + } + + /** + * {@inheritdoc} + */ + public function storeState(FormStateInterface $form_state, $silverback_autosave_session_id, $autosave_timestamp, $uid) { + [$form_id, $entity] = $this->getFormIDandEntity($form_state); + $this->autosaveEntityFormStorage->storeEntityAndFormState($form_id, $silverback_autosave_session_id, $entity->getEntityTypeId(), $entity->id(), $entity->language()->getId(), $uid, $autosave_timestamp, $entity, $form_state); + } + + /** + * {@inheritdoc} + */ + public function getLastAutosavedTimestamp(FormStateInterface $form_state, $uid) { + [$form_id, $entity] = $this->getFormIDandEntity($form_state); + return $entity->isNew() ? NULL : $this->autosaveEntityFormStorage->getLastAutosavedStateTimestamp($form_id, $entity->getEntityTypeId(), $entity->id(), $entity->language()->getId(), $uid); + } + + /** + * {@inheritdoc} + */ + public function purgeCurrentAutosavedState(FormStateInterface $form_state, $uid) { + [$form_id, $entity] = $this->getFormIDandEntity($form_state); + $this->autosaveEntityFormStorage->purgeAutosavedEntityState($entity->getEntityTypeId(), $entity->id(), $this->getAutosaveFormSessionID($form_state), $form_id, $entity->language()->getId(), $uid); + } + + /** + * {@inheritdoc} + */ + public function purgeAllAutosavedStates(FormStateInterface $form_state, $uid) { + [$form_id, $entity] = $this->getFormIDandEntity($form_state); + $this->autosaveEntityFormStorage->purgeAutosavedEntityState($entity->getEntityTypeId(), $entity->id(), NULL, $form_id, $entity->language()->getId(), $uid); + } + + /** + * {@inheritdoc} + */ + public function isAutosaveEnabled(FormStateInterface $form_state) { + [$form_id, $entity] = $this->getFormIDandEntity($form_state); + $allowed = !$entity->isNew() && !$this->currentUser->isAnonymous(); + return $allowed; + } + + /** + * {@inheritdoc} + */ + public static function getAutosaveSessionID(EntityInterface $entity) { + if (isset($entity->{static::AUTOSAVE_SESSION_ID})) { + return $entity->{static::AUTOSAVE_SESSION_ID}; + } + else { + return NULL; + } + } + + /** + * Retrieves the form ID and the form entity object from the form state. + * + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return array + * The first value is the form ID and the second the form entity object. + */ + protected function getFormIDandEntity(FormStateInterface $form_state) { + /** @var \Drupal\Core\Entity\EntityFormInterface $form_object */ + $form_object = $form_state->getFormObject(); + $form_id = $form_object->getFormId(); + $entity = $form_object->getEntity(); + return [$form_id, $entity]; + } + +} diff --git a/packages/drupal/silverback_autosave/src/Form/AutosaveEntityFormHandlerInterface.php b/packages/drupal/silverback_autosave/src/Form/AutosaveEntityFormHandlerInterface.php new file mode 100644 index 000000000..73533bb0c --- /dev/null +++ b/packages/drupal/silverback_autosave/src/Form/AutosaveEntityFormHandlerInterface.php @@ -0,0 +1,25 @@ +isAutosaveEnabled($form_state)) { + return; + } + + $form['#attributes']['class'][] = 'autosave-form'; + $form['#attached']['library'][] = 'silverback_autosave/drupal.silverback_autosave'; + $form['#attached']['drupalSettings']['autosaveForm']['interval'] = $this->configFactory->get('silverback_autosave.settings')->get('interval'); + $form['#attached']['drupalSettings']['autosaveForm']['onlyOnFormChange'] = $this->configFactory->get('silverback_autosave.settings')->get('only_on_form_change'); + $form['#attached']['drupalSettings']['autosaveForm']['notification'] = $this->configFactory->get('silverback_autosave.settings')->get('notification'); + $input = $form_state->getUserInput(); + + // $show_restore_discard = !$form_state->isRebuilding() ?: !empty($input['autosave_restore_discard']); + $show_restore_discard = FALSE; + if ($show_restore_discard && !$form_state->get('silverback_autosave_state_timestamp') && !$form_state->get('silverback_autosave_rejected') && ($silverback_autosave_state_timestamp = $this->getLastAutosavedTimestamp($form_state, $this->currentUser->id()))) { + $form[AutosaveFormInterface::AUTOSAVE_RESTORE_ELEMENT_NAME] = [ + '#type' => 'submit', + '#name' => AutosaveFormInterface::AUTOSAVE_RESTORE_ELEMENT_NAME, + '#value' => $this->t('Autosave restore'), + '#limit_validation_errors' => [], + // '#attributes' => ['class' => ['autosave-form-restore', 'visually-hidden']], + '#attributes' => ['class' => ['autosave-form-restore']], + '#submit' => [[$this, 'autosaveFormRestoreSubmit']], + '#silverback_autosave_state_timestamp' => $silverback_autosave_state_timestamp, + ]; + $form[AutosaveFormInterface::AUTOSAVE_REJECT_ELEMENT_NAME] = [ + '#type' => 'submit', + '#name' => 'silverback_autosave_reject', + '#value' => $this->t('Autosave reject'), + '#limit_validation_errors' => [], + '#attributes' => ['class' => ['autosave-form-reject', 'visually-hidden']], + '#submit' => [[$this, 'autosaveFormRejectSubmit']], + '#ajax' => [ + 'callback' => [$this, 'autosaveFormRejectAjax'], + ], + ]; + $form['autosave_restore_discard'] = [ + '#type' => 'hidden', + '#default_value' => 'autosave_restore_discard', + '#attributes' => ['class' => ['autosave-form-restore-discard']], + ]; + + // Add the message to be shown on the form. Our JS library will check if + // the message exist and only then offer the options for restore and + // reject, otherwise will start the auto save process. + $date = $this->dateFormatter->format($silverback_autosave_state_timestamp, 'custom', 'M d, Y H:i'); + $message = $this->t('A version of this page you were editing at @date was saved as a draft. Do you want to resume editing or discard it?', ['@date' => $date]); + $form['#attached']['drupalSettings']['autosaveForm']['message'] = (string) $message; + } + + $silverback_autosave_session_id = $this->getAutosaveFormSessionID($form_state); + if (!$silverback_autosave_session_id) { + $silverback_autosave_session_id = !empty($input['silverback_autosave_session_id']) ? $input['silverback_autosave_session_id'] : $form['#build_id']; + $this->setAutosaveFormSessionID($form_state, $silverback_autosave_session_id); + } + + $form['silverback_autosave_session_id'] = [ + '#type' => 'hidden', + '#value' => $silverback_autosave_session_id, + '#name' => 'silverback_autosave_session_id', + // Form processing and validation requires this value, so ensure the + // submitted form value appears literally, regardless of custom #tree + // and #parents being set elsewhere. + '#parents' => ['silverback_autosave_session_id'], + ]; + + $form[AutosaveFormInterface::AUTOSAVE_ELEMENT_NAME] = [ + '#type' => 'submit', + '#name' => AutosaveFormInterface::AUTOSAVE_ELEMENT_NAME, + '#value' => $this->t('Autosave save'), + '#attributes' => ['class' => ['autosave-form-save', 'visually-hidden']], + '#submit' => [[$this, 'autosaveFormSubmit']], + '#ajax' => [ + 'callback' => [$this, 'autosaveFormAjax'], + // Do not refocus to prevent losing focus of the element the user might + // be currently editing when the autosave submission is triggered. + 'disable-refocus' => TRUE, + 'progress' => FALSE, + ], + '#silverback_autosave' => TRUE, + // Retrieve the "silverback_autosave_session_id" also from the form state as on + // autosave restore the one from the restored state will be present in + // the form state storage and we want to continue using that session for + // the further autosave states after the restoration. + '#silverback_autosave_session_id' => $silverback_autosave_session_id, + ]; + + $form['silverback_autosave_last_autosave_timestamp'] = [ + '#type' => 'hidden', + '#name' => 'silverback_autosave_last_autosave_timestamp', + '#value' => $form_state->get('silverback_autosave_last_autosave_timestamp') ?: '', + ]; + } + + /** + * Form submission handler for restoring autosaved state. + */ + public function autosaveFormRestoreSubmit($form, FormStateInterface $form_state) { + $triggering_element = $form_state->getTriggeringElement(); + if (!empty($triggering_element['#silverback_autosave_state_timestamp'])) { + // Set the timestamp of the autosaved state which has to be used to + // restore the form on rebuild. + $form_state->set('silverback_autosave_state_timestamp', $triggering_element['#silverback_autosave_state_timestamp']); + $form_state->setRebuild(); + } + } + + /** + * Form submission handler for rejecting autosaved states. + */ + public function autosaveFormRejectSubmit($form, FormStateInterface $form_state) { + // As this processing might take some time we want to prevent that if the + // connection is terminated the user input will be lost. + ignore_user_abort(TRUE); + // Mark the form state with a flag indicating an action for autosave has + // been chosen. + $form_state->set('silverback_autosave_rejected', TRUE); + // Cache the form state in order for the flag to be kept. + $form_state->setCached(); + + // Purge the autosave states. + $this->purgeAllAutosavedStates($form_state, $this->currentUser->id()); + } + + /** + * Ajax callback for rejecting autosaved states. + */ + public function autosaveFormRejectAjax($form, FormStateInterface $form_state) { + return new AjaxResponse(); + } + + /** + * Form submission handler for autosaving forms. + */ + public function autosaveFormSubmit($form, FormStateInterface $form_state) { + // As this processing might take some time we want to prevent that if the + // connection is terminated the user input will be lost. + ignore_user_abort(TRUE); + if (!$this->isAutosaveSubmitValid($form_state)) { + $form_state->disableCache(); + return; + } + // Having an autosave form session id also ensures that after resuming + // editing the new autosaved entities will be saved to the same autosave + // session id. + $silverback_autosave_session_id = $this->getAutosaveFormSessionID($form_state); + $current_user_id = $this->currentUser->id(); + $autosaved_form_state = $this->getLastAutosavedFormState($form_state, $silverback_autosave_session_id, $current_user_id); + // If there is non-autosaved state for this session then we have to put the + // user input into a temporary store and on each autosave submit compare + // against it for changes and after the first change compare with the last + // autosaved state. + if (is_null($autosaved_form_state)) { + if ($initial_user_input = $this->keyValueExpirableFactory->get('silverback_autosave')->get($silverback_autosave_session_id)) { + $autosaved_form_state_input = $initial_user_input; + } + else { + // 6 hours cache life time for forms should be plenty, like the form + // cache. + $expire = 21600; + $this->keyValueExpirableFactory->get('silverback_autosave')->setWithExpire($silverback_autosave_session_id, $form_state->getUserInput(), $expire); + + // This is the first where we cache the user input initially and we are + // done. + $form_state->disableCache(); + return; + } + } + else { + $autosaved_form_state_input = $autosaved_form_state->getUserInput(); + } + + // Subsequent autosaving - compare the user input only. This should be + // sufficient to detect changes in the fields. + $form_state_input = $form_state->getUserInput(); + + $skip_from_comparison_keys = [ + 'form_build_id', + 'form_token', + 'ajax_page_state', + 'silverback_autosave_last_autosave_timestamp', + AutosaveFormInterface::AUTOSAVE_RESTORE_ELEMENT_NAME, + AutosaveFormInterface::AUTOSAVE_REJECT_ELEMENT_NAME, + 'autosave_restore_discard', + ]; + foreach ($skip_from_comparison_keys as $skip_from_comparison_key) { + unset($autosaved_form_state_input[$skip_from_comparison_key]); + unset($form_state_input[$skip_from_comparison_key]); + } + + $store = $autosaved_form_state_input != $form_state_input; + + if ($store) { + $autosave_timestamp = $this->time->getRequestTime(); + $form_state->set('silverback_autosave_last_autosave_timestamp', $autosave_timestamp); + $form_state->setTemporaryValue('silverback_autosave_last_autosave_timestamp', $autosave_timestamp); + + $this->storeState($form_state, $silverback_autosave_session_id, $autosave_timestamp, $current_user_id); + $this->keyValueExpirableFactory->get('silverback_autosave')->delete($silverback_autosave_session_id); + } + + // We don't have to cache the form each time an autosave submission is + // triggered, especially when we've skipped the form validation. + $form_state->disableCache(); + } + + /** + * Ajax callback for autosaving forms. + */ + public function autosaveFormAjax($form, FormStateInterface $form_state) { + $response = new AjaxResponse(); + + $timestamp = $form_state->getTemporaryValue('silverback_autosave_last_autosave_timestamp'); + if (is_numeric($timestamp)) { + $response->addCommand(new InvokeCommand('input[name="silverback_autosave_last_autosave_timestamp"]', 'attr', ['value', $timestamp])); + } + + return $response; + } + + /** + * Retrieves the autosave form session ID. + * + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return string|null + * The autosave form session ID or NULL if none present yet. + */ + protected static function getAutosaveFormSessionID(FormStateInterface $form_state) { + return $form_state->get('silverback_autosave_session_id'); + } + + /** + * Sets the autosave form session ID into the form state. + * + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * @param string $silverback_autosave_session_id + * The autosave form session ID. + */ + protected function setAutosaveFormSessionID(FormStateInterface $form_state, $silverback_autosave_session_id) { + $form_state->set('silverback_autosave_session_id', $silverback_autosave_session_id); + } + + /** + * Returns the HTTP method used by the request that is building the form. + * + * @return string + * Can be any valid HTTP method, such as GET, POST, HEAD, etc. + */ + protected function getRequestMethod() { + return \Drupal::requestStack()->getCurrentRequest()->getMethod(); + } + +} diff --git a/packages/drupal/silverback_autosave/src/Form/AutosaveFormBuilder.php b/packages/drupal/silverback_autosave/src/Form/AutosaveFormBuilder.php new file mode 100644 index 000000000..cc5896905 --- /dev/null +++ b/packages/drupal/silverback_autosave/src/Form/AutosaveFormBuilder.php @@ -0,0 +1,254 @@ +formBuilder = $form_builder; + $this->autosaveEntityFormStorage = $autosave_entity_form_storage; + } + + /** + * + */ + public function buildForm($form_id, FormStateInterface &$form_state) { + $form = parent::buildForm($form_id, $form_state); + if ($form_state::hasAnyErrors()) { + // Under circumstances it might happen that the form is submitted but + // returned with validation errors and the form alter hooks are executed + // thus leading to the autosave form alter code being executed as well and + // putting the autosave resume/discard message to the form, which should + // not happen if the form is being returned to the browser with validation + // errors. In order to prevent this we have to add the resume/discard + // message and options only on HTTP GET requests or on POST requests if + // restore or reject submit operations have been performed or in a more + // complex case if the message has not been yet confirmed but other + // AJAX / POST requests are being triggered in the background. As we could + // not detect the last case we still put the form elements into the form, + // but on the client side we will not show the message if the form is + // returned with validation errors. + $form['#attached']['drupalSettings']['autosaveForm']['formHasErrors'] = TRUE; + + // Additionally unset the form elements and settings which might have been + // added, but aren't actually needed. + unset($form['#attached']['drupalSettings']['autosaveForm']['message']); + unset($form[AutosaveFormInterface::AUTOSAVE_RESTORE_ELEMENT_NAME]); + unset($form[AutosaveFormInterface::AUTOSAVE_REJECT_ELEMENT_NAME]); + unset($form['autosave_restore_discard']); + } + return $form; + } + + /** + * {@inheritdoc} + */ + public function processForm($form_id, &$form, FormStateInterface &$form_state) { + if ($this->isAutosaveTriggered($form_state)) { + // @todo should we add a condition, that the form state is already cached + // in order to stop fully processing the form? + $this->doBuildFormSkip = TRUE; + + // As we'll skip doBuildForm we have to take care of setting the + // triggering element. + $form_state->setTriggeringElement($form[AutosaveFormInterface::AUTOSAVE_ELEMENT_NAME]); + + // Needed to execute the submit handler, as this will not be done if + // duBuildForm is not being executed. + $form_state->setSubmitHandlers($form[AutosaveFormInterface::AUTOSAVE_ELEMENT_NAME]['#submit']); + $form_state->setProcessInput(); + $form_state->setSubmitted(); + } + + $response = parent::processForm($form_id, $form, $form_state); + $this->doBuildFormSkip = FALSE; + + return $response; + } + + /** + * {@inheritdoc} + */ + public function doBuildForm($form_id, &$element, FormStateInterface &$form_state) { + return $this->doBuildFormSkip ? $element : parent::doBuildForm($form_id, $element, $form_state); + } + + /** + * {@inheritdoc} + */ + public function rebuildForm($form_id, FormStateInterface &$form_state, $old_form = NULL) { + $this->restoreAutosavedState($form_id, $form_state); + return parent::rebuildForm($form_id, $form_state, $old_form); + } + + /** + * Restores an autosaved form state. + * + * @param $form_id + * The form id. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + protected function restoreAutosavedState($form_id, FormStateInterface $form_state) { + if (!$form_state->get('silverback_autosave_restored') && ($autosaved_timestamp = $form_state->get('silverback_autosave_state_timestamp'))) { + $form_object = $form_state->getFormObject(); + + // Restore entity form. + if ($form_object instanceof EntityFormInterface) { + $entity = $form_object->getEntity(); + $autosaved_state = $this->autosaveEntityFormStorage->getEntityAndFormState($form_id, $entity->getEntityTypeId(), $entity->id(), $entity->language()->getId(), $this->currentUser()->id(), NULL, $autosaved_timestamp); + + if (is_null($autosaved_state)) { + // @todo Cover the case that the autosaved state has been purged + // meanwhile. + return; + } + + /** @var \Drupal\Core\Entity\EntityInterface $autosaved_entity */ + $autosaved_entity = $autosaved_state['entity']; + /** @var \Drupal\Core\Form\FormStateInterface $autosaved_form_state */ + $autosaved_form_state = $autosaved_state['form_state']; + + // Restore the form with the entity from the autosaved state. + $form_object->setEntity($autosaved_entity); + // Restore the user input. + $current_user_input = $form_state->getUserInput(); + $autosaved_user_input = $autosaved_form_state->getUserInput(); + // We have to rebuild the form and keep the generated form token + // instead of putting the one from the autosaved input, otherwise the + // form builder will set an form state error and, which is going to + // result into an exception, as setting form state errors after the + // validation phase is forbidden. + if (isset($current_user_input['form_token'])) { + $autosaved_user_input['form_token'] = $current_user_input['form_token']; + } + $form_state->setUserInput($autosaved_user_input); + // Recover the form state storage, which is needed to continue from the + // state at which the form was left. + $form_state->setStorage($autosaved_form_state->getStorage()); + + // Flag the form state as being restored from autosave. + $form_state->set('silverback_autosave_restored', TRUE); + } + elseif ($form_object instanceof FormInterface) { + // @todo add support for regular forms. + } + } + } + + /** + * {@inheritdoc} + */ + public function prepareForm($form_id, &$form, FormStateInterface &$form_state) { + $prevent_hooks = FALSE; + + if ($this->isAutosaveTriggered($form_state)) { + // There is no need of generating a new form build id after triggering + // autosave. + $form['#build_id'] = $form_state->getUserInput()['form_build_id']; + + if ($form_state->isCached()) { + $prevent_hooks = TRUE; + } + } + + if ($prevent_hooks) { + // Prevent running hooks. + $module_handler = $this->moduleHandler; + $theme_manager = $this->themeManager; + $this->moduleHandler = new ModuleHandlerEmptyAlter(); + $this->themeManager = new ThemeManagerEmptyAlter(); + } + + parent::prepareForm($form_id, $form, $form_state); + + if ($prevent_hooks) { + $this->moduleHandler = $module_handler; + $this->themeManager = $theme_manager; + } + } + +} diff --git a/packages/drupal/silverback_autosave/src/Form/AutosaveFormErrorHandler.php b/packages/drupal/silverback_autosave/src/Form/AutosaveFormErrorHandler.php new file mode 100644 index 000000000..cd8689c24 --- /dev/null +++ b/packages/drupal/silverback_autosave/src/Form/AutosaveFormErrorHandler.php @@ -0,0 +1,43 @@ +formErrorHandler = $form_error_handler; + } + + /** + * {@inheritdoc} + */ + public function handleFormErrors(array &$form, FormStateInterface $form_state) { + if ($this->isAutosaveTriggered($form_state)) { + return $this; + } + return $this->formErrorHandler->handleFormErrors($form, $form_state); + } + +} diff --git a/packages/drupal/silverback_autosave/src/Form/AutosaveFormInterface.php b/packages/drupal/silverback_autosave/src/Form/AutosaveFormInterface.php new file mode 100644 index 000000000..069e64495 --- /dev/null +++ b/packages/drupal/silverback_autosave/src/Form/AutosaveFormInterface.php @@ -0,0 +1,111 @@ +entityTypeManager = $entity_type_manager; + $this->entityTypeBundleInfo = $entity_type_bundle_info; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager'), + $container->get('entity_type.bundle.info') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'silverback_autosave_admin_settings'; + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames() { + return [ + 'silverback_autosave.settings', + ]; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $config = $this->config('silverback_autosave.settings'); + + $form['interval'] = [ + '#type' => 'number', + '#title' => $this->t('The interval to use for triggering autosave in milliseconds.'), + '#default_value' => $config->get('interval'), + ]; + $form['only_on_form_change'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Run only on form change.') . ' ' . $this->t('(Experimental)'), + '#description' => $this->t('If enabled an autosave submission will only occur if the form changed since the previous autosave submission.'), + '#default_value' => $config->get('only_on_form_change'), + ]; + + $form['active_on'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Active on:'), + '#tree' => TRUE, + ]; + $form['active_on']['content_entity_forms'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Content Entity Forms'), + '#default_value' => $config->get('active_on')['content_entity_forms'], + ]; + $form['active_on']['config_entity_forms'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Config Entity Forms'), + '#default_value' => $config->get('active_on')['config_entity_forms'], + ]; + + $form['notification'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Notification settings'), + '#description' => $this->t('Display a simple notification every time content is saved'), + '#tree' => TRUE, + ]; + $form['notification']['active'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Active'), + '#default_value' => $config->get('notification')['active'], + ]; + $form['notification']['message'] = [ + '#type' => 'textfield', + '#title' => $this->t('Message'), + '#default_value' => $config->get('notification')['message'], + '#states' => [ + 'visible' => [ + ':input[name="notification[active]"]' => ['checked' => TRUE], + ], + ], + ]; + $form['notification']['delay'] = [ + '#type' => 'number', + '#title' => $this->t('The duration of the notification in milliseconds.'), + '#default_value' => $config->get('notification')['delay'], + '#states' => [ + 'visible' => [ + ':input[name="notification[active]"]' => ['checked' => TRUE], + ], + ], + ]; + + $allowed_content_entity_types = $config->get('allowed_content_entity_types'); + $form['allowed_content_entities'] = [ + '#type' => 'details', + '#open' => !empty($allowed_content_entity_types), + '#title' => $this->t('Allowed Content Entity Forms'), + '#description' => $this->t('In case no entity type is selected then autosave is enabled on all entity forms, otherwise it will be enabled only on the selected ones. Selecting only the entity type will enable all corresponding bundles and selecting only a subset of the bundles will enable autosave only for those bundles and will be disabled for the others.'), + '#tree' => TRUE, + '#states' => [ + 'visible' => [ + ':input[name="active_on[content_entity_forms]"]' => ['checked' => TRUE], + ], + ], + ]; + + /** @var \Drupal\Core\Entity\EntityTypeInterface $entity_type */ + foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) { + if ($entity_type instanceof ContentEntityTypeInterface) { + $entity_type_label = $entity_type->getLabel(); + $bundles_info = $this->entityTypeBundleInfo->getBundleInfo($entity_type_id); + $allowed_bundles = !empty($allowed_content_entity_types[$entity_type_id]['bundles']) ? $allowed_content_entity_types[$entity_type_id]['bundles'] : []; + $bundles = []; + foreach ($bundles_info as $key => $bundle) { + $bundles[$key] = $bundle['label']; + } + + $form['allowed_content_entities'][$entity_type_id]['active'] = [ + '#type' => 'checkbox', + '#title' => $entity_type_label, + '#default_value' => isset($allowed_content_entity_types[$entity_type_id]), + ]; + $form['allowed_content_entities'][$entity_type_id]['bundles'] = [ + '#type' => 'details', + '#open' => !empty($allowed_bundles), + '#title' => $entity_type_label . ' ' . $this->t('bundles'), + '#states' => [ + 'visible' => [ + ':input[name="allowed_content_entities[' . $entity_type_id . '][active]"]' => ['checked' => TRUE], + ], + ], + ]; + $form['allowed_content_entities'][$entity_type_id]['bundles']['selection'] = [ + '#type' => 'checkboxes', + '#default_value' => $allowed_bundles, + '#options' => $bundles, + '#prefix' => '
', + '#suffix' => '
', + ]; + } + } + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + /** @var \Drupal\Core\Config\Config $config */ + $config = $this->config('silverback_autosave.settings'); + $allowed_content_entity_types = []; + foreach ($form_state->getValue('allowed_content_entities') as $entity_type_id => $data) { + if (!$data['active']) { + continue; + } + $allowed_bundles = array_filter($data['bundles']['selection']); + $allowed_content_entity_types[$entity_type_id]['bundles'] = $allowed_bundles; + } + $config->set('interval', $form_state->getValue('interval')) + ->set('only_on_form_change', $form_state->getValue('only_on_form_change')) + ->set('active_on', $form_state->getValue('active_on')) + ->set('notification', $form_state->getValue('notification')) + ->set('allowed_content_entity_types', $allowed_content_entity_types) + ->save(); + + parent::submitForm($form, $form_state); + } + +} diff --git a/packages/drupal/silverback_autosave/src/Form/AutosaveFormValidator.php b/packages/drupal/silverback_autosave/src/Form/AutosaveFormValidator.php new file mode 100644 index 000000000..ad0fd6e00 --- /dev/null +++ b/packages/drupal/silverback_autosave/src/Form/AutosaveFormValidator.php @@ -0,0 +1,80 @@ +formValidator = $form_validator; + parent::__construct($request_stack, $string_translation, $csrf_token, $logger, $form_error_handler); + } + + /** + * {@inheritdoc} + */ + public function validateForm($form_id, &$form, FormStateInterface &$form_state) { + $autosave_submission = $this->isAutosaveTriggered($form_state); + + if ($autosave_submission) { + // On subsequent autosaving we don't need to execute the form validation + // as we are not going to build the intermediate entity. However it might + // happen that between here and the autosave submission handler the + // autosaved state has been purged and therefore we have to check + // explicitly for that there instead of building the intermediate entity. + $form_state->setTemporaryValue('silverback_autosave_form_validation_skipped', TRUE); + } + else { + // We have to execute the validation in the case of autosave submission + // for the very first time as in this case we'll build the intermediate + // entity for comparison and some input values are being prepared in the + // validate functions. This is the case with e.g. autocomplete for entity + // references. + $this->formValidator->validateForm($form_id, $form, $form_state); + } + + // In order for the autosave submit callback to be executed we have to + // clear the errors caused from the validation, otherwise no submit + // callbacks will be executed. + if ($autosave_submission && $form_state::hasAnyErrors()) { + $form_state->clearErrors(); + } + } + +} diff --git a/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormDatabaseStorage.php b/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormDatabaseStorage.php new file mode 100644 index 000000000..4e3fdbe74 --- /dev/null +++ b/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormDatabaseStorage.php @@ -0,0 +1,310 @@ +connection = $connection; + $this->serializer = $serializer; + } + + /** + * {@inheritdoc} + */ + public function storeEntityAndFormState($form_id, $form_session_id, $entity_type_id, $entity_id, $langcode, $uid, $timestamp, EntityInterface $entity, FormStateInterface $form_state) { + $serialized_entity = $this->serializeEntity($entity, $form_state); + $serialized_form_state = $this->serializer->encode([ + 'storage' => $form_state->getStorage(), + 'input' => $form_state->getUserInput(), + ]); + + if (!$entity->isNew()) { + $this->connection->insert(static::AUTOSAVE_ENTITY_FORM_TABLE) + ->fields([ + 'form_id', + 'form_session_id', + 'entity_type_id', + 'entity_id', + 'langcode', + 'uid', + 'timestamp', + 'entity', + 'form_state', + ]) + ->values([ + $form_id, + $form_session_id, + $entity_type_id, + $entity_id, + $langcode, + $uid, + $timestamp, + $serialized_entity, + $serialized_form_state, + ]) + ->execute(); + } + } + + /** + * {@inheritdoc} + */ + public function getEntityAndFormState($form_id, $entity_type_id, $entity_id, $langcode, $uid, $form_session_id = NULL, $autosaved_timestamp = NULL) { + $result = NULL; + $query = $this->connection->select(static::AUTOSAVE_ENTITY_FORM_TABLE, 'cefa') + ->fields('cefa', ['entity', 'form_state', 'timestamp']) + ->orderBy('timestamp', 'DESC') + ->condition('form_id', $form_id); + + if (isset($form_session_id)) { + $query->condition('form_session_id', $form_session_id); + } + + $query->condition('entity_type_id', $entity_type_id) + ->condition('entity_id', $entity_id) + ->condition('langcode', $langcode) + ->condition('uid', $uid); + + if (isset($autosaved_timestamp)) { + $query->condition('timestamp', $autosaved_timestamp); + } + + $data = $query->execute()->fetchAssoc(); + + if ($data) { + // Prepare the entity object. + $result['entity'] = $this->serializer->decode($data['entity']); + // Prepare the form state. + $form_state_data = $this->serializer->decode($data['form_state']); + $result['form_state'] = new FormState(); + $result['form_state']->setStorage($form_state_data['storage']); + $result['form_state']->setUserInput($form_state_data['input']); + $result['form_state']->set('silverback_autosave_state_timestamp', $data['timestamp']); + } + + return $result; + } + + /** + * {@inheritdoc} + */ + public function getEntity($form_id, $entity_type_id, $entity_id, $langcode, $uid, $form_session_id = NULL, $autosaved_timestamp = NULL) { + $entity = NULL; + $query = $this->connection->select(static::AUTOSAVE_ENTITY_FORM_TABLE, 'cefa') + ->fields('cefa', ['entity']) + ->orderBy('timestamp', 'DESC') + ->condition('form_id', $form_id); + + if (isset($form_session_id)) { + $query->condition('form_session_id', $form_session_id); + } + + $query->condition('entity_type_id', $entity_type_id) + ->condition('entity_id', $entity_id) + ->condition('langcode', $langcode) + ->condition('uid', $uid); + + if (isset($autosaved_timestamp)) { + $query->condition('timestamp', $autosaved_timestamp); + } + + $data = $query->execute()->fetchAssoc(); + + if ($data) { + // Prepare the entity object. + $entity = $this->serializer->decode($data['entity']); + } + + return $entity; + } + + /** + * {@inheritdoc} + */ + public function getFormState($form_id, $entity_type_id, $entity_id, $langcode, $uid, $form_session_id = NULL, $autosaved_timestamp = NULL) { + $form_state = NULL; + $query = $this->connection->select(static::AUTOSAVE_ENTITY_FORM_TABLE, 'cefa') + ->fields('cefa', ['form_state', 'timestamp']) + ->orderBy('timestamp', 'DESC') + ->condition('form_id', $form_id); + + if (isset($form_session_id)) { + // $query->condition('form_session_id', $form_session_id); + } + + $query->condition('entity_type_id', $entity_type_id) + ->condition('entity_id', $entity_id) + ->condition('langcode', $langcode) + ->condition('uid', $uid); + + if (isset($autosaved_timestamp)) { + $query->condition('timestamp', $autosaved_timestamp); + } + + $data = $query->execute()->fetchAssoc(); + + if ($data) { + // Prepare the form state. + $form_state_data = $this->serializer->decode($data['form_state']); + $form_state = new FormState(); + $form_state->setStorage($form_state_data['storage']); + $form_state->setUserInput($form_state_data['input']); + $form_state->set('silverback_autosave_state_timestamp', $data['timestamp']); + } + + return $form_state; + } + + /** + * {@inheritdoc} + */ + public function hasAutosavedStateForFormState(FormStateInterface $form_state, $uid) { + $result = FALSE; + if (($form_object = $form_state->getFormObject()) && ($form_object instanceof EntityFormInterface) && ($entity = $form_object->getEntity()) && !$entity->isNew()) { + $result = $this->hasAutosavedState($form_object->getFormId(), $entity->getEntityTypeId(), $entity->id(), $entity->language()->getId(), $uid, $form_state->get('silverback_autosave_session_id')); + } + return $result; + } + + /** + * {@inheritdoc} + */ + public function hasAutosavedState($form_id, $entity_type_id, $entity_id, $langcode, $uid, $form_session_id = NULL) { + $query = $this->connection->select(static::AUTOSAVE_ENTITY_FORM_TABLE, 'cefa') + ->condition('form_id', $form_id); + + if (isset($form_session_id)) { + $query->condition('form_session_id', $form_session_id); + } + + $query->condition('entity_type_id', $entity_type_id) + ->condition('entity_id', $entity_id) + ->condition('langcode', $langcode) + ->condition('uid', $uid); + + $count = $query->countQuery() + ->execute() + ->fetchField(); + return (bool) $count; + } + + /** + * {@inheritdoc} + */ + public function getLastAutosavedStateTimestamp($form_id, $entity_type_id, $entity_id, $langcode, $uid) { + $timestamp = $this->connection->select(static::AUTOSAVE_ENTITY_FORM_TABLE, 'cefa') + ->fields('cefa', ['timestamp']) + ->orderBy('timestamp', 'DESC') + ->condition('form_id', $form_id) + ->condition('entity_type_id', $entity_type_id) + ->condition('entity_id', $entity_id) + ->condition('langcode', $langcode) + ->condition('uid', $uid) + ->execute() + ->fetchField(); + + return is_bool($timestamp) ? NULL : $timestamp; + } + + /** + * {@inheritdoc} + */ + public function purgeAutosavedEntityState($entity_type_id, $entity_id, $form_session_id = NULL, $form_id = NULL, $langcode = NULL, $uid = NULL) { + $query = $this->connection->delete(static::AUTOSAVE_ENTITY_FORM_TABLE); + + if (isset($form_session_id)) { + $query->condition('form_session_id', $form_session_id); + } + + if (isset($form_id)) { + $query->condition('form_id', $form_id); + } + + $query->condition('entity_type_id', $entity_type_id) + ->condition('entity_id', $entity_id); + + if (isset($langcode)) { + $query->condition('langcode', $langcode); + } + + if (isset($uid)) { + $query->condition('uid', $uid); + } + + $query->execute(); + } + + /** + * {@inheritdoc} + */ + public function purgeAutosavedEntitiesStates($entity_type_id = NULL, $langcode = NULL, $uid = NULL) { + $query = $this->connection->delete(static::AUTOSAVE_ENTITY_FORM_TABLE); + if (isset($entity_type_id)) { + $query->condition('entity_type_id', $entity_type_id); + } + if (isset($langcode)) { + $query->condition('langcode', $langcode); + } + if (isset($uid)) { + $query->condition('uid', $uid); + } + $query->execute(); + } + + /** + * Returns the serialized entity object. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity object to serialize. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state to retrieve meta information about the serialization. + * + * @return string + * The serialized entity. + */ + protected function serializeEntity(EntityInterface $entity, FormStateInterface $form_state) { + if (($form_object = $form_state->getFormObject()) && ($form_object instanceof ContentEntityFormInterface) && method_exists($form_object, 'isEntityDeepSerializationRequired') && $form_object->isEntityDeepSerializationRequired()) { + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ + if (method_exists($entity, 'setDeepSerialization')) { + $entity->setDeepSerialization(TRUE); + } + } + + return $this->serializer->encode($entity); + } + +} diff --git a/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormStorageInterface.php b/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormStorageInterface.php new file mode 100644 index 000000000..9c14a724a --- /dev/null +++ b/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormStorageInterface.php @@ -0,0 +1,203 @@ +( // If the executor is not a function, pass null to SWR, // so it does not try to fetch. executor instanceof Function ? (arg) => executor(arg[1]) : null, - { - suspense: false, - }, + variables?.pathname?.indexOf('__preview') > 0 ? + { suspense: false, refreshInterval: 1000 } : { suspense: false }, ); return executor instanceof Function ? result From a5ef9bc90532c6c078e4001e823632b388f9b558 Mon Sep 17 00:00:00 2001 From: Dimitris Spachos Date: Wed, 10 Apr 2024 16:24:09 +0300 Subject: [PATCH 02/82] feat(slb-146): disable copy root patches --- .lagoon/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.lagoon/Dockerfile b/.lagoon/Dockerfile index 333bcd2cb..4669b403e 100644 --- a/.lagoon/Dockerfile +++ b/.lagoon/Dockerfile @@ -15,7 +15,7 @@ RUN npm install -g pnpm@8.6.12 && pnpm config set store-dir /tmp/cache/pnpm # Copy pnpm lockfile and install dependencies. COPY pnpm-lock.yaml .npmrc /app/ -COPY patches /app/patches +# COPY patches /app/patches RUN --mount=type=cache,target=/tmp/cache pnpm fetch && \ # There is a bug in pnpm: `pnpm fetch` creates _some_ node_modules folders # with _some_ packages. This can lead to an incomplete package installation. From eb673a928d8c6d05c98326efa49e252b9a684a08 Mon Sep 17 00:00:00 2001 From: Dimitris Spachos Date: Wed, 10 Apr 2024 17:03:13 +0300 Subject: [PATCH 03/82] feat(slb-146): fix format in operations --- packages/ui/src/utils/operation.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/utils/operation.ts b/packages/ui/src/utils/operation.ts index 11f56d6b2..9ec7d1742 100644 --- a/packages/ui/src/utils/operation.ts +++ b/packages/ui/src/utils/operation.ts @@ -18,8 +18,9 @@ export function useOperation( // If the executor is not a function, pass null to SWR, // so it does not try to fetch. executor instanceof Function ? (arg) => executor(arg[1]) : null, - variables?.pathname?.indexOf('__preview') > 0 ? - { suspense: false, refreshInterval: 1000 } : { suspense: false }, + variables?.pathname?.indexOf('__preview') > 0 + ? { suspense: false, refreshInterval: 1000 } + : { suspense: false }, ); return executor instanceof Function ? result From 6bb4e99524388d8739b1b60b0802aa3a45205efe Mon Sep 17 00:00:00 2001 From: Dimitris Spachos Date: Thu, 11 Apr 2024 13:01:52 +0300 Subject: [PATCH 04/82] feat(slb-146): restore aggregation, drush ^12.5 (devel issue) --- apps/cms/composer.json | 3 +- apps/cms/composer.lock | 354 +++++++++++++++++++- apps/cms/config/sync/system.performance.yml | 2 +- apps/cms/gatsby-node.mjs | 2 +- 4 files changed, 350 insertions(+), 11 deletions(-) diff --git a/apps/cms/composer.json b/apps/cms/composer.json index 6235b4cf4..95a18201c 100644 --- a/apps/cms/composer.json +++ b/apps/cms/composer.json @@ -73,7 +73,7 @@ "drupal/stage_file_proxy": "^2.0.2", "drupal/userprotect": "^1.2", "drupal/webform": "^6.1.5", - "drush/drush": "^12", + "drush/drush": "^12.5", "enyo/dropzone": "^5.7.1" }, "conflict": { @@ -155,6 +155,7 @@ }, "require-dev": { "drupal/core-dev": "^10", + "drupal/devel": "^5.2", "phpspec/prophecy-phpunit": "^2.0.2" } } diff --git a/apps/cms/composer.lock b/apps/cms/composer.lock index 07ca8aaa5..35ca468ce 100644 --- a/apps/cms/composer.lock +++ b/apps/cms/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "bee5aff7b7d7fdebb87c36db5dfc343a", + "content-hash": "7fdb7073ff9cc88dcbc78224e439e65f", "packages": [ { "name": "amazeeio/drupal_integrations", @@ -4481,23 +4481,23 @@ }, { "name": "drush/drush", - "version": "12.4.3", + "version": "12.5.1", "source": { "type": "git", "url": "https://github.com/drush-ops/drush.git", - "reference": "8245acede57ecc62a90aa0f19ff3b29e5f11f971" + "reference": "71fcea30a22e7336e17be18bb5945400b2c63fad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/drush-ops/drush/zipball/8245acede57ecc62a90aa0f19ff3b29e5f11f971", - "reference": "8245acede57ecc62a90aa0f19ff3b29e5f11f971", + "url": "https://api.github.com/repos/drush-ops/drush/zipball/71fcea30a22e7336e17be18bb5945400b2c63fad", + "reference": "71fcea30a22e7336e17be18bb5945400b2c63fad", "shasum": "" }, "require": { "chi-teck/drupal-code-generator": "^3.0", "composer-runtime-api": "^2.2", "composer/semver": "^1.4 || ^3", - "consolidation/annotated-command": "^4.9.1", + "consolidation/annotated-command": "^4.9.2", "consolidation/config": "^2.1.2", "consolidation/filter-via-dot-access-data": "^2.0.2", "consolidation/output-formatters": "^4.3.2", @@ -4613,7 +4613,7 @@ "issues": "https://github.com/drush-ops/drush/issues", "security": "https://github.com/drush-ops/drush/security/advisories", "slack": "https://drupal.slack.com/messages/C62H9CWQM", - "source": "https://github.com/drush-ops/drush/tree/12.4.3" + "source": "https://github.com/drush-ops/drush/tree/12.5.1" }, "funding": [ { @@ -4621,7 +4621,7 @@ "type": "github" } ], - "time": "2023-11-16T22:57:24+00:00" + "time": "2024-03-20T15:03:27+00:00" }, { "name": "egulias/email-validator", @@ -11131,6 +11131,188 @@ }, "time": "2023-01-05T11:28:13+00:00" }, + { + "name": "doctrine/common", + "version": "3.4.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/common.git", + "reference": "8b5e5650391f851ed58910b3e3d48a71062eeced" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/common/zipball/8b5e5650391f851ed58910b3e3d48a71062eeced", + "reference": "8b5e5650391f851ed58910b3e3d48a71062eeced", + "shasum": "" + }, + "require": { + "doctrine/persistence": "^2.0 || ^3.0", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9.0 || ^10.0", + "doctrine/collections": "^1", + "phpstan/phpstan": "^1.4.1", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5.20 || ^8.5 || ^9.0", + "squizlabs/php_codesniffer": "^3.0", + "symfony/phpunit-bridge": "^6.1", + "vimeo/psalm": "^4.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "PHP Doctrine Common project is a library that provides additional functionality that other Doctrine projects depend on such as better reflection support, proxies and much more.", + "homepage": "https://www.doctrine-project.org/projects/common.html", + "keywords": [ + "common", + "doctrine", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/common/issues", + "source": "https://github.com/doctrine/common/tree/3.4.3" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcommon", + "type": "tidelift" + } + ], + "time": "2022-10-09T11:47:59+00:00" + }, + { + "name": "doctrine/event-manager", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/event-manager.git", + "reference": "750671534e0241a7c50ea5b43f67e23eb5c96f32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/750671534e0241a7c50ea5b43f67e23eb5c96f32", + "reference": "750671534e0241a7c50ea5b43f67e23eb5c96f32", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/common": "<2.9" + }, + "require-dev": { + "doctrine/coding-standard": "^10", + "phpstan/phpstan": "^1.8.8", + "phpunit/phpunit": "^9.5", + "vimeo/psalm": "^4.28" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", + "homepage": "https://www.doctrine-project.org/projects/event-manager.html", + "keywords": [ + "event", + "event dispatcher", + "event manager", + "event system", + "events" + ], + "support": { + "issues": "https://github.com/doctrine/event-manager/issues", + "source": "https://github.com/doctrine/event-manager/tree/2.0.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager", + "type": "tidelift" + } + ], + "time": "2022-10-12T20:59:15+00:00" + }, { "name": "doctrine/instantiator", "version": "2.0.0", @@ -11201,6 +11383,104 @@ ], "time": "2022-12-30T00:23:10+00:00" }, + { + "name": "doctrine/persistence", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/persistence.git", + "reference": "477da35bd0255e032826f440b94b3e37f2d56f42" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/persistence/zipball/477da35bd0255e032826f440b94b3e37f2d56f42", + "reference": "477da35bd0255e032826f440b94b3e37f2d56f42", + "shasum": "" + }, + "require": { + "doctrine/event-manager": "^1 || ^2", + "php": "^7.2 || ^8.0", + "psr/cache": "^1.0 || ^2.0 || ^3.0" + }, + "conflict": { + "doctrine/common": "<2.10" + }, + "require-dev": { + "composer/package-versions-deprecated": "^1.11", + "doctrine/coding-standard": "^11", + "doctrine/common": "^3.0", + "phpstan/phpstan": "1.9.4", + "phpstan/phpstan-phpunit": "^1", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.5", + "symfony/cache": "^4.4 || ^5.4 || ^6.0", + "vimeo/psalm": "4.30.0 || 5.3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Persistence\\": "src/Persistence" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Persistence project is a set of shared interfaces and functionality that the different Doctrine object mappers share.", + "homepage": "https://www.doctrine-project.org/projects/persistence.html", + "keywords": [ + "mapper", + "object", + "odm", + "orm", + "persistence" + ], + "support": { + "issues": "https://github.com/doctrine/persistence/issues", + "source": "https://github.com/doctrine/persistence/tree/3.3.2" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fpersistence", + "type": "tidelift" + } + ], + "time": "2024-03-12T14:54:36+00:00" + }, { "name": "drupal/coder", "version": "8.3.23", @@ -11308,6 +11588,64 @@ }, "time": "2023-12-12T22:01:45+00:00" }, + { + "name": "drupal/devel", + "version": "5.2.0", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/devel.git", + "reference": "5.2.0" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/devel-5.2.0.zip", + "reference": "5.2.0", + "shasum": "9b7baa45a3caea3908643ef22882205ec2ed0da1" + }, + "require": { + "doctrine/common": "^2.7 || ^3.4", + "drupal/core": ">=10.0 <12.0.0-stable", + "php": ">=8.1", + "symfony/var-dumper": "^4 || ^5 || ^6" + }, + "conflict": { + "kint-php/kint": "<3" + }, + "require-dev": { + "drush/drush": "^12.5.1" + }, + "suggest": { + "kint-php/kint": "Kint provides an informative display of arrays/objects. Useful for debugging and developing." + }, + "type": "drupal-module", + "extra": { + "drupal": { + "version": "5.2.0", + "datestamp": "1711280160", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "moshe weitzman", + "homepage": "https://www.drupal.org/user/23" + } + ], + "description": "Various blocks, pages, and functions for developers.", + "homepage": "https://www.drupal.org/project/devel", + "support": { + "source": "https://gitlab.com/drupalspoons/devel", + "issues": "https://gitlab.com/drupalspoons/devel/-/issues", + "slack": "https://drupal.slack.com/archives/C012WAW1MH6" + } + }, { "name": "google/protobuf", "version": "v3.25.3", diff --git a/apps/cms/config/sync/system.performance.yml b/apps/cms/config/sync/system.performance.yml index bf0902d1c..d33d50d5d 100644 --- a/apps/cms/config/sync/system.performance.yml +++ b/apps/cms/config/sync/system.performance.yml @@ -12,5 +12,5 @@ fast_404: exclude_paths: '/\/(?:styles|imagecache)\//' html: '404 Not Found

Not Found

The requested URL "@path" was not found on this server.

' js: - preprocess: false + preprocess: true gzip: true diff --git a/apps/cms/gatsby-node.mjs b/apps/cms/gatsby-node.mjs index c5957f4c6..02e0027ff 100644 --- a/apps/cms/gatsby-node.mjs +++ b/apps/cms/gatsby-node.mjs @@ -48,7 +48,7 @@ export const createPages = async ({ actions }) => { // Additionally proxy themes and modules as they can have additional // non-aggregated assets. - ['themes', 'modules'].forEach((path) => { + ['themes', 'modules', 'core/assets'].forEach((path) => { actions.createRedirect({ fromPath: `/${path}/*`, toPath: `${process.env.GATSBY_DRUPAL_URL}/${path}/:splat`, From 6060a66d0458e8d81562be398f5fa2f981bd2874 Mon Sep 17 00:00:00 2001 From: Alex Tkachev Date: Thu, 11 Apr 2024 14:13:12 +0400 Subject: [PATCH 05/82] fix: add DRUPAL_EXTERNAL_URL for local Publisher So that packages/schema/src/image.ts can replace URLs properly. --- apps/website/publisher.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/website/publisher.config.ts b/apps/website/publisher.config.ts index abaf98f01..6c5d95797 100644 --- a/apps/website/publisher.config.ts +++ b/apps/website/publisher.config.ts @@ -16,7 +16,7 @@ export default defineConfig({ // cannot report it. // Workaround: Do a double build on the first build. 'if test -d public; then echo "Single build" && pnpm build:gatsby; else echo "Double build" && pnpm build:gatsby && pnpm build:gatsby; fi' - : 'pnpm build:gatsby', + : 'DRUPAL_EXTERNAL_URL=http://127.0.0.1:8888 pnpm build:gatsby', outputTimeout: 1000 * 60 * 10, }, clean: 'pnpm clean', From f94d84be034dd431d8d631d883b35d1f5678f2b1 Mon Sep 17 00:00:00 2001 From: Alex Tkachev Date: Thu, 11 Apr 2024 14:13:51 +0400 Subject: [PATCH 06/82] fix: replace URLs in JSON properly --- packages/schema/src/image.ts | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/schema/src/image.ts b/packages/schema/src/image.ts index 119af6332..f0190975c 100644 --- a/packages/schema/src/image.ts +++ b/packages/schema/src/image.ts @@ -26,10 +26,22 @@ export const imageProps: GraphQLFieldResolver = (source) => { // - If images are processed in Cloudinary, we don't need two CDN's (Netlify & Cloudinary) // - TODO: Once we have image processing in Drupal, it has to be handled here. if (process.env.NETLIFY_URL && process.env.DRUPAL_EXTERNAL_URL) { - return source.replaceAll( - process.env.NETLIFY_URL, - process.env.DRUPAL_EXTERNAL_URL, - ); + try { + const decoded = JSON.parse(source); + if (decoded && typeof decoded === 'object') { + for (const key in decoded) { + if (typeof decoded[key] === 'string') { + decoded[key] = decoded[key].replaceAll( + process.env.NETLIFY_URL, + process.env.DRUPAL_EXTERNAL_URL, + ); + } + } + return JSON.stringify(decoded); + } + } catch (error) { + return source; + } } return source; }; From 181822e58f366fbbf151d0dae12afe08155a1cd6 Mon Sep 17 00:00:00 2001 From: Dimitris Spachos Date: Fri, 12 Apr 2024 08:52:01 +0300 Subject: [PATCH 07/82] feat(slb-146): remove options for config forms --- .../install/silverback_autosave.settings.yml | 1 - .../schema/silverback_autosave.schema.yml | 3 - .../silverback_autosave.module | 7 - .../silverback_autosave.services.yml | 6 - .../src/EventSubscriber/ConfigSubscriber.php | 130 ------------------ .../src/Form/AutosaveFormSettingsForm.php | 6 +- 6 files changed, 1 insertion(+), 152 deletions(-) delete mode 100644 packages/drupal/silverback_autosave/src/EventSubscriber/ConfigSubscriber.php diff --git a/packages/drupal/silverback_autosave/config/install/silverback_autosave.settings.yml b/packages/drupal/silverback_autosave/config/install/silverback_autosave.settings.yml index c5a0c78aa..034332fef 100644 --- a/packages/drupal/silverback_autosave/config/install/silverback_autosave.settings.yml +++ b/packages/drupal/silverback_autosave/config/install/silverback_autosave.settings.yml @@ -2,7 +2,6 @@ interval: 10000 only_on_form_change: FALSE active_on: content_entity_forms: TRUE - config_entity_forms: FALSE notification: active: TRUE message: "Updating preview..." diff --git a/packages/drupal/silverback_autosave/config/schema/silverback_autosave.schema.yml b/packages/drupal/silverback_autosave/config/schema/silverback_autosave.schema.yml index 86bef3549..0fccb354c 100644 --- a/packages/drupal/silverback_autosave/config/schema/silverback_autosave.schema.yml +++ b/packages/drupal/silverback_autosave/config/schema/silverback_autosave.schema.yml @@ -17,9 +17,6 @@ silverback_autosave.settings: content_entity_forms: type: boolean label: 'Content entity forms' - config_entity_forms: - type: boolean - label: 'Config entity forms' notification: type: mapping label: 'Notification' diff --git a/packages/drupal/silverback_autosave/silverback_autosave.module b/packages/drupal/silverback_autosave/silverback_autosave.module index 2f3dabfdb..1f1afe85b 100644 --- a/packages/drupal/silverback_autosave/silverback_autosave.module +++ b/packages/drupal/silverback_autosave/silverback_autosave.module @@ -6,7 +6,6 @@ */ use Drupal\Core\Entity\ContentEntityFormInterface; -use Drupal\Core\Entity\EntityFormInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\silverback_autosave\Form\AutosaveEntityFormHandler; @@ -34,7 +33,6 @@ function silverback_autosave_entity_type_alter(array &$entity_types) { * Implements hook_form_alter(). */ function silverback_autosave_form_alter(&$form, FormStateInterface $form_state, $form_id) { - $active_on = \Drupal::configFactory()->get('silverback_autosave.settings')->get('active_on'); $form_object = $form_state->getFormObject(); @@ -60,11 +58,6 @@ function silverback_autosave_form_alter(&$form, FormStateInterface $form_state, } } } - elseif ($form_object instanceof EntityFormInterface) { - if (!$active_on['config_entity_forms']) { - return; - } - } else { return; } diff --git a/packages/drupal/silverback_autosave/silverback_autosave.services.yml b/packages/drupal/silverback_autosave/silverback_autosave.services.yml index 8a25c019c..b8b45034d 100644 --- a/packages/drupal/silverback_autosave/silverback_autosave.services.yml +++ b/packages/drupal/silverback_autosave/silverback_autosave.services.yml @@ -44,9 +44,3 @@ services: class: Drupal\silverback_autosave\Form\AutosaveFormErrorHandler decorates: form_error_handler arguments: ['@form_error_handler.silverback_autosave.inner'] - - silverback_autosave.config_subscriber: - class: Drupal\silverback_autosave\EventSubscriber\ConfigSubscriber - tags: - - { name: event_subscriber } - arguments: ['@silverback_autosave.entity_form_storage'] diff --git a/packages/drupal/silverback_autosave/src/EventSubscriber/ConfigSubscriber.php b/packages/drupal/silverback_autosave/src/EventSubscriber/ConfigSubscriber.php deleted file mode 100644 index dd7affb82..000000000 --- a/packages/drupal/silverback_autosave/src/EventSubscriber/ConfigSubscriber.php +++ /dev/null @@ -1,130 +0,0 @@ -autosaveEntityFormStorage = $autosave_entity_form_storage; - } - - /** - * Purges all autosave states in case a form-related config is saved. - * - * Deletes all autosaved states if an important property of a form related - * config is changed. We cannot know if some kind of an inline reference has - * been used and its display has been changed, which is why we purge the whole - * autosave storage. Otherwise a much more complex detection mechanism will - * be needed. We though try to prevent the purging in case a change on an - * property is made, which doesn't influence the form functionality - e.g. if - * the label of a field is changed then we don't have to purge the autosave - * states. - * - * @param \Drupal\Core\Config\ConfigCrudEvent $event - * The configuration event. - */ - public function onConfigSave(ConfigCrudEvent $event) { - $saved_config = $event->getConfig(); - $name = $saved_config->getName(); - - // We try to prevent purging autosave states unnecessarily by defining - // properties, which are allowed to change and don't have an influence on - // the form functionality. - $allowed_changes = []; - $purge_autosave_states = TRUE; - if (strpos($name, 'field.field.') === 0) { - $allowed_changes = [ - 'dependencies', - 'label', - 'description', - 'required', - 'default_value', - 'default_value_callback', - ]; - } - elseif (strpos($name, 'field.storage.') === 0) { - $allowed_changes = [ - 'dependencies', - 'module', - 'indexes', - 'persist_with_no_fields', - 'custom_storage', - ]; - } - elseif (strpos($name, 'core.entity_form_display.') === 0) { - $allowed_changes = [ - 'hidden', - ]; - } - elseif (strpos($name, 'user.role.') === 0) { - $allowed_changes = [ - 'label', - 'weight', - ]; - } - else { - $purge_autosave_states = FALSE; - } - - if ($purge_autosave_states) { - $property_names = array_diff(array_keys($saved_config->getRawData()), $allowed_changes); - foreach ($property_names as $property_name) { - if ($event->isChanged($property_name)) { - $this->autosaveEntityFormStorage->purgeAutosavedEntitiesStates(); - break; - } - } - } - } - - /** - * Purges all autosave states in case a form-related config is deleted. - * - * @param \Drupal\Core\Config\ConfigCrudEvent $event - * The configuration event. - */ - public function onConfigDelete(ConfigCrudEvent $event) { - $deleted_config = $event->getConfig(); - $name = $deleted_config->getName(); - - $purge_autosave_states = (strpos($name, 'field.field.') === 0) || - (strpos($name, 'field.storage.') === 0) || - (strpos($name, 'core.entity_form_display.') === 0) || - (strpos($name, 'user.role.') === 0); - - if ($purge_autosave_states) { - $this->autosaveEntityFormStorage->purgeAutosavedEntitiesStates(); - } - } - - /** - * {@inheritdoc} - */ - public static function getSubscribedEvents() { - $events[ConfigEvents::SAVE][] = ['onConfigSave']; - $events[ConfigEvents::DELETE][] = ['onConfigDelete']; - return $events; - } - -} diff --git a/packages/drupal/silverback_autosave/src/Form/AutosaveFormSettingsForm.php b/packages/drupal/silverback_autosave/src/Form/AutosaveFormSettingsForm.php index 2eb3bde81..4f7f5e12b 100644 --- a/packages/drupal/silverback_autosave/src/Form/AutosaveFormSettingsForm.php +++ b/packages/drupal/silverback_autosave/src/Form/AutosaveFormSettingsForm.php @@ -80,6 +80,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { ]; $form['only_on_form_change'] = [ '#type' => 'checkbox', + '#access' => FALSE, '#title' => $this->t('Run only on form change.') . ' ' . $this->t('(Experimental)'), '#description' => $this->t('If enabled an autosave submission will only occur if the form changed since the previous autosave submission.'), '#default_value' => $config->get('only_on_form_change'), @@ -95,11 +96,6 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#title' => $this->t('Content Entity Forms'), '#default_value' => $config->get('active_on')['content_entity_forms'], ]; - $form['active_on']['config_entity_forms'] = [ - '#type' => 'checkbox', - '#title' => $this->t('Config Entity Forms'), - '#default_value' => $config->get('active_on')['config_entity_forms'], - ]; $form['notification'] = [ '#type' => 'fieldset', From 246d84e13bc1cd2babb647066ad0a901abeaa253 Mon Sep 17 00:00:00 2001 From: Dimitris Spachos Date: Fri, 12 Apr 2024 08:57:37 +0300 Subject: [PATCH 08/82] feat(slb-146): remove unused storage interface, constant definition to uppercase --- .../silverback_autosave.install | 4 +- .../AutosaveEntityFormStorageInterface.php | 2 +- .../Storage/AutosaveFormBackendInterface.php | 83 ------------------- 3 files changed, 3 insertions(+), 86 deletions(-) delete mode 100644 packages/drupal/silverback_autosave/src/Storage/AutosaveFormBackendInterface.php diff --git a/packages/drupal/silverback_autosave/silverback_autosave.install b/packages/drupal/silverback_autosave/silverback_autosave.install index 79270393c..6cde0cbdf 100644 --- a/packages/drupal/silverback_autosave/silverback_autosave.install +++ b/packages/drupal/silverback_autosave/silverback_autosave.install @@ -21,7 +21,7 @@ function silverback_autosave_schema() { 'fields' => [ 'form_id' => [ 'type' => 'varchar_ascii', - 'length' => AutosaveEntityFormStorageInterface::silverback_autosave_FORM_ID_LENGTH, + 'length' => AutosaveEntityFormStorageInterface::SILVERBACK_AUTOSAVE_FORM_ID_LENGTH, 'not null' => TRUE, ], // We need the form session id as it is possible that the user opens the @@ -31,7 +31,7 @@ function silverback_autosave_schema() { // case. 'form_session_id' => [ 'type' => 'varchar_ascii', - 'length' => AutosaveEntityFormStorageInterface::silverback_autosave_FORM_ID_LENGTH, + 'length' => AutosaveEntityFormStorageInterface::SILVERBACK_AUTOSAVE_FORM_ID_LENGTH, 'not null' => TRUE, ], 'entity_type_id' => [ diff --git a/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormStorageInterface.php b/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormStorageInterface.php index 9c14a724a..36cf6b0f7 100644 --- a/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormStorageInterface.php +++ b/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormStorageInterface.php @@ -12,7 +12,7 @@ interface AutosaveEntityFormStorageInterface { const AUTOSAVE_ENTITY_FORM_TABLE = 'silverback_autosave_entity_form'; - const silverback_autosave_FORM_ID_LENGTH = 512; + const SILVERBACK_AUTOSAVE_FORM_ID_LENGTH = 512; /** * Stores the entity together with the form state. diff --git a/packages/drupal/silverback_autosave/src/Storage/AutosaveFormBackendInterface.php b/packages/drupal/silverback_autosave/src/Storage/AutosaveFormBackendInterface.php deleted file mode 100644 index 167df5831..000000000 --- a/packages/drupal/silverback_autosave/src/Storage/AutosaveFormBackendInterface.php +++ /dev/null @@ -1,83 +0,0 @@ - Date: Fri, 12 Apr 2024 09:06:42 +0300 Subject: [PATCH 09/82] feat(slb-146): add short readme --- packages/drupal/silverback_autosave/README.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/drupal/silverback_autosave/README.md diff --git a/packages/drupal/silverback_autosave/README.md b/packages/drupal/silverback_autosave/README.md new file mode 100644 index 000000000..33a8d4f82 --- /dev/null +++ b/packages/drupal/silverback_autosave/README.md @@ -0,0 +1,5 @@ +## Silverback autosave + +This module provides an autosave functionality on Guteneberg enabled nodes. + +On node edit, the content under edit is automatically saved for use in real time preview. From cdeb628b884516857ccf882dabb0cb69180d537b Mon Sep 17 00:00:00 2001 From: Dimitris Spachos Date: Fri, 12 Apr 2024 09:19:04 +0300 Subject: [PATCH 10/82] feat(slb-146): remove obsolete restore/reject functionality --- .../js/silverback_autosave.js | 66 +------------------ .../src/Form/AutosaveButtonClickedTrait.php | 39 ----------- .../src/Form/AutosaveFormAlterTrait.php | 41 ------------ .../src/Form/AutosaveFormBuilder.php | 3 - .../src/Form/AutosaveFormInterface.php | 2 - 5 files changed, 1 insertion(+), 150 deletions(-) diff --git a/packages/drupal/silverback_autosave/js/silverback_autosave.js b/packages/drupal/silverback_autosave/js/silverback_autosave.js index be91b640e..3c2777f02 100644 --- a/packages/drupal/silverback_autosave/js/silverback_autosave.js +++ b/packages/drupal/silverback_autosave/js/silverback_autosave.js @@ -124,71 +124,7 @@ .appendTo('body') .append(Drupal.autosaveForm.notification.message); - // Show the resume/discard confirmation message only if the form does - // not contain any errors, otherwise continue with normal autosave - // submissions. This is for the use case where a form is submitted but - // returned to the user with validation errors in which case we should - // not show the resume/discard message but continue the autosave - // submissions. - if (Drupal.autosaveForm.message && !Drupal.autosaveForm.formHasErrors) { - var dialogOptions = { - buttons: { - button_confirm: { - text: Drupal.t('Resume editing'), - class: 'autosave-form-resume-button', - click: function () { - // Non ajax buttons are bound to click. - // autosave-form-restore - $('.' + Drupal.autosaveForm.autosave_restore_class).trigger( - 'click', - ); - }, - }, - button_reject: { - text: Drupal.t('Discard'), - class: 'autosave-form-reject-button', - click: function () { - triggerAjaxSubmitWithoutProgressIndication( - Drupal.autosaveForm.autosave_reject_class, - true, - ); - $(this).dialog('close'); - }, - primary: true, - }, - }, - close: function (event, ui) { - $(this).remove(); - $(context) - .find('.' + Drupal.autosaveForm.autosave_restore_class) - .remove(); - $(context) - .find('.' + Drupal.autosaveForm.autosave_reject_class) - .remove(); - $(context).find('.autosave-form-restore-discard').remove(); - autosavePeriodic(); - }, - }; - - $.extend( - true, - dialogOptions, - Drupal.autosaveForm.defaultDialogOptions, - Drupal.autosaveForm.dialog_options, - ); - - $('
') - .appendTo('body') - .html('
' + Drupal.autosaveForm.message + '
') - .dialog(dialogOptions); - - // Temp - const btn = $('.autosave-form-restore'); - $('.ui-dialog-buttonset').append(btn[0]); - // .... - } else { - autosavePeriodic(); - } + autosavePeriodic(); } /** diff --git a/packages/drupal/silverback_autosave/src/Form/AutosaveButtonClickedTrait.php b/packages/drupal/silverback_autosave/src/Form/AutosaveButtonClickedTrait.php index a51d17f7b..6cd709b10 100644 --- a/packages/drupal/silverback_autosave/src/Form/AutosaveButtonClickedTrait.php +++ b/packages/drupal/silverback_autosave/src/Form/AutosaveButtonClickedTrait.php @@ -30,43 +30,4 @@ protected function isAutosaveTriggered(FormStateInterface $form_state) { return $autosave; } - /** - * Checks if autosave restore has been triggered. - * - * @param array $form - * The form array. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - * - * @return bool - * TRUE if autosave restore has been triggered, FALSE otherwise. - */ - protected function isRestoreTriggered($form, FormStateInterface $form_state) { - $input = $form_state->getUserInput(); - // The restore submit is a non-ajax element and therefore its name will be - // contained in the user input as a key. - $triggered = isset($input[AutosaveFormInterface::AUTOSAVE_RESTORE_ELEMENT_NAME]); - return $triggered; - } - - /** - * Checks if autosave restore has been triggered. - * - * @param array $form - * The form array. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - * - * @return bool - * TRUE if autosave restore has been triggered, FALSE otherwise. - */ - protected function isRejectTriggered($form, FormStateInterface $form_state) { - $user_input = $form_state->getUserInput(); - // The reject submit is an ajax element and therefore its name will not be - // contained in the user input as a key, but will be contained as a value - // under the key "_triggering_element_name". - $triggered = isset($user_input['_triggering_element_name']) && ($user_input['_triggering_element_name'] == AutosaveFormInterface::AUTOSAVE_REJECT_ELEMENT_NAME); - return $triggered; - } - } diff --git a/packages/drupal/silverback_autosave/src/Form/AutosaveFormAlterTrait.php b/packages/drupal/silverback_autosave/src/Form/AutosaveFormAlterTrait.php index 38df546e0..bd571d276 100644 --- a/packages/drupal/silverback_autosave/src/Form/AutosaveFormAlterTrait.php +++ b/packages/drupal/silverback_autosave/src/Form/AutosaveFormAlterTrait.php @@ -35,44 +35,6 @@ public function formAlter(array &$form, FormStateInterface $form_state) { $form['#attached']['drupalSettings']['autosaveForm']['notification'] = $this->configFactory->get('silverback_autosave.settings')->get('notification'); $input = $form_state->getUserInput(); - // $show_restore_discard = !$form_state->isRebuilding() ?: !empty($input['autosave_restore_discard']); - $show_restore_discard = FALSE; - if ($show_restore_discard && !$form_state->get('silverback_autosave_state_timestamp') && !$form_state->get('silverback_autosave_rejected') && ($silverback_autosave_state_timestamp = $this->getLastAutosavedTimestamp($form_state, $this->currentUser->id()))) { - $form[AutosaveFormInterface::AUTOSAVE_RESTORE_ELEMENT_NAME] = [ - '#type' => 'submit', - '#name' => AutosaveFormInterface::AUTOSAVE_RESTORE_ELEMENT_NAME, - '#value' => $this->t('Autosave restore'), - '#limit_validation_errors' => [], - // '#attributes' => ['class' => ['autosave-form-restore', 'visually-hidden']], - '#attributes' => ['class' => ['autosave-form-restore']], - '#submit' => [[$this, 'autosaveFormRestoreSubmit']], - '#silverback_autosave_state_timestamp' => $silverback_autosave_state_timestamp, - ]; - $form[AutosaveFormInterface::AUTOSAVE_REJECT_ELEMENT_NAME] = [ - '#type' => 'submit', - '#name' => 'silverback_autosave_reject', - '#value' => $this->t('Autosave reject'), - '#limit_validation_errors' => [], - '#attributes' => ['class' => ['autosave-form-reject', 'visually-hidden']], - '#submit' => [[$this, 'autosaveFormRejectSubmit']], - '#ajax' => [ - 'callback' => [$this, 'autosaveFormRejectAjax'], - ], - ]; - $form['autosave_restore_discard'] = [ - '#type' => 'hidden', - '#default_value' => 'autosave_restore_discard', - '#attributes' => ['class' => ['autosave-form-restore-discard']], - ]; - - // Add the message to be shown on the form. Our JS library will check if - // the message exist and only then offer the options for restore and - // reject, otherwise will start the auto save process. - $date = $this->dateFormatter->format($silverback_autosave_state_timestamp, 'custom', 'M d, Y H:i'); - $message = $this->t('A version of this page you were editing at @date was saved as a draft. Do you want to resume editing or discard it?', ['@date' => $date]); - $form['#attached']['drupalSettings']['autosaveForm']['message'] = (string) $message; - } - $silverback_autosave_session_id = $this->getAutosaveFormSessionID($form_state); if (!$silverback_autosave_session_id) { $silverback_autosave_session_id = !empty($input['silverback_autosave_session_id']) ? $input['silverback_autosave_session_id'] : $form['#build_id']; @@ -204,9 +166,6 @@ public function autosaveFormSubmit($form, FormStateInterface $form_state) { 'form_token', 'ajax_page_state', 'silverback_autosave_last_autosave_timestamp', - AutosaveFormInterface::AUTOSAVE_RESTORE_ELEMENT_NAME, - AutosaveFormInterface::AUTOSAVE_REJECT_ELEMENT_NAME, - 'autosave_restore_discard', ]; foreach ($skip_from_comparison_keys as $skip_from_comparison_key) { unset($autosaved_form_state_input[$skip_from_comparison_key]); diff --git a/packages/drupal/silverback_autosave/src/Form/AutosaveFormBuilder.php b/packages/drupal/silverback_autosave/src/Form/AutosaveFormBuilder.php index cc5896905..3c8bd0b7f 100644 --- a/packages/drupal/silverback_autosave/src/Form/AutosaveFormBuilder.php +++ b/packages/drupal/silverback_autosave/src/Form/AutosaveFormBuilder.php @@ -116,9 +116,6 @@ public function buildForm($form_id, FormStateInterface &$form_state) { // Additionally unset the form elements and settings which might have been // added, but aren't actually needed. unset($form['#attached']['drupalSettings']['autosaveForm']['message']); - unset($form[AutosaveFormInterface::AUTOSAVE_RESTORE_ELEMENT_NAME]); - unset($form[AutosaveFormInterface::AUTOSAVE_REJECT_ELEMENT_NAME]); - unset($form['autosave_restore_discard']); } return $form; } diff --git a/packages/drupal/silverback_autosave/src/Form/AutosaveFormInterface.php b/packages/drupal/silverback_autosave/src/Form/AutosaveFormInterface.php index 069e64495..d08030316 100644 --- a/packages/drupal/silverback_autosave/src/Form/AutosaveFormInterface.php +++ b/packages/drupal/silverback_autosave/src/Form/AutosaveFormInterface.php @@ -10,8 +10,6 @@ interface AutosaveFormInterface { const AUTOSAVE_ELEMENT_NAME = 'silverback_autosave_save'; - const AUTOSAVE_RESTORE_ELEMENT_NAME = 'silverback_autosave_restore'; - const AUTOSAVE_REJECT_ELEMENT_NAME = 'silverback_autosave_reject'; /** * Performs the needed alterations to the form. From 44fe208d9eaca1258991f28760514ef2f60ca132 Mon Sep 17 00:00:00 2001 From: Dimitris Spachos Date: Fri, 12 Apr 2024 10:34:25 +0300 Subject: [PATCH 11/82] feat(slb-146): purge previous entities and keep only one each time --- packages/drupal/silverback_autosave/README.md | 2 + .../js/silverback_autosave.js | 6 +-- .../src/Form/AutosaveFormAlterTrait.php | 41 ++----------------- .../AutosaveEntityFormDatabaseStorage.php | 9 +++- 4 files changed, 15 insertions(+), 43 deletions(-) diff --git a/packages/drupal/silverback_autosave/README.md b/packages/drupal/silverback_autosave/README.md index 33a8d4f82..625f34115 100644 --- a/packages/drupal/silverback_autosave/README.md +++ b/packages/drupal/silverback_autosave/README.md @@ -3,3 +3,5 @@ This module provides an autosave functionality on Guteneberg enabled nodes. On node edit, the content under edit is automatically saved for use in real time preview. + +The module is based on the [https://www.drupal.org/project/autosave_form](https://www.drupal.org/project/autosave_form). diff --git a/packages/drupal/silverback_autosave/js/silverback_autosave.js b/packages/drupal/silverback_autosave/js/silverback_autosave.js index 3c2777f02..8d421518d 100644 --- a/packages/drupal/silverback_autosave/js/silverback_autosave.js +++ b/packages/drupal/silverback_autosave/js/silverback_autosave.js @@ -20,8 +20,6 @@ message: '', dialog_options: [], autosave_submit_class: 'autosave-form-save', - autosave_restore_class: 'autosave-form-restore', - autosave_reject_class: 'autosave-form-reject', notification: { active: true, message: Drupal.t('Updating preview...'), @@ -188,8 +186,8 @@ return; } - // If there are gutenberg fields, we need to make sure the textarea value - // is updated before autosaving. + // If there are gutenberg fields, we need to make sure + // the textarea value is updated before autosaving. const $gutenbergFields = $('.field--gutenberg textarea').first(); if ($gutenbergFields.length) { const { data } = window.wp; diff --git a/packages/drupal/silverback_autosave/src/Form/AutosaveFormAlterTrait.php b/packages/drupal/silverback_autosave/src/Form/AutosaveFormAlterTrait.php index bd571d276..6466d4187 100644 --- a/packages/drupal/silverback_autosave/src/Form/AutosaveFormAlterTrait.php +++ b/packages/drupal/silverback_autosave/src/Form/AutosaveFormAlterTrait.php @@ -37,7 +37,9 @@ public function formAlter(array &$form, FormStateInterface $form_state) { $silverback_autosave_session_id = $this->getAutosaveFormSessionID($form_state); if (!$silverback_autosave_session_id) { - $silverback_autosave_session_id = !empty($input['silverback_autosave_session_id']) ? $input['silverback_autosave_session_id'] : $form['#build_id']; + $silverback_autosave_session_id = !empty($input['silverback_autosave_session_id']) ? + $input['silverback_autosave_session_id'] : + $form['#build_id']; $this->setAutosaveFormSessionID($form_state, $silverback_autosave_session_id); } @@ -79,43 +81,6 @@ public function formAlter(array &$form, FormStateInterface $form_state) { ]; } - /** - * Form submission handler for restoring autosaved state. - */ - public function autosaveFormRestoreSubmit($form, FormStateInterface $form_state) { - $triggering_element = $form_state->getTriggeringElement(); - if (!empty($triggering_element['#silverback_autosave_state_timestamp'])) { - // Set the timestamp of the autosaved state which has to be used to - // restore the form on rebuild. - $form_state->set('silverback_autosave_state_timestamp', $triggering_element['#silverback_autosave_state_timestamp']); - $form_state->setRebuild(); - } - } - - /** - * Form submission handler for rejecting autosaved states. - */ - public function autosaveFormRejectSubmit($form, FormStateInterface $form_state) { - // As this processing might take some time we want to prevent that if the - // connection is terminated the user input will be lost. - ignore_user_abort(TRUE); - // Mark the form state with a flag indicating an action for autosave has - // been chosen. - $form_state->set('silverback_autosave_rejected', TRUE); - // Cache the form state in order for the flag to be kept. - $form_state->setCached(); - - // Purge the autosave states. - $this->purgeAllAutosavedStates($form_state, $this->currentUser->id()); - } - - /** - * Ajax callback for rejecting autosaved states. - */ - public function autosaveFormRejectAjax($form, FormStateInterface $form_state) { - return new AjaxResponse(); - } - /** * Form submission handler for autosaving forms. */ diff --git a/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormDatabaseStorage.php b/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormDatabaseStorage.php index 4e3fdbe74..574d0934e 100644 --- a/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormDatabaseStorage.php +++ b/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormDatabaseStorage.php @@ -53,6 +53,13 @@ public function storeEntityAndFormState($form_id, $form_session_id, $entity_type ]); if (!$entity->isNew()) { + + // Purge previous stored states for the entity, + // e.g. when user is navigating away from the page. + // Also, this approach ensures that there will be always + // a stored form state in the db, in case we need to restore. + $this->purgeAutosavedEntityState($entity_type_id, $entity_id, NULL, $form_id, $langcode, $uid); + // Now, enter the new values. $this->connection->insert(static::AUTOSAVE_ENTITY_FORM_TABLE) ->fields([ 'form_id', @@ -163,7 +170,7 @@ public function getFormState($form_id, $entity_type_id, $entity_id, $langcode, $ ->condition('form_id', $form_id); if (isset($form_session_id)) { - // $query->condition('form_session_id', $form_session_id); + $query->condition('form_session_id', $form_session_id); } $query->condition('entity_type_id', $entity_type_id) From c8c8276fa70ef68ff4a9cdd2932b257e4dedc7d0 Mon Sep 17 00:00:00 2001 From: Dimitris Spachos Date: Fri, 12 Apr 2024 11:21:34 +0300 Subject: [PATCH 12/82] feat(slb-146): revert purge of previous states (build error) --- .../src/Storage/AutosaveEntityFormDatabaseStorage.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormDatabaseStorage.php b/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormDatabaseStorage.php index 574d0934e..21dae10ad 100644 --- a/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormDatabaseStorage.php +++ b/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormDatabaseStorage.php @@ -58,7 +58,7 @@ public function storeEntityAndFormState($form_id, $form_session_id, $entity_type // e.g. when user is navigating away from the page. // Also, this approach ensures that there will be always // a stored form state in the db, in case we need to restore. - $this->purgeAutosavedEntityState($entity_type_id, $entity_id, NULL, $form_id, $langcode, $uid); + // $this->purgeAutosavedEntityState($entity_type_id, $entity_id, NULL, $form_id, $langcode, $uid); // Now, enter the new values. $this->connection->insert(static::AUTOSAVE_ENTITY_FORM_TABLE) ->fields([ @@ -169,9 +169,10 @@ public function getFormState($form_id, $entity_type_id, $entity_id, $langcode, $ ->orderBy('timestamp', 'DESC') ->condition('form_id', $form_id); - if (isset($form_session_id)) { - $query->condition('form_session_id', $form_session_id); - } + // Use this for future restore state. + /* if (isset($form_session_id)) { + $query->condition('form_session_id', $form_session_id); + } */ $query->condition('entity_type_id', $entity_type_id) ->condition('entity_id', $entity_id) From 53b3a50c0eaa44330adeccc161d7aa8677164450 Mon Sep 17 00:00:00 2001 From: Dimitris Spachos Date: Fri, 12 Apr 2024 12:52:21 +0300 Subject: [PATCH 13/82] feat(slb-146): workaround: purghe stored states on beforeupload --- .../js/silverback_autosave.js | 8 ++++- .../src/Form/AutosaveFormAlterTrait.php | 30 +++++++++++++++++++ .../AutosaveEntityFormDatabaseStorage.php | 2 +- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/packages/drupal/silverback_autosave/js/silverback_autosave.js b/packages/drupal/silverback_autosave/js/silverback_autosave.js index 8d421518d..e9c9438b3 100644 --- a/packages/drupal/silverback_autosave/js/silverback_autosave.js +++ b/packages/drupal/silverback_autosave/js/silverback_autosave.js @@ -32,7 +32,13 @@ * Add a variable which determines if the window is being unloaded. */ Drupal.autosaveForm.beforeUnloadCalled = false; - $(window).on('beforeunload pagehide', function () { + + $(window).on('pagehide', function () { + Drupal.autosaveForm.beforeUnloadCalled = true; + }); + + $(window).on('beforeunload', function () { + $('#purge-button').trigger('click'); Drupal.autosaveForm.beforeUnloadCalled = true; }); diff --git a/packages/drupal/silverback_autosave/src/Form/AutosaveFormAlterTrait.php b/packages/drupal/silverback_autosave/src/Form/AutosaveFormAlterTrait.php index 6466d4187..90399ad35 100644 --- a/packages/drupal/silverback_autosave/src/Form/AutosaveFormAlterTrait.php +++ b/packages/drupal/silverback_autosave/src/Form/AutosaveFormAlterTrait.php @@ -74,6 +74,20 @@ public function formAlter(array &$form, FormStateInterface $form_state) { '#silverback_autosave_session_id' => $silverback_autosave_session_id, ]; + $form['purge'] = [ + '#type' => 'submit', + '#id' => 'purge-button', + '#name' => 'autosave_form_purge', + '#value' => $this->t('Autosave purge'), + '#limit_validation_errors' => [], + '#attributes' => ['class' => ['autosave-form-purge', 'visually-hidden']], + '#submit' => [[$this, 'autosaveFormPurgeSubmit']], + '#ajax' => [ + 'callback' => [$this, 'autosaveFormPurgeAjax'], + 'event' => 'click', + ], + ]; + $form['silverback_autosave_last_autosave_timestamp'] = [ '#type' => 'hidden', '#name' => 'silverback_autosave_last_autosave_timestamp', @@ -81,6 +95,22 @@ public function formAlter(array &$form, FormStateInterface $form_state) { ]; } + /** + * Form submission handler for rejecting autosaved states. + */ + public function autosaveFormPurgeSubmit($form, FormStateInterface $form_state) { + \Drupal::logger('debug')->debug(__METHOD__); + $this->purgeAllAutosavedStates($form_state, $this->currentUser->id()); + } + + /** + * Ajax callback for rejecting autosaved states. + */ + public function autosaveFormPurgeAjax($form, FormStateInterface $form_state) { + \Drupal::logger('debug')->debug(__METHOD__); + return new AjaxResponse(); + } + /** * Form submission handler for autosaving forms. */ diff --git a/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormDatabaseStorage.php b/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormDatabaseStorage.php index 21dae10ad..7ab4b5576 100644 --- a/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormDatabaseStorage.php +++ b/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormDatabaseStorage.php @@ -54,12 +54,12 @@ public function storeEntityAndFormState($form_id, $form_session_id, $entity_type if (!$entity->isNew()) { + // @todo FIX This causes new previews to restore the saved one (instead of current form state). // Purge previous stored states for the entity, // e.g. when user is navigating away from the page. // Also, this approach ensures that there will be always // a stored form state in the db, in case we need to restore. // $this->purgeAutosavedEntityState($entity_type_id, $entity_id, NULL, $form_id, $langcode, $uid); - // Now, enter the new values. $this->connection->insert(static::AUTOSAVE_ENTITY_FORM_TABLE) ->fields([ 'form_id', From 86e489bde138cea6675532adbf2422a499a3279f Mon Sep 17 00:00:00 2001 From: Dimitris Spachos Date: Fri, 12 Apr 2024 13:00:18 +0300 Subject: [PATCH 14/82] feat(slb-146): add form session id on get form state condition (optional) --- .../src/Storage/AutosaveEntityFormDatabaseStorage.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormDatabaseStorage.php b/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormDatabaseStorage.php index 7ab4b5576..0098464b7 100644 --- a/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormDatabaseStorage.php +++ b/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormDatabaseStorage.php @@ -169,10 +169,9 @@ public function getFormState($form_id, $entity_type_id, $entity_id, $langcode, $ ->orderBy('timestamp', 'DESC') ->condition('form_id', $form_id); - // Use this for future restore state. - /* if (isset($form_session_id)) { - $query->condition('form_session_id', $form_session_id); - } */ + if (isset($form_session_id)) { + $query->condition('form_session_id', $form_session_id); + } $query->condition('entity_type_id', $entity_type_id) ->condition('entity_id', $entity_id) From 3d0495296472a9c1b0c7f1fe9952de6a4dddbed9 Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Fri, 12 Apr 2024 17:03:53 +0200 Subject: [PATCH 15/82] feat: directive to load all translations of a menu Necessary to load them into the preview. --- packages/drupal/custom/custom.services.yml | 3 ++ packages/drupal/custom/src/Menus.php | 33 ++++++++++++++++++++++ packages/schema/src/schema.graphql | 15 ++++++++-- 3 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 packages/drupal/custom/src/Menus.php diff --git a/packages/drupal/custom/custom.services.yml b/packages/drupal/custom/custom.services.yml index fd6d0a918..5adbb9068 100644 --- a/packages/drupal/custom/custom.services.yml +++ b/packages/drupal/custom/custom.services.yml @@ -7,3 +7,6 @@ services: custom.webform: class: Drupal\custom\Webform arguments: ['@renderer', '@entity_type.manager', '@serializer'] + + custom.menus: + class: Drupal\custom\Menus diff --git a/packages/drupal/custom/src/Menus.php b/packages/drupal/custom/src/Menus.php new file mode 100644 index 000000000..fbaaf4aa0 --- /dev/null +++ b/packages/drupal/custom/src/Menus.php @@ -0,0 +1,33 @@ +getStorage('menu')->load($args->args['menu_id']); + $languages = \Drupal::languageManager()->getLanguages(); + $translations = []; + foreach ($languages as $language) { + $translation = clone $menu; + $translation->set('langcode', $language->getId()); + $translations[] = $translation; + } + return $translations; + } + +} diff --git a/packages/schema/src/schema.graphql b/packages/schema/src/schema.graphql index 232210302..289d4000b 100644 --- a/packages/schema/src/schema.graphql +++ b/packages/schema/src/schema.graphql @@ -3,6 +3,11 @@ scalar Markup @default @value(string: "") scalar ImageSource @default @value(string: "") scalar JSONString @default @value(string: "{}") +""" +implementation(drupal): custom.menus::getMenuTranslations +""" +directive @menuTranslations(menu_id: String!) on FIELD_DEFINITION + """ implementation(drupal): custom.webform::url """ @@ -246,8 +251,14 @@ type Query { previewDecapPage: DecapPage previewDrupalPage(id: ID!, rid: ID, locale: String!): DrupalPage @fetchEntity(type: "node", id: "$id", rid: "$rid", language: "$locale") - mainNavigations: [MainNavigation] @gatsbyNodes(type: "MainNavigation") - footerNavigations: [FooterNavigation] @gatsbyNodes(type: "FooterNavigation") + + mainNavigations: [MainNavigation] + @gatsbyNodes(type: "MainNavigation") + @menuTranslations(menu_id: "main") + + footerNavigations: [FooterNavigation] + @gatsbyNodes(type: "FooterNavigation") + @menuTranslations(menu_id: "footer") allPages: [Page] @gatsbyNodes(type: "Page") websiteSettings: WebsiteSettings From a7aa166b0f424baead5a1ee129b291f73de3b66b Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Fri, 12 Apr 2024 17:13:31 +0200 Subject: [PATCH 16/82] fix(SLB-209): remove refetch interval --- packages/ui/src/utils/operation.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/utils/operation.ts b/packages/ui/src/utils/operation.ts index 9ec7d1742..3c8872c77 100644 --- a/packages/ui/src/utils/operation.ts +++ b/packages/ui/src/utils/operation.ts @@ -19,7 +19,10 @@ export function useOperation( // so it does not try to fetch. executor instanceof Function ? (arg) => executor(arg[1]) : null, variables?.pathname?.indexOf('__preview') > 0 - ? { suspense: false, refreshInterval: 1000 } + ? { + suspense: false, + keepPreviousData: true, + } : { suspense: false }, ); return executor instanceof Function From 5c9eccc4c2fec537f290cb430a9165980b03a22e Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Fri, 12 Apr 2024 17:14:18 +0200 Subject: [PATCH 17/82] feat(SLB-209): expose function to invalidate an swr query --- packages/ui/src/utils/operation.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/utils/operation.ts b/packages/ui/src/utils/operation.ts index 3c8872c77..f05499b3f 100644 --- a/packages/ui/src/utils/operation.ts +++ b/packages/ui/src/utils/operation.ts @@ -4,9 +4,16 @@ import { OperationVariables, useExecutor, } from '@custom/schema'; -import useSwr, { SWRResponse } from 'swr'; +import useSwr, { mutate, SWRResponse } from 'swr'; import useSWRMutation, { SWRMutationResponse } from 'swr/mutation'; +export function clear( + operation: TOperation, + variables?: OperationVariables, +) { + mutate([operation, variables]); +} + export function useOperation( operation: TOperation, variables?: OperationVariables, From 77665a6c04c64dc8413ae0288a8ded8e0e037be2 Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Fri, 12 Apr 2024 17:14:54 +0200 Subject: [PATCH 18/82] feat(SLB-209): dedicated preview rout that allows to handle refreshes --- packages/ui/src/components/Routes/Preview.tsx | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 packages/ui/src/components/Routes/Preview.tsx diff --git a/packages/ui/src/components/Routes/Preview.tsx b/packages/ui/src/components/Routes/Preview.tsx new file mode 100644 index 000000000..3e6fc0e8f --- /dev/null +++ b/packages/ui/src/components/Routes/Preview.tsx @@ -0,0 +1,29 @@ +import type { OperationVariables } from '@custom/schema'; +import { PreviewDrupalPageQuery, useLocation } from '@custom/schema'; +import React from 'react'; + +import { clear, useOperation } from '../../utils/operation'; +import { PageDisplay } from '../Organisms/PageDisplay'; + +function usePreviewParameters(): OperationVariables< + typeof PreviewDrupalPageQuery +> { + const [location] = useLocation(); + + const nid = location.searchParams.get('nid'); + const rid = location.searchParams.get('rid'); + const lang = location.searchParams.get('lang'); + return { id: nid || '', rid: rid || '', locale: lang || 'en' }; +} + +export function usePreviewRefresh() { + const params = usePreviewParameters(); + return () => clear(PreviewDrupalPageQuery, params); +} + +export function Preview() { + const { data } = useOperation(PreviewDrupalPageQuery, usePreviewParameters()); + if (data?.preview) { + return ; + } +} From 1846283a7282c1aeaca0f6436b262d78c7b7b2c2 Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Fri, 12 Apr 2024 17:15:26 +0200 Subject: [PATCH 19/82] feat(SLB-209): dedicated preview app with websocket server --- apps/preview/.eslintrc.json | 25 ++++++++++++ apps/preview/.gitignore | 25 ++++++++++++ apps/preview/.swcrc | 11 ++++++ apps/preview/index.html | 15 ++++++++ apps/preview/package.json | 41 ++++++++++++++++++++ apps/preview/server/index.ts | 34 +++++++++++++++++ apps/preview/src/App.tsx | 33 ++++++++++++++++ apps/preview/src/drupal-executor.ts | 59 +++++++++++++++++++++++++++++ apps/preview/src/main.tsx | 12 ++++++ apps/preview/src/vite-env.d.ts | 1 + apps/preview/tsconfig.json | 25 ++++++++++++ apps/preview/tsconfig.node.json | 12 ++++++ apps/preview/turbo.json | 20 ++++++++++ apps/preview/vite.config.ts | 7 ++++ 14 files changed, 320 insertions(+) create mode 100644 apps/preview/.eslintrc.json create mode 100644 apps/preview/.gitignore create mode 100644 apps/preview/.swcrc create mode 100644 apps/preview/index.html create mode 100644 apps/preview/package.json create mode 100644 apps/preview/server/index.ts create mode 100644 apps/preview/src/App.tsx create mode 100644 apps/preview/src/drupal-executor.ts create mode 100644 apps/preview/src/main.tsx create mode 100644 apps/preview/src/vite-env.d.ts create mode 100644 apps/preview/tsconfig.json create mode 100644 apps/preview/tsconfig.node.json create mode 100644 apps/preview/turbo.json create mode 100644 apps/preview/vite.config.ts diff --git a/apps/preview/.eslintrc.json b/apps/preview/.eslintrc.json new file mode 100644 index 000000000..4039eeeb2 --- /dev/null +++ b/apps/preview/.eslintrc.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json.schemastore.org/eslintrc.json", + "extends": ["plugin:storybook/recommended", "plugin:tailwindcss/recommended"], + "plugins": ["tailwindcss", "formatjs"], + "rules": { + "react/jsx-no-literals": [ + "warn", + { + "noStrings": true, + "ignoreProps": true, + "noAttributeStrings": false + } + ], + "formatjs/enforce-default-message": "error", + "formatjs/enforce-id": [ + "error", + { + "idInterpolationPattern": "[sha512:contenthash:base64:6]" + } + ], + "formatjs/enforce-placeholders": "error", + "formatjs/no-camel-case": "error", + "tailwindcss/classnames-order": [0] + } +} diff --git a/apps/preview/.gitignore b/apps/preview/.gitignore new file mode 100644 index 000000000..bd98e77ad --- /dev/null +++ b/apps/preview/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +build +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/apps/preview/.swcrc b/apps/preview/.swcrc new file mode 100644 index 000000000..fb32da49a --- /dev/null +++ b/apps/preview/.swcrc @@ -0,0 +1,11 @@ +{ + "$schema": "https://json.schemastore.org/swcrc", + "jsc": { + "parser": { + "syntax": "typescript" + } + }, + "module": { + "type": "es6" + } +} diff --git a/apps/preview/index.html b/apps/preview/index.html new file mode 100644 index 000000000..99e1a3605 --- /dev/null +++ b/apps/preview/index.html @@ -0,0 +1,15 @@ + + + + + + + Preview + + + +
+ + + + \ No newline at end of file diff --git a/apps/preview/package.json b/apps/preview/package.json new file mode 100644 index 000000000..058d74c2a --- /dev/null +++ b/apps/preview/package.json @@ -0,0 +1,41 @@ +{ + "name": "custom/preview", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "test:static": "tsc --noEmit && eslint \"**/*.{ts,tsx,js,jsx}\" --ignore-path=\"./.gitignore\"", + "dev:vite": "vite", + "dev:hono": "tsx watch src/index.ts", + "prep:app": "vite build", + "prep:server": "swc ./server -d ./build", + "start": "node build/index.js" + }, + "dependencies": { + "@custom/schema": "workspace:*", + "@custom/ui": "workspace:*", + "express": "^4.19.2", + "express-ws": "^5.0.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@swc/cli": "^0.1.63", + "@swc/core": "^1.3.102", + "@types/express": "^4.17.21", + "@types/express-ws": "^3.0.4", + "@types/node": "^20.11.17", + "@types/react": "^18.2.46", + "@types/react-dom": "^18.2.18", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", + "@vitejs/plugin-react-swc": "^3.5.0", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.6", + "tsx": "^4.7.1", + "typescript": "^5.2.2", + "vite": "^5.2.0" + } +} diff --git a/apps/preview/server/index.ts b/apps/preview/server/index.ts new file mode 100644 index 000000000..e9db6c288 --- /dev/null +++ b/apps/preview/server/index.ts @@ -0,0 +1,34 @@ +import express from 'express'; +import expressWs from 'express-ws'; +import { Subject } from 'rxjs'; + +const expressServer = express(); +const expressWsInstance = expressWs(expressServer); +const { app } = expressWsInstance; + +const updates$ = new Subject(); + +// TODO: Protect endpoints and preview with Drupal authentication. +app.post('/__preview', (req, res) => { + updates$.next({ body: req.body }); + res.json(true); +}); + +app.ws('/__preview', (ws) => { + // TODO: Separate updates per user session. + const sub = updates$.subscribe((payload) => { + ws.send(JSON.stringify(payload)); + }); + ws.on('close', sub.unsubscribe); +}); + +app.get('/__preview/*', (req, _, next) => { + req.url = '/'; + next(); +}); + +app.use(express.static('./dist')); +const port = process.env.PREVIEW_PORT || 8001; +console.log(`Server is running on port ${port}`); + +app.listen({ port }); diff --git a/apps/preview/src/App.tsx b/apps/preview/src/App.tsx new file mode 100644 index 000000000..e63dd4861 --- /dev/null +++ b/apps/preview/src/App.tsx @@ -0,0 +1,33 @@ +import { OperationExecutor } from '@custom/schema'; +import { Frame } from '@custom/ui/routes/Frame'; +import { Preview, usePreviewRefresh } from '@custom/ui/routes/Preview'; +import { useEffect } from 'react'; +import { webSocket } from 'rxjs/webSocket'; + +import { drupalExecutor } from './drupal-executor'; + +const updates$ = webSocket({ + url: `${window.location.origin.replace('http', 'ws')}/__preview`, +}); + +function App() { + const refresh = usePreviewRefresh(); + useEffect(() => { + const sub = updates$.subscribe(refresh); + return sub.unsubscribe; + }, [refresh]); + return ( + + + + + + ); +} + +export default App; diff --git a/apps/preview/src/drupal-executor.ts b/apps/preview/src/drupal-executor.ts new file mode 100644 index 000000000..cfe9a009c --- /dev/null +++ b/apps/preview/src/drupal-executor.ts @@ -0,0 +1,59 @@ +import { AnyOperationId, OperationVariables } from '@custom/schema'; + +/** + * Create an executor that operates against a Drupal endpoint. + */ +export function drupalExecutor(endpoint: string, forward: boolean = true) { + return async function ( + id: OperationId, + variables?: OperationVariables, + ) { + const url = new URL(endpoint, window.location.origin); + const isMutation = id.includes('Mutation:'); + if (isMutation) { + const { data, errors } = await ( + await fetch(url, { + method: 'POST', + credentials: 'include', + body: JSON.stringify({ + queryId: id, + variables: variables || {}, + }), + headers: forward + ? { + 'SLB-Forwarded-Proto': window.location.protocol.slice(0, -1), + 'SLB-Forwarded-Host': window.location.hostname, + 'SLB-Forwarded-Port': window.location.port, + 'Content-Type': 'application/json', + } + : { + 'Content-Type': 'application/json', + }, + }) + ).json(); + if (errors) { + throw errors; + } + return data; + } else { + url.searchParams.set('queryId', id); + url.searchParams.set('variables', JSON.stringify(variables || {})); + const { data, errors } = await ( + await fetch(url, { + credentials: 'include', + headers: forward + ? { + 'SLB-Forwarded-Proto': window.location.protocol.slice(0, -1), + 'SLB-Forwarded-Host': window.location.hostname, + 'SLB-Forwarded-Port': window.location.port, + } + : {}, + }) + ).json(); + if (errors) { + throw errors; + } + return data; + } + }; +} diff --git a/apps/preview/src/main.tsx b/apps/preview/src/main.tsx new file mode 100644 index 000000000..407d3ea7e --- /dev/null +++ b/apps/preview/src/main.tsx @@ -0,0 +1,12 @@ +import '@custom/ui/styles.css'; + +import React from 'react'; +import ReactDOM from 'react-dom/client'; + +import App from './App.tsx'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/apps/preview/src/vite-env.d.ts b/apps/preview/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/apps/preview/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/apps/preview/tsconfig.json b/apps/preview/tsconfig.json new file mode 100644 index 000000000..a7fc6fbf2 --- /dev/null +++ b/apps/preview/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/apps/preview/tsconfig.node.json b/apps/preview/tsconfig.node.json new file mode 100644 index 000000000..1a555ac37 --- /dev/null +++ b/apps/preview/tsconfig.node.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true, + "types": ["node"] + }, + "include": ["vite.config.ts"] +} diff --git a/apps/preview/turbo.json b/apps/preview/turbo.json new file mode 100644 index 000000000..1b2fdd568 --- /dev/null +++ b/apps/preview/turbo.json @@ -0,0 +1,20 @@ +{ + "extends": ["//"], + "pipeline": { + "prep": { + "dependsOn": ["prep:app", "prep:server"] + }, + "prep:app": { + "dependsOn": ["^prep"], + "inputs": ["src/**", "index.html", "vite.config.ts"], + "outputs": ["dist/**"] + }, + "prep:server": { + "inputs": ["server/**"], + "outputs": ["build/**"] + }, + "test:static": { + "inputs": ["src/**", "!src/gatsby-fragments.js"] + } + } +} diff --git a/apps/preview/vite.config.ts b/apps/preview/vite.config.ts new file mode 100644 index 000000000..e1b1b7638 --- /dev/null +++ b/apps/preview/vite.config.ts @@ -0,0 +1,7 @@ +import react from '@vitejs/plugin-react-swc'; +import { defineConfig } from 'vite'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], +}); From 2ba54ae1da35c42ed765d8765d3dd44a4989e07b Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Fri, 12 Apr 2024 17:17:09 +0200 Subject: [PATCH 20/82] feat(SLB-209): lagoon service for preview app --- .lagoon.yml | 6 ++++++ .lagoon/Dockerfile | 12 ++++++++++++ docker-compose.yml | 12 ++++++++++++ 3 files changed, 30 insertions(+) diff --git a/.lagoon.yml b/.lagoon.yml index 8261b6b63..22179218c 100644 --- a/.lagoon.yml +++ b/.lagoon.yml @@ -36,6 +36,8 @@ environments: - example.cms.amazeelabs.dev - build: - example.build.amazeelabs.dev + - preview: + - example.preview.amazeelabs.dev cronjobs: - name: drush cron schedule: '*/15 * * * *' @@ -47,6 +49,8 @@ environments: - stage-example.cms.amazeelabs.dev - build: - stage-example.build.amazeelabs.dev + - preview: + - stage-example.preview.amazeelabs.dev cronjobs: - name: drush cron schedule: '*/15 * * * *' @@ -58,6 +62,8 @@ environments: - dev-example.cms.amazeelabs.dev - build: - dev-example.build.amazeelabs.dev + - preview: + - dev-example.preview.amazeelabs.dev cronjobs: - name: drush cron schedule: '*/15 * * * *' diff --git a/.lagoon/Dockerfile b/.lagoon/Dockerfile index 4669b403e..686974441 100644 --- a/.lagoon/Dockerfile +++ b/.lagoon/Dockerfile @@ -47,6 +47,7 @@ RUN --mount=type=cache,target=/tmp/cache pnpm i && \ # Deploy apps. RUN --mount=type=cache,target=/tmp/cache pnpm deploy --filter "@custom/cms" /tmp/.deploy/cms --prod RUN --mount=type=cache,target=/tmp/cache pnpm deploy --filter "@custom/website" /tmp/.deploy/website --prod +RUN --mount=type=cache,target=/tmp/cache pnpm deploy --filter "@custom/preview" /tmp/.deploy/preview --prod # ==================================================================================================== # CLI IMAGE @@ -87,6 +88,17 @@ COPY .lagoon/nginx-conf/redirects-map.conf /etc/nginx/redirects-map.conf WORKDIR /app ENV WEBROOT=web +# ==================================================================================================== +# PREVIEW IMAGE +# ==================================================================================================== + +FROM uselagoon/node-18:23.12.0 as publisher + +RUN npm install -g pnpm@8.6.0 +COPY --from=builder /tmp/.deploy/preview /app +ENV PREVIEW_PORT=3000 +CMD pnpm start + # ==================================================================================================== # PUBLISHER IMAGE # ==================================================================================================== diff --git a/docker-compose.yml b/docker-compose.yml index 59d904129..4b85acf86 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -109,6 +109,18 @@ services: lagoon.persistent: /app/persisted-store lagoon.name: build + preview: + build: + context: . + target: preview + dockerfile: .lagoon/Dockerfile + environment: + <<: *default-environment + LAGOON_LOCALDEV_URL: preview-${COMPOSE_PROJECT_NAME:-slbtemplate}.docker.amazee.io + networks: + - amazeeio-network + - default + networks: amazeeio-network: external: true From b643ac26fa232e88d4d55ea7bf980088f4303fc9 Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Fri, 12 Apr 2024 17:17:35 +0200 Subject: [PATCH 21/82] feat(SLB-209): use preview app in drupal --- apps/cms/.lagoon.env | 1 + apps/cms/scaffold/settings.php.append.txt | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/cms/.lagoon.env b/apps/cms/.lagoon.env index a2fa48dbc..0309eb662 100644 --- a/apps/cms/.lagoon.env +++ b/apps/cms/.lagoon.env @@ -1,6 +1,7 @@ PROJECT_NAME=example PUBLISHER_URL="https://${LAGOON_GIT_BRANCH}-${PROJECT_NAME}.build.amazeelabs.dev" NETLIFY_URL="https://${LAGOON_GIT_BRANCH}-${PROJECT_NAME}.amazeelabs.dev" +PREVIEW_URL="https://${LAGOON_GIT_BRANCH}-${PROJECT_NAME}.preview.amazeelabs.dev" # Used to set the original client secret. PUBLISHER_OAUTH2_CLIENT_SECRET=REPLACE_ME diff --git a/apps/cms/scaffold/settings.php.append.txt b/apps/cms/scaffold/settings.php.append.txt index 7627baaa7..00575f853 100644 --- a/apps/cms/scaffold/settings.php.append.txt +++ b/apps/cms/scaffold/settings.php.append.txt @@ -5,9 +5,10 @@ $settings['file_private_path'] = $app_root . '/sites/default/files/private'; $publisherUrl = getenv('PUBLISHER_URL') ?: 'http://127.0.0.1:8000'; $netlifyUrl = getenv('NETLIFY_URL') ?: 'http://127.0.0.1:8000'; +$previewUrl = getenv('PREVIEW_URL') ?: 'http://127.0.0.1:8001'; $config['silverback_external_preview.settings'] = [ - 'preview_host' => $netlifyUrl, + 'preview_host' => $previewUrl, 'live_host' => $netlifyUrl, ]; From d8ca9a7f6d773fdb49868dabc8d78ab15c232d69 Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Fri, 12 Apr 2024 17:39:49 +0200 Subject: [PATCH 22/82] fix(SLB-209): lagoon type for preview service --- docker-compose.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 4b85acf86..c5ad2e374 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -120,6 +120,9 @@ services: networks: - amazeeio-network - default + labels: + lagoon.type: node + lagoon.name: preview networks: amazeeio-network: From 85f919afc8cdd93e24e0a51cb9377d41f05fddac Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Fri, 12 Apr 2024 18:08:14 +0200 Subject: [PATCH 23/82] fix(SLB-209): docker build stage name --- .lagoon/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.lagoon/Dockerfile b/.lagoon/Dockerfile index 686974441..269dfbead 100644 --- a/.lagoon/Dockerfile +++ b/.lagoon/Dockerfile @@ -92,7 +92,7 @@ ENV WEBROOT=web # PREVIEW IMAGE # ==================================================================================================== -FROM uselagoon/node-18:23.12.0 as publisher +FROM uselagoon/node-18:23.12.0 as preview RUN npm install -g pnpm@8.6.0 COPY --from=builder /tmp/.deploy/preview /app From 2d4c5b41b06e9ff8efbe8b1b60385afa909d9d08 Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Fri, 12 Apr 2024 18:09:18 +0200 Subject: [PATCH 24/82] fix(SLB-209): proxy graphql requests So it is possible to change Drupal's url via environment variables, after build. --- apps/preview/.lagoon.env | 1 + apps/preview/package.json | 2 + apps/preview/server/index.ts | 9 + apps/preview/src/App.tsx | 7 +- pnpm-lock.yaml | 437 ++++++++++++++++++++++++++++++++--- 5 files changed, 422 insertions(+), 34 deletions(-) create mode 100644 apps/preview/.lagoon.env diff --git a/apps/preview/.lagoon.env b/apps/preview/.lagoon.env new file mode 100644 index 000000000..2c80dc9c1 --- /dev/null +++ b/apps/preview/.lagoon.env @@ -0,0 +1 @@ +DRUPAL_URL="https://${LAGOON_GIT_BRANCH}-${PROJECT_NAME}.cms.amazeelabs.dev" diff --git a/apps/preview/package.json b/apps/preview/package.json index 058d74c2a..b3f160641 100644 --- a/apps/preview/package.json +++ b/apps/preview/package.json @@ -16,6 +16,7 @@ "@custom/ui": "workspace:*", "express": "^4.19.2", "express-ws": "^5.0.2", + "http-proxy-middleware": "^3.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", "rxjs": "^7.8.1" @@ -25,6 +26,7 @@ "@swc/core": "^1.3.102", "@types/express": "^4.17.21", "@types/express-ws": "^3.0.4", + "@types/http-proxy-middleware": "^1.0.0", "@types/node": "^20.11.17", "@types/react": "^18.2.46", "@types/react-dom": "^18.2.18", diff --git a/apps/preview/server/index.ts b/apps/preview/server/index.ts index e9db6c288..4556dc41b 100644 --- a/apps/preview/server/index.ts +++ b/apps/preview/server/index.ts @@ -1,5 +1,6 @@ import express from 'express'; import expressWs from 'express-ws'; +import { createProxyMiddleware } from 'http-proxy-middleware'; import { Subject } from 'rxjs'; const expressServer = express(); @@ -8,6 +9,14 @@ const { app } = expressWsInstance; const updates$ = new Subject(); +app.use( + '/graphql', + createProxyMiddleware({ + target: process.env.DRUPAL_URL || 'http://localhost:8888', + changeOrigin: true, + }), +); + // TODO: Protect endpoints and preview with Drupal authentication. app.post('/__preview', (req, res) => { updates$.next({ body: req.body }); diff --git a/apps/preview/src/App.tsx b/apps/preview/src/App.tsx index e63dd4861..ba86b75d2 100644 --- a/apps/preview/src/App.tsx +++ b/apps/preview/src/App.tsx @@ -17,12 +17,7 @@ function App() { return sub.unsubscribe; }, [refresh]); return ( - + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8aca358f2..5281ba170 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,7 +16,7 @@ importers: devDependencies: '@commitlint/cli': specifier: ^18.4.3 - version: 18.4.3(@types/node@18.19.31)(typescript@5.3.3) + version: 18.4.3(@types/node@20.12.7)(typescript@5.3.3) '@commitlint/config-conventional': specifier: ^18.4.3 version: 18.4.3 @@ -64,7 +64,7 @@ importers: version: 5.3.3 vitest: specifier: ^1.1.1 - version: 1.1.1(@types/node@18.19.31) + version: 1.1.1(@types/node@20.12.7) apps/cms: dependencies: @@ -184,6 +184,85 @@ importers: specifier: ^5.0.10 version: 5.0.10(@types/node@18.0.0) + apps/preview: + dependencies: + '@custom/schema': + specifier: workspace:* + version: link:../../packages/schema + '@custom/ui': + specifier: workspace:* + version: link:../../packages/ui + express: + specifier: ^4.19.2 + version: 4.19.2 + express-ws: + specifier: ^5.0.2 + version: 5.0.2(express@4.19.2) + http-proxy-middleware: + specifier: ^3.0.0 + version: 3.0.0 + react: + specifier: ^18.2.0 + version: 18.2.0 + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) + rxjs: + specifier: ^7.8.1 + version: 7.8.1 + devDependencies: + '@swc/cli': + specifier: ^0.1.63 + version: 0.1.63(@swc/core@1.4.13) + '@swc/core': + specifier: ^1.3.102 + version: 1.4.13 + '@types/express': + specifier: ^4.17.21 + version: 4.17.21 + '@types/express-ws': + specifier: ^3.0.4 + version: 3.0.4 + '@types/http-proxy-middleware': + specifier: ^1.0.0 + version: 1.0.0 + '@types/node': + specifier: ^20.11.17 + version: 20.12.7 + '@types/react': + specifier: ^18.2.46 + version: 18.2.46 + '@types/react-dom': + specifier: ^18.2.18 + version: 18.2.18 + '@typescript-eslint/eslint-plugin': + specifier: ^7.2.0 + version: 7.6.0(@typescript-eslint/parser@7.6.0)(eslint@7.32.0)(typescript@5.4.4) + '@typescript-eslint/parser': + specifier: ^7.2.0 + version: 7.6.0(eslint@7.32.0)(typescript@5.4.4) + '@vitejs/plugin-react-swc': + specifier: ^3.5.0 + version: 3.5.0(vite@5.2.8) + eslint: + specifier: '7' + version: 7.32.0 + eslint-plugin-react-hooks: + specifier: ^4.6.0 + version: 4.6.0(eslint@7.32.0) + eslint-plugin-react-refresh: + specifier: ^0.4.6 + version: 0.4.6(eslint@7.32.0) + tsx: + specifier: ^4.7.1 + version: 4.7.2 + typescript: + specifier: ^5.2.2 + version: 5.4.4 + vite: + specifier: ^5.2.0 + version: 5.2.8(@types/node@20.12.7) + apps/website: dependencies: '@amazeelabs/bridge-gatsby': @@ -2608,14 +2687,14 @@ packages: resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} engines: {node: '>=0.1.90'} - /@commitlint/cli@18.4.3(@types/node@18.19.31)(typescript@5.3.3): + /@commitlint/cli@18.4.3(@types/node@20.12.7)(typescript@5.3.3): resolution: {integrity: sha512-zop98yfB3A6NveYAZ3P1Mb6bIXuCeWgnUfVNkH4yhIMQpQfzFwseadazOuSn0OOfTt0lWuFauehpm9GcqM5lww==} engines: {node: '>=v18'} hasBin: true dependencies: '@commitlint/format': 18.6.1 '@commitlint/lint': 18.6.1 - '@commitlint/load': 18.6.1(@types/node@18.19.31)(typescript@5.3.3) + '@commitlint/load': 18.6.1(@types/node@20.12.7)(typescript@5.3.3) '@commitlint/read': 18.6.1 '@commitlint/types': 18.6.1 execa: 5.1.1 @@ -2686,7 +2765,7 @@ packages: '@commitlint/types': 18.6.1 dev: true - /@commitlint/load@18.6.1(@types/node@18.19.31)(typescript@5.3.3): + /@commitlint/load@18.6.1(@types/node@20.12.7)(typescript@5.3.3): resolution: {integrity: sha512-p26x8734tSXUHoAw0ERIiHyW4RaI4Bj99D8YgUlVV9SedLf8hlWAfyIFhHRIhfPngLlCe0QYOdRKYFt8gy56TA==} engines: {node: '>=v18'} dependencies: @@ -2696,7 +2775,7 @@ packages: '@commitlint/types': 18.6.1 chalk: 4.1.2 cosmiconfig: 8.3.6(typescript@5.3.3) - cosmiconfig-typescript-loader: 5.0.0(@types/node@18.19.31)(cosmiconfig@8.3.6)(typescript@5.3.3) + cosmiconfig-typescript-loader: 5.0.0(@types/node@20.12.7)(cosmiconfig@8.3.6)(typescript@5.3.3) lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 lodash.uniq: 4.5.0 @@ -8958,6 +9037,26 @@ packages: slash: 3.0.0 source-map: 0.7.4 + /@swc/cli@0.1.63(@swc/core@1.4.13): + resolution: {integrity: sha512-EM9oxxHzmmsprYRbGqsS2M4M/Gr5Gkcl0ROYYIdlUyTkhOiX822EQiRCpPCwdutdnzH2GyaTN7wc6i0Y+CKd3A==} + engines: {node: '>= 12.13'} + hasBin: true + peerDependencies: + '@swc/core': ^1.2.66 + chokidar: ^3.5.1 + peerDependenciesMeta: + chokidar: + optional: true + dependencies: + '@mole-inc/bin-wrapper': 8.0.1 + '@swc/core': 1.4.13 + commander: 7.2.0 + fast-glob: 3.3.2 + semver: 7.6.0 + slash: 3.0.0 + source-map: 0.7.4 + dev: true + /@swc/core-darwin-arm64@1.3.102: resolution: {integrity: sha512-CJDxA5Wd2cUMULj3bjx4GEoiYyyiyL8oIOu4Nhrs9X+tlg8DnkCm4nI57RJGP8Mf6BaXPIJkHX8yjcefK2RlDA==} engines: {node: '>=10'} @@ -9418,7 +9517,7 @@ packages: resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} dependencies: '@types/connect': 3.4.38 - '@types/node': 18.19.31 + '@types/node': 20.12.7 dev: true /@types/cacheable-request@6.0.3: @@ -9426,7 +9525,7 @@ packages: dependencies: '@types/http-cache-semantics': 4.0.4 '@types/keyv': 3.1.4 - '@types/node': 18.19.31 + '@types/node': 20.12.7 '@types/responselike': 1.0.3 /@types/chai-subset@1.3.5: @@ -9450,7 +9549,7 @@ packages: /@types/connect@3.4.38: resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} dependencies: - '@types/node': 18.19.31 + '@types/node': 20.12.7 dev: true /@types/cookie@0.4.1: @@ -9532,12 +9631,20 @@ packages: /@types/express-serve-static-core@4.19.0: resolution: {integrity: sha512-bGyep3JqPCRry1wq+O5n7oiBgGWmeIJXPjXXCo8EK0u8duZGSYar7cGqd3ML2JUsLGeB7fmc06KYo9fLGWqPvQ==} dependencies: - '@types/node': 18.19.31 + '@types/node': 20.12.7 '@types/qs': 6.9.14 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 dev: true + /@types/express-ws@3.0.4: + resolution: {integrity: sha512-Yjj18CaivG5KndgcvzttWe8mPFinPCHJC2wvyQqVzA7hqeufM8EtWMj6mpp5omg3s8XALUexhOu8aXAyi/DyJQ==} + dependencies: + '@types/express': 4.17.21 + '@types/express-serve-static-core': 4.19.0 + '@types/ws': 8.5.10 + dev: true + /@types/express@4.17.21: resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} dependencies: @@ -9598,10 +9705,19 @@ packages: resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} dev: true + /@types/http-proxy-middleware@1.0.0: + resolution: {integrity: sha512-/s8lFX6rT43hSPqjjD8KNuu0SkPKY7uIdR6u9DCxVqCRhAvfKxGbVOixJsAT2mdpSnCyrGFAGoB39KFh6tmRxw==} + deprecated: This is a stub types definition. http-proxy-middleware provides its own type definitions, so you do not need this installed. + dependencies: + http-proxy-middleware: 3.0.0 + transitivePeerDependencies: + - supports-color + dev: true + /@types/http-proxy@1.17.14: resolution: {integrity: sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==} dependencies: - '@types/node': 18.0.0 + '@types/node': 20.12.7 /@types/image-size@0.8.0: resolution: {integrity: sha512-hMlhu25ji75dXQk2uZkN3pTJ+lWrgKr8M1fTpyyFvuu+SJZBdGa5gDm4BVNobWXHZbOU11mBj0vciYp7qOfAFg==} @@ -9644,7 +9760,7 @@ packages: /@types/keyv@3.1.4: resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} dependencies: - '@types/node': 18.0.0 + '@types/node': 20.12.7 /@types/lodash@4.17.0: resolution: {integrity: sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==} @@ -9710,6 +9826,11 @@ packages: dependencies: undici-types: 5.26.5 + /@types/node@20.12.7: + resolution: {integrity: sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==} + dependencies: + undici-types: 5.26.5 + /@types/node@8.10.66: resolution: {integrity: sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==} @@ -9796,7 +9917,7 @@ packages: /@types/responselike@1.0.3: resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} dependencies: - '@types/node': 18.0.0 + '@types/node': 20.12.7 /@types/retry@0.12.1: resolution: {integrity: sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==} @@ -9828,7 +9949,7 @@ packages: resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} dependencies: '@types/mime': 1.3.5 - '@types/node': 18.19.31 + '@types/node': 20.12.7 dev: true /@types/serve-static@1.15.5: @@ -9843,7 +9964,7 @@ packages: resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} dependencies: '@types/http-errors': 2.0.4 - '@types/node': 18.19.31 + '@types/node': 20.12.7 '@types/send': 0.17.4 dev: true @@ -9999,7 +10120,7 @@ packages: /@types/ws@8.5.10: resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} dependencies: - '@types/node': 18.19.31 + '@types/node': 20.12.7 /@types/yargs-parser@21.0.3: resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -10170,6 +10291,35 @@ packages: - supports-color dev: true + /@typescript-eslint/eslint-plugin@7.6.0(@typescript-eslint/parser@7.6.0)(eslint@7.32.0)(typescript@5.4.4): + resolution: {integrity: sha512-gKmTNwZnblUdnTIJu3e9kmeRRzV2j1a/LUO27KNNAnIC5zjy1aSvXSRp4rVNlmAoHlQ7HzX42NbKpcSr4jF80A==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + '@typescript-eslint/parser': ^7.0.0 + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.10.0 + '@typescript-eslint/parser': 7.6.0(eslint@7.32.0)(typescript@5.4.4) + '@typescript-eslint/scope-manager': 7.6.0 + '@typescript-eslint/type-utils': 7.6.0(eslint@7.32.0)(typescript@5.4.4) + '@typescript-eslint/utils': 7.6.0(eslint@7.32.0)(typescript@5.4.4) + '@typescript-eslint/visitor-keys': 7.6.0 + debug: 4.3.4 + eslint: 7.32.0 + graphemer: 1.4.0 + ignore: 5.3.1 + natural-compare: 1.4.0 + semver: 7.6.0 + ts-api-utils: 1.3.0(typescript@5.4.4) + typescript: 5.4.4 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@4.9.5): resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -10270,6 +10420,27 @@ packages: transitivePeerDependencies: - supports-color + /@typescript-eslint/parser@7.6.0(eslint@7.32.0)(typescript@5.4.4): + resolution: {integrity: sha512-usPMPHcwX3ZoPWnBnhhorc14NJw9J4HpSXQX4urF2TPKG0au0XhJoZyX62fmvdHONUkmyUe74Hzm1//XA+BoYg==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 7.6.0 + '@typescript-eslint/types': 7.6.0 + '@typescript-eslint/typescript-estree': 7.6.0(typescript@5.4.4) + '@typescript-eslint/visitor-keys': 7.6.0 + debug: 4.3.4 + eslint: 7.32.0 + typescript: 5.4.4 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/scope-manager@5.62.0: resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -10292,6 +10463,14 @@ packages: '@typescript-eslint/visitor-keys': 6.21.0 dev: true + /@typescript-eslint/scope-manager@7.6.0: + resolution: {integrity: sha512-ngttyfExA5PsHSx0rdFgnADMYQi+Zkeiv4/ZxGYUWd0nLs63Ha0ksmp8VMxAIC0wtCFxMos7Lt3PszJssG/E6w==} + engines: {node: ^18.18.0 || >=20.0.0} + dependencies: + '@typescript-eslint/types': 7.6.0 + '@typescript-eslint/visitor-keys': 7.6.0 + dev: true + /@typescript-eslint/type-utils@5.62.0(eslint@7.32.0)(typescript@4.9.5): resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -10391,6 +10570,26 @@ packages: - supports-color dev: true + /@typescript-eslint/type-utils@7.6.0(eslint@7.32.0)(typescript@5.4.4): + resolution: {integrity: sha512-NxAfqAPNLG6LTmy7uZgpK8KcuiS2NZD/HlThPXQRGwz6u7MDBWRVliEEl1Gj6U7++kVJTpehkhZzCJLMK66Scw==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 7.6.0(typescript@5.4.4) + '@typescript-eslint/utils': 7.6.0(eslint@7.32.0)(typescript@5.4.4) + debug: 4.3.4 + eslint: 7.32.0 + ts-api-utils: 1.3.0(typescript@5.4.4) + typescript: 5.4.4 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/types@5.62.0: resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -10404,6 +10603,11 @@ packages: engines: {node: ^16.0.0 || >=18.0.0} dev: true + /@typescript-eslint/types@7.6.0: + resolution: {integrity: sha512-h02rYQn8J+MureCvHVVzhl69/GAfQGPQZmOMjG1KfCl7o3HtMSlPaPUAPu6lLctXI5ySRGIYk94clD/AUMCUgQ==} + engines: {node: ^18.18.0 || >=20.0.0} + dev: true + /@typescript-eslint/typescript-estree@5.62.0(supports-color@9.4.0)(typescript@5.4.4): resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -10529,6 +10733,28 @@ packages: - supports-color dev: true + /@typescript-eslint/typescript-estree@7.6.0(typescript@5.4.4): + resolution: {integrity: sha512-+7Y/GP9VuYibecrCQWSKgl3GvUM5cILRttpWtnAu8GNL9j11e4tbuGZmZjJ8ejnKYyBRb2ddGQ3rEFCq3QjMJw==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 7.6.0 + '@typescript-eslint/visitor-keys': 7.6.0 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.4 + semver: 7.6.0 + ts-api-utils: 1.3.0(typescript@5.4.4) + typescript: 5.4.4 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/utils@5.62.0(eslint@7.32.0)(typescript@4.9.5): resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -10645,6 +10871,25 @@ packages: - typescript dev: true + /@typescript-eslint/utils@7.6.0(eslint@7.32.0)(typescript@5.4.4): + resolution: {integrity: sha512-x54gaSsRRI+Nwz59TXpCsr6harB98qjXYzsRxGqvA5Ue3kQH+FxS7FYU81g/omn22ML2pZJkisy6Q+ElK8pBCA==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@7.32.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.8 + '@typescript-eslint/scope-manager': 7.6.0 + '@typescript-eslint/types': 7.6.0 + '@typescript-eslint/typescript-estree': 7.6.0(typescript@5.4.4) + eslint: 7.32.0 + semver: 7.6.0 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + /@typescript-eslint/visitor-keys@5.62.0: resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -10667,6 +10912,14 @@ packages: eslint-visitor-keys: 3.4.3 dev: true + /@typescript-eslint/visitor-keys@7.6.0: + resolution: {integrity: sha512-4eLB7t+LlNUmXzfOu1VAIAdkjbu5xNSerURS9X/S5TUKWFRpXRQZbmtPqgKmYx8bj3J0irtQXSiWAOY82v+cgw==} + engines: {node: ^18.18.0 || >=20.0.0} + dependencies: + '@typescript-eslint/types': 7.6.0 + eslint-visitor-keys: 3.4.3 + dev: true + /@vercel/nft@0.23.1: resolution: {integrity: sha512-NE0xSmGWVhgHF1OIoir71XAd0W0C1UE3nzFyhpFiMr3rVhetww7NvM1kc41trBsPG37Bh+dE5FYCTMzM/gBu0w==} engines: {node: '>=14'} @@ -10769,6 +11022,17 @@ packages: - '@swc/helpers' dev: true + /@vitejs/plugin-react-swc@3.5.0(vite@5.2.8): + resolution: {integrity: sha512-1PrOvAaDpqlCV+Up8RkAh9qaiUjoDUcjtttyhXDKw53XA6Ve16SOp6cCOpRs8Dj8DqUQs6eTW5YkLcLJjrXAig==} + peerDependencies: + vite: ^4 || ^5 + dependencies: + '@swc/core': 1.4.13 + vite: 5.2.8(@types/node@20.12.7) + transitivePeerDependencies: + - '@swc/helpers' + dev: true + /@vitejs/plugin-react@3.1.0(vite@5.0.10): resolution: {integrity: sha512-AfgcRL8ZBhAlc3BFdigClmTUMISmmzHn7sB2h9U1odvc5U/MjWXsAaz18b/WoppUTDBzxOJwo2VdClfUcItu9g==} engines: {node: ^14.18.0 || >=16.0.0} @@ -14162,7 +14426,7 @@ packages: object-assign: 4.1.1 vary: 1.1.2 - /cosmiconfig-typescript-loader@5.0.0(@types/node@18.19.31)(cosmiconfig@8.3.6)(typescript@5.3.3): + /cosmiconfig-typescript-loader@5.0.0(@types/node@20.12.7)(cosmiconfig@8.3.6)(typescript@5.3.3): resolution: {integrity: sha512-+8cK7jRAReYkMwMiG+bxhcNKiHJDM6bR9FD/nGBXOWdMLuYawjF5cGrtLilJ+LGd3ZjCXnJjR5DkfWPoIVlqJA==} engines: {node: '>=v16'} peerDependencies: @@ -14170,7 +14434,7 @@ packages: cosmiconfig: '>=8.2' typescript: '>=4' dependencies: - '@types/node': 18.19.31 + '@types/node': 20.12.7 cosmiconfig: 8.3.6(typescript@5.3.3) jiti: 1.21.0 typescript: 5.3.3 @@ -17028,6 +17292,14 @@ packages: dependencies: eslint: 7.32.0 + /eslint-plugin-react-refresh@0.4.6(eslint@7.32.0): + resolution: {integrity: sha512-NjGXdm7zgcKRkKMua34qVO9doI7VOxZ6ancSvBELJSSoX97jyndXcSoa8XBh69JoB31dNz3EEzlMcizZl7LaMA==} + peerDependencies: + eslint: '>=7' + dependencies: + eslint: 7.32.0 + dev: true + /eslint-plugin-react@7.33.2(eslint@7.0.0): resolution: {integrity: sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==} engines: {node: '>=4'} @@ -17365,7 +17637,6 @@ packages: /eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} - dev: false /eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} @@ -17538,6 +17809,19 @@ packages: - utf-8-validate dev: false + /express-ws@5.0.2(express@4.19.2): + resolution: {integrity: sha512-0uvmuk61O9HXgLhGl3QhNSEtRsQevtmbL94/eILaliEADZBHZOQUAiHFrGPrgsjikohyrmSG5g+sCfASTt0lkQ==} + engines: {node: '>=4.5.0'} + peerDependencies: + express: ^4.0.0 || ^5.0.0-alpha.1 + dependencies: + express: 4.19.2 + ws: 7.5.9 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false + /express@4.18.2: resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} engines: {node: '>= 0.10.0'} @@ -19813,6 +20097,12 @@ packages: es-errors: 1.3.0 get-intrinsic: 1.2.4 + /get-tsconfig@4.7.3: + resolution: {integrity: sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg==} + dependencies: + resolve-pkg-maps: 1.0.0 + dev: true + /get-value@2.0.6: resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==} engines: {node: '>=0.10.0'} @@ -20880,6 +21170,19 @@ packages: - debug dev: false + /http-proxy-middleware@3.0.0: + resolution: {integrity: sha512-36AV1fIaI2cWRzHo+rbcxhe3M3jUDCNzc4D5zRl57sEWRAxdXYtw7FSQKYY6PDKssiAKjLYypbssHk+xs/kMXw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@types/http-proxy': 1.17.14 + debug: 4.3.4 + http-proxy: 1.18.1(debug@4.3.4) + is-glob: 4.0.3 + is-plain-obj: 3.0.0 + micromatch: 4.0.5 + transitivePeerDependencies: + - supports-color + /http-proxy@1.18.1: resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} engines: {node: '>=8.0.0'} @@ -20900,7 +21203,6 @@ packages: requires-port: 1.0.0 transitivePeerDependencies: - debug - dev: false /http-shutdown@1.2.2: resolution: {integrity: sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw==} @@ -21677,7 +21979,6 @@ packages: /is-plain-obj@3.0.0: resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==} engines: {node: '>=10'} - dev: false /is-plain-obj@4.1.0: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} @@ -28680,7 +28981,6 @@ packages: /requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} - dev: false /reselect@4.1.8: resolution: {integrity: sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==} @@ -28722,6 +29022,10 @@ packages: resolution: {integrity: sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==} dev: false + /resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + dev: true + /resolve-protobuf-schema@2.1.0: resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==} dependencies: @@ -31027,6 +31331,15 @@ packages: dependencies: typescript: 5.3.3 + /ts-api-utils@1.3.0(typescript@5.4.4): + resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + dependencies: + typescript: 5.4.4 + dev: true + /ts-dedent@2.2.0: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} engines: {node: '>=6.10'} @@ -31176,6 +31489,17 @@ packages: typescript: 5.4.4 dev: false + /tsx@4.7.2: + resolution: {integrity: sha512-BCNd4kz6fz12fyrgCTEdZHGJ9fWTGeUzXmQysh0RVocDY3h4frk05ZNCXSy4kIenF7y/QnrdiVpTsyNRn6vlAw==} + engines: {node: '>=18.0.0'} + hasBin: true + dependencies: + esbuild: 0.19.12 + get-tsconfig: 4.7.3 + optionalDependencies: + fsevents: 2.3.3 + dev: true + /tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} dependencies: @@ -32203,6 +32527,27 @@ packages: - terser dev: true + /vite-node@1.1.1(@types/node@20.12.7): + resolution: {integrity: sha512-2bGE5w4jvym5v8llF6Gu1oBrmImoNSs4WmRVcavnG2me6+8UQntTqLiAMFyiAobp+ZXhj5ZFhI7SmLiFr/jrow==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + dependencies: + cac: 6.7.14 + debug: 4.3.4 + pathe: 1.1.2 + picocolors: 1.0.0 + vite: 5.2.8(@types/node@20.12.7) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /vite-plugin-istanbul@3.0.4: resolution: {integrity: sha512-DJy3cq6yOFbsM3gLQf/3zeuaJNJsfBv5dLFdZdv8sUV30xLtZI+66QeYfHUyP/5vBUYyLA+xNUCSG5uHY6w+5g==} dependencies: @@ -32360,6 +32705,42 @@ packages: optionalDependencies: fsevents: 2.3.3 + /vite@5.2.8(@types/node@20.12.7): + resolution: {integrity: sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + '@types/node': 20.12.7 + esbuild: 0.20.2 + postcss: 8.4.38 + rollup: 4.14.1 + optionalDependencies: + fsevents: 2.3.3 + dev: true + /vitest@0.34.6: resolution: {integrity: sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==} engines: {node: '>=v14.18.0'} @@ -32552,7 +32933,7 @@ packages: - terser dev: true - /vitest@1.1.1(@types/node@18.19.31): + /vitest@1.1.1(@types/node@18.19.31)(happy-dom@12.10.3): resolution: {integrity: sha512-Ry2qs4UOu/KjpXVfOCfQkTnwSXYGrqTbBZxw6reIYEFjSy1QUARRg5pxiI5BEXy+kBVntxUYNMlq4Co+2vD3fQ==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -32588,6 +32969,7 @@ packages: chai: 4.4.1 debug: 4.3.4 execa: 8.0.1 + happy-dom: 12.10.3 local-pkg: 0.5.0 magic-string: 0.30.9 pathe: 1.1.2 @@ -32609,7 +32991,7 @@ packages: - terser dev: true - /vitest@1.1.1(@types/node@18.19.31)(happy-dom@12.10.3): + /vitest@1.1.1(@types/node@20.12.7): resolution: {integrity: sha512-Ry2qs4UOu/KjpXVfOCfQkTnwSXYGrqTbBZxw6reIYEFjSy1QUARRg5pxiI5BEXy+kBVntxUYNMlq4Co+2vD3fQ==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -32634,7 +33016,7 @@ packages: jsdom: optional: true dependencies: - '@types/node': 18.19.31 + '@types/node': 20.12.7 '@vitest/expect': 1.1.1 '@vitest/runner': 1.1.1 '@vitest/snapshot': 1.1.1 @@ -32645,7 +33027,6 @@ packages: chai: 4.4.1 debug: 4.3.4 execa: 8.0.1 - happy-dom: 12.10.3 local-pkg: 0.5.0 magic-string: 0.30.9 pathe: 1.1.2 @@ -32654,8 +33035,8 @@ packages: strip-literal: 1.3.0 tinybench: 2.6.0 tinypool: 0.8.3 - vite: 5.2.8(@types/node@18.19.31) - vite-node: 1.1.1(@types/node@18.19.31) + vite: 5.2.8(@types/node@20.12.7) + vite-node: 1.1.1(@types/node@20.12.7) why-is-node-running: 2.2.2 transitivePeerDependencies: - less From 5f12709c7a56bc49e078d7ac0b953e95e38c4a9f Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Fri, 12 Apr 2024 18:38:04 +0200 Subject: [PATCH 25/82] fix(SLB-209): wrong preview package name --- apps/preview/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/preview/package.json b/apps/preview/package.json index b3f160641..4466ca535 100644 --- a/apps/preview/package.json +++ b/apps/preview/package.json @@ -1,5 +1,5 @@ { - "name": "custom/preview", + "name": "@custom/preview", "private": true, "version": "0.0.0", "type": "module", From b785a6066c90ad55925b12991e1a530e3226af44 Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Fri, 12 Apr 2024 20:14:25 +0200 Subject: [PATCH 26/82] fix(SLB-209): dev server commands --- apps/preview/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/preview/package.json b/apps/preview/package.json index 4466ca535..7e02c7199 100644 --- a/apps/preview/package.json +++ b/apps/preview/package.json @@ -5,8 +5,8 @@ "type": "module", "scripts": { "test:static": "tsc --noEmit && eslint \"**/*.{ts,tsx,js,jsx}\" --ignore-path=\"./.gitignore\"", - "dev:vite": "vite", - "dev:hono": "tsx watch src/index.ts", + "dev:app": "vite", + "dev:server": "tsx watch server/index.ts", "prep:app": "vite build", "prep:server": "swc ./server -d ./build", "start": "node build/index.js" From d26877da518c957c3507864e7c35ca97aa52fbe4 Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Fri, 12 Apr 2024 20:15:04 +0200 Subject: [PATCH 27/82] Revert "fix(SLB-209): proxy graphql requests" This reverts commit 2d4c5b41b06e9ff8efbe8b1b60385afa909d9d08. --- apps/preview/.lagoon.env | 1 - apps/preview/package.json | 2 - apps/preview/server/index.ts | 9 - apps/preview/src/App.tsx | 7 +- pnpm-lock.yaml | 437 +++-------------------------------- 5 files changed, 34 insertions(+), 422 deletions(-) delete mode 100644 apps/preview/.lagoon.env diff --git a/apps/preview/.lagoon.env b/apps/preview/.lagoon.env deleted file mode 100644 index 2c80dc9c1..000000000 --- a/apps/preview/.lagoon.env +++ /dev/null @@ -1 +0,0 @@ -DRUPAL_URL="https://${LAGOON_GIT_BRANCH}-${PROJECT_NAME}.cms.amazeelabs.dev" diff --git a/apps/preview/package.json b/apps/preview/package.json index 7e02c7199..e5ec902c5 100644 --- a/apps/preview/package.json +++ b/apps/preview/package.json @@ -16,7 +16,6 @@ "@custom/ui": "workspace:*", "express": "^4.19.2", "express-ws": "^5.0.2", - "http-proxy-middleware": "^3.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", "rxjs": "^7.8.1" @@ -26,7 +25,6 @@ "@swc/core": "^1.3.102", "@types/express": "^4.17.21", "@types/express-ws": "^3.0.4", - "@types/http-proxy-middleware": "^1.0.0", "@types/node": "^20.11.17", "@types/react": "^18.2.46", "@types/react-dom": "^18.2.18", diff --git a/apps/preview/server/index.ts b/apps/preview/server/index.ts index 4556dc41b..e9db6c288 100644 --- a/apps/preview/server/index.ts +++ b/apps/preview/server/index.ts @@ -1,6 +1,5 @@ import express from 'express'; import expressWs from 'express-ws'; -import { createProxyMiddleware } from 'http-proxy-middleware'; import { Subject } from 'rxjs'; const expressServer = express(); @@ -9,14 +8,6 @@ const { app } = expressWsInstance; const updates$ = new Subject(); -app.use( - '/graphql', - createProxyMiddleware({ - target: process.env.DRUPAL_URL || 'http://localhost:8888', - changeOrigin: true, - }), -); - // TODO: Protect endpoints and preview with Drupal authentication. app.post('/__preview', (req, res) => { updates$.next({ body: req.body }); diff --git a/apps/preview/src/App.tsx b/apps/preview/src/App.tsx index ba86b75d2..e63dd4861 100644 --- a/apps/preview/src/App.tsx +++ b/apps/preview/src/App.tsx @@ -17,7 +17,12 @@ function App() { return sub.unsubscribe; }, [refresh]); return ( - + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5281ba170..8aca358f2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,7 +16,7 @@ importers: devDependencies: '@commitlint/cli': specifier: ^18.4.3 - version: 18.4.3(@types/node@20.12.7)(typescript@5.3.3) + version: 18.4.3(@types/node@18.19.31)(typescript@5.3.3) '@commitlint/config-conventional': specifier: ^18.4.3 version: 18.4.3 @@ -64,7 +64,7 @@ importers: version: 5.3.3 vitest: specifier: ^1.1.1 - version: 1.1.1(@types/node@20.12.7) + version: 1.1.1(@types/node@18.19.31) apps/cms: dependencies: @@ -184,85 +184,6 @@ importers: specifier: ^5.0.10 version: 5.0.10(@types/node@18.0.0) - apps/preview: - dependencies: - '@custom/schema': - specifier: workspace:* - version: link:../../packages/schema - '@custom/ui': - specifier: workspace:* - version: link:../../packages/ui - express: - specifier: ^4.19.2 - version: 4.19.2 - express-ws: - specifier: ^5.0.2 - version: 5.0.2(express@4.19.2) - http-proxy-middleware: - specifier: ^3.0.0 - version: 3.0.0 - react: - specifier: ^18.2.0 - version: 18.2.0 - react-dom: - specifier: ^18.2.0 - version: 18.2.0(react@18.2.0) - rxjs: - specifier: ^7.8.1 - version: 7.8.1 - devDependencies: - '@swc/cli': - specifier: ^0.1.63 - version: 0.1.63(@swc/core@1.4.13) - '@swc/core': - specifier: ^1.3.102 - version: 1.4.13 - '@types/express': - specifier: ^4.17.21 - version: 4.17.21 - '@types/express-ws': - specifier: ^3.0.4 - version: 3.0.4 - '@types/http-proxy-middleware': - specifier: ^1.0.0 - version: 1.0.0 - '@types/node': - specifier: ^20.11.17 - version: 20.12.7 - '@types/react': - specifier: ^18.2.46 - version: 18.2.46 - '@types/react-dom': - specifier: ^18.2.18 - version: 18.2.18 - '@typescript-eslint/eslint-plugin': - specifier: ^7.2.0 - version: 7.6.0(@typescript-eslint/parser@7.6.0)(eslint@7.32.0)(typescript@5.4.4) - '@typescript-eslint/parser': - specifier: ^7.2.0 - version: 7.6.0(eslint@7.32.0)(typescript@5.4.4) - '@vitejs/plugin-react-swc': - specifier: ^3.5.0 - version: 3.5.0(vite@5.2.8) - eslint: - specifier: '7' - version: 7.32.0 - eslint-plugin-react-hooks: - specifier: ^4.6.0 - version: 4.6.0(eslint@7.32.0) - eslint-plugin-react-refresh: - specifier: ^0.4.6 - version: 0.4.6(eslint@7.32.0) - tsx: - specifier: ^4.7.1 - version: 4.7.2 - typescript: - specifier: ^5.2.2 - version: 5.4.4 - vite: - specifier: ^5.2.0 - version: 5.2.8(@types/node@20.12.7) - apps/website: dependencies: '@amazeelabs/bridge-gatsby': @@ -2687,14 +2608,14 @@ packages: resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} engines: {node: '>=0.1.90'} - /@commitlint/cli@18.4.3(@types/node@20.12.7)(typescript@5.3.3): + /@commitlint/cli@18.4.3(@types/node@18.19.31)(typescript@5.3.3): resolution: {integrity: sha512-zop98yfB3A6NveYAZ3P1Mb6bIXuCeWgnUfVNkH4yhIMQpQfzFwseadazOuSn0OOfTt0lWuFauehpm9GcqM5lww==} engines: {node: '>=v18'} hasBin: true dependencies: '@commitlint/format': 18.6.1 '@commitlint/lint': 18.6.1 - '@commitlint/load': 18.6.1(@types/node@20.12.7)(typescript@5.3.3) + '@commitlint/load': 18.6.1(@types/node@18.19.31)(typescript@5.3.3) '@commitlint/read': 18.6.1 '@commitlint/types': 18.6.1 execa: 5.1.1 @@ -2765,7 +2686,7 @@ packages: '@commitlint/types': 18.6.1 dev: true - /@commitlint/load@18.6.1(@types/node@20.12.7)(typescript@5.3.3): + /@commitlint/load@18.6.1(@types/node@18.19.31)(typescript@5.3.3): resolution: {integrity: sha512-p26x8734tSXUHoAw0ERIiHyW4RaI4Bj99D8YgUlVV9SedLf8hlWAfyIFhHRIhfPngLlCe0QYOdRKYFt8gy56TA==} engines: {node: '>=v18'} dependencies: @@ -2775,7 +2696,7 @@ packages: '@commitlint/types': 18.6.1 chalk: 4.1.2 cosmiconfig: 8.3.6(typescript@5.3.3) - cosmiconfig-typescript-loader: 5.0.0(@types/node@20.12.7)(cosmiconfig@8.3.6)(typescript@5.3.3) + cosmiconfig-typescript-loader: 5.0.0(@types/node@18.19.31)(cosmiconfig@8.3.6)(typescript@5.3.3) lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 lodash.uniq: 4.5.0 @@ -9037,26 +8958,6 @@ packages: slash: 3.0.0 source-map: 0.7.4 - /@swc/cli@0.1.63(@swc/core@1.4.13): - resolution: {integrity: sha512-EM9oxxHzmmsprYRbGqsS2M4M/Gr5Gkcl0ROYYIdlUyTkhOiX822EQiRCpPCwdutdnzH2GyaTN7wc6i0Y+CKd3A==} - engines: {node: '>= 12.13'} - hasBin: true - peerDependencies: - '@swc/core': ^1.2.66 - chokidar: ^3.5.1 - peerDependenciesMeta: - chokidar: - optional: true - dependencies: - '@mole-inc/bin-wrapper': 8.0.1 - '@swc/core': 1.4.13 - commander: 7.2.0 - fast-glob: 3.3.2 - semver: 7.6.0 - slash: 3.0.0 - source-map: 0.7.4 - dev: true - /@swc/core-darwin-arm64@1.3.102: resolution: {integrity: sha512-CJDxA5Wd2cUMULj3bjx4GEoiYyyiyL8oIOu4Nhrs9X+tlg8DnkCm4nI57RJGP8Mf6BaXPIJkHX8yjcefK2RlDA==} engines: {node: '>=10'} @@ -9517,7 +9418,7 @@ packages: resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} dependencies: '@types/connect': 3.4.38 - '@types/node': 20.12.7 + '@types/node': 18.19.31 dev: true /@types/cacheable-request@6.0.3: @@ -9525,7 +9426,7 @@ packages: dependencies: '@types/http-cache-semantics': 4.0.4 '@types/keyv': 3.1.4 - '@types/node': 20.12.7 + '@types/node': 18.19.31 '@types/responselike': 1.0.3 /@types/chai-subset@1.3.5: @@ -9549,7 +9450,7 @@ packages: /@types/connect@3.4.38: resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} dependencies: - '@types/node': 20.12.7 + '@types/node': 18.19.31 dev: true /@types/cookie@0.4.1: @@ -9631,20 +9532,12 @@ packages: /@types/express-serve-static-core@4.19.0: resolution: {integrity: sha512-bGyep3JqPCRry1wq+O5n7oiBgGWmeIJXPjXXCo8EK0u8duZGSYar7cGqd3ML2JUsLGeB7fmc06KYo9fLGWqPvQ==} dependencies: - '@types/node': 20.12.7 + '@types/node': 18.19.31 '@types/qs': 6.9.14 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 dev: true - /@types/express-ws@3.0.4: - resolution: {integrity: sha512-Yjj18CaivG5KndgcvzttWe8mPFinPCHJC2wvyQqVzA7hqeufM8EtWMj6mpp5omg3s8XALUexhOu8aXAyi/DyJQ==} - dependencies: - '@types/express': 4.17.21 - '@types/express-serve-static-core': 4.19.0 - '@types/ws': 8.5.10 - dev: true - /@types/express@4.17.21: resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} dependencies: @@ -9705,19 +9598,10 @@ packages: resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} dev: true - /@types/http-proxy-middleware@1.0.0: - resolution: {integrity: sha512-/s8lFX6rT43hSPqjjD8KNuu0SkPKY7uIdR6u9DCxVqCRhAvfKxGbVOixJsAT2mdpSnCyrGFAGoB39KFh6tmRxw==} - deprecated: This is a stub types definition. http-proxy-middleware provides its own type definitions, so you do not need this installed. - dependencies: - http-proxy-middleware: 3.0.0 - transitivePeerDependencies: - - supports-color - dev: true - /@types/http-proxy@1.17.14: resolution: {integrity: sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==} dependencies: - '@types/node': 20.12.7 + '@types/node': 18.0.0 /@types/image-size@0.8.0: resolution: {integrity: sha512-hMlhu25ji75dXQk2uZkN3pTJ+lWrgKr8M1fTpyyFvuu+SJZBdGa5gDm4BVNobWXHZbOU11mBj0vciYp7qOfAFg==} @@ -9760,7 +9644,7 @@ packages: /@types/keyv@3.1.4: resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} dependencies: - '@types/node': 20.12.7 + '@types/node': 18.0.0 /@types/lodash@4.17.0: resolution: {integrity: sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==} @@ -9826,11 +9710,6 @@ packages: dependencies: undici-types: 5.26.5 - /@types/node@20.12.7: - resolution: {integrity: sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==} - dependencies: - undici-types: 5.26.5 - /@types/node@8.10.66: resolution: {integrity: sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==} @@ -9917,7 +9796,7 @@ packages: /@types/responselike@1.0.3: resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} dependencies: - '@types/node': 20.12.7 + '@types/node': 18.0.0 /@types/retry@0.12.1: resolution: {integrity: sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==} @@ -9949,7 +9828,7 @@ packages: resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} dependencies: '@types/mime': 1.3.5 - '@types/node': 20.12.7 + '@types/node': 18.19.31 dev: true /@types/serve-static@1.15.5: @@ -9964,7 +9843,7 @@ packages: resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} dependencies: '@types/http-errors': 2.0.4 - '@types/node': 20.12.7 + '@types/node': 18.19.31 '@types/send': 0.17.4 dev: true @@ -10120,7 +9999,7 @@ packages: /@types/ws@8.5.10: resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} dependencies: - '@types/node': 20.12.7 + '@types/node': 18.19.31 /@types/yargs-parser@21.0.3: resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -10291,35 +10170,6 @@ packages: - supports-color dev: true - /@typescript-eslint/eslint-plugin@7.6.0(@typescript-eslint/parser@7.6.0)(eslint@7.32.0)(typescript@5.4.4): - resolution: {integrity: sha512-gKmTNwZnblUdnTIJu3e9kmeRRzV2j1a/LUO27KNNAnIC5zjy1aSvXSRp4rVNlmAoHlQ7HzX42NbKpcSr4jF80A==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - '@typescript-eslint/parser': ^7.0.0 - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 7.6.0(eslint@7.32.0)(typescript@5.4.4) - '@typescript-eslint/scope-manager': 7.6.0 - '@typescript-eslint/type-utils': 7.6.0(eslint@7.32.0)(typescript@5.4.4) - '@typescript-eslint/utils': 7.6.0(eslint@7.32.0)(typescript@5.4.4) - '@typescript-eslint/visitor-keys': 7.6.0 - debug: 4.3.4 - eslint: 7.32.0 - graphemer: 1.4.0 - ignore: 5.3.1 - natural-compare: 1.4.0 - semver: 7.6.0 - ts-api-utils: 1.3.0(typescript@5.4.4) - typescript: 5.4.4 - transitivePeerDependencies: - - supports-color - dev: true - /@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@4.9.5): resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -10420,27 +10270,6 @@ packages: transitivePeerDependencies: - supports-color - /@typescript-eslint/parser@7.6.0(eslint@7.32.0)(typescript@5.4.4): - resolution: {integrity: sha512-usPMPHcwX3ZoPWnBnhhorc14NJw9J4HpSXQX4urF2TPKG0au0XhJoZyX62fmvdHONUkmyUe74Hzm1//XA+BoYg==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/scope-manager': 7.6.0 - '@typescript-eslint/types': 7.6.0 - '@typescript-eslint/typescript-estree': 7.6.0(typescript@5.4.4) - '@typescript-eslint/visitor-keys': 7.6.0 - debug: 4.3.4 - eslint: 7.32.0 - typescript: 5.4.4 - transitivePeerDependencies: - - supports-color - dev: true - /@typescript-eslint/scope-manager@5.62.0: resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -10463,14 +10292,6 @@ packages: '@typescript-eslint/visitor-keys': 6.21.0 dev: true - /@typescript-eslint/scope-manager@7.6.0: - resolution: {integrity: sha512-ngttyfExA5PsHSx0rdFgnADMYQi+Zkeiv4/ZxGYUWd0nLs63Ha0ksmp8VMxAIC0wtCFxMos7Lt3PszJssG/E6w==} - engines: {node: ^18.18.0 || >=20.0.0} - dependencies: - '@typescript-eslint/types': 7.6.0 - '@typescript-eslint/visitor-keys': 7.6.0 - dev: true - /@typescript-eslint/type-utils@5.62.0(eslint@7.32.0)(typescript@4.9.5): resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -10570,26 +10391,6 @@ packages: - supports-color dev: true - /@typescript-eslint/type-utils@7.6.0(eslint@7.32.0)(typescript@5.4.4): - resolution: {integrity: sha512-NxAfqAPNLG6LTmy7uZgpK8KcuiS2NZD/HlThPXQRGwz6u7MDBWRVliEEl1Gj6U7++kVJTpehkhZzCJLMK66Scw==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/typescript-estree': 7.6.0(typescript@5.4.4) - '@typescript-eslint/utils': 7.6.0(eslint@7.32.0)(typescript@5.4.4) - debug: 4.3.4 - eslint: 7.32.0 - ts-api-utils: 1.3.0(typescript@5.4.4) - typescript: 5.4.4 - transitivePeerDependencies: - - supports-color - dev: true - /@typescript-eslint/types@5.62.0: resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -10603,11 +10404,6 @@ packages: engines: {node: ^16.0.0 || >=18.0.0} dev: true - /@typescript-eslint/types@7.6.0: - resolution: {integrity: sha512-h02rYQn8J+MureCvHVVzhl69/GAfQGPQZmOMjG1KfCl7o3HtMSlPaPUAPu6lLctXI5ySRGIYk94clD/AUMCUgQ==} - engines: {node: ^18.18.0 || >=20.0.0} - dev: true - /@typescript-eslint/typescript-estree@5.62.0(supports-color@9.4.0)(typescript@5.4.4): resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -10733,28 +10529,6 @@ packages: - supports-color dev: true - /@typescript-eslint/typescript-estree@7.6.0(typescript@5.4.4): - resolution: {integrity: sha512-+7Y/GP9VuYibecrCQWSKgl3GvUM5cILRttpWtnAu8GNL9j11e4tbuGZmZjJ8ejnKYyBRb2ddGQ3rEFCq3QjMJw==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/types': 7.6.0 - '@typescript-eslint/visitor-keys': 7.6.0 - debug: 4.3.4 - globby: 11.1.0 - is-glob: 4.0.3 - minimatch: 9.0.4 - semver: 7.6.0 - ts-api-utils: 1.3.0(typescript@5.4.4) - typescript: 5.4.4 - transitivePeerDependencies: - - supports-color - dev: true - /@typescript-eslint/utils@5.62.0(eslint@7.32.0)(typescript@4.9.5): resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -10871,25 +10645,6 @@ packages: - typescript dev: true - /@typescript-eslint/utils@7.6.0(eslint@7.32.0)(typescript@5.4.4): - resolution: {integrity: sha512-x54gaSsRRI+Nwz59TXpCsr6harB98qjXYzsRxGqvA5Ue3kQH+FxS7FYU81g/omn22ML2pZJkisy6Q+ElK8pBCA==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - eslint: ^8.56.0 - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@7.32.0) - '@types/json-schema': 7.0.15 - '@types/semver': 7.5.8 - '@typescript-eslint/scope-manager': 7.6.0 - '@typescript-eslint/types': 7.6.0 - '@typescript-eslint/typescript-estree': 7.6.0(typescript@5.4.4) - eslint: 7.32.0 - semver: 7.6.0 - transitivePeerDependencies: - - supports-color - - typescript - dev: true - /@typescript-eslint/visitor-keys@5.62.0: resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -10912,14 +10667,6 @@ packages: eslint-visitor-keys: 3.4.3 dev: true - /@typescript-eslint/visitor-keys@7.6.0: - resolution: {integrity: sha512-4eLB7t+LlNUmXzfOu1VAIAdkjbu5xNSerURS9X/S5TUKWFRpXRQZbmtPqgKmYx8bj3J0irtQXSiWAOY82v+cgw==} - engines: {node: ^18.18.0 || >=20.0.0} - dependencies: - '@typescript-eslint/types': 7.6.0 - eslint-visitor-keys: 3.4.3 - dev: true - /@vercel/nft@0.23.1: resolution: {integrity: sha512-NE0xSmGWVhgHF1OIoir71XAd0W0C1UE3nzFyhpFiMr3rVhetww7NvM1kc41trBsPG37Bh+dE5FYCTMzM/gBu0w==} engines: {node: '>=14'} @@ -11022,17 +10769,6 @@ packages: - '@swc/helpers' dev: true - /@vitejs/plugin-react-swc@3.5.0(vite@5.2.8): - resolution: {integrity: sha512-1PrOvAaDpqlCV+Up8RkAh9qaiUjoDUcjtttyhXDKw53XA6Ve16SOp6cCOpRs8Dj8DqUQs6eTW5YkLcLJjrXAig==} - peerDependencies: - vite: ^4 || ^5 - dependencies: - '@swc/core': 1.4.13 - vite: 5.2.8(@types/node@20.12.7) - transitivePeerDependencies: - - '@swc/helpers' - dev: true - /@vitejs/plugin-react@3.1.0(vite@5.0.10): resolution: {integrity: sha512-AfgcRL8ZBhAlc3BFdigClmTUMISmmzHn7sB2h9U1odvc5U/MjWXsAaz18b/WoppUTDBzxOJwo2VdClfUcItu9g==} engines: {node: ^14.18.0 || >=16.0.0} @@ -14426,7 +14162,7 @@ packages: object-assign: 4.1.1 vary: 1.1.2 - /cosmiconfig-typescript-loader@5.0.0(@types/node@20.12.7)(cosmiconfig@8.3.6)(typescript@5.3.3): + /cosmiconfig-typescript-loader@5.0.0(@types/node@18.19.31)(cosmiconfig@8.3.6)(typescript@5.3.3): resolution: {integrity: sha512-+8cK7jRAReYkMwMiG+bxhcNKiHJDM6bR9FD/nGBXOWdMLuYawjF5cGrtLilJ+LGd3ZjCXnJjR5DkfWPoIVlqJA==} engines: {node: '>=v16'} peerDependencies: @@ -14434,7 +14170,7 @@ packages: cosmiconfig: '>=8.2' typescript: '>=4' dependencies: - '@types/node': 20.12.7 + '@types/node': 18.19.31 cosmiconfig: 8.3.6(typescript@5.3.3) jiti: 1.21.0 typescript: 5.3.3 @@ -17292,14 +17028,6 @@ packages: dependencies: eslint: 7.32.0 - /eslint-plugin-react-refresh@0.4.6(eslint@7.32.0): - resolution: {integrity: sha512-NjGXdm7zgcKRkKMua34qVO9doI7VOxZ6ancSvBELJSSoX97jyndXcSoa8XBh69JoB31dNz3EEzlMcizZl7LaMA==} - peerDependencies: - eslint: '>=7' - dependencies: - eslint: 7.32.0 - dev: true - /eslint-plugin-react@7.33.2(eslint@7.0.0): resolution: {integrity: sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==} engines: {node: '>=4'} @@ -17637,6 +17365,7 @@ packages: /eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + dev: false /eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} @@ -17809,19 +17538,6 @@ packages: - utf-8-validate dev: false - /express-ws@5.0.2(express@4.19.2): - resolution: {integrity: sha512-0uvmuk61O9HXgLhGl3QhNSEtRsQevtmbL94/eILaliEADZBHZOQUAiHFrGPrgsjikohyrmSG5g+sCfASTt0lkQ==} - engines: {node: '>=4.5.0'} - peerDependencies: - express: ^4.0.0 || ^5.0.0-alpha.1 - dependencies: - express: 4.19.2 - ws: 7.5.9 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - dev: false - /express@4.18.2: resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} engines: {node: '>= 0.10.0'} @@ -20097,12 +19813,6 @@ packages: es-errors: 1.3.0 get-intrinsic: 1.2.4 - /get-tsconfig@4.7.3: - resolution: {integrity: sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg==} - dependencies: - resolve-pkg-maps: 1.0.0 - dev: true - /get-value@2.0.6: resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==} engines: {node: '>=0.10.0'} @@ -21170,19 +20880,6 @@ packages: - debug dev: false - /http-proxy-middleware@3.0.0: - resolution: {integrity: sha512-36AV1fIaI2cWRzHo+rbcxhe3M3jUDCNzc4D5zRl57sEWRAxdXYtw7FSQKYY6PDKssiAKjLYypbssHk+xs/kMXw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@types/http-proxy': 1.17.14 - debug: 4.3.4 - http-proxy: 1.18.1(debug@4.3.4) - is-glob: 4.0.3 - is-plain-obj: 3.0.0 - micromatch: 4.0.5 - transitivePeerDependencies: - - supports-color - /http-proxy@1.18.1: resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} engines: {node: '>=8.0.0'} @@ -21203,6 +20900,7 @@ packages: requires-port: 1.0.0 transitivePeerDependencies: - debug + dev: false /http-shutdown@1.2.2: resolution: {integrity: sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw==} @@ -21979,6 +21677,7 @@ packages: /is-plain-obj@3.0.0: resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==} engines: {node: '>=10'} + dev: false /is-plain-obj@4.1.0: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} @@ -28981,6 +28680,7 @@ packages: /requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + dev: false /reselect@4.1.8: resolution: {integrity: sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==} @@ -29022,10 +28722,6 @@ packages: resolution: {integrity: sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==} dev: false - /resolve-pkg-maps@1.0.0: - resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - dev: true - /resolve-protobuf-schema@2.1.0: resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==} dependencies: @@ -31331,15 +31027,6 @@ packages: dependencies: typescript: 5.3.3 - /ts-api-utils@1.3.0(typescript@5.4.4): - resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} - engines: {node: '>=16'} - peerDependencies: - typescript: '>=4.2.0' - dependencies: - typescript: 5.4.4 - dev: true - /ts-dedent@2.2.0: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} engines: {node: '>=6.10'} @@ -31489,17 +31176,6 @@ packages: typescript: 5.4.4 dev: false - /tsx@4.7.2: - resolution: {integrity: sha512-BCNd4kz6fz12fyrgCTEdZHGJ9fWTGeUzXmQysh0RVocDY3h4frk05ZNCXSy4kIenF7y/QnrdiVpTsyNRn6vlAw==} - engines: {node: '>=18.0.0'} - hasBin: true - dependencies: - esbuild: 0.19.12 - get-tsconfig: 4.7.3 - optionalDependencies: - fsevents: 2.3.3 - dev: true - /tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} dependencies: @@ -32527,27 +32203,6 @@ packages: - terser dev: true - /vite-node@1.1.1(@types/node@20.12.7): - resolution: {integrity: sha512-2bGE5w4jvym5v8llF6Gu1oBrmImoNSs4WmRVcavnG2me6+8UQntTqLiAMFyiAobp+ZXhj5ZFhI7SmLiFr/jrow==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - dependencies: - cac: 6.7.14 - debug: 4.3.4 - pathe: 1.1.2 - picocolors: 1.0.0 - vite: 5.2.8(@types/node@20.12.7) - transitivePeerDependencies: - - '@types/node' - - less - - lightningcss - - sass - - stylus - - sugarss - - supports-color - - terser - dev: true - /vite-plugin-istanbul@3.0.4: resolution: {integrity: sha512-DJy3cq6yOFbsM3gLQf/3zeuaJNJsfBv5dLFdZdv8sUV30xLtZI+66QeYfHUyP/5vBUYyLA+xNUCSG5uHY6w+5g==} dependencies: @@ -32705,42 +32360,6 @@ packages: optionalDependencies: fsevents: 2.3.3 - /vite@5.2.8(@types/node@20.12.7): - resolution: {integrity: sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - dependencies: - '@types/node': 20.12.7 - esbuild: 0.20.2 - postcss: 8.4.38 - rollup: 4.14.1 - optionalDependencies: - fsevents: 2.3.3 - dev: true - /vitest@0.34.6: resolution: {integrity: sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==} engines: {node: '>=v14.18.0'} @@ -32933,7 +32552,7 @@ packages: - terser dev: true - /vitest@1.1.1(@types/node@18.19.31)(happy-dom@12.10.3): + /vitest@1.1.1(@types/node@18.19.31): resolution: {integrity: sha512-Ry2qs4UOu/KjpXVfOCfQkTnwSXYGrqTbBZxw6reIYEFjSy1QUARRg5pxiI5BEXy+kBVntxUYNMlq4Co+2vD3fQ==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -32969,7 +32588,6 @@ packages: chai: 4.4.1 debug: 4.3.4 execa: 8.0.1 - happy-dom: 12.10.3 local-pkg: 0.5.0 magic-string: 0.30.9 pathe: 1.1.2 @@ -32991,7 +32609,7 @@ packages: - terser dev: true - /vitest@1.1.1(@types/node@20.12.7): + /vitest@1.1.1(@types/node@18.19.31)(happy-dom@12.10.3): resolution: {integrity: sha512-Ry2qs4UOu/KjpXVfOCfQkTnwSXYGrqTbBZxw6reIYEFjSy1QUARRg5pxiI5BEXy+kBVntxUYNMlq4Co+2vD3fQ==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -33016,7 +32634,7 @@ packages: jsdom: optional: true dependencies: - '@types/node': 20.12.7 + '@types/node': 18.19.31 '@vitest/expect': 1.1.1 '@vitest/runner': 1.1.1 '@vitest/snapshot': 1.1.1 @@ -33027,6 +32645,7 @@ packages: chai: 4.4.1 debug: 4.3.4 execa: 8.0.1 + happy-dom: 12.10.3 local-pkg: 0.5.0 magic-string: 0.30.9 pathe: 1.1.2 @@ -33035,8 +32654,8 @@ packages: strip-literal: 1.3.0 tinybench: 2.6.0 tinypool: 0.8.3 - vite: 5.2.8(@types/node@20.12.7) - vite-node: 1.1.1(@types/node@20.12.7) + vite: 5.2.8(@types/node@18.19.31) + vite-node: 1.1.1(@types/node@18.19.31) why-is-node-running: 2.2.2 transitivePeerDependencies: - less From d1cda27c9ae386e33e97014b79f58969f65b7ff4 Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Fri, 12 Apr 2024 20:58:50 +0200 Subject: [PATCH 28/82] fix(SLB-209): inject graphql endpoint via script tag its not available during build time --- apps/preview/index.html | 1 + apps/preview/server/index.ts | 5 +++++ apps/preview/src/App.tsx | 11 +++++++---- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/apps/preview/index.html b/apps/preview/index.html index 99e1a3605..639d1df33 100644 --- a/apps/preview/index.html +++ b/apps/preview/index.html @@ -5,6 +5,7 @@ Preview + diff --git a/apps/preview/server/index.ts b/apps/preview/server/index.ts index e9db6c288..7eff08ca3 100644 --- a/apps/preview/server/index.ts +++ b/apps/preview/server/index.ts @@ -8,6 +8,11 @@ const { app } = expressWsInstance; const updates$ = new Subject(); +app.get('/endpoint.js', (_, res) => { + res.send( + `window.GRAPHQL_ENDPOINT = "${process.env.DRUPAL_URL || 'http://localhost:8888'}/graphql";`, + ); +}); // TODO: Protect endpoints and preview with Drupal authentication. app.post('/__preview', (req, res) => { updates$.next({ body: req.body }); diff --git a/apps/preview/src/App.tsx b/apps/preview/src/App.tsx index e63dd4861..3435448e5 100644 --- a/apps/preview/src/App.tsx +++ b/apps/preview/src/App.tsx @@ -6,6 +6,12 @@ import { webSocket } from 'rxjs/webSocket'; import { drupalExecutor } from './drupal-executor'; +declare global { + interface Window { + GRAPHQL_ENDPOINT: string; + } +} + const updates$ = webSocket({ url: `${window.location.origin.replace('http', 'ws')}/__preview`, }); @@ -18,10 +24,7 @@ function App() { }, [refresh]); return ( From 3923671fcfe17d00c315d3794d15323a477b1dcb Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Fri, 12 Apr 2024 20:59:16 +0200 Subject: [PATCH 29/82] fix(SLB-209): use fixed ports based on LAGOON env variable --- .lagoon/Dockerfile | 1 - apps/preview/server/index.ts | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.lagoon/Dockerfile b/.lagoon/Dockerfile index 269dfbead..55590a84d 100644 --- a/.lagoon/Dockerfile +++ b/.lagoon/Dockerfile @@ -96,7 +96,6 @@ FROM uselagoon/node-18:23.12.0 as preview RUN npm install -g pnpm@8.6.0 COPY --from=builder /tmp/.deploy/preview /app -ENV PREVIEW_PORT=3000 CMD pnpm start # ==================================================================================================== diff --git a/apps/preview/server/index.ts b/apps/preview/server/index.ts index 7eff08ca3..eb869e45b 100644 --- a/apps/preview/server/index.ts +++ b/apps/preview/server/index.ts @@ -13,6 +13,7 @@ app.get('/endpoint.js', (_, res) => { `window.GRAPHQL_ENDPOINT = "${process.env.DRUPAL_URL || 'http://localhost:8888'}/graphql";`, ); }); + // TODO: Protect endpoints and preview with Drupal authentication. app.post('/__preview', (req, res) => { updates$.next({ body: req.body }); @@ -33,7 +34,9 @@ app.get('/__preview/*', (req, _, next) => { }); app.use(express.static('./dist')); -const port = process.env.PREVIEW_PORT || 8001; + +const isLagoon = !!process.env.LAGOON; +const port = isLagoon ? 3000 : 8001; console.log(`Server is running on port ${port}`); app.listen({ port }); From 1620139b6f71b3bf316e452dd1983a01bdea4044 Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Fri, 12 Apr 2024 21:07:06 +0200 Subject: [PATCH 30/82] fix(SLB-209): lagoon drupal url --- apps/preview/.lagoon.env | 1 + 1 file changed, 1 insertion(+) create mode 100644 apps/preview/.lagoon.env diff --git a/apps/preview/.lagoon.env b/apps/preview/.lagoon.env new file mode 100644 index 000000000..2c80dc9c1 --- /dev/null +++ b/apps/preview/.lagoon.env @@ -0,0 +1 @@ +DRUPAL_URL="https://${LAGOON_GIT_BRANCH}-${PROJECT_NAME}.cms.amazeelabs.dev" From 00a6c60a630211d4e444b9315ea086e27142ab96 Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Fri, 12 Apr 2024 21:22:47 +0200 Subject: [PATCH 31/82] fix(SLB-209): make preview listen to all hosts --- apps/preview/server/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/preview/server/index.ts b/apps/preview/server/index.ts index eb869e45b..7b17121d1 100644 --- a/apps/preview/server/index.ts +++ b/apps/preview/server/index.ts @@ -39,4 +39,4 @@ const isLagoon = !!process.env.LAGOON; const port = isLagoon ? 3000 : 8001; console.log(`Server is running on port ${port}`); -app.listen({ port }); +app.listen({ port, host: '0.0.0.0' }); From 3189edc84a822f6d8ae0deec92560ea5474cd618 Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Sat, 13 Apr 2024 05:33:50 +0200 Subject: [PATCH 32/82] fix(SLB-209): make preview a dependency of cms so it gets properly prepared --- apps/cms/package.json | 3 +- pnpm-lock.yaml | 676 +++++++++++++++++++++++++++++++++++------- 2 files changed, 574 insertions(+), 105 deletions(-) diff --git a/apps/cms/package.json b/apps/cms/package.json index 40bcfda7c..97c65dafc 100644 --- a/apps/cms/package.json +++ b/apps/cms/package.json @@ -37,6 +37,7 @@ "@custom/gutenberg_blocks": "workspace:*", "@custom/schema": "workspace:*", "@custom/test_content": "workspace:*", - "@custom/ui": "workspace:*" + "@custom/ui": "workspace:*", + "@custom/preview": "workspace:*" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8aca358f2..15cde4b6c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,6 +80,9 @@ importers: '@custom/gutenberg_blocks': specifier: workspace:* version: link:../../packages/drupal/gutenberg_blocks + '@custom/preview': + specifier: workspace:* + version: link:../preview '@custom/schema': specifier: workspace:* version: link:../../packages/schema @@ -184,6 +187,79 @@ importers: specifier: ^5.0.10 version: 5.0.10(@types/node@18.0.0) + apps/preview: + dependencies: + '@custom/schema': + specifier: workspace:* + version: link:../../packages/schema + '@custom/ui': + specifier: workspace:* + version: link:../../packages/ui + express: + specifier: ^4.19.2 + version: 4.19.2 + express-ws: + specifier: ^5.0.2 + version: 5.0.2(express@4.19.2) + react: + specifier: ^18.2.0 + version: 18.2.0 + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) + rxjs: + specifier: ^7.8.1 + version: 7.8.1 + devDependencies: + '@swc/cli': + specifier: ^0.1.63 + version: 0.1.63(@swc/core@1.3.102) + '@swc/core': + specifier: ^1.3.102 + version: 1.3.102 + '@types/express': + specifier: ^4.17.21 + version: 4.17.21 + '@types/express-ws': + specifier: ^3.0.4 + version: 3.0.4 + '@types/node': + specifier: ^20.11.17 + version: 20.11.17 + '@types/react': + specifier: ^18.2.46 + version: 18.2.46 + '@types/react-dom': + specifier: ^18.2.18 + version: 18.2.18 + '@typescript-eslint/eslint-plugin': + specifier: ^7.2.0 + version: 7.2.0(@typescript-eslint/parser@7.2.0)(eslint@7.0.0)(typescript@5.3.3) + '@typescript-eslint/parser': + specifier: ^7.2.0 + version: 7.2.0(eslint@7.0.0)(typescript@5.3.3) + '@vitejs/plugin-react-swc': + specifier: ^3.5.0 + version: 3.5.0(vite@5.2.8) + eslint: + specifier: '7' + version: 7.0.0 + eslint-plugin-react-hooks: + specifier: ^4.6.0 + version: 4.6.0(eslint@7.0.0) + eslint-plugin-react-refresh: + specifier: ^0.4.6 + version: 0.4.6(eslint@7.0.0) + tsx: + specifier: ^4.7.1 + version: 4.7.1 + typescript: + specifier: ^5.2.2 + version: 5.3.3 + vite: + specifier: ^5.2.0 + version: 5.2.8(@types/node@20.11.17) + apps/website: dependencies: '@amazeelabs/bridge-gatsby': @@ -2993,6 +3069,7 @@ packages: cpu: [ppc64] os: [aix] requiresBuild: true + dev: true optional: true /@esbuild/aix-ppc64@0.20.0: @@ -3036,6 +3113,7 @@ packages: cpu: [arm64] os: [android] requiresBuild: true + dev: true optional: true /@esbuild/android-arm64@0.20.0: @@ -3079,6 +3157,7 @@ packages: cpu: [arm] os: [android] requiresBuild: true + dev: true optional: true /@esbuild/android-arm@0.20.0: @@ -3122,6 +3201,7 @@ packages: cpu: [x64] os: [android] requiresBuild: true + dev: true optional: true /@esbuild/android-x64@0.20.0: @@ -3165,6 +3245,7 @@ packages: cpu: [arm64] os: [darwin] requiresBuild: true + dev: true optional: true /@esbuild/darwin-arm64@0.20.0: @@ -3208,6 +3289,7 @@ packages: cpu: [x64] os: [darwin] requiresBuild: true + dev: true optional: true /@esbuild/darwin-x64@0.20.0: @@ -3251,6 +3333,7 @@ packages: cpu: [arm64] os: [freebsd] requiresBuild: true + dev: true optional: true /@esbuild/freebsd-arm64@0.20.0: @@ -3294,6 +3377,7 @@ packages: cpu: [x64] os: [freebsd] requiresBuild: true + dev: true optional: true /@esbuild/freebsd-x64@0.20.0: @@ -3337,6 +3421,7 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true + dev: true optional: true /@esbuild/linux-arm64@0.20.0: @@ -3380,6 +3465,7 @@ packages: cpu: [arm] os: [linux] requiresBuild: true + dev: true optional: true /@esbuild/linux-arm@0.20.0: @@ -3423,6 +3509,7 @@ packages: cpu: [ia32] os: [linux] requiresBuild: true + dev: true optional: true /@esbuild/linux-ia32@0.20.0: @@ -3466,6 +3553,7 @@ packages: cpu: [loong64] os: [linux] requiresBuild: true + dev: true optional: true /@esbuild/linux-loong64@0.20.0: @@ -3509,6 +3597,7 @@ packages: cpu: [mips64el] os: [linux] requiresBuild: true + dev: true optional: true /@esbuild/linux-mips64el@0.20.0: @@ -3552,6 +3641,7 @@ packages: cpu: [ppc64] os: [linux] requiresBuild: true + dev: true optional: true /@esbuild/linux-ppc64@0.20.0: @@ -3595,6 +3685,7 @@ packages: cpu: [riscv64] os: [linux] requiresBuild: true + dev: true optional: true /@esbuild/linux-riscv64@0.20.0: @@ -3638,6 +3729,7 @@ packages: cpu: [s390x] os: [linux] requiresBuild: true + dev: true optional: true /@esbuild/linux-s390x@0.20.0: @@ -3681,6 +3773,7 @@ packages: cpu: [x64] os: [linux] requiresBuild: true + dev: true optional: true /@esbuild/linux-x64@0.20.0: @@ -3724,6 +3817,7 @@ packages: cpu: [x64] os: [netbsd] requiresBuild: true + dev: true optional: true /@esbuild/netbsd-x64@0.20.0: @@ -3767,6 +3861,7 @@ packages: cpu: [x64] os: [openbsd] requiresBuild: true + dev: true optional: true /@esbuild/openbsd-x64@0.20.0: @@ -3810,6 +3905,7 @@ packages: cpu: [x64] os: [sunos] requiresBuild: true + dev: true optional: true /@esbuild/sunos-x64@0.20.0: @@ -3853,6 +3949,7 @@ packages: cpu: [arm64] os: [win32] requiresBuild: true + dev: true optional: true /@esbuild/win32-arm64@0.20.0: @@ -3896,6 +3993,7 @@ packages: cpu: [ia32] os: [win32] requiresBuild: true + dev: true optional: true /@esbuild/win32-ia32@0.20.0: @@ -3939,6 +4037,7 @@ packages: cpu: [x64] os: [win32] requiresBuild: true + dev: true optional: true /@esbuild/win32-x64@0.20.0: @@ -5956,7 +6055,7 @@ packages: '@opentelemetry/api': 1.8.0 '@sindresorhus/slugify': 2.2.1 ansi-escapes: 6.2.1 - chalk: 5.2.0 + chalk: 5.3.0 clean-stack: 4.2.0 execa: 6.1.0 fdir: 6.1.1 @@ -6027,7 +6126,7 @@ packages: hasBin: true dependencies: '@iarna/toml': 2.2.5 - chalk: 5.2.0 + chalk: 5.3.0 cron-parser: 4.8.1 deepmerge: 4.3.1 dot-prop: 7.2.0 @@ -7147,7 +7246,7 @@ packages: react-refresh: 0.14.0 schema-utils: 3.3.0 source-map: 0.7.4 - webpack: 5.91.0(esbuild@0.19.12) + webpack: 5.91.0 /@pnpm/config.env-replace@1.1.0: resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} @@ -8847,8 +8946,8 @@ packages: '@storybook/csf': 0.1.3 '@storybook/csf-tools': 7.6.17 '@storybook/preview-api': 7.6.17 - '@swc/core': 1.3.102 - '@swc/jest': 0.2.36(@swc/core@1.3.102) + '@swc/core': 1.4.13 + '@swc/jest': 0.2.36(@swc/core@1.4.13) can-bind-to-host: 1.1.2 commander: 9.5.0 expect-playwright: 0.8.0 @@ -9191,14 +9290,14 @@ packages: legacy-swc-helpers: /@swc/helpers@0.4.14 tslib: 2.6.2 - /@swc/jest@0.2.36(@swc/core@1.3.102): + /@swc/jest@0.2.36(@swc/core@1.4.13): resolution: {integrity: sha512-8X80dp81ugxs4a11z1ka43FPhP+/e+mJNXJSxiNYk8gIX/jPBtY4gQTrKu/KIoco8bzKuPI5lUxjfLiGsfvnlw==} engines: {npm: '>= 7.0.0'} peerDependencies: '@swc/core': '*' dependencies: '@jest/create-cache-key-function': 29.7.0 - '@swc/core': 1.3.102 + '@swc/core': 1.4.13 '@swc/counter': 0.1.3 jsonc-parser: 3.2.1 dev: true @@ -9418,7 +9517,7 @@ packages: resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} dependencies: '@types/connect': 3.4.38 - '@types/node': 18.19.31 + '@types/node': 20.11.17 dev: true /@types/cacheable-request@6.0.3: @@ -9450,7 +9549,7 @@ packages: /@types/connect@3.4.38: resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} dependencies: - '@types/node': 18.19.31 + '@types/node': 20.11.17 dev: true /@types/cookie@0.4.1: @@ -9459,7 +9558,7 @@ packages: /@types/cors@2.8.17: resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} dependencies: - '@types/node': 18.0.0 + '@types/node': 18.19.31 /@types/cross-spawn@6.0.6: resolution: {integrity: sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==} @@ -9532,12 +9631,20 @@ packages: /@types/express-serve-static-core@4.19.0: resolution: {integrity: sha512-bGyep3JqPCRry1wq+O5n7oiBgGWmeIJXPjXXCo8EK0u8duZGSYar7cGqd3ML2JUsLGeB7fmc06KYo9fLGWqPvQ==} dependencies: - '@types/node': 18.19.31 + '@types/node': 20.11.17 '@types/qs': 6.9.14 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 dev: true + /@types/express-ws@3.0.4: + resolution: {integrity: sha512-Yjj18CaivG5KndgcvzttWe8mPFinPCHJC2wvyQqVzA7hqeufM8EtWMj6mpp5omg3s8XALUexhOu8aXAyi/DyJQ==} + dependencies: + '@types/express': 4.17.21 + '@types/express-serve-static-core': 4.19.0 + '@types/ws': 8.5.10 + dev: true + /@types/express@4.17.21: resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} dependencies: @@ -9558,7 +9665,7 @@ packages: resolution: {integrity: sha512-rTtf75rwyP9G2qO5yRpYtdJ6aU1QqEhWbtW55qEgquEDa6bXW0s2TWZfDm02GuppjEozOWG/F2UnPq5hAQb+gw==} dependencies: '@types/minimatch': 5.1.2 - '@types/node': 18.0.0 + '@types/node': 18.19.31 /@types/glob@7.2.0: resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} @@ -9601,7 +9708,7 @@ packages: /@types/http-proxy@1.17.14: resolution: {integrity: sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==} dependencies: - '@types/node': 18.0.0 + '@types/node': 18.19.31 /@types/image-size@0.8.0: resolution: {integrity: sha512-hMlhu25ji75dXQk2uZkN3pTJ+lWrgKr8M1fTpyyFvuu+SJZBdGa5gDm4BVNobWXHZbOU11mBj0vciYp7qOfAFg==} @@ -9644,7 +9751,7 @@ packages: /@types/keyv@3.1.4: resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} dependencies: - '@types/node': 18.0.0 + '@types/node': 20.11.17 /@types/lodash@4.17.0: resolution: {integrity: sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==} @@ -9683,7 +9790,7 @@ packages: /@types/mkdirp@0.5.2: resolution: {integrity: sha512-U5icWpv7YnZYGsN4/cmh3WD2onMY0aJIiTE6+51TwJCttdHvtCYmkBNOobHlXwrJRL0nkH9jH4kD+1FAdMN4Tg==} dependencies: - '@types/node': 18.0.0 + '@types/node': 18.19.31 /@types/mousetrap@1.6.15: resolution: {integrity: sha512-qL0hyIMNPow317QWW/63RvL1x5MVMV+Ru3NaY9f/CuEpCqrmb7WeuK2071ZY5hczOnm38qExWM2i2WtkXLSqFw==} @@ -9710,6 +9817,11 @@ packages: dependencies: undici-types: 5.26.5 + /@types/node@20.11.17: + resolution: {integrity: sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==} + dependencies: + undici-types: 5.26.5 + /@types/node@8.10.66: resolution: {integrity: sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==} @@ -9796,7 +9908,7 @@ packages: /@types/responselike@1.0.3: resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} dependencies: - '@types/node': 18.0.0 + '@types/node': 20.11.17 /@types/retry@0.12.1: resolution: {integrity: sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==} @@ -9806,7 +9918,7 @@ packages: resolution: {integrity: sha512-YyP+VfeaqAyFmXoTh3HChxOQMyjByRMsHU7kc5KOJkSlXudhMhQIALbYV7rHh/l8d2lX3VUQzprrcAgWdRuU8g==} dependencies: '@types/glob': 5.0.38 - '@types/node': 18.0.0 + '@types/node': 18.19.31 /@types/sax@1.2.7: resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} @@ -9828,7 +9940,7 @@ packages: resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} dependencies: '@types/mime': 1.3.5 - '@types/node': 18.19.31 + '@types/node': 20.11.17 dev: true /@types/serve-static@1.15.5: @@ -9843,7 +9955,7 @@ packages: resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} dependencies: '@types/http-errors': 2.0.4 - '@types/node': 18.19.31 + '@types/node': 20.11.17 '@types/send': 0.17.4 dev: true @@ -9999,7 +10111,7 @@ packages: /@types/ws@8.5.10: resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} dependencies: - '@types/node': 18.19.31 + '@types/node': 20.11.17 /@types/yargs-parser@21.0.3: resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -10082,6 +10194,7 @@ packages: typescript: 5.3.3 transitivePeerDependencies: - supports-color + dev: true /@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0)(eslint@7.32.0)(typescript@5.4.4): resolution: {integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==} @@ -10110,7 +10223,6 @@ packages: transitivePeerDependencies: - supports-color dev: false - optional: true /@typescript-eslint/eslint-plugin@6.17.0(@typescript-eslint/parser@6.17.0)(eslint@7.0.0)(typescript@5.3.3): resolution: {integrity: sha512-Vih/4xLXmY7V490dGwBQJTpIZxH4ZFH6eCVmQ4RFkB+wmaCTDAx4dtgoWwMNGKLkqRY1L6rPqzEbjorRnDo4rQ==} @@ -10170,6 +10282,35 @@ packages: - supports-color dev: true + /@typescript-eslint/eslint-plugin@7.2.0(@typescript-eslint/parser@7.2.0)(eslint@7.0.0)(typescript@5.3.3): + resolution: {integrity: sha512-mdekAHOqS9UjlmyF/LSs6AIEvfceV749GFxoBAjwAv0nkevfKHWQFDMcBZWUiIC5ft6ePWivXoS36aKQ0Cy3sw==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^7.0.0 + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.10.0 + '@typescript-eslint/parser': 7.2.0(eslint@7.0.0)(typescript@5.3.3) + '@typescript-eslint/scope-manager': 7.2.0 + '@typescript-eslint/type-utils': 7.2.0(eslint@7.0.0)(typescript@5.3.3) + '@typescript-eslint/utils': 7.2.0(eslint@7.0.0)(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 7.2.0 + debug: 4.3.4 + eslint: 7.0.0 + graphemer: 1.4.0 + ignore: 5.3.1 + natural-compare: 1.4.0 + semver: 7.6.0 + ts-api-utils: 1.3.0(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@4.9.5): resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -10226,8 +10367,6 @@ packages: typescript: 5.4.4 transitivePeerDependencies: - supports-color - dev: false - optional: true /@typescript-eslint/parser@6.17.0(eslint@7.0.0)(typescript@5.3.3): resolution: {integrity: sha512-C4bBaX2orvhK+LlwrY8oWGmSl4WolCfYm513gEccdWZj0CwGadbIADb0FtVEcI+WzUyjyoBj2JRP8g25E6IB8A==} @@ -10269,6 +10408,28 @@ packages: typescript: 5.3.3 transitivePeerDependencies: - supports-color + dev: true + + /@typescript-eslint/parser@7.2.0(eslint@7.0.0)(typescript@5.3.3): + resolution: {integrity: sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 7.2.0 + '@typescript-eslint/types': 7.2.0 + '@typescript-eslint/typescript-estree': 7.2.0(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 7.2.0 + debug: 4.3.4 + eslint: 7.0.0 + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true /@typescript-eslint/scope-manager@5.62.0: resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} @@ -10283,6 +10444,7 @@ packages: dependencies: '@typescript-eslint/types': 6.17.0 '@typescript-eslint/visitor-keys': 6.17.0 + dev: true /@typescript-eslint/scope-manager@6.21.0: resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} @@ -10292,6 +10454,14 @@ packages: '@typescript-eslint/visitor-keys': 6.21.0 dev: true + /@typescript-eslint/scope-manager@7.2.0: + resolution: {integrity: sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 7.2.0 + '@typescript-eslint/visitor-keys': 7.2.0 + dev: true + /@typescript-eslint/type-utils@5.62.0(eslint@7.32.0)(typescript@4.9.5): resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -10329,6 +10499,7 @@ packages: typescript: 5.3.3 transitivePeerDependencies: - supports-color + dev: true /@typescript-eslint/type-utils@5.62.0(eslint@7.32.0)(typescript@5.4.4): resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==} @@ -10349,7 +10520,6 @@ packages: transitivePeerDependencies: - supports-color dev: false - optional: true /@typescript-eslint/type-utils@6.17.0(eslint@7.0.0)(typescript@5.3.3): resolution: {integrity: sha512-hDXcWmnbtn4P2B37ka3nil3yi3VCQO2QEB9gBiHJmQp5wmyQWqnjA85+ZcE8c4FqnaB6lBwMrPkgd4aBYz3iNg==} @@ -10391,6 +10561,26 @@ packages: - supports-color dev: true + /@typescript-eslint/type-utils@7.2.0(eslint@7.0.0)(typescript@5.3.3): + resolution: {integrity: sha512-xHi51adBHo9O9330J8GQYQwrKBqbIPJGZZVQTHHmy200hvkLZFWJIFtAG/7IYTWUyun6DE6w5InDReePJYJlJA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 7.2.0(typescript@5.3.3) + '@typescript-eslint/utils': 7.2.0(eslint@7.0.0)(typescript@5.3.3) + debug: 4.3.4 + eslint: 7.0.0 + ts-api-utils: 1.3.0(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/types@5.62.0: resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -10398,12 +10588,18 @@ packages: /@typescript-eslint/types@6.17.0: resolution: {integrity: sha512-qRKs9tvc3a4RBcL/9PXtKSehI/q8wuU9xYJxe97WFxnzH8NWWtcW3ffNS+EWg8uPvIerhjsEZ+rHtDqOCiH57A==} engines: {node: ^16.0.0 || >=18.0.0} + dev: true /@typescript-eslint/types@6.21.0: resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} engines: {node: ^16.0.0 || >=18.0.0} dev: true + /@typescript-eslint/types@7.2.0: + resolution: {integrity: sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA==} + engines: {node: ^16.0.0 || >=18.0.0} + dev: true + /@typescript-eslint/typescript-estree@5.62.0(supports-color@9.4.0)(typescript@5.4.4): resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -10484,7 +10680,6 @@ packages: typescript: 5.4.4 transitivePeerDependencies: - supports-color - dev: false /@typescript-eslint/typescript-estree@6.17.0(typescript@5.3.3): resolution: {integrity: sha512-gVQe+SLdNPfjlJn5VNGhlOhrXz4cajwFd5kAgWtZ9dCZf4XJf8xmgCTLIqec7aha3JwgLI2CK6GY1043FRxZwg==} @@ -10506,8 +10701,9 @@ packages: typescript: 5.3.3 transitivePeerDependencies: - supports-color + dev: true - /@typescript-eslint/typescript-estree@6.21.0(typescript@5.3.3): + /@typescript-eslint/typescript-estree@6.21.0(typescript@5.4.4): resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -10523,6 +10719,28 @@ packages: is-glob: 4.0.3 minimatch: 9.0.3 semver: 7.6.0 + ts-api-utils: 1.3.0(typescript@5.4.4) + typescript: 5.4.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/typescript-estree@7.2.0(typescript@5.3.3): + resolution: {integrity: sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 7.2.0 + '@typescript-eslint/visitor-keys': 7.2.0 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.3 + semver: 7.6.0 ts-api-utils: 1.3.0(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: @@ -10566,6 +10784,7 @@ packages: transitivePeerDependencies: - supports-color - typescript + dev: true /@typescript-eslint/utils@5.62.0(eslint@7.32.0)(typescript@5.4.4): resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} @@ -10586,7 +10805,6 @@ packages: - supports-color - typescript dev: false - optional: true /@typescript-eslint/utils@6.17.0(eslint@7.0.0)(typescript@5.3.3): resolution: {integrity: sha512-LofsSPjN/ITNkzV47hxas2JCsNCEnGhVvocfyOcLzT9c/tSZE7SfhS/iWtzP1lKNOEfLhRTZz6xqI8N2RzweSQ==} @@ -10626,7 +10844,7 @@ packages: - typescript dev: true - /@typescript-eslint/utils@6.21.0(eslint@7.32.0)(typescript@5.3.3): + /@typescript-eslint/utils@6.21.0(eslint@7.32.0)(typescript@5.4.4): resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -10637,7 +10855,7 @@ packages: '@types/semver': 7.5.8 '@typescript-eslint/scope-manager': 6.21.0 '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3) + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.4.4) eslint: 7.32.0 semver: 7.6.0 transitivePeerDependencies: @@ -10645,6 +10863,25 @@ packages: - typescript dev: true + /@typescript-eslint/utils@7.2.0(eslint@7.0.0)(typescript@5.3.3): + resolution: {integrity: sha512-YfHpnMAGb1Eekpm3XRK8hcMwGLGsnT6L+7b2XyRv6ouDuJU1tZir1GS2i0+VXRatMwSI1/UfcyPe53ADkU+IuA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^8.56.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@7.0.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.8 + '@typescript-eslint/scope-manager': 7.2.0 + '@typescript-eslint/types': 7.2.0 + '@typescript-eslint/typescript-estree': 7.2.0(typescript@5.3.3) + eslint: 7.0.0 + semver: 7.6.0 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + /@typescript-eslint/visitor-keys@5.62.0: resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -10658,6 +10895,7 @@ packages: dependencies: '@typescript-eslint/types': 6.17.0 eslint-visitor-keys: 3.4.3 + dev: true /@typescript-eslint/visitor-keys@6.21.0: resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} @@ -10667,6 +10905,14 @@ packages: eslint-visitor-keys: 3.4.3 dev: true + /@typescript-eslint/visitor-keys@7.2.0: + resolution: {integrity: sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 7.2.0 + eslint-visitor-keys: 3.4.3 + dev: true + /@vercel/nft@0.23.1: resolution: {integrity: sha512-NE0xSmGWVhgHF1OIoir71XAd0W0C1UE3nzFyhpFiMr3rVhetww7NvM1kc41trBsPG37Bh+dE5FYCTMzM/gBu0w==} engines: {node: '>=14'} @@ -10769,6 +11015,17 @@ packages: - '@swc/helpers' dev: true + /@vitejs/plugin-react-swc@3.5.0(vite@5.2.8): + resolution: {integrity: sha512-1PrOvAaDpqlCV+Up8RkAh9qaiUjoDUcjtttyhXDKw53XA6Ve16SOp6cCOpRs8Dj8DqUQs6eTW5YkLcLJjrXAig==} + peerDependencies: + vite: ^4 || ^5 + dependencies: + '@swc/core': 1.4.13 + vite: 5.2.8(@types/node@20.11.17) + transitivePeerDependencies: + - '@swc/helpers' + dev: true + /@vitejs/plugin-react@3.1.0(vite@5.0.10): resolution: {integrity: sha512-AfgcRL8ZBhAlc3BFdigClmTUMISmmzHn7sB2h9U1odvc5U/MjWXsAaz18b/WoppUTDBzxOJwo2VdClfUcItu9g==} engines: {node: ^14.18.0 || >=16.0.0} @@ -11477,7 +11734,7 @@ packages: /@wry/context@0.4.4: resolution: {integrity: sha512-LrKVLove/zw6h2Md/KZyWxIkFM6AoyKp71OqpH9Hiip1csjPVoD3tPxlbQUNxEnHENks3UGgNpSBCAfq9KWuag==} dependencies: - '@types/node': 18.0.0 + '@types/node': 18.19.31 tslib: 1.14.1 /@wry/equality@0.1.11: @@ -12524,7 +12781,7 @@ packages: loader-utils: 2.0.4 make-dir: 3.1.0 schema-utils: 2.7.1 - webpack: 5.91.0(esbuild@0.19.12) + webpack: 5.91.0 /babel-plugin-add-module-exports@1.0.4: resolution: {integrity: sha512-g+8yxHUZ60RcyaUpfNzy56OtWW+x9cyEe9j+CranqLiqbju2yf/Cy6ZtYK40EZxtrdHllzlVZgLmcOUCTlJ7Jg==} @@ -12630,7 +12887,7 @@ packages: '@babel/core': 7.24.4 '@babel/runtime': 7.24.4 '@babel/types': 7.24.0 - gatsby: 5.13.3(babel-eslint@10.1.0)(esbuild@0.19.12)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + gatsby: 5.13.3(babel-eslint@10.1.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.4) gatsby-core-utils: 4.13.1 dev: false @@ -12947,7 +13204,7 @@ packages: dependencies: ansi-align: 3.0.1 camelcase: 7.0.1 - chalk: 5.0.1 + chalk: 5.3.0 cli-boxes: 3.0.0 string-width: 5.1.2 type-fest: 2.19.0 @@ -12961,7 +13218,7 @@ packages: dependencies: ansi-align: 3.0.1 camelcase: 7.0.1 - chalk: 5.2.0 + chalk: 5.3.0 cli-boxes: 3.0.0 string-width: 5.1.2 type-fest: 2.19.0 @@ -13338,7 +13595,6 @@ packages: /chalk@5.3.0: resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - dev: true /change-case-all@1.0.14: resolution: {integrity: sha512-CWVm2uT7dmSHdO/z1CXT/n47mWonyypzBbuCy5tN7uMg22BsfkhwT6oHmFCAk+gL1LOOxhdbB9SZz3J1KTY3gA==} @@ -13784,7 +14040,7 @@ packages: resolution: {integrity: sha512-DP3FpjsiDDvnQC1OJBsdOJZPuy7r0o6sepY2T5M3L/d2nrE23O/ErFkEqyY3ngVL1ZhTj/H0pCMNObZGkEOaaQ==} engines: {node: '>=12.20.0'} dependencies: - chalk: 5.2.0 + chalk: 5.3.0 filter-obj: 3.0.0 is-plain-obj: 4.1.0 jest-validate: 27.5.1 @@ -13794,7 +14050,7 @@ packages: resolution: {integrity: sha512-Soe5lerRg3erMRgYC0EC696/8dMCGpBzcQchFfi55Yrkja8F+P7cUt0LVTIg7u5ob5BexLZ/F1kO+ejmv+nq8w==} engines: {node: '>=14.18.0'} dependencies: - chalk: 5.2.0 + chalk: 5.3.0 is-plain-obj: 4.1.0 dev: false @@ -14417,7 +14673,7 @@ packages: postcss-value-parser: 4.2.0 schema-utils: 3.3.0 semver: 7.6.0 - webpack: 5.91.0(esbuild@0.19.12) + webpack: 5.91.0 /css-minimizer-webpack-plugin@2.0.0(webpack@5.91.0): resolution: {integrity: sha512-cG/uc94727tx5pBNtb1Sd7gvUPzwmcQi1lkpfqTpdkuNq75hJCw7bIVsCNijLm4dhDcr1atvuysl2rZqOG8Txw==} @@ -14439,7 +14695,7 @@ packages: schema-utils: 3.3.0 serialize-javascript: 5.0.1 source-map: 0.6.1 - webpack: 5.91.0(esbuild@0.19.12) + webpack: 5.91.0 /css-select@4.3.0: resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} @@ -16206,7 +16462,7 @@ packages: dependencies: '@types/cookie': 0.4.1 '@types/cors': 2.8.17 - '@types/node': 18.0.0 + '@types/node': 18.19.31 accepts: 1.3.8 base64id: 2.0.0 cookie: 0.4.2 @@ -16570,6 +16826,7 @@ packages: '@esbuild/win32-arm64': 0.19.12 '@esbuild/win32-ia32': 0.19.12 '@esbuild/win32-x64': 0.19.12 + dev: true /esbuild@0.20.0: resolution: {integrity: sha512-6iwE3Y2RVYCME1jLpBqq7LQWK3MW6vjV2bZy6gt/WrqkY+WE74Spyc0ThAOYpMtITvnjX09CrC6ym7A/m9mebA==} @@ -16751,17 +17008,55 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@7.32.0)(typescript@5.3.3) - '@typescript-eslint/parser': 5.62.0(eslint@7.32.0)(typescript@5.3.3) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@7.32.0)(typescript@5.3.3) + '@typescript-eslint/parser': 5.62.0(eslint@7.32.0)(typescript@5.3.3) + babel-eslint: 10.1.0(eslint@7.32.0) + confusing-browser-globals: 1.0.11 + eslint: 7.32.0 + eslint-plugin-flowtype: 5.10.0(eslint@7.32.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.17.0)(eslint@7.32.0) + eslint-plugin-jsx-a11y: 6.8.0(eslint@7.32.0) + eslint-plugin-react: 7.34.1(eslint@7.32.0) + eslint-plugin-react-hooks: 4.6.0(eslint@7.32.0) + typescript: 5.3.3 + dev: true + + /eslint-config-react-app@6.0.0(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(babel-eslint@10.1.0)(eslint-plugin-flowtype@5.10.0)(eslint-plugin-import@2.29.1)(eslint-plugin-jsx-a11y@6.8.0)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.34.1)(eslint@7.32.0)(typescript@5.4.4): + resolution: {integrity: sha512-bpoAAC+YRfzq0dsTk+6v9aHm/uqnDwayNAXleMypGl6CpxI9oXXscVHo4fk3eJPIn+rsbtNetB4r/ZIidFIE8A==} + engines: {node: ^10.12.0 || >=12.0.0} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^4.0.0 + '@typescript-eslint/parser': ^4.0.0 + babel-eslint: ^10.0.0 + eslint: ^7.5.0 + eslint-plugin-flowtype: ^5.2.0 + eslint-plugin-import: ^2.22.0 + eslint-plugin-jest: ^24.0.0 + eslint-plugin-jsx-a11y: ^6.3.1 + eslint-plugin-react: ^7.20.3 + eslint-plugin-react-hooks: ^4.0.8 + eslint-plugin-testing-library: ^3.9.0 + typescript: '*' + peerDependenciesMeta: + eslint-plugin-jest: + optional: true + eslint-plugin-testing-library: + optional: true + typescript: + optional: true + dependencies: + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@7.32.0)(typescript@5.4.4) + '@typescript-eslint/parser': 5.62.0(eslint@7.32.0)(typescript@5.4.4) babel-eslint: 10.1.0(eslint@7.32.0) confusing-browser-globals: 1.0.11 eslint: 7.32.0 eslint-plugin-flowtype: 5.10.0(eslint@7.32.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.17.0)(eslint@7.32.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0)(eslint@7.32.0) eslint-plugin-jsx-a11y: 6.8.0(eslint@7.32.0) eslint-plugin-react: 7.34.1(eslint@7.32.0) eslint-plugin-react-hooks: 4.6.0(eslint@7.32.0) - typescript: 5.3.3 + typescript: 5.4.4 + dev: false /eslint-import-resolver-node@0.3.9: resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} @@ -16772,6 +17067,34 @@ packages: transitivePeerDependencies: - supports-color + /eslint-module-utils@2.8.1(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint@7.32.0): + resolution: {integrity: sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + '@typescript-eslint/parser': 5.62.0(eslint@7.32.0)(typescript@5.4.4) + debug: 3.2.7 + eslint: 7.32.0 + eslint-import-resolver-node: 0.3.9 + transitivePeerDependencies: + - supports-color + /eslint-module-utils@2.8.1(@typescript-eslint/parser@6.17.0)(eslint-import-resolver-node@0.3.9)(eslint@7.0.0): resolution: {integrity: sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==} engines: {node: '>=4'} @@ -16828,6 +17151,7 @@ packages: eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color + dev: true /eslint-plugin-flowtype@5.10.0(eslint@7.32.0): resolution: {integrity: sha512-vcz32f+7TP+kvTUyMXZmCnNujBQZDNmcqPImw8b9PZ+16w1Qdm6ryRuYZYVaG9xRqqmAPr2Cs9FAX5gN+x/bjw==} @@ -16848,13 +17172,13 @@ packages: '@formatjs/ts-transformer': 3.13.9 '@types/eslint': 8.56.7 '@types/picomatch': 2.3.3 - '@typescript-eslint/utils': 6.21.0(eslint@7.32.0)(typescript@5.3.3) + '@typescript-eslint/utils': 6.21.0(eslint@7.32.0)(typescript@5.4.4) emoji-regex: 10.3.0 eslint: 7.32.0 magic-string: 0.30.9 picomatch: 2.3.1 tslib: 2.6.2 - typescript: 5.3.3 + typescript: 5.4.4 unicode-emoji-utils: 1.2.0 transitivePeerDependencies: - supports-color @@ -16871,7 +17195,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.62.0(eslint@7.32.0)(typescript@5.3.3) + '@typescript-eslint/parser': 5.62.0(eslint@7.32.0)(typescript@5.4.4) array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 array.prototype.flat: 1.3.2 @@ -16880,7 +17204,7 @@ packages: doctrine: 2.1.0 eslint: 7.32.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.17.0)(eslint-import-resolver-node@0.3.9)(eslint@7.32.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint@7.32.0) hasown: 2.0.2 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -16963,6 +17287,7 @@ packages: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color + dev: true /eslint-plugin-jsx-a11y@6.8.0(eslint@7.32.0): resolution: {integrity: sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==} @@ -17028,6 +17353,14 @@ packages: dependencies: eslint: 7.32.0 + /eslint-plugin-react-refresh@0.4.6(eslint@7.0.0): + resolution: {integrity: sha512-NjGXdm7zgcKRkKMua34qVO9doI7VOxZ6ancSvBELJSSoX97jyndXcSoa8XBh69JoB31dNz3EEzlMcizZl7LaMA==} + peerDependencies: + eslint: '>=7' + dependencies: + eslint: 7.0.0 + dev: true + /eslint-plugin-react@7.33.2(eslint@7.0.0): resolution: {integrity: sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==} engines: {node: '>=4'} @@ -17175,7 +17508,7 @@ packages: micromatch: 4.0.5 normalize-path: 3.0.0 schema-utils: 3.3.0 - webpack: 5.91.0(esbuild@0.19.12) + webpack: 5.91.0 /eslint@7.0.0: resolution: {integrity: sha512-qY1cwdOxMONHJfGqw52UOpZDeqXy8xmD0u8CT6jIstil72jkhURC704W8CFyTPDPllz4z4lu0Ql1+07PG/XdIg==} @@ -17538,6 +17871,19 @@ packages: - utf-8-validate dev: false + /express-ws@5.0.2(express@4.19.2): + resolution: {integrity: sha512-0uvmuk61O9HXgLhGl3QhNSEtRsQevtmbL94/eILaliEADZBHZOQUAiHFrGPrgsjikohyrmSG5g+sCfASTt0lkQ==} + engines: {node: '>=4.5.0'} + peerDependencies: + express: ^4.0.0 || ^5.0.0-alpha.1 + dependencies: + express: 4.19.2 + ws: 7.5.9 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false + /express@4.18.2: resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} engines: {node: '>= 0.10.0'} @@ -17979,7 +18325,7 @@ packages: dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.91.0(esbuild@0.19.12) + webpack: 5.91.0 /file-system-cache@2.3.0: resolution: {integrity: sha512-l4DMNdsIPsVnKrgEXbJwDJsA5mB8rGwHYERMgqQx/xAUtChPJMre1bXBzDEqqVbWv9AIbFezXMxeEkZDSrXUOQ==} @@ -18349,6 +18695,39 @@ packages: tapable: 1.1.3 typescript: 5.3.3 webpack: 5.91.0(esbuild@0.19.12) + dev: true + + /fork-ts-checker-webpack-plugin@6.5.3(eslint@7.32.0)(typescript@5.4.4)(webpack@5.91.0): + resolution: {integrity: sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==} + engines: {node: '>=10', yarn: '>=1.0.0'} + peerDependencies: + eslint: '>= 6' + typescript: '>= 2.7' + vue-template-compiler: '*' + webpack: '>= 4' + peerDependenciesMeta: + eslint: + optional: true + vue-template-compiler: + optional: true + dependencies: + '@babel/code-frame': 7.24.2 + '@types/json-schema': 7.0.15 + chalk: 4.1.2 + chokidar: 3.6.0 + cosmiconfig: 6.0.0 + deepmerge: 4.3.1 + eslint: 7.32.0 + fs-extra: 9.1.0 + glob: 7.2.3 + memfs: 3.5.3 + minimatch: 3.1.2 + schema-utils: 2.7.0 + semver: 7.6.0 + tapable: 1.1.3 + typescript: 5.4.4 + webpack: 5.91.0 + dev: false /form-data-encoder@2.1.4: resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} @@ -18623,7 +19002,7 @@ packages: dependencies: '@types/node-fetch': 2.6.11 fs-extra: 9.1.0 - gatsby: 5.13.3(babel-eslint@10.1.0)(esbuild@0.19.12)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + gatsby: 5.13.3(babel-eslint@10.1.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.4) lodash: 4.17.21 node-fetch: 2.7.0 p-queue: 6.6.2 @@ -18785,7 +19164,7 @@ packages: chokidar: 3.6.0 fs-exists-cached: 1.0.0 fs-extra: 11.2.0 - gatsby: 5.13.3(babel-eslint@10.1.0)(esbuild@0.19.12)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + gatsby: 5.13.3(babel-eslint@10.1.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.4) gatsby-core-utils: 4.13.1 gatsby-page-utils: 3.13.1 gatsby-plugin-utils: 4.13.1(gatsby@5.13.3)(graphql@16.8.1) @@ -18854,7 +19233,7 @@ packages: debug: 4.3.4 filenamify: 4.3.0 fs-extra: 11.2.0 - gatsby: 5.13.3(babel-eslint@10.1.0)(esbuild@0.19.12)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + gatsby: 5.13.3(babel-eslint@10.1.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.4) gatsby-core-utils: 4.13.1 gatsby-plugin-utils: 4.13.1(gatsby@5.13.3)(graphql@16.8.1) lodash: 4.17.21 @@ -18913,7 +19292,7 @@ packages: '@babel/preset-typescript': 7.24.1(@babel/core@7.24.4) '@babel/runtime': 7.24.4 babel-plugin-remove-graphql-queries: 5.13.1(@babel/core@7.24.4)(gatsby@5.13.3) - gatsby: 5.13.3(babel-eslint@10.1.0)(esbuild@0.19.12)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + gatsby: 5.13.3(babel-eslint@10.1.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.4) transitivePeerDependencies: - supports-color dev: false @@ -18951,7 +19330,7 @@ packages: '@babel/runtime': 7.24.4 fastq: 1.17.1 fs-extra: 11.2.0 - gatsby: 5.13.3(babel-eslint@10.1.0)(esbuild@0.19.12)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + gatsby: 5.13.3(babel-eslint@10.1.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.4) gatsby-core-utils: 4.13.1 gatsby-sharp: 1.13.0 graphql: 16.8.1 @@ -19453,7 +19832,7 @@ packages: - webpack-hot-middleware - webpack-plugin-serve - /gatsby@5.13.3(babel-eslint@10.1.0)(esbuild@0.19.12)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3): + /gatsby@5.13.3(babel-eslint@10.1.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.4): resolution: {integrity: sha512-SSnGpjswK20BQORcvTbtK8eI+W4QUG+u8rdVswB4suva6BfvTakW2wiktj7E2MdO4NjRvlgJjF5dUUncU5nldA==} engines: {node: '>=18.0.0'} hasBin: true @@ -19487,8 +19866,8 @@ packages: '@pmmmwh/react-refresh-webpack-plugin': 0.5.11(react-refresh@0.14.0)(webpack@5.91.0) '@sigmacomputing/babel-plugin-lodash': 3.3.5 '@types/http-proxy': 1.17.14 - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@7.32.0)(typescript@5.3.3) - '@typescript-eslint/parser': 5.62.0(eslint@7.32.0)(typescript@5.3.3) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@7.32.0)(typescript@5.4.4) + '@typescript-eslint/parser': 5.62.0(eslint@7.32.0)(typescript@5.4.4) '@vercel/webpack-asset-relocator-loader': 1.7.3 acorn-loose: 8.4.0 acorn-walk: 8.3.2 @@ -19526,9 +19905,9 @@ packages: enhanced-resolve: 5.16.0 error-stack-parser: 2.1.4 eslint: 7.32.0 - eslint-config-react-app: 6.0.0(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(babel-eslint@10.1.0)(eslint-plugin-flowtype@5.10.0)(eslint-plugin-import@2.29.1)(eslint-plugin-jsx-a11y@6.8.0)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.34.1)(eslint@7.32.0)(typescript@5.3.3) + eslint-config-react-app: 6.0.0(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(babel-eslint@10.1.0)(eslint-plugin-flowtype@5.10.0)(eslint-plugin-import@2.29.1)(eslint-plugin-jsx-a11y@6.8.0)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.34.1)(eslint@7.32.0)(typescript@5.4.4) eslint-plugin-flowtype: 5.10.0(eslint@7.32.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.17.0)(eslint@7.32.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0)(eslint@7.32.0) eslint-plugin-jsx-a11y: 6.8.0(eslint@7.32.0) eslint-plugin-react: 7.34.1(eslint@7.32.0) eslint-plugin-react-hooks: 4.6.0(eslint@7.32.0) @@ -19600,7 +19979,7 @@ packages: query-string: 6.14.1 raw-loader: 4.0.2(webpack@5.91.0) react: 18.2.0 - react-dev-utils: 12.0.1(eslint@7.32.0)(typescript@5.3.3)(webpack@5.91.0) + react-dev-utils: 12.0.1(eslint@7.32.0)(typescript@5.4.4)(webpack@5.91.0) react-dom: 18.2.0(react@18.2.0) react-refresh: 0.14.0 react-server-dom-webpack: 0.0.0-experimental-c8b778b7f-20220825(react@18.2.0)(webpack@5.91.0) @@ -19618,13 +19997,13 @@ packages: strip-ansi: 6.0.1 style-loader: 2.0.0(webpack@5.91.0) style-to-object: 0.4.4 - terser-webpack-plugin: 5.3.10(esbuild@0.19.12)(webpack@5.91.0) + terser-webpack-plugin: 5.3.10(webpack@5.91.0) tmp: 0.2.3 true-case-path: 2.2.1 type-of: 2.0.1 url-loader: 4.1.1(file-loader@6.2.0)(webpack@5.91.0) uuid: 8.3.2 - webpack: 5.91.0(esbuild@0.19.12) + webpack: 5.91.0 webpack-dev-middleware: 4.3.0(webpack@5.91.0) webpack-merge: 5.10.0 webpack-stats-plugin: 1.1.3 @@ -19813,6 +20192,12 @@ packages: es-errors: 1.3.0 get-intrinsic: 1.2.4 + /get-tsconfig@4.7.3: + resolution: {integrity: sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg==} + dependencies: + resolve-pkg-maps: 1.0.0 + dev: true + /get-value@2.0.6: resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==} engines: {node: '>=0.10.0'} @@ -22531,7 +22916,7 @@ packages: resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 18.0.0 + '@types/node': 18.19.31 merge-stream: 2.0.0 supports-color: 7.2.0 @@ -22539,7 +22924,7 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 18.0.0 + '@types/node': 18.19.31 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -23260,7 +23645,7 @@ packages: resolution: {integrity: sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==} engines: {node: '>=12'} dependencies: - chalk: 5.2.0 + chalk: 5.3.0 is-unicode-supported: 1.3.0 dev: false @@ -24269,7 +24654,7 @@ packages: dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.91.0(esbuild@0.19.12) + webpack: 5.91.0 webpack-sources: 1.4.3 /mini-svg-data-uri@1.4.4: @@ -24299,6 +24684,7 @@ packages: engines: {node: '>=16 || 14 >=14.17'} dependencies: brace-expansion: 2.0.1 + dev: true /minimatch@9.0.4: resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==} @@ -25121,7 +25507,7 @@ packages: dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.91.0(esbuild@0.19.12) + webpack: 5.91.0 /nullthrows@1.1.1: resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==} @@ -25410,7 +25796,7 @@ packages: resolution: {integrity: sha512-ERAyNnZOfqM+Ao3RAvIXkYh5joP220yf59gVe2X/cI6SiCxIdi4c9HZKZD8R6q/RDXEje1THBju6iExiSsgJaQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dependencies: - chalk: 5.2.0 + chalk: 5.3.0 cli-cursor: 4.0.0 cli-spinners: 2.9.2 is-interactive: 2.0.0 @@ -26376,7 +26762,7 @@ packages: klona: 2.0.6 postcss: 8.4.38 semver: 7.6.0 - webpack: 5.91.0(esbuild@0.19.12) + webpack: 5.91.0 /postcss-merge-longhand@5.1.7(postcss@8.4.38): resolution: {integrity: sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==} @@ -27371,7 +27757,7 @@ packages: dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.91.0(esbuild@0.19.12) + webpack: 5.91.0 /rbush@3.0.1: resolution: {integrity: sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==} @@ -27537,6 +27923,49 @@ packages: - eslint - supports-color - vue-template-compiler + dev: true + + /react-dev-utils@12.0.1(eslint@7.32.0)(typescript@5.4.4)(webpack@5.91.0): + resolution: {integrity: sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=2.7' + webpack: '>=4' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@babel/code-frame': 7.24.2 + address: 1.2.2 + browserslist: 4.23.0 + chalk: 4.1.2 + cross-spawn: 7.0.3 + detect-port-alt: 1.1.6 + escape-string-regexp: 4.0.0 + filesize: 8.0.7 + find-up: 5.0.0 + fork-ts-checker-webpack-plugin: 6.5.3(eslint@7.32.0)(typescript@5.4.4)(webpack@5.91.0) + global-modules: 2.0.0 + globby: 11.1.0 + gzip-size: 6.0.0 + immer: 9.0.21 + is-root: 2.1.0 + loader-utils: 3.2.1 + open: 8.4.2 + pkg-up: 3.1.0 + prompts: 2.4.2 + react-error-overlay: 6.0.11 + recursive-readdir: 2.2.3 + shell-quote: 1.8.1 + strip-ansi: 6.0.1 + text-table: 0.2.0 + typescript: 5.4.4 + webpack: 5.91.0 + transitivePeerDependencies: + - eslint + - supports-color + - vue-template-compiler + dev: false /react-dnd-html5-backend@14.1.0: resolution: {integrity: sha512-6ONeqEC3XKVf4eVmMTe0oPds+c5B9Foyj8p/ZKLb7kL2qh9COYxiBHv3szd6gztqi/efkmriywLUVlPotqoJyw==} @@ -27936,7 +28365,7 @@ packages: loose-envify: 1.4.0 neo-async: 2.6.2 react: 18.2.0 - webpack: 5.91.0(esbuild@0.19.12) + webpack: 5.91.0 /react-split-pane@0.1.92(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-GfXP1xSzLMcLJI5BM36Vh7GgZBpy+U/X0no+VM3fxayv+p1Jly5HpMofZJraeaMl73b3hvlr+N9zJKvLB/uz9w==} @@ -28722,6 +29151,10 @@ packages: resolution: {integrity: sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==} dev: false + /resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + dev: true + /resolve-protobuf-schema@2.1.0: resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==} dependencies: @@ -30290,7 +30723,7 @@ packages: dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.91.0(esbuild@0.19.12) + webpack: 5.91.0 /style-to-object@0.3.0: resolution: {integrity: sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==} @@ -30682,6 +31115,7 @@ packages: serialize-javascript: 6.0.2 terser: 5.30.3 webpack: 5.91.0(esbuild@0.19.12) + dev: true /terser-webpack-plugin@5.3.10(webpack@5.91.0): resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==} @@ -31026,6 +31460,16 @@ packages: typescript: '>=4.2.0' dependencies: typescript: 5.3.3 + dev: true + + /ts-api-utils@1.3.0(typescript@5.4.4): + resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + dependencies: + typescript: 5.4.4 + dev: true /ts-dedent@2.2.0: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} @@ -31174,7 +31618,17 @@ packages: dependencies: tslib: 1.14.1 typescript: 5.4.4 - dev: false + + /tsx@4.7.1: + resolution: {integrity: sha512-8d6VuibXHtlN5E3zFkgY8u4DX7Y3Z27zvvPKVmLon/D4AjuKzarkUBTLDBgj9iTQ0hg5xM7c/mYiRVM+HETf0g==} + engines: {node: '>=18.0.0'} + hasBin: true + dependencies: + esbuild: 0.19.12 + get-tsconfig: 4.7.3 + optionalDependencies: + fsevents: 2.3.3 + dev: true /tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} @@ -31789,7 +32243,7 @@ packages: engines: {node: '>=14.16'} dependencies: boxen: 7.1.1 - chalk: 5.2.0 + chalk: 5.3.0 configstore: 6.0.0 has-yarn: 3.0.0 import-lazy: 4.0.0 @@ -31846,7 +32300,7 @@ packages: loader-utils: 2.0.4 mime-types: 2.1.35 schema-utils: 3.3.0 - webpack: 5.91.0(esbuild@0.19.12) + webpack: 5.91.0 /url@0.11.3: resolution: {integrity: sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==} @@ -32114,30 +32568,6 @@ packages: - rollup dev: true - /vite-node@0.34.6(@types/node@18.0.0): - resolution: {integrity: sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==} - engines: {node: '>=v14.18.0'} - hasBin: true - requiresBuild: true - dependencies: - cac: 6.7.14 - debug: 4.3.4 - mlly: 1.6.1 - pathe: 1.1.2 - picocolors: 1.0.0 - vite: 5.2.8(@types/node@18.0.0) - transitivePeerDependencies: - - '@types/node' - - less - - lightningcss - - sass - - stylus - - sugarss - - supports-color - - terser - dev: false - optional: true - /vite-node@0.34.6(@types/node@18.19.31): resolution: {integrity: sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==} engines: {node: '>=v14.18.0'} @@ -32324,6 +32754,7 @@ packages: rollup: 4.14.1 optionalDependencies: fsevents: 2.3.3 + dev: true /vite@5.2.8(@types/node@18.19.31): resolution: {integrity: sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==} @@ -32360,6 +32791,42 @@ packages: optionalDependencies: fsevents: 2.3.3 + /vite@5.2.8(@types/node@20.11.17): + resolution: {integrity: sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + '@types/node': 20.11.17 + esbuild: 0.20.2 + postcss: 8.4.38 + rollup: 4.14.1 + optionalDependencies: + fsevents: 2.3.3 + dev: true + /vitest@0.34.6: resolution: {integrity: sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==} engines: {node: '>=v14.18.0'} @@ -32394,7 +32861,7 @@ packages: dependencies: '@types/chai': 4.3.14 '@types/chai-subset': 1.3.5 - '@types/node': 18.0.0 + '@types/node': 18.19.31 '@vitest/expect': 0.34.6 '@vitest/runner': 0.34.6 '@vitest/snapshot': 0.34.6 @@ -32413,8 +32880,8 @@ packages: strip-literal: 1.3.0 tinybench: 2.6.0 tinypool: 0.7.0 - vite: 5.2.8(@types/node@18.0.0) - vite-node: 0.34.6(@types/node@18.0.0) + vite: 5.2.8(@types/node@18.19.31) + vite-node: 0.34.6(@types/node@18.19.31) why-is-node-running: 2.2.2 transitivePeerDependencies: - less @@ -32817,7 +33284,7 @@ packages: mime-types: 2.1.35 range-parser: 1.2.1 schema-utils: 3.3.0 - webpack: 5.91.0(esbuild@0.19.12) + webpack: 5.91.0 /webpack-merge@5.10.0: resolution: {integrity: sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==} @@ -32964,6 +33431,7 @@ packages: - '@swc/core' - esbuild - uglify-js + dev: true /website-scraper@5.3.1: resolution: {integrity: sha512-gogqPXD2gVsxoyd2yRiympw3rA5GuEpD1CaDEJ/J8zzanx7hkbTtneoO1SGs436PpLbWVcUge+6APGLhzsuZPA==} From b643648734d482026cf1df34f9bc20b1909e0a94 Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Sat, 13 Apr 2024 09:41:02 +0200 Subject: [PATCH 33/82] refactor: improve lagoon environment variables Generic default variables that work with stock hostnames and specifics for dev/stage/prod that allow custom domains. --- apps/cms/.lagoon.env | 7 +++---- apps/cms/.lagoon.env.prod | 7 +++++++ apps/cms/.lagoon.env.stage | 7 +++++++ apps/cms/lagoon.env.dev | 7 +++++++ apps/preview/.lagoon.env | 2 +- apps/preview/.lagoon.env.dev | 1 + apps/preview/.lagoon.env.prod | 1 + apps/preview/.lagoon.env.stage | 1 + apps/website/.lagoon.env | 5 ++--- apps/website/.lagoon.env.dev | 28 ++++++++++++++++++++++++++++ apps/website/.lagoon.env.prod | 28 ++++++++++++++++++++++++++++ apps/website/.lagoon.env.stage | 28 ++++++++++++++++++++++++++++ 12 files changed, 114 insertions(+), 8 deletions(-) create mode 100644 apps/cms/.lagoon.env.prod create mode 100644 apps/cms/.lagoon.env.stage create mode 100644 apps/cms/lagoon.env.dev create mode 100644 apps/preview/.lagoon.env.dev create mode 100644 apps/preview/.lagoon.env.prod create mode 100644 apps/preview/.lagoon.env.stage create mode 100644 apps/website/.lagoon.env.dev create mode 100644 apps/website/.lagoon.env.prod create mode 100644 apps/website/.lagoon.env.stage diff --git a/apps/cms/.lagoon.env b/apps/cms/.lagoon.env index 0309eb662..3c8c78842 100644 --- a/apps/cms/.lagoon.env +++ b/apps/cms/.lagoon.env @@ -1,7 +1,6 @@ -PROJECT_NAME=example -PUBLISHER_URL="https://${LAGOON_GIT_BRANCH}-${PROJECT_NAME}.build.amazeelabs.dev" -NETLIFY_URL="https://${LAGOON_GIT_BRANCH}-${PROJECT_NAME}.amazeelabs.dev" -PREVIEW_URL="https://${LAGOON_GIT_BRANCH}-${PROJECT_NAME}.preview.amazeelabs.dev" +PUBLISHER_URL="https://build.${LAGOON_ENVIRONMENT}.${LAGOON_PROJECT}.ch4.amazee.io" +NETLIFY_URL="https://build.${LAGOON_ENVIRONMENT}.${LAGOON_PROJECT}.ch4.amazee.io" +PREVIEW_URL="https://preview.${LAGOON_ENVIRONMENT}.${LAGOON_PROJECT}.ch4.amazee.io" # Used to set the original client secret. PUBLISHER_OAUTH2_CLIENT_SECRET=REPLACE_ME diff --git a/apps/cms/.lagoon.env.prod b/apps/cms/.lagoon.env.prod new file mode 100644 index 000000000..0309eb662 --- /dev/null +++ b/apps/cms/.lagoon.env.prod @@ -0,0 +1,7 @@ +PROJECT_NAME=example +PUBLISHER_URL="https://${LAGOON_GIT_BRANCH}-${PROJECT_NAME}.build.amazeelabs.dev" +NETLIFY_URL="https://${LAGOON_GIT_BRANCH}-${PROJECT_NAME}.amazeelabs.dev" +PREVIEW_URL="https://${LAGOON_GIT_BRANCH}-${PROJECT_NAME}.preview.amazeelabs.dev" + +# Used to set the original client secret. +PUBLISHER_OAUTH2_CLIENT_SECRET=REPLACE_ME diff --git a/apps/cms/.lagoon.env.stage b/apps/cms/.lagoon.env.stage new file mode 100644 index 000000000..0309eb662 --- /dev/null +++ b/apps/cms/.lagoon.env.stage @@ -0,0 +1,7 @@ +PROJECT_NAME=example +PUBLISHER_URL="https://${LAGOON_GIT_BRANCH}-${PROJECT_NAME}.build.amazeelabs.dev" +NETLIFY_URL="https://${LAGOON_GIT_BRANCH}-${PROJECT_NAME}.amazeelabs.dev" +PREVIEW_URL="https://${LAGOON_GIT_BRANCH}-${PROJECT_NAME}.preview.amazeelabs.dev" + +# Used to set the original client secret. +PUBLISHER_OAUTH2_CLIENT_SECRET=REPLACE_ME diff --git a/apps/cms/lagoon.env.dev b/apps/cms/lagoon.env.dev new file mode 100644 index 000000000..0309eb662 --- /dev/null +++ b/apps/cms/lagoon.env.dev @@ -0,0 +1,7 @@ +PROJECT_NAME=example +PUBLISHER_URL="https://${LAGOON_GIT_BRANCH}-${PROJECT_NAME}.build.amazeelabs.dev" +NETLIFY_URL="https://${LAGOON_GIT_BRANCH}-${PROJECT_NAME}.amazeelabs.dev" +PREVIEW_URL="https://${LAGOON_GIT_BRANCH}-${PROJECT_NAME}.preview.amazeelabs.dev" + +# Used to set the original client secret. +PUBLISHER_OAUTH2_CLIENT_SECRET=REPLACE_ME diff --git a/apps/preview/.lagoon.env b/apps/preview/.lagoon.env index 2c80dc9c1..752e3109d 100644 --- a/apps/preview/.lagoon.env +++ b/apps/preview/.lagoon.env @@ -1 +1 @@ -DRUPAL_URL="https://${LAGOON_GIT_BRANCH}-${PROJECT_NAME}.cms.amazeelabs.dev" +DRUPAL_URL="https://nginx.${LAGOON_ENVIRONMENT}.${LAGOON_PROJECT}.ch4.amazee.io" diff --git a/apps/preview/.lagoon.env.dev b/apps/preview/.lagoon.env.dev new file mode 100644 index 000000000..2c80dc9c1 --- /dev/null +++ b/apps/preview/.lagoon.env.dev @@ -0,0 +1 @@ +DRUPAL_URL="https://${LAGOON_GIT_BRANCH}-${PROJECT_NAME}.cms.amazeelabs.dev" diff --git a/apps/preview/.lagoon.env.prod b/apps/preview/.lagoon.env.prod new file mode 100644 index 000000000..2c80dc9c1 --- /dev/null +++ b/apps/preview/.lagoon.env.prod @@ -0,0 +1 @@ +DRUPAL_URL="https://${LAGOON_GIT_BRANCH}-${PROJECT_NAME}.cms.amazeelabs.dev" diff --git a/apps/preview/.lagoon.env.stage b/apps/preview/.lagoon.env.stage new file mode 100644 index 000000000..2c80dc9c1 --- /dev/null +++ b/apps/preview/.lagoon.env.stage @@ -0,0 +1 @@ +DRUPAL_URL="https://${LAGOON_GIT_BRANCH}-${PROJECT_NAME}.cms.amazeelabs.dev" diff --git a/apps/website/.lagoon.env b/apps/website/.lagoon.env index 89379ad89..1f5bcd4b8 100644 --- a/apps/website/.lagoon.env +++ b/apps/website/.lagoon.env @@ -1,7 +1,6 @@ -PROJECT_NAME=example DRUPAL_INTERNAL_URL="http://nginx:8080" -DRUPAL_EXTERNAL_URL="https://${LAGOON_GIT_BRANCH}-${PROJECT_NAME}.cms.amazeelabs.dev" -NETLIFY_URL="https://${LAGOON_GIT_BRANCH}-${PROJECT_NAME}.amazeelabs.dev" +DRUPAL_EXTERNAL_URL="https://nginx.${LAGOON_ENVIRONMENT}.${LAGOON_PROJECT}.ch4.amazee.io" +NETLIFY_URL="https://build.${LAGOON_ENVIRONMENT}.${LAGOON_PROJECT}.ch4.amazee.io" # ----------------------------------------------- # Publisher authentication with Drupal (OAuth2). diff --git a/apps/website/.lagoon.env.dev b/apps/website/.lagoon.env.dev new file mode 100644 index 000000000..89379ad89 --- /dev/null +++ b/apps/website/.lagoon.env.dev @@ -0,0 +1,28 @@ +PROJECT_NAME=example +DRUPAL_INTERNAL_URL="http://nginx:8080" +DRUPAL_EXTERNAL_URL="https://${LAGOON_GIT_BRANCH}-${PROJECT_NAME}.cms.amazeelabs.dev" +NETLIFY_URL="https://${LAGOON_GIT_BRANCH}-${PROJECT_NAME}.amazeelabs.dev" + +# ----------------------------------------------- +# Publisher authentication with Drupal (OAuth2). +# See main ./README.md for more information. +# ----------------------------------------------- +# Set to true to fully skip authentication. +PUBLISHER_SKIP_AUTHENTICATION=false + +# Secret from the Drupal Publisher Consumer. +PUBLISHER_OAUTH2_CLIENT_SECRET=REPLACE_ME + +# Client id from the Drupal Publisher Consumer. +PUBLISHER_OAUTH2_CLIENT_ID=publisher + +# A random string, used to encrypt the session. +PUBLISHER_OAUTH2_SESSION_SECRET=REPLACE_ME + +# "development" or "production", production will trust first proxy +# and serve secure cookies. +PUBLISHER_OAUTH2_ENVIRONMENT_TYPE=production + +# DRUPAL_EXTERNAL_URL is used by default, but can be overridden +# to match the Drupal production url, without the nginx prefix. +PUBLISHER_OAUTH2_TOKEN_HOST="${DRUPAL_EXTERNAL_URL}" diff --git a/apps/website/.lagoon.env.prod b/apps/website/.lagoon.env.prod new file mode 100644 index 000000000..89379ad89 --- /dev/null +++ b/apps/website/.lagoon.env.prod @@ -0,0 +1,28 @@ +PROJECT_NAME=example +DRUPAL_INTERNAL_URL="http://nginx:8080" +DRUPAL_EXTERNAL_URL="https://${LAGOON_GIT_BRANCH}-${PROJECT_NAME}.cms.amazeelabs.dev" +NETLIFY_URL="https://${LAGOON_GIT_BRANCH}-${PROJECT_NAME}.amazeelabs.dev" + +# ----------------------------------------------- +# Publisher authentication with Drupal (OAuth2). +# See main ./README.md for more information. +# ----------------------------------------------- +# Set to true to fully skip authentication. +PUBLISHER_SKIP_AUTHENTICATION=false + +# Secret from the Drupal Publisher Consumer. +PUBLISHER_OAUTH2_CLIENT_SECRET=REPLACE_ME + +# Client id from the Drupal Publisher Consumer. +PUBLISHER_OAUTH2_CLIENT_ID=publisher + +# A random string, used to encrypt the session. +PUBLISHER_OAUTH2_SESSION_SECRET=REPLACE_ME + +# "development" or "production", production will trust first proxy +# and serve secure cookies. +PUBLISHER_OAUTH2_ENVIRONMENT_TYPE=production + +# DRUPAL_EXTERNAL_URL is used by default, but can be overridden +# to match the Drupal production url, without the nginx prefix. +PUBLISHER_OAUTH2_TOKEN_HOST="${DRUPAL_EXTERNAL_URL}" diff --git a/apps/website/.lagoon.env.stage b/apps/website/.lagoon.env.stage new file mode 100644 index 000000000..89379ad89 --- /dev/null +++ b/apps/website/.lagoon.env.stage @@ -0,0 +1,28 @@ +PROJECT_NAME=example +DRUPAL_INTERNAL_URL="http://nginx:8080" +DRUPAL_EXTERNAL_URL="https://${LAGOON_GIT_BRANCH}-${PROJECT_NAME}.cms.amazeelabs.dev" +NETLIFY_URL="https://${LAGOON_GIT_BRANCH}-${PROJECT_NAME}.amazeelabs.dev" + +# ----------------------------------------------- +# Publisher authentication with Drupal (OAuth2). +# See main ./README.md for more information. +# ----------------------------------------------- +# Set to true to fully skip authentication. +PUBLISHER_SKIP_AUTHENTICATION=false + +# Secret from the Drupal Publisher Consumer. +PUBLISHER_OAUTH2_CLIENT_SECRET=REPLACE_ME + +# Client id from the Drupal Publisher Consumer. +PUBLISHER_OAUTH2_CLIENT_ID=publisher + +# A random string, used to encrypt the session. +PUBLISHER_OAUTH2_SESSION_SECRET=REPLACE_ME + +# "development" or "production", production will trust first proxy +# and serve secure cookies. +PUBLISHER_OAUTH2_ENVIRONMENT_TYPE=production + +# DRUPAL_EXTERNAL_URL is used by default, but can be overridden +# to match the Drupal production url, without the nginx prefix. +PUBLISHER_OAUTH2_TOKEN_HOST="${DRUPAL_EXTERNAL_URL}" From b50369131345a008e03cb5bd8c92a9c21dcf6db4 Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Sat, 13 Apr 2024 16:39:15 +0200 Subject: [PATCH 34/82] chore(SLB-209): increase autosave interval for demonstration purposes --- apps/cms/config/sync/silverback_autosave.settings.yml | 2 +- packages/drupal/silverback_autosave/js/silverback_autosave.js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/cms/config/sync/silverback_autosave.settings.yml b/apps/cms/config/sync/silverback_autosave.settings.yml index 3bd7d1556..9fc203c20 100644 --- a/apps/cms/config/sync/silverback_autosave.settings.yml +++ b/apps/cms/config/sync/silverback_autosave.settings.yml @@ -1,6 +1,6 @@ _core: default_config_hash: HaTNbtQe6_A0WF-ZElHNWcv0VK_aqOj5lBEFhAgY85s -interval: 10000 +interval: 3000 only_on_form_change: false active_on: content_entity_forms: true diff --git a/packages/drupal/silverback_autosave/js/silverback_autosave.js b/packages/drupal/silverback_autosave/js/silverback_autosave.js index e9c9438b3..cfebc5704 100644 --- a/packages/drupal/silverback_autosave/js/silverback_autosave.js +++ b/packages/drupal/silverback_autosave/js/silverback_autosave.js @@ -307,6 +307,8 @@ ); }, 500); } + // TODO: Implement a mechanism that takes user interaction into account. + // E.g. autosave after the user did not interact for 2 seconds. Drupal.autosaveForm.timer = setInterval(function () { if (!Drupal.ajax.instances.some(isAjaxing)) { From 7dd097f165b992fab9c7ad5359141c4ac8feb2e6 Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Sat, 13 Apr 2024 16:42:07 +0200 Subject: [PATCH 35/82] feat(SLB-209): trigger preview refresh on autosaves --- .../src/Storage/AutosaveEntityFormDatabaseStorage.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormDatabaseStorage.php b/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormDatabaseStorage.php index 0098464b7..ae41e5c82 100644 --- a/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormDatabaseStorage.php +++ b/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormDatabaseStorage.php @@ -84,6 +84,9 @@ public function storeEntityAndFormState($form_id, $form_session_id, $entity_type $serialized_form_state, ]) ->execute(); + // @todo Clean up this prototype. + // @todo pass entity information so clients can filter if they have to update. + \Drupal::service('http_client')->post((getenv('PREVIEW_URL') ?? 'http://localhost:8001') . '/__preview'); } } From 004a708e933e95313e6d48ba5a18bea9ebeedd16 Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Sun, 14 Apr 2024 08:52:28 +0200 Subject: [PATCH 36/82] feat(SLB-209): debounce-based autosave --- .../sync/silverback_autosave.settings.yml | 2 +- .../js/silverback_autosave.js | 25 ++++++++++--------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/apps/cms/config/sync/silverback_autosave.settings.yml b/apps/cms/config/sync/silverback_autosave.settings.yml index 9fc203c20..c67f05571 100644 --- a/apps/cms/config/sync/silverback_autosave.settings.yml +++ b/apps/cms/config/sync/silverback_autosave.settings.yml @@ -1,6 +1,6 @@ _core: default_config_hash: HaTNbtQe6_A0WF-ZElHNWcv0VK_aqOj5lBEFhAgY85s -interval: 3000 +interval: 2000 only_on_form_change: false active_on: content_entity_forms: true diff --git a/packages/drupal/silverback_autosave/js/silverback_autosave.js b/packages/drupal/silverback_autosave/js/silverback_autosave.js index cfebc5704..c4501987f 100644 --- a/packages/drupal/silverback_autosave/js/silverback_autosave.js +++ b/packages/drupal/silverback_autosave/js/silverback_autosave.js @@ -84,7 +84,7 @@ Drupal.autosaveForm.form.submit(function () { if (Drupal.autosaveForm.autosaveFormRunning) { Drupal.autosaveForm.autosaveFormRunning = false; - clearInterval(Drupal.autosaveForm.timer); + clearTimeout(Drupal.autosaveForm.timer); Drupal.autosaveForm.timer = null; } }); @@ -100,7 +100,7 @@ !Drupal.autosaveForm.autosaveFormRunning && Drupal.autosaveForm.timer ) { - clearInterval(Drupal.autosaveForm.timer); + clearTimeout(Drupal.autosaveForm.timer); Drupal.autosaveForm.timer = null; } else { return; @@ -237,7 +237,7 @@ ) { if (xmlhttprequest.status === 0 || xmlhttprequest.status >= 400) { Drupal.autosaveForm.autosaveFormRunning = false; - clearInterval(Drupal.autosaveForm.timer); + clearTimeout(Drupal.autosaveForm.timer); Drupal.autosaveForm.timer = null; if (!Drupal.autosaveForm.beforeUnloadCalled) { @@ -307,16 +307,17 @@ ); }, 500); } - // TODO: Implement a mechanism that takes user interaction into account. - // E.g. autosave after the user did not interact for 2 seconds. - Drupal.autosaveForm.timer = setInterval(function () { - if (!Drupal.ajax.instances.some(isAjaxing)) { - triggerAjaxSubmitWithoutProgressIndication( - Drupal.autosaveForm.autosave_submit_class, - ); - } - }, Drupal.autosaveForm.interval); + $('body').on('click keyup', () => { + clearTimeout(Drupal.autosaveForm.timer); + Drupal.autosaveForm.timer = setTimeout(function () { + if (!Drupal.ajax.instances.some(isAjaxing)) { + triggerAjaxSubmitWithoutProgressIndication( + Drupal.autosaveForm.autosave_submit_class, + ); + } + }, Drupal.autosaveForm.interval); + }); } } From 7c5bcff38aa009934fbc0a3630f045d3e60d58eb Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Tue, 16 Apr 2024 13:02:49 +0200 Subject: [PATCH 37/82] fix: copy iframe.css on lagoon --- .lagoon/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/.lagoon/Dockerfile b/.lagoon/Dockerfile index 55590a84d..214c1fdbb 100644 --- a/.lagoon/Dockerfile +++ b/.lagoon/Dockerfile @@ -83,6 +83,7 @@ FROM uselagoon/nginx-drupal as nginx COPY --from=cli /app /app RUN cd /app/web && cp ../node_modules/@custom/ui/build/gutenberg.css . +RUN cd /app/web && cp ../node_modules/@custom/ui/build/iframe.css . COPY .lagoon/nginx-conf/redirects-map.conf /etc/nginx/redirects-map.conf WORKDIR /app From 0ecf9d5e9745a60da749eb96bfea4062079047ea Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Tue, 16 Apr 2024 17:16:59 +0200 Subject: [PATCH 38/82] fix: don't set drupal external url for publisher test builds --- apps/website/publisher.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/website/publisher.config.ts b/apps/website/publisher.config.ts index 6c5d95797..abaf98f01 100644 --- a/apps/website/publisher.config.ts +++ b/apps/website/publisher.config.ts @@ -16,7 +16,7 @@ export default defineConfig({ // cannot report it. // Workaround: Do a double build on the first build. 'if test -d public; then echo "Single build" && pnpm build:gatsby; else echo "Double build" && pnpm build:gatsby && pnpm build:gatsby; fi' - : 'DRUPAL_EXTERNAL_URL=http://127.0.0.1:8888 pnpm build:gatsby', + : 'pnpm build:gatsby', outputTimeout: 1000 * 60 * 10, }, clean: 'pnpm clean', From 0abbe64bf7b27c64ef8fd86e5fd37f5720f124c1 Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Wed, 17 Apr 2024 07:36:06 +0200 Subject: [PATCH 39/82] chore: prettier formatting --- apps/preview/index.html | 24 +++++++++++------------- apps/preview/server/index.ts | 4 +++- packages/schema/src/schema.graphql | 2 +- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/apps/preview/index.html b/apps/preview/index.html index 639d1df33..86c766277 100644 --- a/apps/preview/index.html +++ b/apps/preview/index.html @@ -1,16 +1,14 @@ + + + + Preview + + - - - - Preview - - - - -
- - - - \ No newline at end of file + +
+ + + diff --git a/apps/preview/server/index.ts b/apps/preview/server/index.ts index 7b17121d1..46bf8cfc9 100644 --- a/apps/preview/server/index.ts +++ b/apps/preview/server/index.ts @@ -10,7 +10,9 @@ const updates$ = new Subject(); app.get('/endpoint.js', (_, res) => { res.send( - `window.GRAPHQL_ENDPOINT = "${process.env.DRUPAL_URL || 'http://localhost:8888'}/graphql";`, + `window.GRAPHQL_ENDPOINT = "${ + process.env.DRUPAL_URL || 'http://localhost:8888' + }/graphql";`, ); }); diff --git a/packages/schema/src/schema.graphql b/packages/schema/src/schema.graphql index 289d4000b..d3c404ee7 100644 --- a/packages/schema/src/schema.graphql +++ b/packages/schema/src/schema.graphql @@ -199,7 +199,7 @@ type Hero { } union PageContent @resolveEditorBlockType = - BlockMarkup + | BlockMarkup | BlockMedia | BlockForm | BlockImageTeasers From d359edf42c534b541b9aad9155fab9abf593f626 Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Wed, 17 Apr 2024 07:56:05 +0200 Subject: [PATCH 40/82] chore: more prettier fixes --- packages/schema/src/schema.graphql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/schema/src/schema.graphql b/packages/schema/src/schema.graphql index 4bb75e6f6..ab0821e57 100644 --- a/packages/schema/src/schema.graphql +++ b/packages/schema/src/schema.graphql @@ -199,7 +199,7 @@ type Hero { } union PageContent @resolveEditorBlockType = - | BlockMarkup + BlockMarkup | BlockMedia | BlockForm | BlockImageTeasers From 49142d16830a5b779d4d276e3521d27b49c087f2 Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Wed, 17 Apr 2024 08:45:04 +0200 Subject: [PATCH 41/82] fix(SLB-146): preview url fallback --- .../src/Storage/AutosaveEntityFormDatabaseStorage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormDatabaseStorage.php b/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormDatabaseStorage.php index ae41e5c82..5334a408c 100644 --- a/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormDatabaseStorage.php +++ b/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormDatabaseStorage.php @@ -86,7 +86,7 @@ public function storeEntityAndFormState($form_id, $form_session_id, $entity_type ->execute(); // @todo Clean up this prototype. // @todo pass entity information so clients can filter if they have to update. - \Drupal::service('http_client')->post((getenv('PREVIEW_URL') ?? 'http://localhost:8001') . '/__preview'); + \Drupal::service('http_client')->post((getenv('PREVIEW_URL') ?: 'http://localhost:8001') . '/__preview'); } } From 22cf7dafd3f8d7969e5bd49d8deb87ffae9fba5a Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Wed, 17 Apr 2024 08:45:30 +0200 Subject: [PATCH 42/82] fix(SLB-209): reconnect websocket on close --- apps/preview/src/App.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/preview/src/App.tsx b/apps/preview/src/App.tsx index 3435448e5..981244d51 100644 --- a/apps/preview/src/App.tsx +++ b/apps/preview/src/App.tsx @@ -2,6 +2,7 @@ import { OperationExecutor } from '@custom/schema'; import { Frame } from '@custom/ui/routes/Frame'; import { Preview, usePreviewRefresh } from '@custom/ui/routes/Preview'; import { useEffect } from 'react'; +import { retry } from 'rxjs'; import { webSocket } from 'rxjs/webSocket'; import { drupalExecutor } from './drupal-executor'; @@ -14,7 +15,11 @@ declare global { const updates$ = webSocket({ url: `${window.location.origin.replace('http', 'ws')}/__preview`, -}); +}).pipe( + retry({ + delay: 3000, + }), +); function App() { const refresh = usePreviewRefresh(); From ede54635e938fd42295320d9cbac9c8ec334a59c Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Wed, 17 Apr 2024 09:31:22 +0200 Subject: [PATCH 43/82] fix(SLB-209): default preview host to loopback --- apps/preview/server/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/preview/server/index.ts b/apps/preview/server/index.ts index 46bf8cfc9..2f3c88d36 100644 --- a/apps/preview/server/index.ts +++ b/apps/preview/server/index.ts @@ -11,7 +11,7 @@ const updates$ = new Subject(); app.get('/endpoint.js', (_, res) => { res.send( `window.GRAPHQL_ENDPOINT = "${ - process.env.DRUPAL_URL || 'http://localhost:8888' + process.env.DRUPAL_URL || 'http://127.0.0.1:8888' }/graphql";`, ); }); From 085ea7e2ca5d1b243ecbc6e4a3b53f4be57556a0 Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Wed, 17 Apr 2024 09:31:50 +0200 Subject: [PATCH 44/82] test(SLB-209): re-enable preview integration test --- tests/e2e/specs/drupal/preview.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/specs/drupal/preview.spec.ts b/tests/e2e/specs/drupal/preview.spec.ts index 89ae402e8..78865a978 100644 --- a/tests/e2e/specs/drupal/preview.spec.ts +++ b/tests/e2e/specs/drupal/preview.spec.ts @@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test'; import { cmsUrl } from '../../helpers/url'; -test.describe.fixme('instant preview', () => { +test.describe('instant preview', () => { test.use({ storageState: '.auth/admin.json' }); test('display an unpublished page in an iframe', async ({ page }) => { await page.goto(cmsUrl('/admin/content')); From 5c531bbb5dbef658438f50c41bba61bdc2211840 Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Wed, 17 Apr 2024 14:47:35 +0200 Subject: [PATCH 45/82] feat(SLB-146): filter preview updates by node id --- apps/preview/server/index.ts | 3 +- .../silverback_autosave.services.yml | 7 +++- .../AutosaveEntityFormDatabaseStorage.php | 36 ++++++++++++++++--- packages/ui/src/components/Routes/Preview.tsx | 15 +++++++- 4 files changed, 54 insertions(+), 7 deletions(-) diff --git a/apps/preview/server/index.ts b/apps/preview/server/index.ts index 2f3c88d36..333df04a0 100644 --- a/apps/preview/server/index.ts +++ b/apps/preview/server/index.ts @@ -7,6 +7,7 @@ const expressWsInstance = expressWs(expressServer); const { app } = expressWsInstance; const updates$ = new Subject(); +app.use(express.json()); app.get('/endpoint.js', (_, res) => { res.send( @@ -18,7 +19,7 @@ app.get('/endpoint.js', (_, res) => { // TODO: Protect endpoints and preview with Drupal authentication. app.post('/__preview', (req, res) => { - updates$.next({ body: req.body }); + updates$.next(req.body || {}); res.json(true); }); diff --git a/packages/drupal/silverback_autosave/silverback_autosave.services.yml b/packages/drupal/silverback_autosave/silverback_autosave.services.yml index b8b45034d..f09b27af4 100644 --- a/packages/drupal/silverback_autosave/silverback_autosave.services.yml +++ b/packages/drupal/silverback_autosave/silverback_autosave.services.yml @@ -1,7 +1,12 @@ services: silverback_autosave.entity_form_storage: class: \Drupal\silverback_autosave\Storage\AutosaveEntityFormDatabaseStorage - arguments: ['@database', '@serialization.phpserialize'] + arguments: [ + '@database', + '@serialization.phpserialize', + '@http_client', + '@logger.channel.default', + ] form_validator.silverback_autosave: public: false diff --git a/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormDatabaseStorage.php b/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormDatabaseStorage.php index 5334a408c..d03b89365 100644 --- a/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormDatabaseStorage.php +++ b/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormDatabaseStorage.php @@ -9,6 +9,10 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Form\FormState; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Logger\LoggerChannelInterface; +use GuzzleHttp\ClientInterface; +use GuzzleHttp\Exception\GuzzleException; +use GuzzleHttp\RequestOptions; /** * A database backend for autosave of entity forms. @@ -29,6 +33,16 @@ class AutosaveEntityFormDatabaseStorage implements AutosaveEntityFormStorageInte */ protected $serializer; + /** + * @var \GuzzleHttp\ClientInterface + */ + protected $httpClient; + + /** + * @var \Drupal\Core\Logger\LoggerChannelInterface + */ + protected $logger; + /** * Constructs an AutosaveEntityStorage. * @@ -37,9 +51,11 @@ class AutosaveEntityFormDatabaseStorage implements AutosaveEntityFormStorageInte * @param \Drupal\Component\Serialization\SerializationInterface $serializer * The serializer to use. */ - public function __construct(Connection $connection, SerializationInterface $serializer) { + public function __construct(Connection $connection, SerializationInterface $serializer, ClientInterface $httpClient, LoggerChannelInterface $logger) { $this->connection = $connection; $this->serializer = $serializer; + $this->httpClient = $httpClient; + $this->logger = $logger; } /** @@ -84,9 +100,21 @@ public function storeEntityAndFormState($form_id, $form_session_id, $entity_type $serialized_form_state, ]) ->execute(); - // @todo Clean up this prototype. - // @todo pass entity information so clients can filter if they have to update. - \Drupal::service('http_client')->post((getenv('PREVIEW_URL') ?: 'http://localhost:8001') . '/__preview'); + try { + $this->httpClient->post((getenv('PREVIEW_URL') ?: 'http://localhost:8001') . '/__preview', [ + RequestOptions::HEADERS => [ + 'Content-Type' => 'application/json', + ], + RequestOptions::JSON => [ + 'entity_type_id' => $entity_type_id, + 'entity_id' => $entity_id, + 'langcode' => $langcode, + ], + ]); + } catch (GuzzleException $exc) { + $this->logger->critical('Error while to update preview.'); + $this->logger->critical($exc->getMessage()); + } } } diff --git a/packages/ui/src/components/Routes/Preview.tsx b/packages/ui/src/components/Routes/Preview.tsx index 3e6fc0e8f..61c6dcbb7 100644 --- a/packages/ui/src/components/Routes/Preview.tsx +++ b/packages/ui/src/components/Routes/Preview.tsx @@ -18,7 +18,20 @@ function usePreviewParameters(): OperationVariables< export function usePreviewRefresh() { const params = usePreviewParameters(); - return () => clear(PreviewDrupalPageQuery, params); + return (input: { + entity_type_id?: string; + entity_id?: string; + langcode?: string; + }) => { + if ( + // TODO: Extend for non-node entities? + input.entity_type_id === 'node' && + input.entity_id === params.id && + input.langcode === params.locale + ) { + clear(PreviewDrupalPageQuery, params); + } + }; } export function Preview() { From 6beb83e0e26452cbfc4adb5fb33840146e96b9a1 Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Wed, 17 Apr 2024 15:08:28 +0200 Subject: [PATCH 46/82] test(SLB-209): start preview server for drupal tests --- tests/e2e/playwright.config.drupal.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/e2e/playwright.config.drupal.ts b/tests/e2e/playwright.config.drupal.ts index 11295776f..5679e7450 100644 --- a/tests/e2e/playwright.config.drupal.ts +++ b/tests/e2e/playwright.config.drupal.ts @@ -22,6 +22,11 @@ export default defineConfig({ port: 8000, reuseExistingServer: !process.env.CI, }, + { + command: 'pnpm run --filter "@custom/preview" start', + port: 8001, + reuseExistingServer: !process.env.CI, + }, ], /* Run tests in files in parallel */ fullyParallel: true, From fc2b7e81b8e41db73bd2ab5b4d3a02f5ae41a8bc Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Wed, 17 Apr 2024 15:27:27 +0200 Subject: [PATCH 47/82] refactor(SLB-209): clean up gatsby preview files --- apps/website/gatsby-config.mjs | 7 +---- apps/website/src/pages/__preview/page.tsx | 11 ------- apps/website/src/preview/page.tsx | 37 ----------------------- apps/website/src/utils/preview.ts | 14 --------- 4 files changed, 1 insertion(+), 68 deletions(-) delete mode 100644 apps/website/src/pages/__preview/page.tsx delete mode 100644 apps/website/src/preview/page.tsx delete mode 100644 apps/website/src/utils/preview.ts diff --git a/apps/website/gatsby-config.mjs b/apps/website/gatsby-config.mjs index 74bc5a9fc..842076dff 100644 --- a/apps/website/gatsby-config.mjs +++ b/apps/website/gatsby-config.mjs @@ -42,17 +42,12 @@ const plugins = [ { resolve: 'gatsby-plugin-netlify', options: { - // To avoid "X-Frame-Options: DENY" and let it work in the preview - // iframe. + // To avoid "X-Frame-Options: DENY" in Drupal iframes. mergeSecurityHeaders: false, }, }, { - // TODO: Move preview to Drupal and remove this. resolve: 'gatsby-plugin-sitemap', - options: { - excludes: ['/__preview/**'], - }, }, { resolve: 'gatsby-plugin-robots-txt', diff --git a/apps/website/src/pages/__preview/page.tsx b/apps/website/src/pages/__preview/page.tsx deleted file mode 100644 index 9ca14376a..000000000 --- a/apps/website/src/pages/__preview/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; - -import PagePreview from '../../preview/page'; - -export function Head() { - return Page preview; -} - -export default function PagePreviewTemplate() { - return ; -} diff --git a/apps/website/src/preview/page.tsx b/apps/website/src/preview/page.tsx deleted file mode 100644 index dc2a1dac5..000000000 --- a/apps/website/src/preview/page.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { - OperationExecutor, - PreviewDrupalPageQuery, - ViewPageQuery, -} from '@custom/schema'; -import { Page } from '@custom/ui/routes/Page'; -import React from 'react'; - -import { drupalExecutor } from '../utils/drupal-executor'; -import { usePreviewParameters } from '../utils/preview'; - -const previewExecutor = drupalExecutor( - `${process.env.GATSBY_DRUPAL_URL}/graphql`, - false, -); - -export default function PagePreview() { - const { nid, rid, lang } = usePreviewParameters(); - if (nid && rid && lang) { - return ( - { - const data = await previewExecutor(PreviewDrupalPageQuery, { - id: nid, - locale: lang, - rid, - }); - return { page: data.preview }; - }} - > - - - ); - } - return null; -} diff --git a/apps/website/src/utils/preview.ts b/apps/website/src/utils/preview.ts deleted file mode 100644 index a6ee02d5d..000000000 --- a/apps/website/src/utils/preview.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { useEffect, useState } from 'react'; - -export function usePreviewParameters() { - const [search, setState] = useState(''); - useEffect(() => { - setState(window.location.search); - }, [setState]); - - const args = new URLSearchParams(search); - const nid = args.get('nid'); - const rid = args.get('rid'); - const lang = args.get('lang'); - return { nid, rid, lang }; -} From e9317a6279ccc06fc0f42071da9b171bc6cc87a5 Mon Sep 17 00:00:00 2001 From: Dan Lemon Date: Wed, 17 Apr 2024 18:25:49 +0200 Subject: [PATCH 48/82] fix: spelling gutenberg --- packages/drupal/silverback_autosave/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/drupal/silverback_autosave/README.md b/packages/drupal/silverback_autosave/README.md index 625f34115..73b5da62f 100644 --- a/packages/drupal/silverback_autosave/README.md +++ b/packages/drupal/silverback_autosave/README.md @@ -1,6 +1,6 @@ ## Silverback autosave -This module provides an autosave functionality on Guteneberg enabled nodes. +This module provides an autosave functionality on Gutenberg enabled nodes. On node edit, the content under edit is automatically saved for use in real time preview. From 6a0533f524eca00be640fa90cc0d60a5d2923922 Mon Sep 17 00:00:00 2001 From: Dan Lemon Date: Wed, 17 Apr 2024 18:26:06 +0200 Subject: [PATCH 49/82] fix: spelling losing --- packages/drupal/silverback_autosave/silverback_autosave.module | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/drupal/silverback_autosave/silverback_autosave.module b/packages/drupal/silverback_autosave/silverback_autosave.module index 1f1afe85b..7cc29d715 100644 --- a/packages/drupal/silverback_autosave/silverback_autosave.module +++ b/packages/drupal/silverback_autosave/silverback_autosave.module @@ -122,7 +122,7 @@ function silverback_autosave_entity_update(EntityInterface $entity) { * * If the permissions of a user are changed, then we delete the autosave states * belonging to that user. It is possible that new permissions the user is - * loosing the ability to access certain fields, but the autosave states contain + * losing the ability to access certain fields, but the autosave states contain * data for them. */ function silverback_autosave_user_update(EntityInterface $user) { From 5f076d80e8237831c163044f3d4f83678d538e00 Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Thu, 18 Apr 2024 09:08:41 +0200 Subject: [PATCH 50/82] feat(SLB-146): upgrade amazeelabs/silverback_gutenberg to avoid preview-button ambiguity --- apps/cms/composer.lock | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/apps/cms/composer.lock b/apps/cms/composer.lock index 35ca468ce..ef2a0aa18 100644 --- a/apps/cms/composer.lock +++ b/apps/cms/composer.lock @@ -495,16 +495,16 @@ }, { "name": "amazeelabs/silverback_gutenberg", - "version": "2.5.12", + "version": "2.5.15", "source": { "type": "git", "url": "https://github.com/AmazeeLabs/silverback_gutenberg.git", - "reference": "ccd955f455a6cd55ee8ce3063876e0d707b816a0" + "reference": "413248bb07ca02405bf84f01dfd55b86f121c97c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/AmazeeLabs/silverback_gutenberg/zipball/ccd955f455a6cd55ee8ce3063876e0d707b816a0", - "reference": "ccd955f455a6cd55ee8ce3063876e0d707b816a0", + "url": "https://api.github.com/repos/AmazeeLabs/silverback_gutenberg/zipball/413248bb07ca02405bf84f01dfd55b86f121c97c", + "reference": "413248bb07ca02405bf84f01dfd55b86f121c97c", "shasum": "" }, "require": { @@ -519,9 +519,9 @@ "homepage": "https://github.com/AmazeeLabs/silverback-mono/tree/development/packages/composer/amazeelabs/silverback_gutenberg#readme", "support": { "issues": "https://github.com/AmazeeLabs/silverback_gutenberg/issues", - "source": "https://github.com/AmazeeLabs/silverback_gutenberg/tree/2.5.12" + "source": "https://github.com/AmazeeLabs/silverback_gutenberg/tree/2.5.15" }, - "time": "2024-02-20T11:44:25+00:00" + "time": "2024-04-18T06:58:24+00:00" }, { "name": "amazeelabs/silverback_iframe", @@ -15789,5 +15789,8 @@ "php": "^8.2 <8.3" }, "platform-dev": [], + "platform-overrides": { + "php": "8.2" + }, "plugin-api-version": "2.6.0" } From 66e05c49025622979b402720afdd3a1fec581d04 Mon Sep 17 00:00:00 2001 From: Dimitris Spachos Date: Mon, 22 Apr 2024 08:54:11 +0300 Subject: [PATCH 51/82] feat(slb-146): add missing patch extension --- apps/cms/.idea/.gitignore | 8 + apps/cms/.idea/cms.iml | 273 ++++++ apps/cms/.idea/codeStyles/Project.xml | 140 ++++ apps/cms/.idea/codeStyles/codeStyleConfig.xml | 5 + .../Drush_4_12_24__2_49_PM.xml | 784 ++++++++++++++++++ .../frameworkDescriptionVersion1.1.4.xsd | 47 ++ .../inspectionProfiles/Project_Default.xml | 6 + apps/cms/.idea/modules.xml | 8 + apps/cms/.idea/php.xml | 264 ++++++ apps/cms/.idea/vcs.xml | 6 + apps/cms/composer.json | 2 +- ...{fetch-entity-patch => fetch-entity.patch} | 0 12 files changed, 1542 insertions(+), 1 deletion(-) create mode 100644 apps/cms/.idea/.gitignore create mode 100644 apps/cms/.idea/cms.iml create mode 100644 apps/cms/.idea/codeStyles/Project.xml create mode 100644 apps/cms/.idea/codeStyles/codeStyleConfig.xml create mode 100644 apps/cms/.idea/commandlinetools/Drush_4_12_24__2_49_PM.xml create mode 100644 apps/cms/.idea/commandlinetools/schemas/frameworkDescriptionVersion1.1.4.xsd create mode 100644 apps/cms/.idea/inspectionProfiles/Project_Default.xml create mode 100644 apps/cms/.idea/modules.xml create mode 100644 apps/cms/.idea/php.xml create mode 100644 apps/cms/.idea/vcs.xml rename apps/cms/patches/{fetch-entity-patch => fetch-entity.patch} (100%) diff --git a/apps/cms/.idea/.gitignore b/apps/cms/.idea/.gitignore new file mode 100644 index 000000000..13566b81b --- /dev/null +++ b/apps/cms/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/apps/cms/.idea/cms.iml b/apps/cms/.idea/cms.iml new file mode 100644 index 000000000..41df93773 --- /dev/null +++ b/apps/cms/.idea/cms.iml @@ -0,0 +1,273 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/cms/.idea/codeStyles/Project.xml b/apps/cms/.idea/codeStyles/Project.xml new file mode 100644 index 000000000..a1863d2ed --- /dev/null +++ b/apps/cms/.idea/codeStyles/Project.xml @@ -0,0 +1,140 @@ + + + + + \ No newline at end of file diff --git a/apps/cms/.idea/codeStyles/codeStyleConfig.xml b/apps/cms/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 000000000..79ee123c2 --- /dev/null +++ b/apps/cms/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/apps/cms/.idea/commandlinetools/Drush_4_12_24__2_49_PM.xml b/apps/cms/.idea/commandlinetools/Drush_4_12_24__2_49_PM.xml new file mode 100644 index 000000000..0f81233af --- /dev/null +++ b/apps/cms/.idea/commandlinetools/Drush_4_12_24__2_49_PM.xml @@ -0,0 +1,784 @@ + + + + _complete + Internal command to provide shell completion suggestions

No arguments
The shell type ("bash", "fish", "zsh")An array of input tokens (e.g. COMP_WORDS or argv)The index of the "input" array that the cursor is in (e.g. COMP_CWORD)The API version of the completion scriptdeprecatedDisplay help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
shell
input
current
api-version
symfony
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + browse + Display a link to a given path or open link in a browser.
Path to open. If omitted, the site front page will be opened.
Arguments:
path

Open the URL in the default browser. Use --no-browser to avoid opening a browser.The port that the web server is redirected to (e.g. when running within a Vagrant environment).Negate --browser option.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
browser
redirect-port
no-browser
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ path[=null] +
+ + completion + Dump the shell completion script
The shell type (e.g. "bash"), the value of the "$SHELL" env var will be used if this is not given
Arguments:
shell

Tail the completion debug logDisplay help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
debug
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ shell[=null] +
+ + deploy + Run several commands after performing a code deployment.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + help + Display usage details for a command.
A command name
Arguments:
command_name

Format the result data. Available formats: csv,json,list,null,php,print-r,string,tsv,var_dump,var_export,xml,yamlDisplay help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
format
include-field-labels
table-style
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ command_name[=null] +
+ + list + List available commands.

No arguments
Show a simple table of command names and descriptions.Restrict command list to those commands defined in the specified file. Omit value to choose from a list of names.Notify upon command completion. If set to a number, commands that finish in fewer seconds won't notify.Shows the druplicon as glorious ASCII art.URL to your XHProf report site.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
format
raw
filter
notify
druplicon
xh-link
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + runserver + Runs PHP's built-in http server for development.
IP address and port number to bind to and path to open in web browser. Format is addr:port/path. Only opens a browser if a path is specified.
Arguments:
uri

A default addr:port/path to use for any values not specified as an argument.Open the URL in the default browser. Use --no-browser to avoid opening a browser.Resolve hostnames/IPs using DNS/rDNS (if possible) to determine binding IPs and/or human friendly hostnames for URLs and browser.Negate --browser option.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
default-server
browser
dns
no-browser
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ uri[=null] +
+ + rs + Runs PHP's built-in http server for development.
IP address and port number to bind to and path to open in web browser. Format is addr:port/path. Only opens a browser if a path is specified.
Arguments:
uri

A default addr:port/path to use for any values not specified as an argument.Open the URL in the default browser. Use --no-browser to avoid opening a browser.Resolve hostnames/IPs using DNS/rDNS (if possible) to determine binding IPs and/or human friendly hostnames for URLs and browser.Negate --browser option.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
default-server
browser
dns
no-browser
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ uri[=null] +
+ + serve + Runs PHP's built-in http server for development.
IP address and port number to bind to and path to open in web browser. Format is addr:port/path. Only opens a browser if a path is specified.
Arguments:
uri

A default addr:port/path to use for any values not specified as an argument.Open the URL in the default browser. Use --no-browser to avoid opening a browser.Resolve hostnames/IPs using DNS/rDNS (if possible) to determine binding IPs and/or human friendly hostnames for URLs and browser.Negate --browser option.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
default-server
browser
dns
no-browser
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ uri[=null] +
+ + updatedb + Apply any database updates required (as with running update.php).

No arguments
Clear caches upon completion.Negate --cache-clear option.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
cache-clear
no-cache-clear
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + updb + Apply any database updates required (as with running update.php).

No arguments
Clear caches upon completion.Negate --cache-clear option.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
cache-clear
no-cache-clear
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + version + Show Drush version.

No arguments
Format the result data. Available formats: csv,json,list,null,php,print-r,string,table,tsv,var_dump,var_export,xml,yamlAvailable fields: Drush version (drush-version)Select just one field, and force format to *string*.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
format
fields
field
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + archive:dump + Backup your code, files, and database into a single file.

No arguments
Archive codebase.Archive Drupal files.Archive database SQL dump.The full path and filename in which the archive should be stored. Any relative path will be calculated from Drupal root (usually web for drupal/recommended-project projects). If omitted, it will be saved to the configured temp directory.Overwrite destination file if exists.Describe the archive contents.Add tags to the archive manifest. Delimit several by commas.The generator name to store in the MANIFEST.yml file. The default is "Drush archive-dump".The generator version number to store in the MANIFEST file. The default is Drush version.Comma-separated list of paths (or regular expressions matching paths) to exclude from the code archive.Add custom arguments/options to the dumping of the database (e.g. mysqldump command).A key in the $skip_tables array. @see [Site aliases](../site-aliases.md)A key in the $structure_tables array. @see [Site aliases](../site-aliases.md)A key in the $tables array.A comma-separated list of tables to exclude completely.A comma-separated list of tables to include for structure, but not data.A comma-separated list of tables to transfer.The DB connection key if using multiple connections in settings.php.A Drupal 6 style database URL. For example mysql://root:pass@localhost:port/dbnameThe name of a target within the specified database connection.Show password on the CLI. Useful for debugging.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
code
files
db
destination
overwrite
description
tags
generator
generatorversion
exclude-code-paths
extra-dump
skip-tables-key
structure-tables-key
tables-key
skip-tables-list
structure-tables-list
tables-list
database
db-url
target
show-passwords
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + ard + Backup your code, files, and database into a single file.

No arguments
Archive codebase.Archive Drupal files.Archive database SQL dump.The full path and filename in which the archive should be stored. Any relative path will be calculated from Drupal root (usually web for drupal/recommended-project projects). If omitted, it will be saved to the configured temp directory.Overwrite destination file if exists.Describe the archive contents.Add tags to the archive manifest. Delimit several by commas.The generator name to store in the MANIFEST.yml file. The default is "Drush archive-dump".The generator version number to store in the MANIFEST file. The default is Drush version.Comma-separated list of paths (or regular expressions matching paths) to exclude from the code archive.Add custom arguments/options to the dumping of the database (e.g. mysqldump command).A key in the $skip_tables array. @see [Site aliases](../site-aliases.md)A key in the $structure_tables array. @see [Site aliases](../site-aliases.md)A key in the $tables array.A comma-separated list of tables to exclude completely.A comma-separated list of tables to include for structure, but not data.A comma-separated list of tables to transfer.The DB connection key if using multiple connections in settings.php.A Drupal 6 style database URL. For example mysql://root:pass@localhost:port/dbnameThe name of a target within the specified database connection.Show password on the CLI. Useful for debugging.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
code
files
db
destination
overwrite
description
tags
generator
generatorversion
exclude-code-paths
extra-dump
skip-tables-key
structure-tables-key
tables-key
skip-tables-list
structure-tables-list
tables-list
database
db-url
target
show-passwords
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + archive:restore + Restore (import) your code, files, and database.
The full path to a single archive file (*.tar.gz) or a directory with components to import. * May contain the following components generated by `archive:dump` command: * 1) code ("code" directory); * 2) database dump file ("database/database.sql" file); * 3) Drupal files ("files" directory).Destination site alias. Defaults to @self.
Arguments:
path
site

The base path to restore the code/files into.Overwrite files if exists when un-compressing an archive.Site subdirectory to put settings.local.php into.Sets up the database connection in settings.local.php file if either --db-url option or set of specific --db-* options are provided.Import code.Import code from specified directory. Has higher priority over "path" argument.Import Drupal files.Import Drupal files from specified directory. Has higher priority over "path" argument.Import Drupal files into specified directory relative to Composer root.Import database.Import database from specified dump file. Has higher priority over "path" argument.Destination database driver.Destination database port.Destination database host.Destination database name.Destination database user.Destination database user password.Destination database prefix.A key in the $skip_tables array. @see [Site aliases](../site-aliases.md)A key in the $structure_tables array. @see [Site aliases](../site-aliases.md)A key in the $tables array.A comma-separated list of tables to exclude completely.A comma-separated list of tables to include for structure, but not data.A comma-separated list of tables to transfer.The DB connection key if using multiple connections in settings.php.A Drupal 6 style database URL. For example mysql://root:pass@localhost:port/dbnameThe name of a target within the specified database connection.Show password on the CLI. Useful for debugging.Negate --setup-database-connection option.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
destination-path
overwrite
site-subdir
setup-database-connection
code
code-source-path
files
files-source-path
files-destination-relative-path
db
db-source-path
db-driver
db-port
db-host
db-name
db-user
db-password
db-prefix
skip-tables-key
structure-tables-key
tables-key
skip-tables-list
structure-tables-list
tables-list
database
db-url
target
show-passwords
no-setup-database-connection
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ path[=null] site[=null] +
+ + arr + Restore (import) your code, files, and database.
The full path to a single archive file (*.tar.gz) or a directory with components to import. * May contain the following components generated by `archive:dump` command: * 1) code ("code" directory); * 2) database dump file ("database/database.sql" file); * 3) Drupal files ("files" directory).Destination site alias. Defaults to @self.
Arguments:
path
site

The base path to restore the code/files into.Overwrite files if exists when un-compressing an archive.Site subdirectory to put settings.local.php into.Sets up the database connection in settings.local.php file if either --db-url option or set of specific --db-* options are provided.Import code.Import code from specified directory. Has higher priority over "path" argument.Import Drupal files.Import Drupal files from specified directory. Has higher priority over "path" argument.Import Drupal files into specified directory relative to Composer root.Import database.Import database from specified dump file. Has higher priority over "path" argument.Destination database driver.Destination database port.Destination database host.Destination database name.Destination database user.Destination database user password.Destination database prefix.A key in the $skip_tables array. @see [Site aliases](../site-aliases.md)A key in the $structure_tables array. @see [Site aliases](../site-aliases.md)A key in the $tables array.A comma-separated list of tables to exclude completely.A comma-separated list of tables to include for structure, but not data.A comma-separated list of tables to transfer.The DB connection key if using multiple connections in settings.php.A Drupal 6 style database URL. For example mysql://root:pass@localhost:port/dbnameThe name of a target within the specified database connection.Show password on the CLI. Useful for debugging.Negate --setup-database-connection option.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
destination-path
overwrite
site-subdir
setup-database-connection
code
code-source-path
files
files-source-path
files-destination-relative-path
db
db-source-path
db-driver
db-port
db-host
db-name
db-user
db-password
db-prefix
skip-tables-key
structure-tables-key
tables-key
skip-tables-list
structure-tables-list
tables-list
database
db-url
target
show-passwords
no-setup-database-connection
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ path[=null] site[=null] +
+ + batch:process + Process operations in the specified batch set.
The batch id that will be processed.
Arguments:
batch_id

Format the result data. Available formats: csv,json,list,null,php,print-r,tsv,var_dump,var_export,xml,yamlLimit output to only the listed elements. Name top-level elements by key, e.g. "--fields=name,date", or use dot notation to select a nested element, e.g. "--fields=a.b.c as example".Select just one field, and force format to *string*.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
format
fields
field
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ batch_id +
+ + batch-process + Process operations in the specified batch set.
The batch id that will be processed.
Arguments:
batch_id

Format the result data. Available formats: csv,json,list,null,php,print-r,tsv,var_dump,var_export,xml,yamlLimit output to only the listed elements. Name top-level elements by key, e.g. "--fields=name,date", or use dot notation to select a nested element, e.g. "--fields=a.b.c as example".Select just one field, and force format to *string*.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
format
fields
field
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ batch_id +
+ + cache:rebuild + Rebuild all caches.

No arguments
Set to 0 to suppress normal cache clearing; the caller should then clear if needed.Negate --cache-clear option.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
cache-clear
no-cache-clear
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + cr + Rebuild all caches.

No arguments
Set to 0 to suppress normal cache clearing; the caller should then clear if needed.Negate --cache-clear option.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
cache-clear
no-cache-clear
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + rebuild + Rebuild all caches.

No arguments
Set to 0 to suppress normal cache clearing; the caller should then clear if needed.Negate --cache-clear option.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
cache-clear
no-cache-clear
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + cache-rebuild + Rebuild all caches.

No arguments
Set to 0 to suppress normal cache clearing; the caller should then clear if needed.Negate --cache-clear option.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
cache-clear
no-cache-clear
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + config:pull + Export and transfer config from one environment to another.
A site-alias or the name of a subdirectory within /sites whose config you want to copy from.A site-alias or the name of a subdirectory within /sites whose config you want to replace.
Arguments:
source
destination

Validate that there are no git uncommitted changes before proceedingWhere to run the rsync command; defaults to the local site. Can also be source or destination.Format the result data. Available formats: csv,json,list,null,php,print-r,string,table,tsv,var_dump,var_export,xml,yamlAvailable fields: Path (path)Select just one field, and force format to *string*.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
safe
runner
format
fields
field
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ source destination +
+ + cpull + Export and transfer config from one environment to another.
A site-alias or the name of a subdirectory within /sites whose config you want to copy from.A site-alias or the name of a subdirectory within /sites whose config you want to replace.
Arguments:
source
destination

Validate that there are no git uncommitted changes before proceedingWhere to run the rsync command; defaults to the local site. Can also be source or destination.Format the result data. Available formats: csv,json,list,null,php,print-r,string,table,tsv,var_dump,var_export,xml,yamlAvailable fields: Path (path)Select just one field, and force format to *string*.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
safe
runner
format
fields
field
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ source destination +
+ + config-pull + Export and transfer config from one environment to another.
A site-alias or the name of a subdirectory within /sites whose config you want to copy from.A site-alias or the name of a subdirectory within /sites whose config you want to replace.
Arguments:
source
destination

Validate that there are no git uncommitted changes before proceedingWhere to run the rsync command; defaults to the local site. Can also be source or destination.Format the result data. Available formats: csv,json,list,null,php,print-r,string,table,tsv,var_dump,var_export,xml,yamlAvailable fields: Path (path)Select just one field, and force format to *string*.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
safe
runner
format
fields
field
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ source destination +
+ + core:edit + Edit drush.yml, site alias, and Drupal settings.php files.
A substring for filtering the list of files. Omit this argument to choose from loaded files.
Arguments:
filter

A string of bash which launches user's preferred text editor. Defaults to ${VISUAL-${EDITOR-vi}}.Launch editor in background process.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
editor
bg
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ filter[=null] +
+ + conf + Edit drush.yml, site alias, and Drupal settings.php files.
A substring for filtering the list of files. Omit this argument to choose from loaded files.
Arguments:
filter

A string of bash which launches user's preferred text editor. Defaults to ${VISUAL-${EDITOR-vi}}.Launch editor in background process.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
editor
bg
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ filter[=null] +
+ + config + Edit drush.yml, site alias, and Drupal settings.php files.
A substring for filtering the list of files. Omit this argument to choose from loaded files.
Arguments:
filter

A string of bash which launches user's preferred text editor. Defaults to ${VISUAL-${EDITOR-vi}}.Launch editor in background process.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
editor
bg
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ filter[=null] +
+ + core-edit + Edit drush.yml, site alias, and Drupal settings.php files.
A substring for filtering the list of files. Omit this argument to choose from loaded files.
Arguments:
filter

A string of bash which launches user's preferred text editor. Defaults to ${VISUAL-${EDITOR-vi}}.Launch editor in background process.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
editor
bg
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ filter[=null] +
+ + core:global-options + All global options.

No arguments
Format the result data. Available formats: csv,json,list,null,php,print-r,sections,string,table,tsv,var_dump,var_export,xml,yamlAvailable fields: Name (name), Description (description)Select just one field, and force format to *string*.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
format
fields
field
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + core-global-options + All global options.

No arguments
Format the result data. Available formats: csv,json,list,null,php,print-r,sections,string,table,tsv,var_dump,var_export,xml,yamlAvailable fields: Name (name), Description (description)Select just one field, and force format to *string*.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
format
fields
field
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + core:rsync + Rsync Drupal code or files to/from another server using ssh.
A site alias and optional path. See rsync documentation and [Site aliases](../site-aliases.md).A site alias and optional path. See rsync documentation and [Site aliases](../site-aliases.md).Additional parameters after the ssh statement.
Arguments:
source
target
extra

List of paths to exclude, seperated by : (Unix-based systems) or ; (Windows).List of paths to include, seperated by : (Unix-based systems) or ; (Windows).The unary flags to pass to rsync; --mode=rultz implies rsync -rultz. Default is -akz.A string appended to ssh command during rsync, sql-sync, etc.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
exclude-paths
include-paths
mode
ssh-options
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ source target extra[=null] +
+ + rsync + Rsync Drupal code or files to/from another server using ssh.
A site alias and optional path. See rsync documentation and [Site aliases](../site-aliases.md).A site alias and optional path. See rsync documentation and [Site aliases](../site-aliases.md).Additional parameters after the ssh statement.
Arguments:
source
target
extra

List of paths to exclude, seperated by : (Unix-based systems) or ; (Windows).List of paths to include, seperated by : (Unix-based systems) or ; (Windows).The unary flags to pass to rsync; --mode=rultz implies rsync -rultz. Default is -akz.A string appended to ssh command during rsync, sql-sync, etc.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
exclude-paths
include-paths
mode
ssh-options
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ source target extra[=null] +
+ + core-rsync + Rsync Drupal code or files to/from another server using ssh.
A site alias and optional path. See rsync documentation and [Site aliases](../site-aliases.md).A site alias and optional path. See rsync documentation and [Site aliases](../site-aliases.md).Additional parameters after the ssh statement.
Arguments:
source
target
extra

List of paths to exclude, seperated by : (Unix-based systems) or ; (Windows).List of paths to include, seperated by : (Unix-based systems) or ; (Windows).The unary flags to pass to rsync; --mode=rultz implies rsync -rultz. Default is -akz.A string appended to ssh command during rsync, sql-sync, etc.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
exclude-paths
include-paths
mode
ssh-options
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ source target extra[=null] +
+ + core:status + An overview of the environment - Drush and Drupal.

No arguments
A comma delimited list of projects. Their paths will be added to path-aliases section.Format the result data. Available formats: csv,json,list,null,php,print-r,string,table,tsv,var_dump,var_export,xml,yamlAvailable fields: Drupal version (drupal-version), Site URI (uri), DB driver (db-driver), DB hostname (db-hostname), DB port (db-port), DB username (db-username), DB password (db-password), DB name (db-name), Database (db-status), Drupal bootstrap (bootstrap), Default theme (theme), Admin theme (admin-theme), PHP binary (php-bin), PHP config (php-conf), PHP OS (php-os), PHP version (php-version), Drush script (drush-script), Drush version (drush-version), Drush temp (drush-temp), Drush configs (drush-conf), Drush aliases (drush-alias-files), Alias search paths (alias-searchpaths), Install profile (install-profile), Drupal root (root), Drupal Settings (drupal-settings-file), Site path (site-path), Site path (site), Themes path (themes), Modules path (modules), Files, Public (files), Files, Private (private), Files, Temp (temp), Drupal config (config-sync), Files, Public (files-path), Files, Temp (temp-path), Other paths (%paths)Select just one field, and force format to *string*.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
project
format
fields
field
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + status + An overview of the environment - Drush and Drupal.

No arguments
A comma delimited list of projects. Their paths will be added to path-aliases section.Format the result data. Available formats: csv,json,list,null,php,print-r,string,table,tsv,var_dump,var_export,xml,yamlAvailable fields: Drupal version (drupal-version), Site URI (uri), DB driver (db-driver), DB hostname (db-hostname), DB port (db-port), DB username (db-username), DB password (db-password), DB name (db-name), Database (db-status), Drupal bootstrap (bootstrap), Default theme (theme), Admin theme (admin-theme), PHP binary (php-bin), PHP config (php-conf), PHP OS (php-os), PHP version (php-version), Drush script (drush-script), Drush version (drush-version), Drush temp (drush-temp), Drush configs (drush-conf), Drush aliases (drush-alias-files), Alias search paths (alias-searchpaths), Install profile (install-profile), Drupal root (root), Drupal Settings (drupal-settings-file), Site path (site-path), Site path (site), Themes path (themes), Modules path (modules), Files, Public (files), Files, Private (private), Files, Temp (temp), Drupal config (config-sync), Files, Public (files-path), Files, Temp (temp-path), Other paths (%paths)Select just one field, and force format to *string*.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
project
format
fields
field
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + st + An overview of the environment - Drush and Drupal.

No arguments
A comma delimited list of projects. Their paths will be added to path-aliases section.Format the result data. Available formats: csv,json,list,null,php,print-r,string,table,tsv,var_dump,var_export,xml,yamlAvailable fields: Drupal version (drupal-version), Site URI (uri), DB driver (db-driver), DB hostname (db-hostname), DB port (db-port), DB username (db-username), DB password (db-password), DB name (db-name), Database (db-status), Drupal bootstrap (bootstrap), Default theme (theme), Admin theme (admin-theme), PHP binary (php-bin), PHP config (php-conf), PHP OS (php-os), PHP version (php-version), Drush script (drush-script), Drush version (drush-version), Drush temp (drush-temp), Drush configs (drush-conf), Drush aliases (drush-alias-files), Alias search paths (alias-searchpaths), Install profile (install-profile), Drupal root (root), Drupal Settings (drupal-settings-file), Site path (site-path), Site path (site), Themes path (themes), Modules path (modules), Files, Public (files), Files, Private (private), Files, Temp (temp), Drupal config (config-sync), Files, Public (files-path), Files, Temp (temp-path), Other paths (%paths)Select just one field, and force format to *string*.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
project
format
fields
field
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + core-status + An overview of the environment - Drush and Drupal.

No arguments
A comma delimited list of projects. Their paths will be added to path-aliases section.Format the result data. Available formats: csv,json,list,null,php,print-r,string,table,tsv,var_dump,var_export,xml,yamlAvailable fields: Drupal version (drupal-version), Site URI (uri), DB driver (db-driver), DB hostname (db-hostname), DB port (db-port), DB username (db-username), DB password (db-password), DB name (db-name), Database (db-status), Drupal bootstrap (bootstrap), Default theme (theme), Admin theme (admin-theme), PHP binary (php-bin), PHP config (php-conf), PHP OS (php-os), PHP version (php-version), Drush script (drush-script), Drush version (drush-version), Drush temp (drush-temp), Drush configs (drush-conf), Drush aliases (drush-alias-files), Alias search paths (alias-searchpaths), Install profile (install-profile), Drupal root (root), Drupal Settings (drupal-settings-file), Site path (site-path), Site path (site), Themes path (themes), Modules path (modules), Files, Public (files), Files, Private (private), Files, Temp (temp), Drupal config (config-sync), Files, Public (files-path), Files, Temp (temp-path), Other paths (%paths)Select just one field, and force format to *string*.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
project
format
fields
field
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + core:topic + Read detailed documentation on a given topic.
The name of the topic you wish to view. If omitted, list all topic descriptions (and names in parenthesis).
Arguments:
topic_name

Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ topic_name +
+ + topic + Read detailed documentation on a given topic.
The name of the topic you wish to view. If omitted, list all topic descriptions (and names in parenthesis).
Arguments:
topic_name

Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ topic_name +
+ + core-topic + Read detailed documentation on a given topic.
The name of the topic you wish to view. If omitted, list all topic descriptions (and names in parenthesis).
Arguments:
topic_name

Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ topic_name +
+ + deploy:batch-process + Process operations in the specified batch set.
The batch id that will be processed.
Arguments:
batch_id

Format the result data. Available formats: csv,json,list,null,php,print-r,tsv,var_dump,var_export,xml,yamlLimit output to only the listed elements. Name top-level elements by key, e.g. "--fields=name,date", or use dot notation to select a nested element, e.g. "--fields=a.b.c as example".Select just one field, and force format to *string*.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
format
fields
field
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ batch_id +
+ + deploy:hook + Run pending deploy update hooks.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + deploy:hook-status + Prints information about pending deploy update hooks.

No arguments
Format the result data. Available formats: csv,json,list,null,php,print-r,sections,string,table,tsv,var_dump,var_export,xml,yamlAvailable fields: Module (module), Hook (hook), Description (description)Select just one field, and force format to *string*.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
format
fields
field
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + deploy:mark-complete + Mark all deploy hooks as having run.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + docs:aliases + Creating site aliases for running Drush on remote sites.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + docs-aliases + Creating site aliases for running Drush on remote sites.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + docs:bisect + Drush docs for Git Bisect.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + docs-bisect + Drush docs for Git Bisect.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + docs:bootstrap + Bootstrap explanation: how Drush starts up and prepares the Drupal environment.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + docs-bootstrap + Bootstrap explanation: how Drush starts up and prepares the Drupal environment.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + docs:commands + Instructions on creating your own Drush commands.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + docs-commands + Instructions on creating your own Drush commands.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + docs:config:exporting + Drupal config export instructions, including customizing config by environment.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + docs-config-exporting + Drupal config export instructions, including customizing config by environment.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + docs:configuration + Drush configuration.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + docs-configuration + Drush configuration.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + docs:cron + Crontab instructions for running your Drupal cron tasks via `drush cron`.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + docs-cron + Crontab instructions for running your Drupal cron tasks via `drush cron`.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + docs:deploy + Deploy command for Drupal.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + docs-deploy + Deploy command for Drupal.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + docs:example-sync-via-http + Extend sql-sync to allow transfer of the sql dump file via http.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + docs-example-sync-via-http + Extend sql-sync to allow transfer of the sql dump file via http.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + docs:examplecommand + Example Drush command file.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + docs-examplecommand + Example Drush command file.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + docs:generators + Instructions on creating your own Drush Generators.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + docs-generators + Instructions on creating your own Drush Generators.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + docs:hooks + Drush hooks.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + docs-hooks + Drush hooks.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + docs:migrate + Defining and running migrations.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + docs:output-formats-filters + Output formatters and filters: control the command output

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + docs:output + Output formatters and filters: control the command output

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + docs:policy + Example policy file.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + docs-policy + Example policy file.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + docs:readme + README.md

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + docs-readme + README.md

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + docs:script + An example Drush script.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + docs-script + An example Drush script.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + drupal:directory + Return the filesystem path for modules/themes and other key folders.
A module/theme name, or special names like root, files, private, or an alias:path string such as @alias:%files.
Arguments:
target

Reject any target that specifies a remote site.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
local-only
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ target[=null] +
+ + dd + Return the filesystem path for modules/themes and other key folders.
A module/theme name, or special names like root, files, private, or an alias:path string such as @alias:%files.
Arguments:
target

Reject any target that specifies a remote site.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
local-only
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ target[=null] +
+ + drupal-directory + Return the filesystem path for modules/themes and other key folders.
A module/theme name, or special names like root, files, private, or an alias:path string such as @alias:%files.
Arguments:
target

Reject any target that specifies a remote site.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
local-only
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ target[=null] +
+ + example:table + Demonstrate output formatters. Default format is 'table'.

No arguments
Format the result data. Available formats: csv,json,list,null,php,print-r,sections,string,table,tsv,var_dump,var_export,xml,yamlAvailable fields: I (first), II (second), III (third)Select just one field, and force format to *string*.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
format
fields
field
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + tf + Demonstrate output formatters. Default format is 'table'.

No arguments
Format the result data. Available formats: csv,json,list,null,php,print-r,sections,string,table,tsv,var_dump,var_export,xml,yamlAvailable fields: I (first), II (second), III (third)Select just one field, and force format to *string*.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
format
fields
field
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + image:derive + Create an image derivative.
An image style machine name.Path to a source image. Optionally prepend stream wrapper scheme. Relative paths calculated from Drupal root.
Arguments:
style_name
source

Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ style_name source +
+ + id + Create an image derivative.
An image style machine name.Path to a source image. Optionally prepend stream wrapper scheme. Relative paths calculated from Drupal root.
Arguments:
style_name
source

Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ style_name source +
+ + image-derive + Create an image derivative.
An image style machine name.Path to a source image. Optionally prepend stream wrapper scheme. Relative paths calculated from Drupal root.
Arguments:
style_name
source

Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ style_name source +
+ + image:flush + Flush all derived images for a given style.
A comma delimited list of image style machine names. If not provided, user may choose from a list of names.
Arguments:
style_names

Flush all derived imagesDisplay help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
all
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ style_names +
+ + if + Flush all derived images for a given style.
A comma delimited list of image style machine names. If not provided, user may choose from a list of names.
Arguments:
style_names

Flush all derived imagesDisplay help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
all
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ style_names +
+ + image-flush + Flush all derived images for a given style.
A comma delimited list of image style machine names. If not provided, user may choose from a list of names.
Arguments:
style_names

Flush all derived imagesDisplay help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
all
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ style_names +
+ + jn:get + Execute a JSONAPI request.
The JSONAPI URL to request.
Arguments:
url

Format the result data. Available formats: csv,json,list,null,php,print-r,tsv,var_dump,var_export,xml,yamlLimit output to only the listed elements. Name top-level elements by key, e.g. "--fields=name,date", or use dot notation to select a nested element, e.g. "--fields=a.b.c as example".Select just one field, and force format to *string*.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
format
fields
field
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ url +
+ + lagoon:aliases + Get all remote aliases from lagoon API.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + la + Get all remote aliases from lagoon API.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + lagoon:jwt + Generate a JWT token for the lagoon API.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + jwt + Generate a JWT token for the lagoon API.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + lagoon:post-rollout-tasks + Run post-rollout tasks.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + lagoon:pre-rollout-tasks + Run pre-rollout tasks.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + php:eval + Evaluate arbitrary php code after bootstrapping Drupal (if available).
PHP code. If shell escaping gets too tedious, consider using the php:script command.
Arguments:
code

Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
format
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ code +
+ + eval + Evaluate arbitrary php code after bootstrapping Drupal (if available).
PHP code. If shell escaping gets too tedious, consider using the php:script command.
Arguments:
code

Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
format
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ code +
+ + ev + Evaluate arbitrary php code after bootstrapping Drupal (if available).
PHP code. If shell escaping gets too tedious, consider using the php:script command.
Arguments:
code

Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
format
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ code +
+ + php-eval + Evaluate arbitrary php code after bootstrapping Drupal (if available).
PHP code. If shell escaping gets too tedious, consider using the php:script command.
Arguments:
code

Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
format
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ code +
+ + php:script + Run php a script after a full Drupal bootstrap.
Arguments:
extra

Additional paths to search for scripts, separated by : (Unix-based systems) or ; (Windows).Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
format
script-path
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ extra[=null] +
+ + scr + Run php a script after a full Drupal bootstrap.
Arguments:
extra

Additional paths to search for scripts, separated by : (Unix-based systems) or ; (Windows).Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
format
script-path
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ extra[=null] +
+ + php-script + Run php a script after a full Drupal bootstrap.
Arguments:
extra

Additional paths to search for scripts, separated by : (Unix-based systems) or ; (Windows).Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
format
script-path
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ extra[=null] +
+ + pm:security + pm:security has been removed. Please use `composer audit` command. See https://www.drupal.org/project/project_composer/issues/3301876.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + sec + pm:security has been removed. Please use `composer audit` command. See https://www.drupal.org/project/project_composer/issues/3301876.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + pm-security + pm:security has been removed. Please use `composer audit` command. See https://www.drupal.org/project/project_composer/issues/3301876.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + pm:security-php + pm:security-php has been removed. Please use `composer audit` command.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + sec-php + pm:security-php has been removed. Please use `composer audit` command.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + pm-security-php + pm:security-php has been removed. Please use `composer audit` command.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + role:create + Create a new role.
The symbolic machine name for the role.A descriptive name for the role.
Arguments:
machine_name
human_readable_name

Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ machine_name human_readable_name[=null] +
+ + rcrt + Create a new role.
The symbolic machine name for the role.A descriptive name for the role.
Arguments:
machine_name
human_readable_name

Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ machine_name human_readable_name[=null] +
+ + role-create + Create a new role.
The symbolic machine name for the role.A descriptive name for the role.
Arguments:
machine_name
human_readable_name

Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ machine_name human_readable_name[=null] +
+ + role:delete + Delete a role.
The symbolic machine name for the role.
Arguments:
machine_name

Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ machine_name +
+ + rdel + Delete a role.
The symbolic machine name for the role.
Arguments:
machine_name

Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ machine_name +
+ + role-delete + Delete a role.
The symbolic machine name for the role.
Arguments:
machine_name

Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ machine_name +
+ + role:list + Display roles and their permissions.

No arguments
Format the result data. Available formats: csv,json,list,null,php,print-r,sections,string,table,tsv,var_dump,var_export,xml,yamlAvailable fields: ID (rid), Role Label (label), Permissions (perms)Select just one field, and force format to *string*.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
format
fields
field
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + rls + Display roles and their permissions.

No arguments
Format the result data. Available formats: csv,json,list,null,php,print-r,sections,string,table,tsv,var_dump,var_export,xml,yamlAvailable fields: ID (rid), Role Label (label), Permissions (perms)Select just one field, and force format to *string*.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
format
fields
field
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + role-list + Display roles and their permissions.

No arguments
Format the result data. Available formats: csv,json,list,null,php,print-r,sections,string,table,tsv,var_dump,var_export,xml,yamlAvailable fields: ID (rid), Role Label (label), Permissions (perms)Select just one field, and force format to *string*.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
format
fields
field
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + role:perm:add + Grant specified permission(s) to a role.
The role to modify.The list of permission to grant, delimited by commas.
Arguments:
machine_name
permissions

Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ machine_name permissions +
+ + rap + Grant specified permission(s) to a role.
The role to modify.The list of permission to grant, delimited by commas.
Arguments:
machine_name
permissions

Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ machine_name permissions +
+ + role-add-perm + Grant specified permission(s) to a role.
The role to modify.The list of permission to grant, delimited by commas.
Arguments:
machine_name
permissions

Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ machine_name permissions +
+ + role:perm:remove + Remove specified permission(s) from a role.
The role to modify.The list of permission to grant, delimited by commas.
Arguments:
machine_name
permissions

Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ machine_name permissions +
+ + rmp + Remove specified permission(s) from a role.
The role to modify.The list of permission to grant, delimited by commas.
Arguments:
machine_name
permissions

Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ machine_name permissions +
+ + role-remove-perm + Remove specified permission(s) from a role.
The role to modify.The list of permission to grant, delimited by commas.
Arguments:
machine_name
permissions

Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ machine_name permissions +
+ + site:alias + Show site alias details, or a list of available site aliases.
Site alias or site specification.
Arguments:
site

Format the result data. Available formats: csv,json,list,null,php,print-r,tsv,var_dump,var_export,xml,yamlLimit output to only the listed elements. Name top-level elements by key, e.g. "--fields=name,date", or use dot notation to select a nested element, e.g. "--fields=a.b.c as example".Select just one field, and force format to *string*.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
format
fields
field
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ site[=null] +
+ + sa + Show site alias details, or a list of available site aliases.
Site alias or site specification.
Arguments:
site

Format the result data. Available formats: csv,json,list,null,php,print-r,tsv,var_dump,var_export,xml,yamlLimit output to only the listed elements. Name top-level elements by key, e.g. "--fields=name,date", or use dot notation to select a nested element, e.g. "--fields=a.b.c as example".Select just one field, and force format to *string*.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
format
fields
field
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ site[=null] +
+ + site:alias-convert + site:alias-convert has been removed. Please use Drush 11 or convert by hand.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + sa-convert + site:alias-convert has been removed. Please use Drush 11 or convert by hand.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + sac + site:alias-convert has been removed. Please use Drush 11 or convert by hand.

No arguments
Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + site:install + Install Drupal along with modules/themes/configuration/profile.
An install profile name. Defaults to standard unless an install profile is marked as a distribution. Use minimal for a bare minimum installation. Additional info for the install profile may also be provided with additional arguments. The key is in the form [form name].[parameter name]
Arguments:
profile

A Drupal 6 style database URL. Required for initial install, not re-install. If omitted and required, Drush prompts for this item.An optional table prefix to use for initial install.Account to use when creating a new database. Must have Grant permission (mysql only). Optional.Password for the db-su account. Optional.uid1 name.uid1 email.From: for system mailings.uid1 pass. Defaults to a randomly generated password. If desired, set a fixed password in drush.yml.A short language code. Sets the default site language. Language files must already be present.Site nameName of directory under sites which should be created.Configuration from sync directory should be imported during installation.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
db-url
db-prefix
db-su
db-su-pw
account-name
account-mail
site-mail
account-pass
locale
site-name
site-pass
sites-subdir
config-dir
existing-config
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ profile[=null] +
+ + si + Install Drupal along with modules/themes/configuration/profile.
An install profile name. Defaults to standard unless an install profile is marked as a distribution. Use minimal for a bare minimum installation. Additional info for the install profile may also be provided with additional arguments. The key is in the form [form name].[parameter name]
Arguments:
profile

A Drupal 6 style database URL. Required for initial install, not re-install. If omitted and required, Drush prompts for this item.An optional table prefix to use for initial install.Account to use when creating a new database. Must have Grant permission (mysql only). Optional.Password for the db-su account. Optional.uid1 name.uid1 email.From: for system mailings.uid1 pass. Defaults to a randomly generated password. If desired, set a fixed password in drush.yml.A short language code. Sets the default site language. Language files must already be present.Site nameName of directory under sites which should be created.Configuration from sync directory should be imported during installation.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
db-url
db-prefix
db-su
db-su-pw
account-name
account-mail
site-mail
account-pass
locale
site-name
site-pass
sites-subdir
config-dir
existing-config
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ profile[=null] +
+ + sin + Install Drupal along with modules/themes/configuration/profile.
An install profile name. Defaults to standard unless an install profile is marked as a distribution. Use minimal for a bare minimum installation. Additional info for the install profile may also be provided with additional arguments. The key is in the form [form name].[parameter name]
Arguments:
profile

A Drupal 6 style database URL. Required for initial install, not re-install. If omitted and required, Drush prompts for this item.An optional table prefix to use for initial install.Account to use when creating a new database. Must have Grant permission (mysql only). Optional.Password for the db-su account. Optional.uid1 name.uid1 email.From: for system mailings.uid1 pass. Defaults to a randomly generated password. If desired, set a fixed password in drush.yml.A short language code. Sets the default site language. Language files must already be present.Site nameName of directory under sites which should be created.Configuration from sync directory should be imported during installation.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
db-url
db-prefix
db-su
db-su-pw
account-name
account-mail
site-mail
account-pass
locale
site-name
site-pass
sites-subdir
config-dir
existing-config
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ profile[=null] +
+ + site-install + Install Drupal along with modules/themes/configuration/profile.
An install profile name. Defaults to standard unless an install profile is marked as a distribution. Use minimal for a bare minimum installation. Additional info for the install profile may also be provided with additional arguments. The key is in the form [form name].[parameter name]
Arguments:
profile

A Drupal 6 style database URL. Required for initial install, not re-install. If omitted and required, Drush prompts for this item.An optional table prefix to use for initial install.Account to use when creating a new database. Must have Grant permission (mysql only). Optional.Password for the db-su account. Optional.uid1 name.uid1 email.From: for system mailings.uid1 pass. Defaults to a randomly generated password. If desired, set a fixed password in drush.yml.A short language code. Sets the default site language. Language files must already be present.Site nameName of directory under sites which should be created.Configuration from sync directory should be imported during installation.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
db-url
db-prefix
db-su
db-su-pw
account-name
account-mail
site-mail
account-pass
locale
site-name
site-pass
sites-subdir
config-dir
existing-config
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ profile[=null] +
+ + site:set + Set a site alias that will persist for the current session.
Site specification to use, or - for previous site. Omit this argument to unset.
Arguments:
site

Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ site[=null] +
+ + use + Set a site alias that will persist for the current session.
Site specification to use, or - for previous site. Omit this argument to unset.
Arguments:
site

Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ site[=null] +
+ + site-set + Set a site alias that will persist for the current session.
Site specification to use, or - for previous site. Omit this argument to unset.
Arguments:
site

Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ site[=null] +
+ + site:ssh + Connect to a webserver via SSH, and optionally run a shell command.
Code which should run at remote host.
Arguments:
code

Directory to change to. Defaults to Drupal root.A string of extra options that will be passed to the ssh command (e.g. -p 100)Create a tty (e.g. to run an interactive program).Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
cd
ssh-options
tty
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ code[=null] +
+ + ssh + Connect to a webserver via SSH, and optionally run a shell command.
Code which should run at remote host.
Arguments:
code

Directory to change to. Defaults to Drupal root.A string of extra options that will be passed to the ssh command (e.g. -p 100)Create a tty (e.g. to run an interactive program).Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
cd
ssh-options
tty
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ code[=null] +
+ + site-ssh + Connect to a webserver via SSH, and optionally run a shell command.
Code which should run at remote host.
Arguments:
code

Directory to change to. Defaults to Drupal root.A string of extra options that will be passed to the ssh command (e.g. -p 100)Create a tty (e.g. to run an interactive program).Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
cd
ssh-options
tty
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ code[=null] +
+ + sql:cli + Open a SQL command-line interface using Drupal's credentials.

No arguments
Add custom options to the connect string (e.g. --extra=--skip-column-names)The DB connection key if using multiple connections in settings.php.A Drupal 6 style database URL. For example mysql://root:pass@localhost:port/dbnameThe name of a target within the specified database connection.Show password on the CLI. Useful for debugging.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
extra
database
db-url
target
show-passwords
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + sqlc + Open a SQL command-line interface using Drupal's credentials.

No arguments
Add custom options to the connect string (e.g. --extra=--skip-column-names)The DB connection key if using multiple connections in settings.php.A Drupal 6 style database URL. For example mysql://root:pass@localhost:port/dbnameThe name of a target within the specified database connection.Show password on the CLI. Useful for debugging.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
extra
database
db-url
target
show-passwords
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + sql-cli + Open a SQL command-line interface using Drupal's credentials.

No arguments
Add custom options to the connect string (e.g. --extra=--skip-column-names)The DB connection key if using multiple connections in settings.php.A Drupal 6 style database URL. For example mysql://root:pass@localhost:port/dbnameThe name of a target within the specified database connection.Show password on the CLI. Useful for debugging.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
extra
database
db-url
target
show-passwords
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + sql:conf +
No arguments
Show all database connections, instead of just one.Show password on the CLI. Useful for debugging.The DB connection key if using multiple connections in settings.php.A Drupal 6 style database URL. For example mysql://root:pass@localhost:port/dbnameThe name of a target within the specified database connection.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
format
all
show-passwords
database
db-url
target
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + sql-conf +
No arguments
Show all database connections, instead of just one.Show password on the CLI. Useful for debugging.The DB connection key if using multiple connections in settings.php.A Drupal 6 style database URL. For example mysql://root:pass@localhost:port/dbnameThe name of a target within the specified database connection.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
format
all
show-passwords
database
db-url
target
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + sql:connect + A string for connecting to the DB.

No arguments
Add custom options to the connect string (e.g. --extra=--skip-column-names)The DB connection key if using multiple connections in settings.php.A Drupal 6 style database URL. For example mysql://root:pass@localhost:port/dbnameThe name of a target within the specified database connection.Show password on the CLI. Useful for debugging.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
extra
database
db-url
target
show-passwords
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + sql-connect + A string for connecting to the DB.

No arguments
Add custom options to the connect string (e.g. --extra=--skip-column-names)The DB connection key if using multiple connections in settings.php.A Drupal 6 style database URL. For example mysql://root:pass@localhost:port/dbnameThe name of a target within the specified database connection.Show password on the CLI. Useful for debugging.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
extra
database
db-url
target
show-passwords
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + sql:create + Create a database.

No arguments
Account to use when creating a new database.Password for the db-su account.The DB connection key if using multiple connections in settings.php.A Drupal 6 style database URL. For example mysql://root:pass@localhost:port/dbnameThe name of a target within the specified database connection.Show password on the CLI. Useful for debugging.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
db-su
db-su-pw
database
db-url
target
show-passwords
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + sql-create + Create a database.

No arguments
Account to use when creating a new database.Password for the db-su account.The DB connection key if using multiple connections in settings.php.A Drupal 6 style database URL. For example mysql://root:pass@localhost:port/dbnameThe name of a target within the specified database connection.Show password on the CLI. Useful for debugging.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
db-su
db-su-pw
database
db-url
target
show-passwords
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + sql:drop + Drop all tables in a given database.

No arguments
The DB connection key if using multiple connections in settings.php.A Drupal 6 style database URL. For example mysql://root:pass@localhost:port/dbnameThe name of a target within the specified database connection.Show password on the CLI. Useful for debugging.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
database
db-url
target
show-passwords
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + sql-drop + Drop all tables in a given database.

No arguments
The DB connection key if using multiple connections in settings.php.A Drupal 6 style database URL. For example mysql://root:pass@localhost:port/dbnameThe name of a target within the specified database connection.Show password on the CLI. Useful for debugging.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
database
db-url
target
show-passwords
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + sql:dump + Exports the Drupal DB as SQL using mysqldump or equivalent.

No arguments
Save to a file. The file should be relative to Drupal root. If --result-file is provided with the value 'auto', a date-based filename will be created under ~/drush-backups directory.Omit DROP TABLE statements. Used by Postgres and Oracle only.Dump data without statements to create any of the schema.Order by primary key and add line breaks for efficient diffs. Slows down the dump. Mysql only.Compress the dump using the gzip program which must be in your $PATH.Add custom arguments/options when connecting to database (used internally to list tables).Add custom arguments/options to the dumping of the database (e.g. mysqldump command).Format the result data. Available formats: csv,json,list,null,php,print-r,string,table,tsv,var_dump,var_export,xml,yamlThe DB connection key if using multiple connections in settings.php.A Drupal 6 style database URL. For example mysql://root:pass@localhost:port/dbnameThe name of a target within the specified database connection.Show password on the CLI. Useful for debugging.A key in the $skip_tables array. @see [Site aliases](../site-aliases.md)A key in the $structure_tables array. @see [Site aliases](../site-aliases.md)A key in the $tables array.A comma-separated list of tables to exclude completely.A comma-separated list of tables to include for structure, but not data.A comma-separated list of tables to transfer.Available fields: Path (path)Select just one field, and force format to *string*.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
result-file
create-db
data-only
ordered-dump
gzip
extra
extra-dump
format
database
db-url
target
show-passwords
skip-tables-key
structure-tables-key
tables-key
skip-tables-list
structure-tables-list
tables-list
fields
field
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + sql-dump + Exports the Drupal DB as SQL using mysqldump or equivalent.

No arguments
Save to a file. The file should be relative to Drupal root. If --result-file is provided with the value 'auto', a date-based filename will be created under ~/drush-backups directory.Omit DROP TABLE statements. Used by Postgres and Oracle only.Dump data without statements to create any of the schema.Order by primary key and add line breaks for efficient diffs. Slows down the dump. Mysql only.Compress the dump using the gzip program which must be in your $PATH.Add custom arguments/options when connecting to database (used internally to list tables).Add custom arguments/options to the dumping of the database (e.g. mysqldump command).Format the result data. Available formats: csv,json,list,null,php,print-r,string,table,tsv,var_dump,var_export,xml,yamlThe DB connection key if using multiple connections in settings.php.A Drupal 6 style database URL. For example mysql://root:pass@localhost:port/dbnameThe name of a target within the specified database connection.Show password on the CLI. Useful for debugging.A key in the $skip_tables array. @see [Site aliases](../site-aliases.md)A key in the $structure_tables array. @see [Site aliases](../site-aliases.md)A key in the $tables array.A comma-separated list of tables to exclude completely.A comma-separated list of tables to include for structure, but not data.A comma-separated list of tables to transfer.Available fields: Path (path)Select just one field, and force format to *string*.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
result-file
create-db
data-only
ordered-dump
gzip
extra
extra-dump
format
database
db-url
target
show-passwords
skip-tables-key
structure-tables-key
tables-key
skip-tables-list
structure-tables-list
tables-list
fields
field
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + sql:query + Execute a query against a database.
An SQL query. Ignored if --file is provided.
Arguments:
query

Save to a file. The file should be relative to Drupal root.Path to a file containing the SQL to be run. Gzip files are accepted.Delete the --file after running it.Add custom options to the connect string (e.g. --extra=--skip-column-names)Enable replacement of braces in your query.The DB connection key if using multiple connections in settings.php.A Drupal 6 style database URL. For example mysql://root:pass@localhost:port/dbnameThe name of a target within the specified database connection.Show password on the CLI. Useful for debugging.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
result-file
file
file-delete
extra
db-prefix
database
db-url
target
show-passwords
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ query[=null] +
+ + sqlq + Execute a query against a database.
An SQL query. Ignored if --file is provided.
Arguments:
query

Save to a file. The file should be relative to Drupal root.Path to a file containing the SQL to be run. Gzip files are accepted.Delete the --file after running it.Add custom options to the connect string (e.g. --extra=--skip-column-names)Enable replacement of braces in your query.The DB connection key if using multiple connections in settings.php.A Drupal 6 style database URL. For example mysql://root:pass@localhost:port/dbnameThe name of a target within the specified database connection.Show password on the CLI. Useful for debugging.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
result-file
file
file-delete
extra
db-prefix
database
db-url
target
show-passwords
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ query[=null] +
+ + sql-query + Execute a query against a database.
An SQL query. Ignored if --file is provided.
Arguments:
query

Save to a file. The file should be relative to Drupal root.Path to a file containing the SQL to be run. Gzip files are accepted.Delete the --file after running it.Add custom options to the connect string (e.g. --extra=--skip-column-names)Enable replacement of braces in your query.The DB connection key if using multiple connections in settings.php.A Drupal 6 style database URL. For example mysql://root:pass@localhost:port/dbnameThe name of a target within the specified database connection.Show password on the CLI. Useful for debugging.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
result-file
file
file-delete
extra
db-prefix
database
db-url
target
show-passwords
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ query[=null] +
+ + sql:sync + Copy DB data from a source site to a target site. Transfers data via rsync.
A site-alias or site specification whose database you want to copy from.A site-alias or site specification whose database you want to replace.
Arguments:
source
target

Do not dump the sql database; always use an existing dump file.Do not rsync the database dump file from source to target.Where to run the rsync command; defaults to the local site. Can also be source or target.Create a new database before importing the database dump on the target machine.Account to use when creating a new database (e.g. root).Password for the db-su account.The path for storing the sql-dump on target machine.The path for retrieving the sql-dump on source machine.Add custom arguments/options to the dumping of the database (e.g. mysqldump command).A key in the $skip_tables array. @see [Site aliases](../site-aliases.md)A key in the $structure_tables array. @see [Site aliases](../site-aliases.md)A key in the $tables array.A comma-separated list of tables to exclude completely.A comma-separated list of tables to include for structure, but not data.A comma-separated list of tables to transfer.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
no-dump
no-sync
runner
create-db
db-su
db-su-pw
target-dump
source-dump
extra-dump
skip-tables-key
structure-tables-key
tables-key
skip-tables-list
structure-tables-list
tables-list
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ source target +
+ + sql-sync + Copy DB data from a source site to a target site. Transfers data via rsync.
A site-alias or site specification whose database you want to copy from.A site-alias or site specification whose database you want to replace.
Arguments:
source
target

Do not dump the sql database; always use an existing dump file.Do not rsync the database dump file from source to target.Where to run the rsync command; defaults to the local site. Can also be source or target.Create a new database before importing the database dump on the target machine.Account to use when creating a new database (e.g. root).Password for the db-su account.The path for storing the sql-dump on target machine.The path for retrieving the sql-dump on source machine.Add custom arguments/options to the dumping of the database (e.g. mysqldump command).A key in the $skip_tables array. @see [Site aliases](../site-aliases.md)A key in the $structure_tables array. @see [Site aliases](../site-aliases.md)A key in the $tables array.A comma-separated list of tables to exclude completely.A comma-separated list of tables to include for structure, but not data.A comma-separated list of tables to transfer.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
no-dump
no-sync
runner
create-db
db-su
db-su-pw
target-dump
source-dump
extra-dump
skip-tables-key
structure-tables-key
tables-key
skip-tables-list
structure-tables-list
tables-list
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ source target +
+ + updatedb:batch-process + Process operations in the specified batch set.
The batch id that will be processed.
Arguments:
batch_id

Format the result data. Available formats: csv,json,list,null,php,print-r,tsv,var_dump,var_export,xml,yamlLimit output to only the listed elements. Name top-level elements by key, e.g. "--fields=name,date", or use dot notation to select a nested element, e.g. "--fields=a.b.c as example".Select just one field, and force format to *string*.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
format
fields
field
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ batch_id +
+ + updatedb:status + List any pending database updates.

No arguments
Format the result data. Available formats: csv,json,list,null,php,print-r,sections,string,table,tsv,var_dump,var_export,xml,yamlAvailable fields: Module (module), Update ID (update_id), Description (description), Type (type)Select just one field, and force format to *string*.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
format
fields
field
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + updbst + List any pending database updates.

No arguments
Format the result data. Available formats: csv,json,list,null,php,print-r,sections,string,table,tsv,var_dump,var_export,xml,yamlAvailable fields: Module (module), Update ID (update_id), Description (description), Type (type)Select just one field, and force format to *string*.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
format
fields
field
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + updatedb-status + List any pending database updates.

No arguments
Format the result data. Available formats: csv,json,list,null,php,print-r,sections,string,table,tsv,var_dump,var_export,xml,yamlAvailable fields: Module (module), Update ID (update_id), Description (description), Type (type)Select just one field, and force format to *string*.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
format
fields
field
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+
+ + user:login + Display a one time login link for user ID 1, or another user.
Optional path to redirect to after logging in.
Arguments:
path

A user name to log in as.A user ID to log in as.A user email to log in as.Open the URL in the default browser. Use --no-browser to avoid opening a browser.A custom port for redirecting to (e.g., when running within a Vagrant environment)Negate --browser option.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
name
uid
mail
browser
redirect-port
no-browser
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ path[=null] +
+ + uli + Display a one time login link for user ID 1, or another user.
Optional path to redirect to after logging in.
Arguments:
path

A user name to log in as.A user ID to log in as.A user email to log in as.Open the URL in the default browser. Use --no-browser to avoid opening a browser.A custom port for redirecting to (e.g., when running within a Vagrant environment)Negate --browser option.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
name
uid
mail
browser
redirect-port
no-browser
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ path[=null] +
+ + user-login + Display a one time login link for user ID 1, or another user.
Optional path to redirect to after logging in.
Arguments:
path

A user name to log in as.A user ID to log in as.A user email to log in as.Open the URL in the default browser. Use --no-browser to avoid opening a browser.A custom port for redirecting to (e.g., when running within a Vagrant environment)Negate --browser option.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
name
uid
mail
browser
redirect-port
no-browser
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ path[=null] +
+ + yaml:get:value + Get a value for a specific key in a YAML file.
The filename of the YAML fileThe key for the value to get, in dot notation.
Arguments:
filename
key

Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ filename key +
+ + y:get:value + Get a value for a specific key in a YAML file.
The filename of the YAML fileThe key for the value to get, in dot notation.
Arguments:
filename
key

Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ filename key +
+ + yaml:lint + Validates that a given YAML file has valid syntax.
The filename of the YAML file
Arguments:
filename

Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ filename +
+ + y:lint + Validates that a given YAML file has valid syntax.
The filename of the YAML file
Arguments:
filename

Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ filename +
+ + yaml:unset:key + Unset a specific key in a YAML file.
The filename of the YAML fileThe key to unset, in dot notation
Arguments:
filename
key

Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ filename key +
+ + y:unset:key + Unset a specific key in a YAML file.
The filename of the YAML fileThe key to unset, in dot notation
Arguments:
filename
key

Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ filename key +
+ + yaml:update:key + Update a specific key in a YAML file.
The filename of the YAML fileThe original key, in dot notationThe new key, in dot notation
Arguments:
filename
key
new-key

Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ filename key new-key +
+ + y:update:key + Update a specific key in a YAML file.
The filename of the YAML fileThe original key, in dot notationThe new key, in dot notation
Arguments:
filename
key
new-key

Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ filename key new-key +
+ + yaml:update:value + Update the value for a specific key in a YAML file.
The filename of the YAML fileThe key for the value to set, in dot notationThe new value
Arguments:
filename
key
value

Set the variable type for the value. Accepted types are int, integer, bool, boolean, str, and string.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
type
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ filename key value +
+ + y:update:value + Update the value for a specific key in a YAML file.
The filename of the YAML fileThe key for the value to set, in dot notationThe new value
Arguments:
filename
key
value

Set the variable type for the value. Accepted types are int, integer, bool, boolean, str, and string.Display help for the given command. When no command is given display help for the list commandDo not output any messageIncrease the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugDisplay this application versionForce (or disable --no-ansi) ANSI outputNegate the "--ansi" optionDo not ask any interactive questionAuto-accept the default for all user prompts. Equivalent to --no-interaction.Cancels at any confirmation prompt.The Drupal root for this site.A base URL for building links and selecting a multi-site. Defaults to https://default.Run in simulated mode (show what would have happened).Define a configuration item value.
Options:
type
help
quiet
verbose
version
ansi
no-ansi
no-interaction
yes
no
root
uri
simulate
define

]]>
+ filename key value +
+
+ diff --git a/apps/cms/.idea/commandlinetools/schemas/frameworkDescriptionVersion1.1.4.xsd b/apps/cms/.idea/commandlinetools/schemas/frameworkDescriptionVersion1.1.4.xsd new file mode 100644 index 000000000..f2efc6de1 --- /dev/null +++ b/apps/cms/.idea/commandlinetools/schemas/frameworkDescriptionVersion1.1.4.xsd @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/cms/.idea/inspectionProfiles/Project_Default.xml b/apps/cms/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 000000000..03d9549ea --- /dev/null +++ b/apps/cms/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/apps/cms/.idea/modules.xml b/apps/cms/.idea/modules.xml new file mode 100644 index 000000000..dc2033c9a --- /dev/null +++ b/apps/cms/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/apps/cms/.idea/php.xml b/apps/cms/.idea/php.xml new file mode 100644 index 000000000..67da51258 --- /dev/null +++ b/apps/cms/.idea/php.xml @@ -0,0 +1,264 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/cms/.idea/vcs.xml b/apps/cms/.idea/vcs.xml new file mode 100644 index 000000000..b2bdec2d7 --- /dev/null +++ b/apps/cms/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/apps/cms/composer.json b/apps/cms/composer.json index 95a18201c..e187e9ef9 100644 --- a/apps/cms/composer.json +++ b/apps/cms/composer.json @@ -98,7 +98,7 @@ "Fix site install": "https://www.drupal.org/files/issues/2023-07-28/3349663-8.patch" }, "amazeelabs/silverback_gatsby": { - "Autosave preview": "./patches/fetch-entity-patch" + "Autosave preview": "./patches/fetch-entity.patch" } }, "patchLevel": { diff --git a/apps/cms/patches/fetch-entity-patch b/apps/cms/patches/fetch-entity.patch similarity index 100% rename from apps/cms/patches/fetch-entity-patch rename to apps/cms/patches/fetch-entity.patch From b5cc3b09df10b7db26085e72c7caa16bc08d8564 Mon Sep 17 00:00:00 2001 From: Dimitris Spachos Date: Mon, 22 Apr 2024 09:00:21 +0300 Subject: [PATCH 52/82] feat(slb-146): add version to autosave library --- .../silverback_autosave/silverback_autosave.libraries.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/drupal/silverback_autosave/silverback_autosave.libraries.yml b/packages/drupal/silverback_autosave/silverback_autosave.libraries.yml index aa5489ad3..fe2005e5b 100644 --- a/packages/drupal/silverback_autosave/silverback_autosave.libraries.yml +++ b/packages/drupal/silverback_autosave/silverback_autosave.libraries.yml @@ -1,5 +1,5 @@ drupal.silverback_autosave: - version: VERSION + version: 1.0 css: theme: css/silverback_autosave.css: {} From fa98dc821b96b84649d03201ce4dd0c6283a5270 Mon Sep 17 00:00:00 2001 From: Mattia Simonato Date: Tue, 23 Apr 2024 19:32:08 +0200 Subject: [PATCH 53/82] fix(SLB-325): adjust default hero height --- packages/ui/src/components/Organisms/PageHero.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/components/Organisms/PageHero.tsx b/packages/ui/src/components/Organisms/PageHero.tsx index 7eea65402..c7c18b202 100644 --- a/packages/ui/src/components/Organisms/PageHero.tsx +++ b/packages/ui/src/components/Organisms/PageHero.tsx @@ -15,7 +15,7 @@ export function PageHero(props: NonNullable) { function DefaultHero(props: NonNullable) { return ( -
+
{props.image ? ( {props.image.alt} Date: Wed, 17 Apr 2024 16:58:29 +0400 Subject: [PATCH 54/82] chore(SLB-289): disable color and typography options on the list block --- packages/drupal/gutenberg_blocks/src/customisations.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/drupal/gutenberg_blocks/src/customisations.ts b/packages/drupal/gutenberg_blocks/src/customisations.ts index 70d97ccac..fb0355c8e 100644 --- a/packages/drupal/gutenberg_blocks/src/customisations.ts +++ b/packages/drupal/gutenberg_blocks/src/customisations.ts @@ -16,6 +16,10 @@ drupalSettings.gutenberg._listeners.init.push( const paragraphBlock = wp.blocks.getBlockType('core/paragraph'); paragraphBlock.supports.typography.fontSize = false; paragraphBlock.supports.typography.dropCap = false; + // @ts-ignore + const listBlock = wp.blocks.getBlockType('core/list'); + listBlock.supports.color = false; + listBlock.supports.typography = false; }, // Allow common blocks to be placed only in the Content block. From 4db75851014c965a9e9115c59a57980cac080d0c Mon Sep 17 00:00:00 2001 From: Alex Tkachev Date: Wed, 17 Apr 2024 16:58:57 +0400 Subject: [PATCH 55/82] feat(SLB-289): extend the core/list block with a new style option --- .../gutenberg_blocks/src/filters/list.tsx | 105 ++++++++++++++++++ packages/drupal/gutenberg_blocks/src/index.ts | 1 + 2 files changed, 106 insertions(+) create mode 100644 packages/drupal/gutenberg_blocks/src/filters/list.tsx diff --git a/packages/drupal/gutenberg_blocks/src/filters/list.tsx b/packages/drupal/gutenberg_blocks/src/filters/list.tsx new file mode 100644 index 000000000..707e7aa61 --- /dev/null +++ b/packages/drupal/gutenberg_blocks/src/filters/list.tsx @@ -0,0 +1,105 @@ +import clsx from 'clsx'; +import { InspectorControls } from 'wordpress__block-editor'; +import { PanelBody, SelectControl } from 'wordpress__components'; +import { createHigherOrderComponent } from 'wordpress__compose'; +import { addFilter } from 'wordpress__hooks'; + +// @ts-ignore +const __ = Drupal.t; + +addFilter( + 'blocks.registerBlockType', + 'custom/list', + (settings: { name: string; attributes: any }) => { + if (settings.name === 'core/list') { + settings.attributes = Object.assign(settings.attributes, { + customListStyle: { + type: 'string', + default: '', + }, + }); + } + return settings; + }, +); + +addFilter( + 'editor.BlockEdit', + 'custom/list', + createHigherOrderComponent( + // eslint-disable-next-line react/display-name + (BlockEdit) => (props) => { + const { name, attributes, setAttributes, isSelected } = props; + const { customListStyle, ordered } = attributes; + if (!customListStyle === undefined) { + setAttributes({ customListStyle: '' }); + } + return ( + <> + + {isSelected && name === 'core/list' && !ordered ? ( + + + { + setAttributes({ + customListStyle, + }); + }} + /> + + + ) : null} + + ); + }, + 'withCustomListStyleControls', + ), +); + +addFilter( + 'editor.BlockListBlock', + 'custom/list', + createHigherOrderComponent( + // eslint-disable-next-line react/display-name + (BlockListBlock) => (props) => { + const { name, attributes } = props; + if (name === 'core/list') { + const { customListStyle, ordered } = attributes; + if (!ordered && customListStyle) { + props.className = getCustomListClass( + props.className, + customListStyle, + ); + } + } + return ; + }, + 'withCustomListStyleBlockClass', + ), +); + +addFilter( + 'blocks.getSaveContent.extraProps', + 'custom/list', + (props: any, blockType: { name: string }, attributes: any) => { + if (blockType.name === 'core/list') { + const { customListStyle, ordered } = attributes; + if (!ordered && customListStyle) { + props.className = getCustomListClass(props.className, customListStyle); + } + } + return props; + }, +); + +function getCustomListClass(existingClassName: string, style: string) { + return clsx(existingClassName, 'list-style--' + style); +} diff --git a/packages/drupal/gutenberg_blocks/src/index.ts b/packages/drupal/gutenberg_blocks/src/index.ts index a0aff90af..74af52921 100644 --- a/packages/drupal/gutenberg_blocks/src/index.ts +++ b/packages/drupal/gutenberg_blocks/src/index.ts @@ -5,4 +5,5 @@ import './blocks/heading'; import './blocks/form'; import './blocks/image-teaser'; import './blocks/image-teasers'; +import './filters/list'; import './blocks/cta'; From 2f3fafc340fc7f11b5ebe6ea539730988a69059b Mon Sep 17 00:00:00 2001 From: Alex Tkachev Date: Wed, 17 Apr 2024 16:59:14 +0400 Subject: [PATCH 56/82] feat(SLB-289): introduce custom/image-with-text block --- packages/drupal/gutenberg_blocks/package.json | 1 + .../src/blocks/image-with-text.tsx | 79 ++++++ packages/drupal/gutenberg_blocks/src/index.ts | 1 + .../a397ca48-8fad-411e-8901-0eba2feb989c.yml | 26 ++ .../ceb9b2a7-4c4c-4084-ada9-d5f6505d466b.yml | 6 + packages/schema/src/fragments/Page.gql | 1 + .../PageContent/BlockImageWithText.gql | 9 + packages/schema/src/schema.graphql | 6 + .../src/components/Organisms/PageDisplay.tsx | 15 ++ pnpm-lock.yaml | 254 +++++++++++++++--- tests/schema/specs/blocks.spec.ts | 41 +++ 11 files changed, 402 insertions(+), 37 deletions(-) create mode 100644 packages/drupal/gutenberg_blocks/src/blocks/image-with-text.tsx create mode 100644 packages/schema/src/fragments/PageContent/BlockImageWithText.gql diff --git a/packages/drupal/gutenberg_blocks/package.json b/packages/drupal/gutenberg_blocks/package.json index 093ca1970..b31ec99c1 100644 --- a/packages/drupal/gutenberg_blocks/package.json +++ b/packages/drupal/gutenberg_blocks/package.json @@ -27,6 +27,7 @@ "@types/wordpress__core-data": "2.4.5", "@types/wordpress__data": "6.0.2", "@types/wordpress__editor": "13.0.0", + "@types/wordpress__hooks": "2.4.0", "@vitejs/plugin-react": "4.2.1", "typescript": "^5.3.3", "vite": "^5.0.10" diff --git a/packages/drupal/gutenberg_blocks/src/blocks/image-with-text.tsx b/packages/drupal/gutenberg_blocks/src/blocks/image-with-text.tsx new file mode 100644 index 000000000..d4717d4aa --- /dev/null +++ b/packages/drupal/gutenberg_blocks/src/blocks/image-with-text.tsx @@ -0,0 +1,79 @@ +import { InnerBlocks, InspectorControls } from 'wordpress__block-editor'; +import { registerBlockType } from 'wordpress__blocks'; +import { PanelBody, SelectControl } from 'wordpress__components'; +import { dispatch } from 'wordpress__data'; + +import { DrupalMediaEntity } from '../utils/drupal-media'; + +// @ts-ignore +const { t: __ } = Drupal; + +registerBlockType('custom/image-with-text', { + title: __('Image with Text'), + icon: 'cover-image', + category: 'layout', + attributes: { + mediaEntityIds: { + type: 'array', + }, + imagePosition: { + type: 'string', + default: 'left', + }, + }, + edit: (props) => { + const { setAttributes } = props; + return ( + <> + + + { + setAttributes({ + imagePosition, + }); + }} + /> + + +
+
{__('Image with Text')}
+ { + // @ts-ignore + error = typeof error === 'string' ? error : error[2]; + dispatch('core/notices').createWarningNotice(error); + }} + /> + +
+ + ); + }, + save: () => , +}); diff --git a/packages/drupal/gutenberg_blocks/src/index.ts b/packages/drupal/gutenberg_blocks/src/index.ts index 74af52921..6eae568bf 100644 --- a/packages/drupal/gutenberg_blocks/src/index.ts +++ b/packages/drupal/gutenberg_blocks/src/index.ts @@ -5,5 +5,6 @@ import './blocks/heading'; import './blocks/form'; import './blocks/image-teaser'; import './blocks/image-teasers'; +import './blocks/image-with-text'; import './filters/list'; import './blocks/cta'; diff --git a/packages/drupal/test_content/content/node/a397ca48-8fad-411e-8901-0eba2feb989c.yml b/packages/drupal/test_content/content/node/a397ca48-8fad-411e-8901-0eba2feb989c.yml index de316ba27..43e5fe62d 100644 --- a/packages/drupal/test_content/content/node/a397ca48-8fad-411e-8901-0eba2feb989c.yml +++ b/packages/drupal/test_content/content/node/a397ca48-8fad-411e-8901-0eba2feb989c.yml @@ -60,6 +60,32 @@ default: + + +

All kinds of allowed blocks

+ + + +
  • bla
+ + + +

Heading

+ + + +
12
34
Caption
+ + + +

Quote

Citation
+ + + +

+ + +

Starting from this paragraph, all the following blocks should be aggregated, as they are just HTML

diff --git a/packages/drupal/test_content/content/node/ceb9b2a7-4c4c-4084-ada9-d5f6505d466b.yml b/packages/drupal/test_content/content/node/ceb9b2a7-4c4c-4084-ada9-d5f6505d466b.yml index 6c19a491d..53ade0b64 100644 --- a/packages/drupal/test_content/content/node/ceb9b2a7-4c4c-4084-ada9-d5f6505d466b.yml +++ b/packages/drupal/test_content/content/node/ceb9b2a7-4c4c-4084-ada9-d5f6505d466b.yml @@ -67,6 +67,12 @@ default: + + + +

+ + format: gutenberg summary: '' diff --git a/packages/schema/src/fragments/Page.gql b/packages/schema/src/fragments/Page.gql index 38c057340..0a78d9dea 100644 --- a/packages/schema/src/fragments/Page.gql +++ b/packages/schema/src/fragments/Page.gql @@ -33,6 +33,7 @@ fragment Page on Page { ...BlockForm ...BlockImageTeasers ...BlockCta + ...BlockImageWithText } metaTags { tag diff --git a/packages/schema/src/fragments/PageContent/BlockImageWithText.gql b/packages/schema/src/fragments/PageContent/BlockImageWithText.gql new file mode 100644 index 000000000..b026e8676 --- /dev/null +++ b/packages/schema/src/fragments/PageContent/BlockImageWithText.gql @@ -0,0 +1,9 @@ +fragment BlockImageWithText on BlockImageWithText { + image { + source(width: 1536, sizes: [[768, 768], [1536, 1536]]) + alt + } + textContent { + markup + } +} diff --git a/packages/schema/src/schema.graphql b/packages/schema/src/schema.graphql index 9300972cb..3d86b6850 100644 --- a/packages/schema/src/schema.graphql +++ b/packages/schema/src/schema.graphql @@ -199,6 +199,7 @@ union PageContent @resolveEditorBlockType = | BlockForm | BlockImageTeasers | BlockCta + | BlockImageWithText type BlockForm @type(id: "custom/form") { url: Url @resolveEditorBlockAttribute(key: "formId") @webformIdToUrl(id: "$") @@ -244,6 +245,11 @@ type BlockCta @type(id: "custom/cta") { openInNewTab: Boolean @resolveEditorBlockAttribute(key: "openInNewTab") } +type BlockImageWithText @type(id: "custom/image-with-text") { + image: MediaImage @resolveEditorBlockMedia + textContent: BlockMarkup @resolveEditorBlockChildren @seek(pos: 0) +} + input PaginationInput { limit: Int! offset: Int! diff --git a/packages/ui/src/components/Organisms/PageDisplay.tsx b/packages/ui/src/components/Organisms/PageDisplay.tsx index bcefcb2ec..975d447c0 100644 --- a/packages/ui/src/components/Organisms/PageDisplay.tsx +++ b/packages/ui/src/components/Organisms/PageDisplay.tsx @@ -43,6 +43,21 @@ export function PageDisplay(page: PageFragment) { ); case 'BlockCta': return ; + case 'BlockImageWithText': + return ( + // TODO: Implement BlockImageWithText +
+ BlockImageWithText goes here +
+ ); default: throw new UnreachableCaseError(block); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8aca358f2..d516514ba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -354,6 +354,9 @@ importers: '@types/wordpress__editor': specifier: 13.0.0 version: 13.0.0(react-dom@18.2.0)(react@18.2.0) + '@types/wordpress__hooks': + specifier: 2.4.0 + version: 2.4.0 '@vitejs/plugin-react': specifier: 4.2.1 version: 4.2.1(vite@5.0.10) @@ -2993,6 +2996,7 @@ packages: cpu: [ppc64] os: [aix] requiresBuild: true + dev: true optional: true /@esbuild/aix-ppc64@0.20.0: @@ -3036,6 +3040,7 @@ packages: cpu: [arm64] os: [android] requiresBuild: true + dev: true optional: true /@esbuild/android-arm64@0.20.0: @@ -3079,6 +3084,7 @@ packages: cpu: [arm] os: [android] requiresBuild: true + dev: true optional: true /@esbuild/android-arm@0.20.0: @@ -3122,6 +3128,7 @@ packages: cpu: [x64] os: [android] requiresBuild: true + dev: true optional: true /@esbuild/android-x64@0.20.0: @@ -3165,6 +3172,7 @@ packages: cpu: [arm64] os: [darwin] requiresBuild: true + dev: true optional: true /@esbuild/darwin-arm64@0.20.0: @@ -3208,6 +3216,7 @@ packages: cpu: [x64] os: [darwin] requiresBuild: true + dev: true optional: true /@esbuild/darwin-x64@0.20.0: @@ -3251,6 +3260,7 @@ packages: cpu: [arm64] os: [freebsd] requiresBuild: true + dev: true optional: true /@esbuild/freebsd-arm64@0.20.0: @@ -3294,6 +3304,7 @@ packages: cpu: [x64] os: [freebsd] requiresBuild: true + dev: true optional: true /@esbuild/freebsd-x64@0.20.0: @@ -3337,6 +3348,7 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true + dev: true optional: true /@esbuild/linux-arm64@0.20.0: @@ -3380,6 +3392,7 @@ packages: cpu: [arm] os: [linux] requiresBuild: true + dev: true optional: true /@esbuild/linux-arm@0.20.0: @@ -3423,6 +3436,7 @@ packages: cpu: [ia32] os: [linux] requiresBuild: true + dev: true optional: true /@esbuild/linux-ia32@0.20.0: @@ -3466,6 +3480,7 @@ packages: cpu: [loong64] os: [linux] requiresBuild: true + dev: true optional: true /@esbuild/linux-loong64@0.20.0: @@ -3509,6 +3524,7 @@ packages: cpu: [mips64el] os: [linux] requiresBuild: true + dev: true optional: true /@esbuild/linux-mips64el@0.20.0: @@ -3552,6 +3568,7 @@ packages: cpu: [ppc64] os: [linux] requiresBuild: true + dev: true optional: true /@esbuild/linux-ppc64@0.20.0: @@ -3595,6 +3612,7 @@ packages: cpu: [riscv64] os: [linux] requiresBuild: true + dev: true optional: true /@esbuild/linux-riscv64@0.20.0: @@ -3638,6 +3656,7 @@ packages: cpu: [s390x] os: [linux] requiresBuild: true + dev: true optional: true /@esbuild/linux-s390x@0.20.0: @@ -3681,6 +3700,7 @@ packages: cpu: [x64] os: [linux] requiresBuild: true + dev: true optional: true /@esbuild/linux-x64@0.20.0: @@ -3724,6 +3744,7 @@ packages: cpu: [x64] os: [netbsd] requiresBuild: true + dev: true optional: true /@esbuild/netbsd-x64@0.20.0: @@ -3767,6 +3788,7 @@ packages: cpu: [x64] os: [openbsd] requiresBuild: true + dev: true optional: true /@esbuild/openbsd-x64@0.20.0: @@ -3810,6 +3832,7 @@ packages: cpu: [x64] os: [sunos] requiresBuild: true + dev: true optional: true /@esbuild/sunos-x64@0.20.0: @@ -3853,6 +3876,7 @@ packages: cpu: [arm64] os: [win32] requiresBuild: true + dev: true optional: true /@esbuild/win32-arm64@0.20.0: @@ -3896,6 +3920,7 @@ packages: cpu: [ia32] os: [win32] requiresBuild: true + dev: true optional: true /@esbuild/win32-ia32@0.20.0: @@ -3939,6 +3964,7 @@ packages: cpu: [x64] os: [win32] requiresBuild: true + dev: true optional: true /@esbuild/win32-x64@0.20.0: @@ -7147,7 +7173,7 @@ packages: react-refresh: 0.14.0 schema-utils: 3.3.0 source-map: 0.7.4 - webpack: 5.91.0(esbuild@0.19.12) + webpack: 5.91.0 /@pnpm/config.env-replace@1.1.0: resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} @@ -9956,6 +9982,10 @@ packages: - react-dom dev: true + /@types/wordpress__hooks@2.4.0: + resolution: {integrity: sha512-SixjDsBvPynmMQxBFX3i4nuWtLifqtml7PhSosoCk5RJy9S7xs/G6vvtcnAuslV3BJTyvxEbAsz4WpKaF0bqMw==} + dev: true + /@types/wordpress__keycodes@2.18.0: resolution: {integrity: sha512-09ku81E6tjB//bI5PatKM/rhTJ0aEmNLvhIn380orea+SX6/09t7luO27zxI0uHSs/1Pxue1ifrMKY+gE5lfzw==} deprecated: This is a stub types definition. @wordpress/keycodes provides its own type definitions, so you do not need this installed. @@ -10082,6 +10112,7 @@ packages: typescript: 5.3.3 transitivePeerDependencies: - supports-color + dev: true /@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0)(eslint@7.32.0)(typescript@5.4.4): resolution: {integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==} @@ -10110,7 +10141,6 @@ packages: transitivePeerDependencies: - supports-color dev: false - optional: true /@typescript-eslint/eslint-plugin@6.17.0(@typescript-eslint/parser@6.17.0)(eslint@7.0.0)(typescript@5.3.3): resolution: {integrity: sha512-Vih/4xLXmY7V490dGwBQJTpIZxH4ZFH6eCVmQ4RFkB+wmaCTDAx4dtgoWwMNGKLkqRY1L6rPqzEbjorRnDo4rQ==} @@ -10226,8 +10256,6 @@ packages: typescript: 5.4.4 transitivePeerDependencies: - supports-color - dev: false - optional: true /@typescript-eslint/parser@6.17.0(eslint@7.0.0)(typescript@5.3.3): resolution: {integrity: sha512-C4bBaX2orvhK+LlwrY8oWGmSl4WolCfYm513gEccdWZj0CwGadbIADb0FtVEcI+WzUyjyoBj2JRP8g25E6IB8A==} @@ -10269,6 +10297,7 @@ packages: typescript: 5.3.3 transitivePeerDependencies: - supports-color + dev: true /@typescript-eslint/scope-manager@5.62.0: resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} @@ -10283,6 +10312,7 @@ packages: dependencies: '@typescript-eslint/types': 6.17.0 '@typescript-eslint/visitor-keys': 6.17.0 + dev: true /@typescript-eslint/scope-manager@6.21.0: resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} @@ -10329,6 +10359,7 @@ packages: typescript: 5.3.3 transitivePeerDependencies: - supports-color + dev: true /@typescript-eslint/type-utils@5.62.0(eslint@7.32.0)(typescript@5.4.4): resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==} @@ -10349,7 +10380,6 @@ packages: transitivePeerDependencies: - supports-color dev: false - optional: true /@typescript-eslint/type-utils@6.17.0(eslint@7.0.0)(typescript@5.3.3): resolution: {integrity: sha512-hDXcWmnbtn4P2B37ka3nil3yi3VCQO2QEB9gBiHJmQp5wmyQWqnjA85+ZcE8c4FqnaB6lBwMrPkgd4aBYz3iNg==} @@ -10398,6 +10428,7 @@ packages: /@typescript-eslint/types@6.17.0: resolution: {integrity: sha512-qRKs9tvc3a4RBcL/9PXtKSehI/q8wuU9xYJxe97WFxnzH8NWWtcW3ffNS+EWg8uPvIerhjsEZ+rHtDqOCiH57A==} engines: {node: ^16.0.0 || >=18.0.0} + dev: true /@typescript-eslint/types@6.21.0: resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} @@ -10484,7 +10515,6 @@ packages: typescript: 5.4.4 transitivePeerDependencies: - supports-color - dev: false /@typescript-eslint/typescript-estree@6.17.0(typescript@5.3.3): resolution: {integrity: sha512-gVQe+SLdNPfjlJn5VNGhlOhrXz4cajwFd5kAgWtZ9dCZf4XJf8xmgCTLIqec7aha3JwgLI2CK6GY1043FRxZwg==} @@ -10506,6 +10536,7 @@ packages: typescript: 5.3.3 transitivePeerDependencies: - supports-color + dev: true /@typescript-eslint/typescript-estree@6.21.0(typescript@5.3.3): resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} @@ -10566,6 +10597,7 @@ packages: transitivePeerDependencies: - supports-color - typescript + dev: true /@typescript-eslint/utils@5.62.0(eslint@7.32.0)(typescript@5.4.4): resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} @@ -10586,7 +10618,6 @@ packages: - supports-color - typescript dev: false - optional: true /@typescript-eslint/utils@6.17.0(eslint@7.0.0)(typescript@5.3.3): resolution: {integrity: sha512-LofsSPjN/ITNkzV47hxas2JCsNCEnGhVvocfyOcLzT9c/tSZE7SfhS/iWtzP1lKNOEfLhRTZz6xqI8N2RzweSQ==} @@ -10658,6 +10689,7 @@ packages: dependencies: '@typescript-eslint/types': 6.17.0 eslint-visitor-keys: 3.4.3 + dev: true /@typescript-eslint/visitor-keys@6.21.0: resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} @@ -12524,7 +12556,7 @@ packages: loader-utils: 2.0.4 make-dir: 3.1.0 schema-utils: 2.7.1 - webpack: 5.91.0(esbuild@0.19.12) + webpack: 5.91.0 /babel-plugin-add-module-exports@1.0.4: resolution: {integrity: sha512-g+8yxHUZ60RcyaUpfNzy56OtWW+x9cyEe9j+CranqLiqbju2yf/Cy6ZtYK40EZxtrdHllzlVZgLmcOUCTlJ7Jg==} @@ -12630,7 +12662,7 @@ packages: '@babel/core': 7.24.4 '@babel/runtime': 7.24.4 '@babel/types': 7.24.0 - gatsby: 5.13.3(babel-eslint@10.1.0)(esbuild@0.19.12)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + gatsby: 5.13.3(babel-eslint@10.1.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.4) gatsby-core-utils: 4.13.1 dev: false @@ -14417,7 +14449,7 @@ packages: postcss-value-parser: 4.2.0 schema-utils: 3.3.0 semver: 7.6.0 - webpack: 5.91.0(esbuild@0.19.12) + webpack: 5.91.0 /css-minimizer-webpack-plugin@2.0.0(webpack@5.91.0): resolution: {integrity: sha512-cG/uc94727tx5pBNtb1Sd7gvUPzwmcQi1lkpfqTpdkuNq75hJCw7bIVsCNijLm4dhDcr1atvuysl2rZqOG8Txw==} @@ -14439,7 +14471,7 @@ packages: schema-utils: 3.3.0 serialize-javascript: 5.0.1 source-map: 0.6.1 - webpack: 5.91.0(esbuild@0.19.12) + webpack: 5.91.0 /css-select@4.3.0: resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} @@ -16570,6 +16602,7 @@ packages: '@esbuild/win32-arm64': 0.19.12 '@esbuild/win32-ia32': 0.19.12 '@esbuild/win32-x64': 0.19.12 + dev: true /esbuild@0.20.0: resolution: {integrity: sha512-6iwE3Y2RVYCME1jLpBqq7LQWK3MW6vjV2bZy6gt/WrqkY+WE74Spyc0ThAOYpMtITvnjX09CrC6ym7A/m9mebA==} @@ -16762,6 +16795,44 @@ packages: eslint-plugin-react: 7.34.1(eslint@7.32.0) eslint-plugin-react-hooks: 4.6.0(eslint@7.32.0) typescript: 5.3.3 + dev: true + + /eslint-config-react-app@6.0.0(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(babel-eslint@10.1.0)(eslint-plugin-flowtype@5.10.0)(eslint-plugin-import@2.29.1)(eslint-plugin-jsx-a11y@6.8.0)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.34.1)(eslint@7.32.0)(typescript@5.4.4): + resolution: {integrity: sha512-bpoAAC+YRfzq0dsTk+6v9aHm/uqnDwayNAXleMypGl6CpxI9oXXscVHo4fk3eJPIn+rsbtNetB4r/ZIidFIE8A==} + engines: {node: ^10.12.0 || >=12.0.0} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^4.0.0 + '@typescript-eslint/parser': ^4.0.0 + babel-eslint: ^10.0.0 + eslint: ^7.5.0 + eslint-plugin-flowtype: ^5.2.0 + eslint-plugin-import: ^2.22.0 + eslint-plugin-jest: ^24.0.0 + eslint-plugin-jsx-a11y: ^6.3.1 + eslint-plugin-react: ^7.20.3 + eslint-plugin-react-hooks: ^4.0.8 + eslint-plugin-testing-library: ^3.9.0 + typescript: '*' + peerDependenciesMeta: + eslint-plugin-jest: + optional: true + eslint-plugin-testing-library: + optional: true + typescript: + optional: true + dependencies: + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@7.32.0)(typescript@5.4.4) + '@typescript-eslint/parser': 5.62.0(eslint@7.32.0)(typescript@5.4.4) + babel-eslint: 10.1.0(eslint@7.32.0) + confusing-browser-globals: 1.0.11 + eslint: 7.32.0 + eslint-plugin-flowtype: 5.10.0(eslint@7.32.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0)(eslint@7.32.0) + eslint-plugin-jsx-a11y: 6.8.0(eslint@7.32.0) + eslint-plugin-react: 7.34.1(eslint@7.32.0) + eslint-plugin-react-hooks: 4.6.0(eslint@7.32.0) + typescript: 5.4.4 + dev: false /eslint-import-resolver-node@0.3.9: resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} @@ -16772,6 +16843,34 @@ packages: transitivePeerDependencies: - supports-color + /eslint-module-utils@2.8.1(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint@7.32.0): + resolution: {integrity: sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + '@typescript-eslint/parser': 5.62.0(eslint@7.32.0)(typescript@5.4.4) + debug: 3.2.7 + eslint: 7.32.0 + eslint-import-resolver-node: 0.3.9 + transitivePeerDependencies: + - supports-color + /eslint-module-utils@2.8.1(@typescript-eslint/parser@6.17.0)(eslint-import-resolver-node@0.3.9)(eslint@7.0.0): resolution: {integrity: sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==} engines: {node: '>=4'} @@ -16828,6 +16927,7 @@ packages: eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color + dev: true /eslint-plugin-flowtype@5.10.0(eslint@7.32.0): resolution: {integrity: sha512-vcz32f+7TP+kvTUyMXZmCnNujBQZDNmcqPImw8b9PZ+16w1Qdm6ryRuYZYVaG9xRqqmAPr2Cs9FAX5gN+x/bjw==} @@ -16871,7 +16971,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.62.0(eslint@7.32.0)(typescript@5.3.3) + '@typescript-eslint/parser': 5.62.0(eslint@7.32.0)(typescript@5.4.4) array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 array.prototype.flat: 1.3.2 @@ -16880,7 +16980,7 @@ packages: doctrine: 2.1.0 eslint: 7.32.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.17.0)(eslint-import-resolver-node@0.3.9)(eslint@7.32.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint@7.32.0) hasown: 2.0.2 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -16963,6 +17063,7 @@ packages: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color + dev: true /eslint-plugin-jsx-a11y@6.8.0(eslint@7.32.0): resolution: {integrity: sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==} @@ -17175,7 +17276,7 @@ packages: micromatch: 4.0.5 normalize-path: 3.0.0 schema-utils: 3.3.0 - webpack: 5.91.0(esbuild@0.19.12) + webpack: 5.91.0 /eslint@7.0.0: resolution: {integrity: sha512-qY1cwdOxMONHJfGqw52UOpZDeqXy8xmD0u8CT6jIstil72jkhURC704W8CFyTPDPllz4z4lu0Ql1+07PG/XdIg==} @@ -17979,7 +18080,7 @@ packages: dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.91.0(esbuild@0.19.12) + webpack: 5.91.0 /file-system-cache@2.3.0: resolution: {integrity: sha512-l4DMNdsIPsVnKrgEXbJwDJsA5mB8rGwHYERMgqQx/xAUtChPJMre1bXBzDEqqVbWv9AIbFezXMxeEkZDSrXUOQ==} @@ -18349,6 +18450,39 @@ packages: tapable: 1.1.3 typescript: 5.3.3 webpack: 5.91.0(esbuild@0.19.12) + dev: true + + /fork-ts-checker-webpack-plugin@6.5.3(eslint@7.32.0)(typescript@5.4.4)(webpack@5.91.0): + resolution: {integrity: sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==} + engines: {node: '>=10', yarn: '>=1.0.0'} + peerDependencies: + eslint: '>= 6' + typescript: '>= 2.7' + vue-template-compiler: '*' + webpack: '>= 4' + peerDependenciesMeta: + eslint: + optional: true + vue-template-compiler: + optional: true + dependencies: + '@babel/code-frame': 7.24.2 + '@types/json-schema': 7.0.15 + chalk: 4.1.2 + chokidar: 3.6.0 + cosmiconfig: 6.0.0 + deepmerge: 4.3.1 + eslint: 7.32.0 + fs-extra: 9.1.0 + glob: 7.2.3 + memfs: 3.5.3 + minimatch: 3.1.2 + schema-utils: 2.7.0 + semver: 7.6.0 + tapable: 1.1.3 + typescript: 5.4.4 + webpack: 5.91.0 + dev: false /form-data-encoder@2.1.4: resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} @@ -18623,7 +18757,7 @@ packages: dependencies: '@types/node-fetch': 2.6.11 fs-extra: 9.1.0 - gatsby: 5.13.3(babel-eslint@10.1.0)(esbuild@0.19.12)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + gatsby: 5.13.3(babel-eslint@10.1.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.4) lodash: 4.17.21 node-fetch: 2.7.0 p-queue: 6.6.2 @@ -18785,7 +18919,7 @@ packages: chokidar: 3.6.0 fs-exists-cached: 1.0.0 fs-extra: 11.2.0 - gatsby: 5.13.3(babel-eslint@10.1.0)(esbuild@0.19.12)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + gatsby: 5.13.3(babel-eslint@10.1.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.4) gatsby-core-utils: 4.13.1 gatsby-page-utils: 3.13.1 gatsby-plugin-utils: 4.13.1(gatsby@5.13.3)(graphql@16.8.1) @@ -18854,7 +18988,7 @@ packages: debug: 4.3.4 filenamify: 4.3.0 fs-extra: 11.2.0 - gatsby: 5.13.3(babel-eslint@10.1.0)(esbuild@0.19.12)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + gatsby: 5.13.3(babel-eslint@10.1.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.4) gatsby-core-utils: 4.13.1 gatsby-plugin-utils: 4.13.1(gatsby@5.13.3)(graphql@16.8.1) lodash: 4.17.21 @@ -18913,7 +19047,7 @@ packages: '@babel/preset-typescript': 7.24.1(@babel/core@7.24.4) '@babel/runtime': 7.24.4 babel-plugin-remove-graphql-queries: 5.13.1(@babel/core@7.24.4)(gatsby@5.13.3) - gatsby: 5.13.3(babel-eslint@10.1.0)(esbuild@0.19.12)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + gatsby: 5.13.3(babel-eslint@10.1.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.4) transitivePeerDependencies: - supports-color dev: false @@ -18951,7 +19085,7 @@ packages: '@babel/runtime': 7.24.4 fastq: 1.17.1 fs-extra: 11.2.0 - gatsby: 5.13.3(babel-eslint@10.1.0)(esbuild@0.19.12)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + gatsby: 5.13.3(babel-eslint@10.1.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.4) gatsby-core-utils: 4.13.1 gatsby-sharp: 1.13.0 graphql: 16.8.1 @@ -19453,7 +19587,7 @@ packages: - webpack-hot-middleware - webpack-plugin-serve - /gatsby@5.13.3(babel-eslint@10.1.0)(esbuild@0.19.12)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3): + /gatsby@5.13.3(babel-eslint@10.1.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.4): resolution: {integrity: sha512-SSnGpjswK20BQORcvTbtK8eI+W4QUG+u8rdVswB4suva6BfvTakW2wiktj7E2MdO4NjRvlgJjF5dUUncU5nldA==} engines: {node: '>=18.0.0'} hasBin: true @@ -19487,8 +19621,8 @@ packages: '@pmmmwh/react-refresh-webpack-plugin': 0.5.11(react-refresh@0.14.0)(webpack@5.91.0) '@sigmacomputing/babel-plugin-lodash': 3.3.5 '@types/http-proxy': 1.17.14 - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@7.32.0)(typescript@5.3.3) - '@typescript-eslint/parser': 5.62.0(eslint@7.32.0)(typescript@5.3.3) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@7.32.0)(typescript@5.4.4) + '@typescript-eslint/parser': 5.62.0(eslint@7.32.0)(typescript@5.4.4) '@vercel/webpack-asset-relocator-loader': 1.7.3 acorn-loose: 8.4.0 acorn-walk: 8.3.2 @@ -19526,9 +19660,9 @@ packages: enhanced-resolve: 5.16.0 error-stack-parser: 2.1.4 eslint: 7.32.0 - eslint-config-react-app: 6.0.0(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(babel-eslint@10.1.0)(eslint-plugin-flowtype@5.10.0)(eslint-plugin-import@2.29.1)(eslint-plugin-jsx-a11y@6.8.0)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.34.1)(eslint@7.32.0)(typescript@5.3.3) + eslint-config-react-app: 6.0.0(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(babel-eslint@10.1.0)(eslint-plugin-flowtype@5.10.0)(eslint-plugin-import@2.29.1)(eslint-plugin-jsx-a11y@6.8.0)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.34.1)(eslint@7.32.0)(typescript@5.4.4) eslint-plugin-flowtype: 5.10.0(eslint@7.32.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.17.0)(eslint@7.32.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0)(eslint@7.32.0) eslint-plugin-jsx-a11y: 6.8.0(eslint@7.32.0) eslint-plugin-react: 7.34.1(eslint@7.32.0) eslint-plugin-react-hooks: 4.6.0(eslint@7.32.0) @@ -19600,7 +19734,7 @@ packages: query-string: 6.14.1 raw-loader: 4.0.2(webpack@5.91.0) react: 18.2.0 - react-dev-utils: 12.0.1(eslint@7.32.0)(typescript@5.3.3)(webpack@5.91.0) + react-dev-utils: 12.0.1(eslint@7.32.0)(typescript@5.4.4)(webpack@5.91.0) react-dom: 18.2.0(react@18.2.0) react-refresh: 0.14.0 react-server-dom-webpack: 0.0.0-experimental-c8b778b7f-20220825(react@18.2.0)(webpack@5.91.0) @@ -19618,13 +19752,13 @@ packages: strip-ansi: 6.0.1 style-loader: 2.0.0(webpack@5.91.0) style-to-object: 0.4.4 - terser-webpack-plugin: 5.3.10(esbuild@0.19.12)(webpack@5.91.0) + terser-webpack-plugin: 5.3.10(webpack@5.91.0) tmp: 0.2.3 true-case-path: 2.2.1 type-of: 2.0.1 url-loader: 4.1.1(file-loader@6.2.0)(webpack@5.91.0) uuid: 8.3.2 - webpack: 5.91.0(esbuild@0.19.12) + webpack: 5.91.0 webpack-dev-middleware: 4.3.0(webpack@5.91.0) webpack-merge: 5.10.0 webpack-stats-plugin: 1.1.3 @@ -24269,7 +24403,7 @@ packages: dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.91.0(esbuild@0.19.12) + webpack: 5.91.0 webpack-sources: 1.4.3 /mini-svg-data-uri@1.4.4: @@ -24299,6 +24433,7 @@ packages: engines: {node: '>=16 || 14 >=14.17'} dependencies: brace-expansion: 2.0.1 + dev: true /minimatch@9.0.4: resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==} @@ -25121,7 +25256,7 @@ packages: dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.91.0(esbuild@0.19.12) + webpack: 5.91.0 /nullthrows@1.1.1: resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==} @@ -26376,7 +26511,7 @@ packages: klona: 2.0.6 postcss: 8.4.38 semver: 7.6.0 - webpack: 5.91.0(esbuild@0.19.12) + webpack: 5.91.0 /postcss-merge-longhand@5.1.7(postcss@8.4.38): resolution: {integrity: sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==} @@ -27371,7 +27506,7 @@ packages: dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.91.0(esbuild@0.19.12) + webpack: 5.91.0 /rbush@3.0.1: resolution: {integrity: sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==} @@ -27537,6 +27672,49 @@ packages: - eslint - supports-color - vue-template-compiler + dev: true + + /react-dev-utils@12.0.1(eslint@7.32.0)(typescript@5.4.4)(webpack@5.91.0): + resolution: {integrity: sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=2.7' + webpack: '>=4' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@babel/code-frame': 7.24.2 + address: 1.2.2 + browserslist: 4.23.0 + chalk: 4.1.2 + cross-spawn: 7.0.3 + detect-port-alt: 1.1.6 + escape-string-regexp: 4.0.0 + filesize: 8.0.7 + find-up: 5.0.0 + fork-ts-checker-webpack-plugin: 6.5.3(eslint@7.32.0)(typescript@5.4.4)(webpack@5.91.0) + global-modules: 2.0.0 + globby: 11.1.0 + gzip-size: 6.0.0 + immer: 9.0.21 + is-root: 2.1.0 + loader-utils: 3.2.1 + open: 8.4.2 + pkg-up: 3.1.0 + prompts: 2.4.2 + react-error-overlay: 6.0.11 + recursive-readdir: 2.2.3 + shell-quote: 1.8.1 + strip-ansi: 6.0.1 + text-table: 0.2.0 + typescript: 5.4.4 + webpack: 5.91.0 + transitivePeerDependencies: + - eslint + - supports-color + - vue-template-compiler + dev: false /react-dnd-html5-backend@14.1.0: resolution: {integrity: sha512-6ONeqEC3XKVf4eVmMTe0oPds+c5B9Foyj8p/ZKLb7kL2qh9COYxiBHv3szd6gztqi/efkmriywLUVlPotqoJyw==} @@ -27936,7 +28114,7 @@ packages: loose-envify: 1.4.0 neo-async: 2.6.2 react: 18.2.0 - webpack: 5.91.0(esbuild@0.19.12) + webpack: 5.91.0 /react-split-pane@0.1.92(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-GfXP1xSzLMcLJI5BM36Vh7GgZBpy+U/X0no+VM3fxayv+p1Jly5HpMofZJraeaMl73b3hvlr+N9zJKvLB/uz9w==} @@ -30290,7 +30468,7 @@ packages: dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.91.0(esbuild@0.19.12) + webpack: 5.91.0 /style-to-object@0.3.0: resolution: {integrity: sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==} @@ -30682,6 +30860,7 @@ packages: serialize-javascript: 6.0.2 terser: 5.30.3 webpack: 5.91.0(esbuild@0.19.12) + dev: true /terser-webpack-plugin@5.3.10(webpack@5.91.0): resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==} @@ -31026,6 +31205,7 @@ packages: typescript: '>=4.2.0' dependencies: typescript: 5.3.3 + dev: true /ts-dedent@2.2.0: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} @@ -31174,7 +31354,6 @@ packages: dependencies: tslib: 1.14.1 typescript: 5.4.4 - dev: false /tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} @@ -31846,7 +32025,7 @@ packages: loader-utils: 2.0.4 mime-types: 2.1.35 schema-utils: 3.3.0 - webpack: 5.91.0(esbuild@0.19.12) + webpack: 5.91.0 /url@0.11.3: resolution: {integrity: sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==} @@ -32817,7 +32996,7 @@ packages: mime-types: 2.1.35 range-parser: 1.2.1 schema-utils: 3.3.0 - webpack: 5.91.0(esbuild@0.19.12) + webpack: 5.91.0 /webpack-merge@5.10.0: resolution: {integrity: sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==} @@ -32964,6 +33143,7 @@ packages: - '@swc/core' - esbuild - uglify-js + dev: true /website-scraper@5.3.1: resolution: {integrity: sha512-gogqPXD2gVsxoyd2yRiympw3rA5GuEpD1CaDEJ/J8zzanx7hkbTtneoO1SGs436PpLbWVcUge+6APGLhzsuZPA==} diff --git a/tests/schema/specs/blocks.spec.ts b/tests/schema/specs/blocks.spec.ts index 9a246e3f6..4b86529d4 100644 --- a/tests/schema/specs/blocks.spec.ts +++ b/tests/schema/specs/blocks.spec.ts @@ -53,6 +53,15 @@ test('Blocks', async () => { text openInNewTab } + ... on BlockImageWithText { + image { + __typename + } + textContent { + __typename + markup + } + } } } { @@ -106,6 +115,28 @@ test('Blocks', async () => { "__typename": "BlockForm", "url": "http://127.0.0.1:8000/en/form/contact", }, + { + "__typename": "BlockImageWithText", + "image": { + "__typename": "MediaImage", + }, + "textContent": { + "__typename": "BlockMarkup", + "markup": " +

All kinds of allowed blocks

+ +
  • bla
+ +

Heading

+ +
12
34
Caption
+ +

Quote

Citation
+ +

+ ", + }, + }, { "__typename": "BlockMarkup", "markup": " @@ -209,6 +240,16 @@ test('Blocks', async () => { "text": null, "url": null, }, + { + "__typename": "BlockImageWithText", + "image": null, + "textContent": { + "__typename": "BlockMarkup", + "markup": " +

+ ", + }, + }, ], "hero": { "__typename": "Hero", From 16236d3665213ae29821bbad52b2bcd65391ce73 Mon Sep 17 00:00:00 2001 From: Alex Tkachev Date: Wed, 24 Apr 2024 14:14:29 +0400 Subject: [PATCH 57/82] chore(SLB-289): add imagePosition to the schema --- .../content/node/a397ca48-8fad-411e-8901-0eba2feb989c.yml | 3 ++- packages/schema/src/schema.graphql | 7 +++++++ tests/schema/specs/blocks.spec.ts | 3 +++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/drupal/test_content/content/node/a397ca48-8fad-411e-8901-0eba2feb989c.yml b/packages/drupal/test_content/content/node/a397ca48-8fad-411e-8901-0eba2feb989c.yml index 43e5fe62d..61b3a4ac9 100644 --- a/packages/drupal/test_content/content/node/a397ca48-8fad-411e-8901-0eba2feb989c.yml +++ b/packages/drupal/test_content/content/node/a397ca48-8fad-411e-8901-0eba2feb989c.yml @@ -60,7 +60,7 @@ default: - +

All kinds of allowed blocks

@@ -117,6 +117,7 @@ default: +

diff --git a/packages/schema/src/schema.graphql b/packages/schema/src/schema.graphql index 3d86b6850..e3b54ace8 100644 --- a/packages/schema/src/schema.graphql +++ b/packages/schema/src/schema.graphql @@ -247,9 +247,16 @@ type BlockCta @type(id: "custom/cta") { type BlockImageWithText @type(id: "custom/image-with-text") { image: MediaImage @resolveEditorBlockMedia + imagePosition: ImagePosition! + @resolveEditorBlockAttribute(key: "imagePosition") textContent: BlockMarkup @resolveEditorBlockChildren @seek(pos: 0) } +enum ImagePosition @default @value(string: "left") { + left + right +} + input PaginationInput { limit: Int! offset: Int! diff --git a/tests/schema/specs/blocks.spec.ts b/tests/schema/specs/blocks.spec.ts index 4b86529d4..b5cf47cf5 100644 --- a/tests/schema/specs/blocks.spec.ts +++ b/tests/schema/specs/blocks.spec.ts @@ -57,6 +57,7 @@ test('Blocks', async () => { image { __typename } + imagePosition textContent { __typename markup @@ -120,6 +121,7 @@ test('Blocks', async () => { "image": { "__typename": "MediaImage", }, + "imagePosition": "right", "textContent": { "__typename": "BlockMarkup", "markup": " @@ -243,6 +245,7 @@ test('Blocks', async () => { { "__typename": "BlockImageWithText", "image": null, + "imagePosition": "left", "textContent": { "__typename": "BlockMarkup", "markup": " From 958724e36b47be435c7c5289939d830335582400 Mon Sep 17 00:00:00 2001 From: Alex Tkachev Date: Wed, 24 Apr 2024 14:58:26 +0400 Subject: [PATCH 58/82] chore(SLB-335): update Drupal to 10.2.5 To fix https://www.drupal.org/project/drupal/issues/3394048 --- apps/cms/composer.lock | 44 +++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/apps/cms/composer.lock b/apps/cms/composer.lock index 07ca8aaa5..e913103c2 100644 --- a/apps/cms/composer.lock +++ b/apps/cms/composer.lock @@ -2371,16 +2371,16 @@ }, { "name": "drupal/core", - "version": "10.2.3", + "version": "10.2.5", "source": { "type": "git", "url": "https://github.com/drupal/core.git", - "reference": "cc8c7952f7013795b735f5c15290e76937163bb7" + "reference": "dddd242b74f40df892a7f16a48245c3b76d9b003" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/drupal/core/zipball/cc8c7952f7013795b735f5c15290e76937163bb7", - "reference": "cc8c7952f7013795b735f5c15290e76937163bb7", + "url": "https://api.github.com/repos/drupal/core/zipball/dddd242b74f40df892a7f16a48245c3b76d9b003", + "reference": "dddd242b74f40df892a7f16a48245c3b76d9b003", "shasum": "" }, "require": { @@ -2528,13 +2528,13 @@ ], "description": "Drupal is an open source content management platform powering millions of websites and applications.", "support": { - "source": "https://github.com/drupal/core/tree/10.2.3" + "source": "https://github.com/drupal/core/tree/10.2.5" }, - "time": "2024-02-07T22:44:48+00:00" + "time": "2024-04-03T07:19:20+00:00" }, { "name": "drupal/core-composer-scaffold", - "version": "10.2.3", + "version": "10.2.5", "source": { "type": "git", "url": "https://github.com/drupal/core-composer-scaffold.git", @@ -2578,22 +2578,22 @@ "drupal" ], "support": { - "source": "https://github.com/drupal/core-composer-scaffold/tree/10.2.3" + "source": "https://github.com/drupal/core-composer-scaffold/tree/10.2.5" }, "time": "2024-01-26T14:59:30+00:00" }, { "name": "drupal/core-recommended", - "version": "10.2.3", + "version": "10.2.5", "source": { "type": "git", "url": "https://github.com/drupal/core-recommended.git", - "reference": "ee5d148455ca5792108a1fd007ae162ea2ffe859" + "reference": "bd7fe9e734a82762814d9c31255cd362d9c044f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/drupal/core-recommended/zipball/ee5d148455ca5792108a1fd007ae162ea2ffe859", - "reference": "ee5d148455ca5792108a1fd007ae162ea2ffe859", + "url": "https://api.github.com/repos/drupal/core-recommended/zipball/bd7fe9e734a82762814d9c31255cd362d9c044f1", + "reference": "bd7fe9e734a82762814d9c31255cd362d9c044f1", "shasum": "" }, "require": { @@ -2602,7 +2602,7 @@ "doctrine/annotations": "~1.14.3", "doctrine/deprecations": "~1.1.2", "doctrine/lexer": "~2.1.0", - "drupal/core": "10.2.3", + "drupal/core": "10.2.5", "egulias/email-validator": "~4.0.2", "guzzlehttp/guzzle": "~7.8.1", "guzzlehttp/promises": "~2.0.2", @@ -2663,9 +2663,9 @@ ], "description": "Core and its dependencies with known-compatible minor versions. Require this project INSTEAD OF drupal/core.", "support": { - "source": "https://github.com/drupal/core-recommended/tree/10.2.3" + "source": "https://github.com/drupal/core-recommended/tree/10.2.5" }, - "time": "2024-02-07T22:44:48+00:00" + "time": "2024-04-03T07:19:20+00:00" }, { "name": "drupal/ctools", @@ -11254,16 +11254,16 @@ }, { "name": "drupal/core-dev", - "version": "10.2.3", + "version": "10.2.5", "source": { "type": "git", "url": "https://github.com/drupal/core-dev.git", - "reference": "8c1bf854f2cf47d4f06918099ea866ce2471b2c6" + "reference": "71d714deff8c277b8ef2f331f3bddbba03274dc1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/drupal/core-dev/zipball/8c1bf854f2cf47d4f06918099ea866ce2471b2c6", - "reference": "8c1bf854f2cf47d4f06918099ea866ce2471b2c6", + "url": "https://api.github.com/repos/drupal/core-dev/zipball/71d714deff8c277b8ef2f331f3bddbba03274dc1", + "reference": "71d714deff8c277b8ef2f331f3bddbba03274dc1", "shasum": "" }, "require": { @@ -11271,7 +11271,7 @@ "behat/mink-browserkit-driver": "^2.1", "behat/mink-selenium2-driver": "^1.4", "colinodell/psr-testlogger": "^1.2", - "composer/composer": "^2.6.4", + "composer/composer": "^2.7", "drupal/coder": "^8.3.10", "instaclick/php-webdriver": "^1.4.1", "justinrainbow/json-schema": "^5.2", @@ -11304,9 +11304,9 @@ ], "description": "require-dev dependencies from drupal/drupal; use in addition to drupal/core-recommended to run tests from drupal/core.", "support": { - "source": "https://github.com/drupal/core-dev/tree/10.2.3" + "source": "https://github.com/drupal/core-dev/tree/10.2.5" }, - "time": "2023-12-12T22:01:45+00:00" + "time": "2024-02-14T18:07:20+00:00" }, { "name": "google/protobuf", From 7ebfcaf072e3ea8924f3a1cf972906fcb9692546 Mon Sep 17 00:00:00 2001 From: Mattia Simonato Date: Wed, 24 Apr 2024 18:01:25 +0200 Subject: [PATCH 59/82] fix(SLB-345): decrease title font size and remove max-width --- .../ui/src/components/Organisms/PageHero.tsx | 54 +++++++++---------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/packages/ui/src/components/Organisms/PageHero.tsx b/packages/ui/src/components/Organisms/PageHero.tsx index c7c18b202..ca227836c 100644 --- a/packages/ui/src/components/Organisms/PageHero.tsx +++ b/packages/ui/src/components/Organisms/PageHero.tsx @@ -115,36 +115,34 @@ function FormHero(props: NonNullable) { function NoImageHero(props: NonNullable) { return ( -
+
-
-

- {props.headline} -

- {props.lead ? ( -

{props.lead}

- ) : null} - {props.ctaText && props.ctaUrl ? ( - + {props.headline} + + {props.lead ? ( +

{props.lead}

+ ) : null} + {props.ctaText && props.ctaUrl ? ( + + - - - - {props.ctaText} - - ) : null} -
+ + + {props.ctaText} + + ) : null}
); From f044a2d6dc83655f95334cd8fe31b7b693556a42 Mon Sep 17 00:00:00 2001 From: Mattia Simonato Date: Wed, 24 Apr 2024 18:06:25 +0200 Subject: [PATCH 60/82] fix(SLB-346): reduce vertical spacing between Hero and content --- .../src/components/Organisms/PageDisplay.tsx | 92 +++++++++---------- 1 file changed, 45 insertions(+), 47 deletions(-) diff --git a/packages/ui/src/components/Organisms/PageDisplay.tsx b/packages/ui/src/components/Organisms/PageDisplay.tsx index 975d447c0..1b3a2d212 100644 --- a/packages/ui/src/components/Organisms/PageDisplay.tsx +++ b/packages/ui/src/components/Organisms/PageDisplay.tsx @@ -15,54 +15,52 @@ export function PageDisplay(page: PageFragment) {
{page.hero ? : null} -
+
-
- {page?.content?.filter(isTruthy).map((block, index) => { - switch (block.__typename) { - case 'BlockMedia': - return ; - case 'BlockMarkup': - return ; - case 'BlockForm': - return ; - case 'BlockImageTeasers': - return ( - // TODO: Implement BlockImageTeasers -
- BlockImageTeasers goes here -
- ); - case 'BlockCta': - return ; - case 'BlockImageWithText': - return ( - // TODO: Implement BlockImageWithText -
- BlockImageWithText goes here -
- ); - default: - throw new UnreachableCaseError(block); - } - })} -
+ {page?.content?.filter(isTruthy).map((block, index) => { + switch (block.__typename) { + case 'BlockMedia': + return ; + case 'BlockMarkup': + return ; + case 'BlockForm': + return ; + case 'BlockImageTeasers': + return ( + // TODO: Implement BlockImageTeasers +
+ BlockImageTeasers goes here +
+ ); + case 'BlockCta': + return ; + case 'BlockImageWithText': + return ( + // TODO: Implement BlockImageWithText +
+ BlockImageWithText goes here +
+ ); + default: + throw new UnreachableCaseError(block); + } + })}
From f3e702125d8d0f8d505dd3cb070f319e25589f65 Mon Sep 17 00:00:00 2001 From: Mattia Simonato Date: Thu, 25 Apr 2024 07:26:22 +0200 Subject: [PATCH 61/82] fix(SLB-337-334): add dark overlay and vertically center the content --- .../ui/src/components/Organisms/PageHero.tsx | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/ui/src/components/Organisms/PageHero.tsx b/packages/ui/src/components/Organisms/PageHero.tsx index ca227836c..55a1088ca 100644 --- a/packages/ui/src/components/Organisms/PageHero.tsx +++ b/packages/ui/src/components/Organisms/PageHero.tsx @@ -62,18 +62,21 @@ function DefaultHero(props: NonNullable) { function FormHero(props: NonNullable) { return (
-
+
{props.image ? ( - {props.image.alt} + <> + {props.image.alt} +
+ ) : null} -
+

{props.headline}

@@ -117,7 +120,7 @@ function NoImageHero(props: NonNullable) { return (
-

+

{props.headline}

{props.lead ? ( From 71b1e8a9da3c8a2faea3f1e626ed6007c41cf6a6 Mon Sep 17 00:00:00 2001 From: Vasile Chindris Date: Tue, 16 Apr 2024 17:13:15 +0300 Subject: [PATCH 62/82] feat(SLB-297 SLB-296): replace the core quote block with a custom one --- apps/cms/config/sync/gutenberg.settings.yml | 6 +- packages/drupal/gutenberg_blocks/css/edit.css | 5 + .../GutenbergValidator/QuoteValidator.php | 40 +++++++ .../gutenberg_blocks/src/blocks/quote.tsx | 100 ++++++++++++++++++ packages/drupal/gutenberg_blocks/src/index.ts | 1 + 5 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 packages/drupal/gutenberg_blocks/src/Plugin/Validation/GutenbergValidator/QuoteValidator.php create mode 100644 packages/drupal/gutenberg_blocks/src/blocks/quote.tsx diff --git a/apps/cms/config/sync/gutenberg.settings.yml b/apps/cms/config/sync/gutenberg.settings.yml index e58422162..ed9ef9d7d 100644 --- a/apps/cms/config/sync/gutenberg.settings.yml +++ b/apps/cms/config/sync/gutenberg.settings.yml @@ -4,12 +4,12 @@ page_template_lock: all page_allowed_blocks: core/paragraph: core/paragraph core/list: core/list - core/quote: core/quote core/table: core/table core/all: 0 core/image: 0 core/heading: 0 core/gallery: 0 + core/quote: 0 core/audio: 0 core/button: 0 core/buttons: 0 @@ -75,6 +75,7 @@ page_allowed_drupal_blocks: drupalblock/all_core: 0 page_title_block: 0 drupalblock/all_forms: 0 + masquerade: 0 user_login_block: 0 drupalblock/all_lists_views_: 0 'views_block:content_recent-block_1': 0 @@ -91,3 +92,6 @@ page_allowed_drupal_blocks: system_branding_block: 0 node_syndicate_block: 0 drupalblock/all_user: 0 + drupalblock/all_webform: 0 + webform_block: 0 + webform_submission_limit_block: 0 diff --git a/packages/drupal/gutenberg_blocks/css/edit.css b/packages/drupal/gutenberg_blocks/css/edit.css index f4a49c277..1c603451e 100644 --- a/packages/drupal/gutenberg_blocks/css/edit.css +++ b/packages/drupal/gutenberg_blocks/css/edit.css @@ -12,6 +12,11 @@ content: ''; } +.gutenberg__editor blockquote .quote-author, +.gutenberg__editor blockquote .quote-role { + text-align: right; +} + .gutenberg__editor .container-wrapper { display: block; position: relative; diff --git a/packages/drupal/gutenberg_blocks/src/Plugin/Validation/GutenbergValidator/QuoteValidator.php b/packages/drupal/gutenberg_blocks/src/Plugin/Validation/GutenbergValidator/QuoteValidator.php new file mode 100644 index 000000000..1f9f9d9c9 --- /dev/null +++ b/packages/drupal/gutenberg_blocks/src/Plugin/Validation/GutenbergValidator/QuoteValidator.php @@ -0,0 +1,40 @@ + [ + 'field_label' => $this->t('Quote text'), + 'rules' => ['required'], + ], + 'author' => [ + 'field_label' => $this->t('Quote author'), + 'rules' => ['required'], + ], + ]; + } + +} diff --git a/packages/drupal/gutenberg_blocks/src/blocks/quote.tsx b/packages/drupal/gutenberg_blocks/src/blocks/quote.tsx new file mode 100644 index 000000000..1dacdf98b --- /dev/null +++ b/packages/drupal/gutenberg_blocks/src/blocks/quote.tsx @@ -0,0 +1,100 @@ +import { RichText } from 'wordpress__block-editor'; +import { registerBlockType } from 'wordpress__blocks'; +import { compose, withState } from 'wordpress__compose'; +import { dispatch } from 'wordpress__data'; + +import { cleanUpText } from '../utils/clean-up-text'; +import { DrupalMediaEntity } from '../utils/drupal-media'; + +declare const Drupal: { t: (s: string) => string }; + +const { t: __ } = Drupal; + +// @ts-ignore +const { setPlainTextAttribute } = silverbackGutenbergUtils; + +// @ts-ignore +registerBlockType(`custom/quote`, { + title: __('Quote'), + icon: 'format-quote', + category: 'text', + attributes: { + text: { + type: 'string', + }, + author: { + tpye: 'string', + }, + role: { + type: 'string', + }, + mediaEntityIds: { + type: 'array', + }, + }, + // @ts-ignore + edit: compose(withState())((props) => { + return ( +
+ { + props.setAttributes({ + text: cleanUpText(text), + }); + }} + /> + { + setPlainTextAttribute(props, 'author', author); + }} + /> + { + setPlainTextAttribute(props, 'role', role); + }} + /> + { + // @ts-ignore + error = typeof error === 'string' ? error : error[2]; + dispatch('core/notices').createWarningNotice(error); + }} + /> +
+ ); + }), + save() { + return null; + }, +}); diff --git a/packages/drupal/gutenberg_blocks/src/index.ts b/packages/drupal/gutenberg_blocks/src/index.ts index 6eae568bf..2763bdad8 100644 --- a/packages/drupal/gutenberg_blocks/src/index.ts +++ b/packages/drupal/gutenberg_blocks/src/index.ts @@ -8,3 +8,4 @@ import './blocks/image-teasers'; import './blocks/image-with-text'; import './filters/list'; import './blocks/cta'; +import './blocks/quote'; From b5de2661bf1c88ed9ffad64eec007bd46f8c0feb Mon Sep 17 00:00:00 2001 From: Vasile Chindris Date: Tue, 16 Apr 2024 17:28:27 +0300 Subject: [PATCH 63/82] feat(SLB-297 SLB-296): quote graphql block type --- packages/drupal/gutenberg_blocks/src/blocks/quote.tsx | 2 +- packages/schema/src/schema.graphql | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/drupal/gutenberg_blocks/src/blocks/quote.tsx b/packages/drupal/gutenberg_blocks/src/blocks/quote.tsx index 1dacdf98b..f42c3f82d 100644 --- a/packages/drupal/gutenberg_blocks/src/blocks/quote.tsx +++ b/packages/drupal/gutenberg_blocks/src/blocks/quote.tsx @@ -46,7 +46,7 @@ registerBlockType(`custom/quote`, { keepPlaceholderOnFocus={false} onChange={(text) => { props.setAttributes({ - text: cleanUpText(text), + text: cleanUpText(text, ['strong']), }); }} /> diff --git a/packages/schema/src/schema.graphql b/packages/schema/src/schema.graphql index e3b54ace8..15f964aba 100644 --- a/packages/schema/src/schema.graphql +++ b/packages/schema/src/schema.graphql @@ -200,6 +200,7 @@ union PageContent @resolveEditorBlockType = | BlockImageTeasers | BlockCta | BlockImageWithText + | BlockQuote type BlockForm @type(id: "custom/form") { url: Url @resolveEditorBlockAttribute(key: "formId") @webformIdToUrl(id: "$") @@ -257,6 +258,13 @@ enum ImagePosition @default @value(string: "left") { right } +type BlockQuote @type(id: "custom/quote") { + text: Markup @resolveEditorBlockAttribute(key: "text") + author: String @resolveEditorBlockAttribute(key: "author") + role: String @resolveEditorBlockAttribute(key: "role") + image: MediaImage @resolveEditorBlockMedia +} + input PaginationInput { limit: Int! offset: Int! From dbd22bd9df2a69ee8e88a0b7b8de3067d12c089a Mon Sep 17 00:00:00 2001 From: Vasile Chindris Date: Tue, 16 Apr 2024 17:42:21 +0300 Subject: [PATCH 64/82] feat(SLB-296): organism placeholder for quote blocks --- packages/schema/src/fragments/Page.gql | 1 + packages/schema/src/fragments/PageContent/BlockQuote.gql | 9 +++++++++ .../src/components/Organisms/PageContent/BlockQuote.tsx | 7 +++++++ packages/ui/src/components/Organisms/PageDisplay.tsx | 3 +++ 4 files changed, 20 insertions(+) create mode 100644 packages/schema/src/fragments/PageContent/BlockQuote.gql create mode 100644 packages/ui/src/components/Organisms/PageContent/BlockQuote.tsx diff --git a/packages/schema/src/fragments/Page.gql b/packages/schema/src/fragments/Page.gql index 0a78d9dea..cbf2227eb 100644 --- a/packages/schema/src/fragments/Page.gql +++ b/packages/schema/src/fragments/Page.gql @@ -34,6 +34,7 @@ fragment Page on Page { ...BlockImageTeasers ...BlockCta ...BlockImageWithText + ...BlockQuote } metaTags { tag diff --git a/packages/schema/src/fragments/PageContent/BlockQuote.gql b/packages/schema/src/fragments/PageContent/BlockQuote.gql new file mode 100644 index 000000000..b50478e0a --- /dev/null +++ b/packages/schema/src/fragments/PageContent/BlockQuote.gql @@ -0,0 +1,9 @@ +fragment BlockQuote on BlockQuote { + text + author + role + image { + source(width: 1536, sizes: [[768, 768], [1536, 1536]]) + alt + } +} diff --git a/packages/ui/src/components/Organisms/PageContent/BlockQuote.tsx b/packages/ui/src/components/Organisms/PageContent/BlockQuote.tsx new file mode 100644 index 000000000..4676f6302 --- /dev/null +++ b/packages/ui/src/components/Organisms/PageContent/BlockQuote.tsx @@ -0,0 +1,7 @@ +import { BlockQuoteFragment } from '@custom/schema'; +import React from 'react'; + +export function BlockQuote(props: BlockQuoteFragment) { + console.log('Block quote props:', props); + return <>; +} diff --git a/packages/ui/src/components/Organisms/PageDisplay.tsx b/packages/ui/src/components/Organisms/PageDisplay.tsx index 1b3a2d212..86813de03 100644 --- a/packages/ui/src/components/Organisms/PageDisplay.tsx +++ b/packages/ui/src/components/Organisms/PageDisplay.tsx @@ -8,6 +8,7 @@ import { BlockCta } from './PageContent/BlockCta'; import { BlockForm } from './PageContent/BlockForm'; import { BlockMarkup } from './PageContent/BlockMarkup'; import { BlockMedia } from './PageContent/BlockMedia'; +import { BlockQuote } from './PageContent/BlockQuote'; import { PageHero } from './PageHero'; export function PageDisplay(page: PageFragment) { @@ -57,6 +58,8 @@ export function PageDisplay(page: PageFragment) { BlockImageWithText goes here
); + case 'BlockQuote': + return
; default: throw new UnreachableCaseError(block); } From d8b802ce8d14b65a0c924a2a673528bdcfadc56f Mon Sep 17 00:00:00 2001 From: Vasile Chindris Date: Tue, 16 Apr 2024 20:26:22 +0300 Subject: [PATCH 65/82] chore(SLB-297 SLB-296): update the quote text from text to quote --- .../Validation/GutenbergValidator/QuoteValidator.php | 2 +- packages/drupal/gutenberg_blocks/src/blocks/quote.tsx | 10 +++++----- .../schema/src/fragments/PageContent/BlockQuote.gql | 2 +- packages/schema/src/schema.graphql | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/drupal/gutenberg_blocks/src/Plugin/Validation/GutenbergValidator/QuoteValidator.php b/packages/drupal/gutenberg_blocks/src/Plugin/Validation/GutenbergValidator/QuoteValidator.php index 1f9f9d9c9..53df53041 100644 --- a/packages/drupal/gutenberg_blocks/src/Plugin/Validation/GutenbergValidator/QuoteValidator.php +++ b/packages/drupal/gutenberg_blocks/src/Plugin/Validation/GutenbergValidator/QuoteValidator.php @@ -26,7 +26,7 @@ public function applies(array $block): bool { */ public function validatedFields($block = []): array { return [ - 'text' => [ + 'quote' => [ 'field_label' => $this->t('Quote text'), 'rules' => ['required'], ], diff --git a/packages/drupal/gutenberg_blocks/src/blocks/quote.tsx b/packages/drupal/gutenberg_blocks/src/blocks/quote.tsx index f42c3f82d..1aea31b6a 100644 --- a/packages/drupal/gutenberg_blocks/src/blocks/quote.tsx +++ b/packages/drupal/gutenberg_blocks/src/blocks/quote.tsx @@ -19,7 +19,7 @@ registerBlockType(`custom/quote`, { icon: 'format-quote', category: 'text', attributes: { - text: { + quote: { type: 'string', }, author: { @@ -37,16 +37,16 @@ registerBlockType(`custom/quote`, { return (
{ + onChange={(quote) => { props.setAttributes({ - text: cleanUpText(text, ['strong']), + quote: cleanUpText(quote, ['strong']), }); }} /> diff --git a/packages/schema/src/fragments/PageContent/BlockQuote.gql b/packages/schema/src/fragments/PageContent/BlockQuote.gql index b50478e0a..785efb620 100644 --- a/packages/schema/src/fragments/PageContent/BlockQuote.gql +++ b/packages/schema/src/fragments/PageContent/BlockQuote.gql @@ -1,5 +1,5 @@ fragment BlockQuote on BlockQuote { - text + quote author role image { diff --git a/packages/schema/src/schema.graphql b/packages/schema/src/schema.graphql index 15f964aba..47ca8af98 100644 --- a/packages/schema/src/schema.graphql +++ b/packages/schema/src/schema.graphql @@ -259,7 +259,7 @@ enum ImagePosition @default @value(string: "left") { } type BlockQuote @type(id: "custom/quote") { - text: Markup @resolveEditorBlockAttribute(key: "text") + quote: Markup @resolveEditorBlockAttribute(key: "quote") author: String @resolveEditorBlockAttribute(key: "author") role: String @resolveEditorBlockAttribute(key: "role") image: MediaImage @resolveEditorBlockMedia From dc25b433a42828898be20d46733921fcd71b3ebc Mon Sep 17 00:00:00 2001 From: Vasile Chindris Date: Tue, 16 Apr 2024 20:44:05 +0300 Subject: [PATCH 66/82] test(SLB-297): update default content and schema tests to include quote blocks --- .../a397ca48-8fad-411e-8901-0eba2feb989c.yml | 4 ++++ .../ceb9b2a7-4c4c-4084-ada9-d5f6505d466b.yml | 1 + tests/schema/specs/blocks.spec.ts | 24 +++++++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/packages/drupal/test_content/content/node/a397ca48-8fad-411e-8901-0eba2feb989c.yml b/packages/drupal/test_content/content/node/a397ca48-8fad-411e-8901-0eba2feb989c.yml index 61b3a4ac9..6a4e6eded 100644 --- a/packages/drupal/test_content/content/node/a397ca48-8fad-411e-8901-0eba2feb989c.yml +++ b/packages/drupal/test_content/content/node/a397ca48-8fad-411e-8901-0eba2feb989c.yml @@ -118,6 +118,8 @@ default: + +

@@ -199,6 +201,8 @@ translations: + + format: gutenberg summary: '' diff --git a/packages/drupal/test_content/content/node/ceb9b2a7-4c4c-4084-ada9-d5f6505d466b.yml b/packages/drupal/test_content/content/node/ceb9b2a7-4c4c-4084-ada9-d5f6505d466b.yml index 53ade0b64..5ad90259d 100644 --- a/packages/drupal/test_content/content/node/ceb9b2a7-4c4c-4084-ada9-d5f6505d466b.yml +++ b/packages/drupal/test_content/content/node/ceb9b2a7-4c4c-4084-ada9-d5f6505d466b.yml @@ -73,6 +73,7 @@ default:

+ format: gutenberg summary: '' diff --git a/tests/schema/specs/blocks.spec.ts b/tests/schema/specs/blocks.spec.ts index b5cf47cf5..1f26435dd 100644 --- a/tests/schema/specs/blocks.spec.ts +++ b/tests/schema/specs/blocks.spec.ts @@ -63,6 +63,14 @@ test('Blocks', async () => { markup } } + ... on BlockQuote { + quote + author + role + image { + __typename + } + } } } { @@ -194,6 +202,15 @@ test('Blocks', async () => { "text": "CTA with link to media", "url": "/media/[numeric]", }, + { + "__typename": "BlockQuote", + "author": "John Doe", + "image": { + "__typename": "MediaImage", + }, + "quote": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sagittis nisi nec neque porta, a ornare ligula efficitur.", + "role": "Project manager", + }, { "__typename": "BlockMarkup", "markup": " @@ -253,6 +270,13 @@ test('Blocks', async () => { ", }, }, + { + "__typename": "BlockQuote", + "author": "Jane Doe", + "image": null, + "quote": "In vitae diam quis odio tincidunt faucibus eget ut libero", + "role": null, + }, ], "hero": { "__typename": "Hero", From 05e3ccac04c543373b53a3d2556ba6badd788597 Mon Sep 17 00:00:00 2001 From: Luqmaan Essop Date: Tue, 16 Apr 2024 14:51:14 +0000 Subject: [PATCH 67/82] style: adjust quote styling --- .../Organisms/PageContent/BlockMarkup.tsx | 19 +++++++++++++++++++ packages/ui/src/tailwind.css | 6 ++++++ 2 files changed, 25 insertions(+) diff --git a/packages/ui/src/components/Organisms/PageContent/BlockMarkup.tsx b/packages/ui/src/components/Organisms/PageContent/BlockMarkup.tsx index 54f29a9fb..443b8fd19 100644 --- a/packages/ui/src/components/Organisms/PageContent/BlockMarkup.tsx +++ b/packages/ui/src/components/Organisms/PageContent/BlockMarkup.tsx @@ -49,6 +49,25 @@ export function BlockMarkup(props: BlockMarkupFragment) { ); }, + blockquote: ({ children }: PropsWithChildren<{}>) => { + return ( +
+ + + + {children} +
+ ); + }, }} markup={props.markup} /> diff --git a/packages/ui/src/tailwind.css b/packages/ui/src/tailwind.css index f9b68485a..39d6591e2 100644 --- a/packages/ui/src/tailwind.css +++ b/packages/ui/src/tailwind.css @@ -4,3 +4,9 @@ @tailwind base; @tailwind components; @tailwind utilities; + +/* Prose overrides */ +.lg\:prose-xl + :where(blockquote):not(:where([class~='not-prose'], [class~='not-prose'] *)) { + padding-left: 0 !important; +} From 0a95fe55b5514fab24b52bc0aa533da6c842465d Mon Sep 17 00:00:00 2001 From: Luqmaan Essop Date: Wed, 17 Apr 2024 09:02:17 +0000 Subject: [PATCH 68/82] style: add block quote styling --- .../Organisms/PageContent/BlockMarkup.tsx | 1 - .../PageContent/BlockQuote.stories.ts | 23 ++++++++++ .../Organisms/PageContent/BlockQuote.tsx | 46 +++++++++++++++++-- packages/ui/src/tailwind.css | 14 ++++++ 4 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 packages/ui/src/components/Organisms/PageContent/BlockQuote.stories.ts diff --git a/packages/ui/src/components/Organisms/PageContent/BlockMarkup.tsx b/packages/ui/src/components/Organisms/PageContent/BlockMarkup.tsx index 443b8fd19..a83f81b79 100644 --- a/packages/ui/src/components/Organisms/PageContent/BlockMarkup.tsx +++ b/packages/ui/src/components/Organisms/PageContent/BlockMarkup.tsx @@ -18,7 +18,6 @@ export function BlockMarkup(props: BlockMarkupFragment) { className={clsx([ 'mx-auto max-w-3xl prose lg:prose-xl mt-10', 'prose-a:text-indigo-600', - 'prose-blockquote:border-indigo-200', 'prose-em:text-indigo-600', 'prose-strong:text-indigo-600', 'marker:text-indigo-600 marker:font-bold', diff --git a/packages/ui/src/components/Organisms/PageContent/BlockQuote.stories.ts b/packages/ui/src/components/Organisms/PageContent/BlockQuote.stories.ts new file mode 100644 index 000000000..e5efd0c53 --- /dev/null +++ b/packages/ui/src/components/Organisms/PageContent/BlockQuote.stories.ts @@ -0,0 +1,23 @@ +import { Markup } from '@custom/schema'; +import Portrait from '@stories/portrait.jpg?as=metadata'; +import { Meta, StoryObj } from '@storybook/react'; + +import { image } from '../../../helpers/image'; +import { BlockQuote } from './BlockQuote'; + +export default { + component: BlockQuote, +} satisfies Meta; + +export const Quote = { + args: { + role: 'test role', + author: 'Author name', + image: { + source: image({ src: Portrait.src, width: 50, height: 50 }), + alt: 'Portrait', + }, + quote: + '

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

' as Markup, + }, +} satisfies StoryObj; diff --git a/packages/ui/src/components/Organisms/PageContent/BlockQuote.tsx b/packages/ui/src/components/Organisms/PageContent/BlockQuote.tsx index 4676f6302..adab18a20 100644 --- a/packages/ui/src/components/Organisms/PageContent/BlockQuote.tsx +++ b/packages/ui/src/components/Organisms/PageContent/BlockQuote.tsx @@ -1,7 +1,47 @@ -import { BlockQuoteFragment } from '@custom/schema'; +import { BlockQuoteFragment, Html, Image } from '@custom/schema'; import React from 'react'; export function BlockQuote(props: BlockQuoteFragment) { - console.log('Block quote props:', props); - return <>; + return ( +
+
+ + Quote Symbol + + + {props.quote && } +
+ {props.image && ( + {props.image.alt + )} +
+ {props.author &&

{props.author}

} +
+ {props.role && ( +

+ / + + {props.role} + +

+ )} +
+
+
+ ); } diff --git a/packages/ui/src/tailwind.css b/packages/ui/src/tailwind.css index 39d6591e2..f6eb52ad0 100644 --- a/packages/ui/src/tailwind.css +++ b/packages/ui/src/tailwind.css @@ -10,3 +10,17 @@ :where(blockquote):not(:where([class~='not-prose'], [class~='not-prose'] *)) { padding-left: 0 !important; } + +.prose + :where(blockquote p:first-of-type):not( + :where([class~='not-prose'], [class~='not-prose'] *) + )::before { + content: '' !important; +} + +.prose + :where(blockquote p:first-of-type):not( + :where([class~='not-prose'], [class~='not-prose'] *) + )::after { + content: '' !important; +} From 866e1e6cc8ca1004fe297a09555524a0ce157744 Mon Sep 17 00:00:00 2001 From: Luqmaan Essop Date: Thu, 18 Apr 2024 05:02:12 +0000 Subject: [PATCH 69/82] chore: adjust image for quote uthor --- .../Organisms/PageContent/BlockQuote.stories.ts | 4 ++-- packages/ui/static/stories/avatar.jpg | Bin 0 -> 6782 bytes 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 packages/ui/static/stories/avatar.jpg diff --git a/packages/ui/src/components/Organisms/PageContent/BlockQuote.stories.ts b/packages/ui/src/components/Organisms/PageContent/BlockQuote.stories.ts index e5efd0c53..ffe6090f1 100644 --- a/packages/ui/src/components/Organisms/PageContent/BlockQuote.stories.ts +++ b/packages/ui/src/components/Organisms/PageContent/BlockQuote.stories.ts @@ -1,5 +1,5 @@ import { Markup } from '@custom/schema'; -import Portrait from '@stories/portrait.jpg?as=metadata'; +import Avatar from '@stories/avatar.jpg?as=metadata'; import { Meta, StoryObj } from '@storybook/react'; import { image } from '../../../helpers/image'; @@ -14,7 +14,7 @@ export const Quote = { role: 'test role', author: 'Author name', image: { - source: image({ src: Portrait.src, width: 50, height: 50 }), + source: image(Avatar), alt: 'Portrait', }, quote: diff --git a/packages/ui/static/stories/avatar.jpg b/packages/ui/static/stories/avatar.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e796b545eff1cbe835334f4ddba927c3802c719b GIT binary patch literal 6782 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 W!8{yho95C(Tb0H<%=AkcX#5Ybp@(Sz literal 0 HcmV?d00001 From a67450f150ed4995a694fe4eabf06dd1839b65a1 Mon Sep 17 00:00:00 2001 From: Luqmaan Essop Date: Thu, 18 Apr 2024 05:24:01 +0000 Subject: [PATCH 70/82] chore: update block test image check --- tests/e2e/specs/drupal/blocks.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/specs/drupal/blocks.spec.ts b/tests/e2e/specs/drupal/blocks.spec.ts index ec7dc6a1b..1f54560a8 100644 --- a/tests/e2e/specs/drupal/blocks.spec.ts +++ b/tests/e2e/specs/drupal/blocks.spec.ts @@ -27,7 +27,7 @@ test('All blocks are rendered', async ({ page }) => { page.locator( 'img:not([data-test-id=hero-image])[alt="A beautiful landscape."]', ), - ).toHaveCount(1); + ).toHaveCount(2); await expect(page.locator('figcaption:text("Media image")')).toHaveCount(1); // Video From 8e3c03f4a76e4610dc5229c2079c105e554074dd Mon Sep 17 00:00:00 2001 From: Luqmaan Essop Date: Fri, 19 Apr 2024 13:02:24 +0000 Subject: [PATCH 71/82] style: spacing adjustments --- .../components/Organisms/PageContent/BlockQuote.tsx | 10 +++++----- packages/ui/src/tailwind.css | 5 +++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/ui/src/components/Organisms/PageContent/BlockQuote.tsx b/packages/ui/src/components/Organisms/PageContent/BlockQuote.tsx index adab18a20..6c1abaad9 100644 --- a/packages/ui/src/components/Organisms/PageContent/BlockQuote.tsx +++ b/packages/ui/src/components/Organisms/PageContent/BlockQuote.tsx @@ -29,13 +29,13 @@ export function BlockQuote(props: BlockQuoteFragment) { alt={props.image.alt || 'Author image'} /> )} -
- {props.author &&

{props.author}

} +
+ {props.author &&

{props.author}

}
{props.role && ( -

- / - +

+ / + {props.role}

diff --git a/packages/ui/src/tailwind.css b/packages/ui/src/tailwind.css index f6eb52ad0..2d464b850 100644 --- a/packages/ui/src/tailwind.css +++ b/packages/ui/src/tailwind.css @@ -11,6 +11,11 @@ padding-left: 0 !important; } +.prose :where(blockquote p):not( :where([class~='not-prose']) ) { + margin-top: 12px !important; + margin-bottom: 12px !important; +} + .prose :where(blockquote p:first-of-type):not( :where([class~='not-prose'], [class~='not-prose'] *) From e34e4b80f27efcf93ef78c80f3b9040ce6461e17 Mon Sep 17 00:00:00 2001 From: Vasile Chindris Date: Mon, 22 Apr 2024 14:48:09 +0300 Subject: [PATCH 72/82] test(SLB-297): update quote tests --- .../2f1be18f-dc18-4b56-8ff0-ce24d8ff7df7.yml | 27 +++++++ .../content/file/the_silverback.jpeg | Bin 0 -> 146269 bytes .../5dfc1856-e9e4-4f02-9cd6-9d888870ce1a.yml | 74 ++++++++++++++++++ .../a397ca48-8fad-411e-8901-0eba2feb989c.yml | 13 +-- .../ceb9b2a7-4c4c-4084-ada9-d5f6505d466b.yml | 4 - tests/e2e/specs/drupal/blocks.spec.ts | 10 +-- tests/schema/specs/blocks.spec.ts | 4 - 7 files changed, 109 insertions(+), 23 deletions(-) create mode 100644 packages/drupal/test_content/content/file/2f1be18f-dc18-4b56-8ff0-ce24d8ff7df7.yml create mode 100644 packages/drupal/test_content/content/file/the_silverback.jpeg create mode 100644 packages/drupal/test_content/content/media/5dfc1856-e9e4-4f02-9cd6-9d888870ce1a.yml diff --git a/packages/drupal/test_content/content/file/2f1be18f-dc18-4b56-8ff0-ce24d8ff7df7.yml b/packages/drupal/test_content/content/file/2f1be18f-dc18-4b56-8ff0-ce24d8ff7df7.yml new file mode 100644 index 000000000..8947e3bbc --- /dev/null +++ b/packages/drupal/test_content/content/file/2f1be18f-dc18-4b56-8ff0-ce24d8ff7df7.yml @@ -0,0 +1,27 @@ +_meta: + version: '1.0' + entity_type: file + uuid: 2f1be18f-dc18-4b56-8ff0-ce24d8ff7df7 + default_langcode: en +default: + uid: + - + target_id: 1 + filename: + - + value: the_silverback.jpeg + uri: + - + value: 'public://2024-04/the_silverback.jpeg' + filemime: + - + value: image/jpeg + filesize: + - + value: 146269 + status: + - + value: true + created: + - + value: 1713785099 diff --git a/packages/drupal/test_content/content/file/the_silverback.jpeg b/packages/drupal/test_content/content/file/the_silverback.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..25cc96bcef3f34d50dac92b3fb29a0d4a7e97881 GIT binary patch literal 146269 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

$cHaNN!u^wefP7j>fm116{1@N<8PkIg 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

!EV5Nfp^&=_H`_?ky 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 literal 0 HcmV?d00001 diff --git a/packages/drupal/test_content/content/media/5dfc1856-e9e4-4f02-9cd6-9d888870ce1a.yml b/packages/drupal/test_content/content/media/5dfc1856-e9e4-4f02-9cd6-9d888870ce1a.yml new file mode 100644 index 000000000..5dbede2d2 --- /dev/null +++ b/packages/drupal/test_content/content/media/5dfc1856-e9e4-4f02-9cd6-9d888870ce1a.yml @@ -0,0 +1,74 @@ +_meta: + version: '1.0' + entity_type: media + uuid: 5dfc1856-e9e4-4f02-9cd6-9d888870ce1a + bundle: image + default_langcode: en + depends: + 2f1be18f-dc18-4b56-8ff0-ce24d8ff7df7: file +default: + revision_user: + - + target_id: 1 + status: + - + value: true + uid: + - + target_id: 1 + name: + - + value: the_silverback.jpeg + created: + - + value: 1713785099 + path: + - + alias: '' + langcode: en + pathauto: 0 + content_translation_source: + - + value: und + content_translation_outdated: + - + value: false + field_media_image: + - + entity: 2f1be18f-dc18-4b56-8ff0-ce24d8ff7df7 + alt: 'The silverback' + title: '' + width: 1280 + height: 720 +translations: + de: + status: + - + value: true + uid: + - + target_id: 1 + name: + - + value: the_silverback.jpeg + created: + - + value: 1713785205 + path: + - + alias: '' + langcode: de + pathauto: 0 + content_translation_source: + - + value: en + content_translation_outdated: + - + value: false + field_media_image: + - + entity: 2f1be18f-dc18-4b56-8ff0-ce24d8ff7df7 + alt: 'The silverback DE' + title: '' + width: 1280 + height: 720 diff --git a/packages/drupal/test_content/content/node/a397ca48-8fad-411e-8901-0eba2feb989c.yml b/packages/drupal/test_content/content/node/a397ca48-8fad-411e-8901-0eba2feb989c.yml index 6a4e6eded..ebd66747b 100644 --- a/packages/drupal/test_content/content/node/a397ca48-8fad-411e-8901-0eba2feb989c.yml +++ b/packages/drupal/test_content/content/node/a397ca48-8fad-411e-8901-0eba2feb989c.yml @@ -8,6 +8,7 @@ _meta: 3a0fe860-a6d6-428a-9474-365bd57509aa: media 478c4289-961d-4ce8-85d6-578ae05f3019: media 72187a1f-3e48-4b45-a9b7-189c6fd7ee26: media + 5dfc1856-e9e4-4f02-9cd6-9d888870ce1a: media default: revision_uid: - @@ -102,10 +103,6 @@ default:

Heading 3

- -

Quote

Citation
- - @@ -118,7 +115,7 @@ default: - +

@@ -192,17 +189,13 @@ translations:

Heading 3 DE

- -

Quote DE

Citation DE
- - - + format: gutenberg summary: '' diff --git a/packages/drupal/test_content/content/node/ceb9b2a7-4c4c-4084-ada9-d5f6505d466b.yml b/packages/drupal/test_content/content/node/ceb9b2a7-4c4c-4084-ada9-d5f6505d466b.yml index 5ad90259d..a367f61b2 100644 --- a/packages/drupal/test_content/content/node/ceb9b2a7-4c4c-4084-ada9-d5f6505d466b.yml +++ b/packages/drupal/test_content/content/node/ceb9b2a7-4c4c-4084-ada9-d5f6505d466b.yml @@ -58,10 +58,6 @@ default:
- -

- -

diff --git a/tests/e2e/specs/drupal/blocks.spec.ts b/tests/e2e/specs/drupal/blocks.spec.ts index 1f54560a8..727920dcd 100644 --- a/tests/e2e/specs/drupal/blocks.spec.ts +++ b/tests/e2e/specs/drupal/blocks.spec.ts @@ -27,7 +27,7 @@ test('All blocks are rendered', async ({ page }) => { page.locator( 'img:not([data-test-id=hero-image])[alt="A beautiful landscape."]', ), - ).toHaveCount(2); + ).toHaveCount(1); await expect(page.locator('figcaption:text("Media image")')).toHaveCount(1); // Video @@ -50,10 +50,10 @@ test('All blocks are rendered', async ({ page }) => { await expect(page.locator('h3:text("Heading 3")')).toHaveCount(1); // Quote - await expect(page.locator('blockquote > p:text("Quote")')).toHaveCount(1); - await expect(page.locator('blockquote > cite:text("Citation")')).toHaveCount( - 1, - ); + await expect(page.locator('blockquote:text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sagittis nisi nec neque porta, a ornare ligula efficitur.")')).toHaveCount(1); + await expect(page.locator('blockquote p.not-prose:text("John Doe")')).toHaveCount(1); + await expect(page.locator('blockquote p.not-prose span:text("Project manager")')).toHaveCount(1); + await expect(page.locator('blockquote img[alt="The silverback"]')).toHaveCount(1); // Form await expect( diff --git a/tests/schema/specs/blocks.spec.ts b/tests/schema/specs/blocks.spec.ts index 1f26435dd..4730f8d2a 100644 --- a/tests/schema/specs/blocks.spec.ts +++ b/tests/schema/specs/blocks.spec.ts @@ -157,8 +157,6 @@ test('Blocks', async () => {
  • list 1
  • list 2
    1. list 2.2

Heading 3

- -

Quote

Citation
", }, { @@ -248,8 +246,6 @@ test('Blocks', async () => {
-

-

", }, From b0bac3dc60e4b7dba64e0d11efbc029d3932f7c2 Mon Sep 17 00:00:00 2001 From: Vasile Chindris Date: Mon, 22 Apr 2024 15:02:11 +0300 Subject: [PATCH 73/82] fix(SLB-297): prettier --- tests/e2e/specs/drupal/blocks.spec.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/e2e/specs/drupal/blocks.spec.ts b/tests/e2e/specs/drupal/blocks.spec.ts index 727920dcd..1e9e881dc 100644 --- a/tests/e2e/specs/drupal/blocks.spec.ts +++ b/tests/e2e/specs/drupal/blocks.spec.ts @@ -50,10 +50,20 @@ test('All blocks are rendered', async ({ page }) => { await expect(page.locator('h3:text("Heading 3")')).toHaveCount(1); // Quote - await expect(page.locator('blockquote:text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sagittis nisi nec neque porta, a ornare ligula efficitur.")')).toHaveCount(1); - await expect(page.locator('blockquote p.not-prose:text("John Doe")')).toHaveCount(1); - await expect(page.locator('blockquote p.not-prose span:text("Project manager")')).toHaveCount(1); - await expect(page.locator('blockquote img[alt="The silverback"]')).toHaveCount(1); + await expect( + page.locator( + 'blockquote:text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sagittis nisi nec neque porta, a ornare ligula efficitur.")', + ), + ).toHaveCount(1); + await expect( + page.locator('blockquote p.not-prose:text("John Doe")'), + ).toHaveCount(1); + await expect( + page.locator('blockquote p.not-prose span:text("Project manager")'), + ).toHaveCount(1); + await expect( + page.locator('blockquote img[alt="The silverback"]'), + ).toHaveCount(1); // Form await expect( From 74e756d9266183c3667e5b31b1ae10779ff4ac5b Mon Sep 17 00:00:00 2001 From: Vasile Chindris Date: Tue, 23 Apr 2024 21:31:51 +0300 Subject: [PATCH 74/82] chore(SLB-339): quote editor block style --- ....entity_view_display.media.image.quote.yml | 33 +++++ .../core.entity_view_mode.media.quote.yml | 11 ++ apps/cms/config/sync/image.style.quote.yml | 15 ++ packages/drupal/gutenberg_blocks/css/edit.css | 11 +- .../gutenberg_blocks/src/blocks/quote.tsx | 132 ++++++++++-------- 5 files changed, 141 insertions(+), 61 deletions(-) create mode 100644 apps/cms/config/sync/core.entity_view_display.media.image.quote.yml create mode 100644 apps/cms/config/sync/core.entity_view_mode.media.quote.yml create mode 100644 apps/cms/config/sync/image.style.quote.yml diff --git a/apps/cms/config/sync/core.entity_view_display.media.image.quote.yml b/apps/cms/config/sync/core.entity_view_display.media.image.quote.yml new file mode 100644 index 000000000..b88657103 --- /dev/null +++ b/apps/cms/config/sync/core.entity_view_display.media.image.quote.yml @@ -0,0 +1,33 @@ +uuid: 195a40ca-6da4-4e2e-bc2f-c9c1fc0385bb +langcode: en +status: true +dependencies: + config: + - core.entity_view_mode.media.quote + - field.field.media.image.field_media_image + - image.style.quote + - media.type.image + module: + - image +id: media.image.quote +targetEntityType: media +bundle: image +mode: quote +content: + field_media_image: + type: image + label: visually_hidden + settings: + image_link: '' + image_style: quote + image_loading: + attribute: lazy + third_party_settings: { } + weight: 0 + region: content +hidden: + created: true + langcode: true + name: true + thumbnail: true + uid: true diff --git a/apps/cms/config/sync/core.entity_view_mode.media.quote.yml b/apps/cms/config/sync/core.entity_view_mode.media.quote.yml new file mode 100644 index 000000000..51148e9b4 --- /dev/null +++ b/apps/cms/config/sync/core.entity_view_mode.media.quote.yml @@ -0,0 +1,11 @@ +uuid: f54c9109-a0ff-4a09-9772-748af96b8eb0 +langcode: en +status: true +dependencies: + module: + - media +id: media.quote +label: Quote +description: '' +targetEntityType: media +cache: true diff --git a/apps/cms/config/sync/image.style.quote.yml b/apps/cms/config/sync/image.style.quote.yml new file mode 100644 index 000000000..b7a85ee89 --- /dev/null +++ b/apps/cms/config/sync/image.style.quote.yml @@ -0,0 +1,15 @@ +uuid: 983e88dc-78f8-463d-b088-026d7d96bcc0 +langcode: en +status: true +dependencies: { } +name: quote +label: 'Quote (24x24)' +effects: + 3ac18111-aeda-4cd7-aafc-5cd66d0cbf7e: + uuid: 3ac18111-aeda-4cd7-aafc-5cd66d0cbf7e + id: image_scale_and_crop + weight: 1 + data: + width: 24 + height: 24 + anchor: center-center diff --git a/packages/drupal/gutenberg_blocks/css/edit.css b/packages/drupal/gutenberg_blocks/css/edit.css index 1c603451e..39ab66473 100644 --- a/packages/drupal/gutenberg_blocks/css/edit.css +++ b/packages/drupal/gutenberg_blocks/css/edit.css @@ -7,16 +7,17 @@ margin:0; } +.gutenberg__editor blockquote .quote-image img { + object-fit: cover; + max-width: 100%; + border-radius: 9999px; +} + .gutenberg__editor blockquote::before, .gutenberg__editor blockquote::after { content: ''; } -.gutenberg__editor blockquote .quote-author, -.gutenberg__editor blockquote .quote-role { - text-align: right; -} - .gutenberg__editor .container-wrapper { display: block; position: relative; diff --git a/packages/drupal/gutenberg_blocks/src/blocks/quote.tsx b/packages/drupal/gutenberg_blocks/src/blocks/quote.tsx index 1aea31b6a..d5873d2d7 100644 --- a/packages/drupal/gutenberg_blocks/src/blocks/quote.tsx +++ b/packages/drupal/gutenberg_blocks/src/blocks/quote.tsx @@ -35,63 +35,83 @@ registerBlockType(`custom/quote`, { // @ts-ignore edit: compose(withState())((props) => { return ( -
- { - props.setAttributes({ - quote: cleanUpText(quote, ['strong']), - }); - }} - /> - { - setPlainTextAttribute(props, 'author', author); - }} - /> - { - setPlainTextAttribute(props, 'role', role); - }} - /> - { +
+
+ + + + -
+ disableLineBreaks={false} + placeholder={__('Quote')} + keepPlaceholderOnFocus={false} + onChange={(quote) => { + props.setAttributes({ + quote: cleanUpText(quote, ['strong']), + }); + }} + /> +
+
+ { + // @ts-ignore + error = typeof error === 'string' ? error : error[2]; + dispatch('core/notices').createWarningNotice(error); + }} + /> +
+ { + setPlainTextAttribute(props, 'author', author); + }} + /> + / + { + setPlainTextAttribute(props, 'role', role); + }} + /> +
+
+
); }), save() { From f0564b76ebb24c0d312f0176094338c930f834fb Mon Sep 17 00:00:00 2001 From: Luqmaan Essop Date: Wed, 24 Apr 2024 08:39:18 +0200 Subject: [PATCH 75/82] style: adjust margin --- .../ui/src/components/Organisms/PageContent/BlockQuote.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/components/Organisms/PageContent/BlockQuote.tsx b/packages/ui/src/components/Organisms/PageContent/BlockQuote.tsx index 6c1abaad9..d4f84abe1 100644 --- a/packages/ui/src/components/Organisms/PageContent/BlockQuote.tsx +++ b/packages/ui/src/components/Organisms/PageContent/BlockQuote.tsx @@ -24,12 +24,12 @@ export function BlockQuote(props: BlockQuoteFragment) {
{props.image && ( {props.image.alt )} -
+
{props.author &&

{props.author}

}
{props.role && ( From eeb3cb615990e25eb7821f97e4c782acbf33835a Mon Sep 17 00:00:00 2001 From: Luqmaan Essop Date: Wed, 24 Apr 2024 09:13:30 +0200 Subject: [PATCH 76/82] style(SLB-298): add no author avatar block quote story --- .../Organisms/PageContent/BlockQuote.stories.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/ui/src/components/Organisms/PageContent/BlockQuote.stories.ts b/packages/ui/src/components/Organisms/PageContent/BlockQuote.stories.ts index ffe6090f1..6b40c8afd 100644 --- a/packages/ui/src/components/Organisms/PageContent/BlockQuote.stories.ts +++ b/packages/ui/src/components/Organisms/PageContent/BlockQuote.stories.ts @@ -21,3 +21,12 @@ export const Quote = { '

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

' as Markup, }, } satisfies StoryObj; + +export const NoAvatarQuote = { + args: { + role: 'test role', + author: 'Author name', + quote: + '

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

' as Markup, + }, +} satisfies StoryObj; From 2e4b557538aeab948e9d43706f5435800dc67592 Mon Sep 17 00:00:00 2001 From: Luqmaan Essop Date: Wed, 24 Apr 2024 10:41:49 +0200 Subject: [PATCH 77/82] style(SLB-298): add blockquote padding --- packages/ui/src/components/Organisms/PageContent/BlockQuote.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/components/Organisms/PageContent/BlockQuote.tsx b/packages/ui/src/components/Organisms/PageContent/BlockQuote.tsx index d4f84abe1..7ad1d3ab0 100644 --- a/packages/ui/src/components/Organisms/PageContent/BlockQuote.tsx +++ b/packages/ui/src/components/Organisms/PageContent/BlockQuote.tsx @@ -4,7 +4,7 @@ import React from 'react'; export function BlockQuote(props: BlockQuoteFragment) { return (
-
+
Date: Thu, 25 Apr 2024 09:32:20 +0200 Subject: [PATCH 78/82] style(SLB-298): typography tweaks --- .../ui/src/components/Organisms/PageContent/BlockQuote.tsx | 6 +++--- packages/ui/src/tailwind.css | 7 ++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/ui/src/components/Organisms/PageContent/BlockQuote.tsx b/packages/ui/src/components/Organisms/PageContent/BlockQuote.tsx index 7ad1d3ab0..c99955e06 100644 --- a/packages/ui/src/components/Organisms/PageContent/BlockQuote.tsx +++ b/packages/ui/src/components/Organisms/PageContent/BlockQuote.tsx @@ -3,7 +3,7 @@ import React from 'react'; export function BlockQuote(props: BlockQuoteFragment) { return ( -
+
- {props.quote && } +

{props.quote && }

{props.image && ( {props.image.alt )} -
+
{props.author &&

{props.author}

}
{props.role && ( diff --git a/packages/ui/src/tailwind.css b/packages/ui/src/tailwind.css index 2d464b850..944e19871 100644 --- a/packages/ui/src/tailwind.css +++ b/packages/ui/src/tailwind.css @@ -6,12 +6,17 @@ @tailwind utilities; /* Prose overrides */ +.prose + :where(blockquote):not(:where([class~='not-prose'], [class~='not-prose'] *)) { + font-weight: 400; +} + .lg\:prose-xl :where(blockquote):not(:where([class~='not-prose'], [class~='not-prose'] *)) { padding-left: 0 !important; } -.prose :where(blockquote p):not( :where([class~='not-prose']) ) { +.prose :where(blockquote p):not(:where([class~='not-prose'])) { margin-top: 12px !important; margin-bottom: 12px !important; } From 80da9cb6e69435394fbd3045a5ccccbcc6607978 Mon Sep 17 00:00:00 2001 From: Luqmaan Essop Date: Thu, 25 Apr 2024 10:27:10 +0200 Subject: [PATCH 79/82] chore: update tests --- tests/e2e/specs/drupal/blocks.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/specs/drupal/blocks.spec.ts b/tests/e2e/specs/drupal/blocks.spec.ts index 1e9e881dc..c3c775333 100644 --- a/tests/e2e/specs/drupal/blocks.spec.ts +++ b/tests/e2e/specs/drupal/blocks.spec.ts @@ -52,7 +52,7 @@ test('All blocks are rendered', async ({ page }) => { // Quote await expect( page.locator( - 'blockquote:text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sagittis nisi nec neque porta, a ornare ligula efficitur.")', + 'blockquote p:text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sagittis nisi nec neque porta, a ornare ligula efficitur.")', ), ).toHaveCount(1); await expect( From 1ff2f16e114c0731e51472bde898d1265fc2f340 Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Thu, 25 Apr 2024 14:20:51 +0200 Subject: [PATCH 80/82] refactor: use shared version of silverback_autosave --- apps/cms/composer.json | 1 + apps/cms/composer.lock | 28 +- .../drupal/silverback_autosave/LICENSE.txt | 339 ------------- packages/drupal/silverback_autosave/README.md | 7 - .../drupal/silverback_autosave/composer.json | 6 - .../install/silverback_autosave.messages.yml | 1 - .../install/silverback_autosave.settings.yml | 9 - .../schema/silverback_autosave.schema.yml | 53 -- .../css/silverback_autosave.css | 15 - .../js/silverback_autosave.js | 455 ------------------ .../silverback_autosave.info.yml | 8 - .../silverback_autosave.install | 111 ----- .../silverback_autosave.libraries.yml | 16 - .../silverback_autosave.links.menu.yml | 6 - .../silverback_autosave.module | 140 ------ .../silverback_autosave.routing.yml | 7 - .../silverback_autosave.services.yml | 51 -- .../src/Ajax/OpenAutosaveDisabledDialog.php | 21 - .../Extension/ModuleHandlerEmptyAlter.php | 22 - .../Theme/ThemeManagerEmptyAlter.php | 22 - .../src/Form/AutosaveButtonClickedTrait.php | 33 -- .../src/Form/AutosaveEntityFormHandler.php | 327 ------------- .../AutosaveEntityFormHandlerInterface.php | 25 - .../src/Form/AutosaveFormAlterTrait.php | 235 --------- .../src/Form/AutosaveFormBuilder.php | 251 ---------- .../src/Form/AutosaveFormErrorHandler.php | 43 -- .../src/Form/AutosaveFormInterface.php | 109 ----- .../src/Form/AutosaveFormSettingsForm.php | 209 -------- .../src/Form/AutosaveFormValidator.php | 80 --- .../AutosaveEntityFormDatabaseStorage.php | 348 -------------- .../AutosaveEntityFormStorageInterface.php | 203 -------- 31 files changed, 28 insertions(+), 3153 deletions(-) delete mode 100644 packages/drupal/silverback_autosave/LICENSE.txt delete mode 100644 packages/drupal/silverback_autosave/README.md delete mode 100644 packages/drupal/silverback_autosave/composer.json delete mode 100644 packages/drupal/silverback_autosave/config/install/silverback_autosave.messages.yml delete mode 100644 packages/drupal/silverback_autosave/config/install/silverback_autosave.settings.yml delete mode 100644 packages/drupal/silverback_autosave/config/schema/silverback_autosave.schema.yml delete mode 100644 packages/drupal/silverback_autosave/css/silverback_autosave.css delete mode 100644 packages/drupal/silverback_autosave/js/silverback_autosave.js delete mode 100644 packages/drupal/silverback_autosave/silverback_autosave.info.yml delete mode 100644 packages/drupal/silverback_autosave/silverback_autosave.install delete mode 100644 packages/drupal/silverback_autosave/silverback_autosave.libraries.yml delete mode 100644 packages/drupal/silverback_autosave/silverback_autosave.links.menu.yml delete mode 100644 packages/drupal/silverback_autosave/silverback_autosave.module delete mode 100644 packages/drupal/silverback_autosave/silverback_autosave.routing.yml delete mode 100644 packages/drupal/silverback_autosave/silverback_autosave.services.yml delete mode 100644 packages/drupal/silverback_autosave/src/Ajax/OpenAutosaveDisabledDialog.php delete mode 100644 packages/drupal/silverback_autosave/src/EmptyAlter/Extension/ModuleHandlerEmptyAlter.php delete mode 100644 packages/drupal/silverback_autosave/src/EmptyAlter/Theme/ThemeManagerEmptyAlter.php delete mode 100644 packages/drupal/silverback_autosave/src/Form/AutosaveButtonClickedTrait.php delete mode 100644 packages/drupal/silverback_autosave/src/Form/AutosaveEntityFormHandler.php delete mode 100644 packages/drupal/silverback_autosave/src/Form/AutosaveEntityFormHandlerInterface.php delete mode 100644 packages/drupal/silverback_autosave/src/Form/AutosaveFormAlterTrait.php delete mode 100644 packages/drupal/silverback_autosave/src/Form/AutosaveFormBuilder.php delete mode 100644 packages/drupal/silverback_autosave/src/Form/AutosaveFormErrorHandler.php delete mode 100644 packages/drupal/silverback_autosave/src/Form/AutosaveFormInterface.php delete mode 100644 packages/drupal/silverback_autosave/src/Form/AutosaveFormSettingsForm.php delete mode 100644 packages/drupal/silverback_autosave/src/Form/AutosaveFormValidator.php delete mode 100644 packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormDatabaseStorage.php delete mode 100644 packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormStorageInterface.php diff --git a/apps/cms/composer.json b/apps/cms/composer.json index e187e9ef9..bb6feeb43 100644 --- a/apps/cms/composer.json +++ b/apps/cms/composer.json @@ -35,6 +35,7 @@ "amazeelabs/proxy-graphql": "^1.1.7", "amazeelabs/proxy-gutenberg": "^1.3", "amazeelabs/silverback-cli": "^2.9.4", + "amazeelabs/silverback_autosave": "^1.1", "amazeelabs/silverback_campaign_urls": "^1.0.5", "amazeelabs/silverback_cloudinary": "^1.3.0", "amazeelabs/silverback_external_preview": "^2.0.3", diff --git a/apps/cms/composer.lock b/apps/cms/composer.lock index ef2a0aa18..3477e203f 100644 --- a/apps/cms/composer.lock +++ b/apps/cms/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7fdb7073ff9cc88dcbc78224e439e65f", + "content-hash": "f2375207790855cc777235f699c7c2b9", "packages": [ { "name": "amazeeio/drupal_integrations", @@ -343,6 +343,32 @@ }, "time": "2024-01-03T06:30:01+00:00" }, + { + "name": "amazeelabs/silverback_autosave", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/AmazeeLabs/silverback_autosave.git", + "reference": "8208ddba6d5da916bf5649b7e0e53efdf491bb7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/AmazeeLabs/silverback_autosave/zipball/8208ddba6d5da916bf5649b7e0e53efdf491bb7a", + "reference": "8208ddba6d5da916bf5649b7e0e53efdf491bb7a", + "shasum": "" + }, + "type": "drupal-module", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Adds autosave feature on forms.", + "support": { + "issues": "https://github.com/AmazeeLabs/silverback_autosave/issues", + "source": "https://github.com/AmazeeLabs/silverback_autosave/tree/1.2.0" + }, + "time": "2024-04-25T13:14:00+00:00" + }, { "name": "amazeelabs/silverback_campaign_urls", "version": "1.0.5", diff --git a/packages/drupal/silverback_autosave/LICENSE.txt b/packages/drupal/silverback_autosave/LICENSE.txt deleted file mode 100644 index d159169d1..000000000 --- a/packages/drupal/silverback_autosave/LICENSE.txt +++ /dev/null @@ -1,339 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - , 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. diff --git a/packages/drupal/silverback_autosave/README.md b/packages/drupal/silverback_autosave/README.md deleted file mode 100644 index 73b5da62f..000000000 --- a/packages/drupal/silverback_autosave/README.md +++ /dev/null @@ -1,7 +0,0 @@ -## Silverback autosave - -This module provides an autosave functionality on Gutenberg enabled nodes. - -On node edit, the content under edit is automatically saved for use in real time preview. - -The module is based on the [https://www.drupal.org/project/autosave_form](https://www.drupal.org/project/autosave_form). diff --git a/packages/drupal/silverback_autosave/composer.json b/packages/drupal/silverback_autosave/composer.json deleted file mode 100644 index d1359e6b8..000000000 --- a/packages/drupal/silverback_autosave/composer.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "drupal/silverback_autosave", - "type": "drupal-module", - "description": "Adds autosave feature on forms.", - "license": "GPL-2.0-or-later" -} diff --git a/packages/drupal/silverback_autosave/config/install/silverback_autosave.messages.yml b/packages/drupal/silverback_autosave/config/install/silverback_autosave.messages.yml deleted file mode 100644 index 338d94046..000000000 --- a/packages/drupal/silverback_autosave/config/install/silverback_autosave.messages.yml +++ /dev/null @@ -1 +0,0 @@ -entity_saved_in_background_alert_message: 'The content has been modified elsewhere. As a result, autosaving has been disabled and autosaved states for the current page have been deleted. Autosave will be reactivated after reloading the page.' \ No newline at end of file diff --git a/packages/drupal/silverback_autosave/config/install/silverback_autosave.settings.yml b/packages/drupal/silverback_autosave/config/install/silverback_autosave.settings.yml deleted file mode 100644 index 034332fef..000000000 --- a/packages/drupal/silverback_autosave/config/install/silverback_autosave.settings.yml +++ /dev/null @@ -1,9 +0,0 @@ -interval: 10000 -only_on_form_change: FALSE -active_on: - content_entity_forms: TRUE -notification: - active: TRUE - message: "Updating preview..." - delay: 1000 -allowed_content_entity_types: { } diff --git a/packages/drupal/silverback_autosave/config/schema/silverback_autosave.schema.yml b/packages/drupal/silverback_autosave/config/schema/silverback_autosave.schema.yml deleted file mode 100644 index 0fccb354c..000000000 --- a/packages/drupal/silverback_autosave/config/schema/silverback_autosave.schema.yml +++ /dev/null @@ -1,53 +0,0 @@ -# Schema for the configuration files of the Autosave Form module. - -silverback_autosave.settings: - type: config_object - label: 'Autosave Form Settings' - mapping: - interval: - type: integer - label: 'The interval used to trigger autosave.' - only_on_form_change: - type: boolean - label: 'Run autosave submission within the specified interval only if there was a form change.' - active_on: - type: mapping - label: 'Active on' - mapping: - content_entity_forms: - type: boolean - label: 'Content entity forms' - notification: - type: mapping - label: 'Notification' - mapping: - active: - type: boolean - label: 'Show notification each time autosave is triggered' - message: - type: text - label: 'Notification message' - delay: - type: integer - label: 'Notification duration' - allowed_content_entity_types: - type: sequence - label: 'Allowed content entity types' - sequence: - type: mapping - label: 'Entity type' - mapping: - bundles: - type: sequence - label: 'Allowed bundles of the entity type' - sequence: - type: string - label: 'Bundle' - -silverback_autosave.messages: - type: config_object - label: 'Autosave Form Messages' - mapping: - entity_saved_in_background_alert_message: - type: string - label: 'The alert message to show to the user if the entity has been saved meanwhile in the background.' diff --git a/packages/drupal/silverback_autosave/css/silverback_autosave.css b/packages/drupal/silverback_autosave/css/silverback_autosave.css deleted file mode 100644 index 85d069e50..000000000 --- a/packages/drupal/silverback_autosave/css/silverback_autosave.css +++ /dev/null @@ -1,15 +0,0 @@ -#autosave-notification { - display: none; - position: fixed; - bottom: 5px; - right: 5px; - z-index: 9999; - background-color: black; - color: white; - padding: 0.5em 1em; -} - -#autosave-notification:before { - content: "\2714"; - padding-right: 10px; -} diff --git a/packages/drupal/silverback_autosave/js/silverback_autosave.js b/packages/drupal/silverback_autosave/js/silverback_autosave.js deleted file mode 100644 index c4501987f..000000000 --- a/packages/drupal/silverback_autosave/js/silverback_autosave.js +++ /dev/null @@ -1,455 +0,0 @@ -/** - * @file - * Defines Javascript behaviors for the silverback_autosave module. - */ - -(function ($, Drupal, drupalSettings, once) { - 'use strict'; - - /** - * Define defaults. - */ - Drupal.autosaveForm = { - timer: null, - interval: null, - onlyOnFormChange: false, - autosaveFormRunning: false, - autosaveFormInstances: {}, - initialized: false, - formHasErrors: false, - message: '', - dialog_options: [], - autosave_submit_class: 'autosave-form-save', - notification: { - active: true, - message: Drupal.t('Updating preview...'), - delay: 1000, - }, - form: null, - }; - - /** - * Add a variable which determines if the window is being unloaded. - */ - Drupal.autosaveForm.beforeUnloadCalled = false; - - $(window).on('pagehide', function () { - Drupal.autosaveForm.beforeUnloadCalled = true; - }); - - $(window).on('beforeunload', function () { - $('#purge-button').trigger('click'); - Drupal.autosaveForm.beforeUnloadCalled = true; - }); - - /** - * Default dialog options. - */ - Drupal.autosaveForm.defaultDialogOptions = { - open: function () { - $(this).siblings('.ui-dialog-titlebar').remove(); - }, - modal: true, - zIndex: 10000, - position: { my: 'top', at: 'top+25%' }, - autoOpen: true, - width: 'auto', - resizable: false, - closeOnEscape: false, - }; - - /** - * Behaviors the autosave form feature. - * - * @type {Drupal~behavior} - * - * @prop {Drupal~behaviorAttach} attach - * Attaches the autosave behavior. - */ - Drupal.behaviors.autosaveForm = { - attach: function (context, settings) { - var $context = $(context); - var autosave_submit = $context.find( - 'form .' + Drupal.autosaveForm.autosave_submit_class, - ); - - // Disable autosave when the form is submitted in order to prevent race - // conditions and creating further autosave entries after the entity is - // saved. - if (autosave_submit.length > 0) { - Drupal.autosaveForm.form = $(autosave_submit[0]).parents( - 'form.autosave-form', - ); - - Drupal.autosaveForm.form.submit(function () { - if (Drupal.autosaveForm.autosaveFormRunning) { - Drupal.autosaveForm.autosaveFormRunning = false; - clearTimeout(Drupal.autosaveForm.timer); - Drupal.autosaveForm.timer = null; - } - }); - } - - if (settings.hasOwnProperty('autosaveForm')) { - $.extend(Drupal.autosaveForm, settings.autosaveForm); - } - - if (Drupal.autosaveForm.initialized) { - // If requested so turn off ajax submission. - if ( - !Drupal.autosaveForm.autosaveFormRunning && - Drupal.autosaveForm.timer - ) { - clearTimeout(Drupal.autosaveForm.timer); - Drupal.autosaveForm.timer = null; - } else { - return; - } - } - - // Continue to show the dialog or activate autosave functionality only in - // case the context contains the autosave submit and it is not disabled. - if ( - autosave_submit.length === 0 || - autosave_submit.is(':disabled') || - autosave_submit.hasClass('is-disabled') - ) { - return; - } - Drupal.autosaveForm.autosaveSubmit = autosave_submit; - - if ( - !Drupal.autosaveForm.initialized && - !Drupal.autosaveForm.autosaveFormRunning - ) { - Drupal.autosaveForm.initialized = true; - - $('
') - .appendTo('body') - .append(Drupal.autosaveForm.notification.message); - - autosavePeriodic(); - } - - /** - * Returns the ajax instance corresponding to an element. - * - * @param {string} class_name - * The element class name for which to return its ajax instance. - * - * @return {Drupal.Ajax | null} - * The ajax instance if found, otherwise null. - */ - function findAjaxInstance(class_name) { - if ( - !Drupal.autosaveForm.autosaveFormInstances.hasOwnProperty(class_name) - ) { - var element = document.getElementsByClassName(class_name)[0]; - var ajax = null; - var selector = '#' + element.id; - for (var index in Drupal.ajax.instances) { - if (Drupal.ajax.instances.hasOwnProperty(index)) { - var ajaxInstance = Drupal.ajax.instances[index]; - if (ajaxInstance && ajaxInstance.selector === selector) { - ajax = ajaxInstance; - break; - } - } - } - Drupal.autosaveForm.autosaveFormInstances[class_name] = ajax; - } - return Drupal.autosaveForm.autosaveFormInstances[class_name]; - } - - /** - * Triggers an ajax submit based on the class of the ajax element. - * - * @param {string} ajax_class - * The class of the ajax element. - * @param {boolean} skip_checks - * Skip checks. - */ - function triggerAjaxSubmitWithoutProgressIndication( - ajax_class, - skip_checks = false, - ) { - // If the autosave button suddenly gets the 'is-disabled' class then - // autosave submission should not run until the class is removed. - if ( - Drupal.autosaveForm.autosaveSubmit.is(':disabled') || - Drupal.autosaveForm.autosaveSubmit.hasClass('is-disabled') - ) { - return; - } - - // If configured so run only in the specified interval only if there - // was a form change. - if ( - !skip_checks && - Drupal.autosaveForm.onlyOnFormChange && - !Drupal.autosaveForm.form.data('autosave-form-changed') - ) { - return; - } - - // If there are gutenberg fields, we need to make sure - // the textarea value is updated before autosaving. - const $gutenbergFields = $('.field--gutenberg textarea').first(); - if ($gutenbergFields.length) { - const { data } = window.wp; - $gutenbergFields.each(function (i, element) { - // Update editor textarea with gutenberg content. - $(element).val(data.select('core/editor').getEditedPostContent()); - // We need to update the 'editor-value-is-changed' flag otherwise - // the content won't be updated. - $(element).data({ 'editor-value-is-changed': true }); - $(element).attr('data-editor-value-is-changed', true); - }); - } - - var ajax = findAjaxInstance(ajax_class); - if (ajax) { - if (Drupal.autosaveForm.notification.active) { - $('#autosave-notification') - .fadeIn() - .delay(Drupal.autosaveForm.notification.delay) - .fadeOut(); - } - ajax.success = function (response, status) { - // @todo: On success send a post request to update the preview - - // If interval submission is configured to happen only on form - // change, then reset the changed flag on successful autosave. - if ( - Drupal.autosaveForm.onlyOnFormChange && - Drupal.autosaveForm.form.data('autosave-form-changed') - ) { - Drupal.autosaveForm.form.data('autosave-form-changed', false); - } - - // Call original method with main functionality. - Drupal.Ajax.prototype.success.call(this, response, status); - }; - ajax.options.error = function ( - xmlhttprequest, - text_status, - error_thrown, - ) { - if (xmlhttprequest.status === 0 || xmlhttprequest.status >= 400) { - Drupal.autosaveForm.autosaveFormRunning = false; - clearTimeout(Drupal.autosaveForm.timer); - Drupal.autosaveForm.timer = null; - - if (!Drupal.autosaveForm.beforeUnloadCalled) { - var dialogOptions = { - buttons: { - button_confirm: { - text: Drupal.t('Ok'), - primary: true, - click: function () { - $(this).dialog('close'); - }, - }, - }, - }; - $.extend( - true, - dialogOptions, - Drupal.autosaveForm.defaultDialogOptions, - ); - - $('
') - .appendTo('body') - .html( - '
' + - Drupal.t( - 'A server error occurred during autosaving the current page. As a result autosave is disabled. To activate it please revisit the page and continue the editing from the last autosaved state of the current page.', - ) + - '
', - ) - .dialog(dialogOptions); - } - } - }; - // Disable progress indication. - ajax.progress = false; - $(ajax.element).trigger( - ajax.element_settings - ? ajax.element_settings.event - : ajax.elementSettings.event, - ); - } - } - - /** - * Starts periodic autosave submission. - */ - function autosavePeriodic() { - if (Drupal.autosaveForm.interval) { - Drupal.autosaveForm.autosaveFormRunning = true; - - // Run the autosave submission at the beginning to capture the user - // input and compare it later for changes, however wait for sometime - // until triggering the submission in order to let all the Drupal - // behaviors be executed and probably alter the page before doing the - // first submission, otherwise we might capture not the correct user - // input and on the second submission detect changes even if there - // aren't actually any changes. - // @todo Remove this and let autosave attach itself instead as the - // last behavior as soon as the following issues are fixed: - // @see https://www.drupal.org/node/2367655 - // @see https://www.drupal.org/node/2474019 - if (Drupal.autosaveForm.interval > 500) { - setTimeout(function () { - triggerAjaxSubmitWithoutProgressIndication( - Drupal.autosaveForm.autosave_submit_class, - true, - ); - }, 500); - } - - $('body').on('click keyup', () => { - clearTimeout(Drupal.autosaveForm.timer); - Drupal.autosaveForm.timer = setTimeout(function () { - if (!Drupal.ajax.instances.some(isAjaxing)) { - triggerAjaxSubmitWithoutProgressIndication( - Drupal.autosaveForm.autosave_submit_class, - ); - } - }, Drupal.autosaveForm.interval); - }); - } - } - - /** - * Checks if an ajax instance is currently running a submission. - * - * @param {Drupal.Ajax} instance - * The ajax instance. - * - * @return {boolean} - * TRUE if the ajax instance is in a state of submitting. - */ - function isAjaxing(instance) { - return ( - instance && - instance.hasOwnProperty('ajaxing') && - instance.ajaxing === true - ); - } - }, - }; - - /** - * Command to open a dialog for notifying that autosave has been disabled. - * - * We have to use a dedicated command, as otherwise there is no way to define - * the "click" on the button and close the dialog when the button is clicked. - * - * @param {Drupal.Ajax} ajax - * The Drupal Ajax object. - * @param {object} response - * Object holding the server response. - * @param {number} [status] - * The HTTP status code. - */ - Drupal.AjaxCommands.prototype.openAutosaveDisabledDialog = function ( - ajax, - response, - status, - ) { - response.dialogOptions.buttons = { - button_confirm: { - text: Drupal.t('Ok'), - click: function () { - $(this).dialog('close'); - }, - }, - }; - // Remove the "x" button and force confirmation through the "Ok" button. - response.dialogOptions.open = function () { - $(this) - .siblings('.ui-dialog-titlebar') - .find('.ui-dialog-titlebar-close') - .remove(); - }; - - Drupal.AjaxCommands.prototype.openDialog(ajax, response, status); - }; - - /** - * Attach behaviors to monitor changes on entity forms. - * - * This is a modified version of the monitoring functionality provided by - * https://www.drupal.org/project/entity_form_monitor. - * - * @type {Drupal~behavior} - * - * @prop {Drupal~behaviorAttach} attach - * Attaches triggers. - */ - Drupal.behaviors.autosaveFormMonitor = { - attach: function (context, settings) { - var $context = $(context); - var $form = $context.find('form.autosave-form'); - if ($form.length === 0) { - var $form = $context.parents('form.autosave-form'); - } - - // Disable autosave when the form is submitted in order to prevent race - // conditions and creating further autosave entries after the entity is - // saved. - if ($form.length > 0) { - // Detect new elements added through field widgets. - if ($context.find('.ajax-new-content').length > 0) { - $form.data('autosave-form-changed', true); - } - - // Add a change handler that will help us determine if any inputs - // inside the entity forms have changed values. - var inputs = $form - .find(':input, [contenteditable="true"]') - // Filter out buttons - .not('button, input[type="button"]'); - - $(once('autosave-form-input-monitor', inputs)) - .on('change textInput input', function (event) { - var $form = $(event.target).parents('.autosave-form').first(); - if ($form.length) { - var val = $(this).val(); - if (val != $(this).attr('autosave-old-val')) { - $(this).attr('autosave-old-val', val); - $form.data('autosave-form-changed', true); - } - } - }) - // Detect Ajax changes e.g. removing an element. - .on('mousedown', function (event) { - if (event.target.type === 'submit') { - $form.data('autosave-form-changed', true); - } - }); - - // Add change handlers to any CKEditor instances. - if (typeof CKEDITOR !== 'undefined') { - CKEDITOR.on('instanceCreated', function (event) { - event.editor.on('change', function (event) { - // Handle CKEditor change events. - if ( - typeof event.editor !== 'undefined' && - typeof event.target === 'undefined' - ) { - event.target = event.editor.element.$; - } - - var $form = $(event.target).parents('.autosave-form').first(); - if ($form.length) { - $form.data('autosave-form-changed', true); - } - }); - }); - } - } - }, - }; -})(jQuery, Drupal, drupalSettings, once); diff --git a/packages/drupal/silverback_autosave/silverback_autosave.info.yml b/packages/drupal/silverback_autosave/silverback_autosave.info.yml deleted file mode 100644 index 6608a328d..000000000 --- a/packages/drupal/silverback_autosave/silverback_autosave.info.yml +++ /dev/null @@ -1,8 +0,0 @@ -name: Silverback autosave -type: module -description: 'Adds autosave & preview feature on gutenberg nodes.' -package: Content -core_version_requirement: ^9.5 || ^10 -configure: silverback_autosave.admin_settings -dependencies: - - drupal:system diff --git a/packages/drupal/silverback_autosave/silverback_autosave.install b/packages/drupal/silverback_autosave/silverback_autosave.install deleted file mode 100644 index 6cde0cbdf..000000000 --- a/packages/drupal/silverback_autosave/silverback_autosave.install +++ /dev/null @@ -1,111 +0,0 @@ - 'Saves the form state of partially filled content entity form for restoration by the silverback_autosave module.', - 'fields' => [ - 'form_id' => [ - 'type' => 'varchar_ascii', - 'length' => AutosaveEntityFormStorageInterface::SILVERBACK_AUTOSAVE_FORM_ID_LENGTH, - 'not null' => TRUE, - ], - // We need the form session id as it is possible that the user opens the - // same form in two tabs and concurrently edits it. Therefore we have to - // assign each form session to an unique auto save session. - // We use the form build id for this and add an extra length to cover any - // case. - 'form_session_id' => [ - 'type' => 'varchar_ascii', - 'length' => AutosaveEntityFormStorageInterface::SILVERBACK_AUTOSAVE_FORM_ID_LENGTH, - 'not null' => TRUE, - ], - 'entity_type_id' => [ - 'type' => 'varchar_ascii', - 'length' => EntityTypeInterface::ID_MAX_LENGTH, - 'not null' => TRUE, - ], - 'entity_id' => [ - 'type' => 'varchar_ascii', - 'length' => EntityTypeInterface::ID_MAX_LENGTH, - 'not null' => TRUE, - ], - 'langcode' => [ - 'type' => 'varchar_ascii', - 'length' => 12, - 'not null' => TRUE, - ], - 'uid' => [ - 'type' => 'int', - 'not null' => TRUE, - ], - 'timestamp' => [ - 'type' => 'int', - 'not null' => TRUE, - ], - 'entity' => [ - 'type' => 'blob', - 'not null' => TRUE, - 'size' => 'big', - ], - 'form_state' => [ - 'type' => 'blob', - 'not null' => TRUE, - 'size' => 'big', - ], - ], - 'primary key' => ['form_id', 'form_session_id', 'entity_type_id', 'entity_id', 'langcode', 'uid', 'timestamp'], - ]; - - return $schema; -} - -/** - * Implements hook_install(). - */ -function silverback_autosave_install() { - // For some reasons it might happen that the autosave form library is not - // loaded or the handler is missing because on module install those both - // cache entries have not been invalidated, therefore we do it manually here. - Cache::invalidateTags(['entity_types', 'library_info']); -} - -/** - * Add the settings for the notification to the settings config. - */ -function silverback_autosave_update_8001() { - $config = \Drupal::configFactory()->getEditable('silverback_autosave.settings'); - if ($config->get('notification') === NULL) { - $config->set('notification', [ - 'active' => TRUE, - 'message' => 'Saving draft...', - 'delay' => 1000, - ]); - $config->save(TRUE); - } -} - -/** - * Populates the setting "only_on_form_change". - */ -function silverback_autosave_update_8002() { - $config = \Drupal::configFactory()->getEditable('silverback_autosave.settings'); - if ($config->get('only_on_form_change') === NULL) { - $config->set('only_on_form_change', FALSE); - $config->save(TRUE); - } -} diff --git a/packages/drupal/silverback_autosave/silverback_autosave.libraries.yml b/packages/drupal/silverback_autosave/silverback_autosave.libraries.yml deleted file mode 100644 index fe2005e5b..000000000 --- a/packages/drupal/silverback_autosave/silverback_autosave.libraries.yml +++ /dev/null @@ -1,16 +0,0 @@ -drupal.silverback_autosave: - version: 1.0 - css: - theme: - css/silverback_autosave.css: {} - js: - js/silverback_autosave.js: {} - dependencies: - - core/jquery - - core/drupal - - core/drupalSettings - - core/drupal.dialog - - core/drupal.dialog.ajax - - core/drupal.ajax - - core/once - - gutenberg/edit-node diff --git a/packages/drupal/silverback_autosave/silverback_autosave.links.menu.yml b/packages/drupal/silverback_autosave/silverback_autosave.links.menu.yml deleted file mode 100644 index cb94aa739..000000000 --- a/packages/drupal/silverback_autosave/silverback_autosave.links.menu.yml +++ /dev/null @@ -1,6 +0,0 @@ -silverback_autosave.admin_settings: - title: 'Silverback autosave and preview settings' - parent: system.admin_config_content - description: 'Change interval and forms on which autosave is active.' - route_name: silverback_autosave.admin_settings - weight: -20 diff --git a/packages/drupal/silverback_autosave/silverback_autosave.module b/packages/drupal/silverback_autosave/silverback_autosave.module deleted file mode 100644 index 7cc29d715..000000000 --- a/packages/drupal/silverback_autosave/silverback_autosave.module +++ /dev/null @@ -1,140 +0,0 @@ -hasHandlerClass('silverback_autosave')) { - $entity_type->setHandlerClass('silverback_autosave', AutosaveEntityFormHandler::class); - } - } -} - -/** - * Implements hook_form_alter(). - */ -function silverback_autosave_form_alter(&$form, FormStateInterface $form_state, $form_id) { - $active_on = \Drupal::configFactory()->get('silverback_autosave.settings')->get('active_on'); - $form_object = $form_state->getFormObject(); - - if ($form_object instanceof ContentEntityFormInterface) { - if (!$active_on['content_entity_forms']) { - return; - } - - $entity = $form_object->getEntity(); - $entity_type_id = $entity->getEntityTypeId(); - $allowed_content_entity_types = \Drupal::configFactory() - ->get('silverback_autosave.settings') - ->get('allowed_content_entity_types'); - - // Autosave is enabled if either no restriction has been made or the current - // entity type is part of the restriction and as well either no restriction - // on bundle level has been made or the current bundle is part of that - // restriction. - if (!empty($allowed_content_entity_types)) { - if (!isset($allowed_content_entity_types[$entity_type_id]) || - (!empty($allowed_content_entity_types[$entity_type_id]['bundles']) && !isset($allowed_content_entity_types[$entity_type_id]['bundles'][$entity->bundle()]))) { - return; - } - } - } - else { - return; - } - - // Allow autosave only for entity form routes, as forms might be included in - // blocks and other places and it is impossible to determine to which URL we - // have to post the autosave submit to. Also we don't support embedded forms - // as e.g. it might be surprising for the user getting autosave on the entity - // view, because e.g. a block is using an entity form. - $route = \Drupal::routeMatch()->getRouteObject(); - if ($route && ($route_defaults = $route->getDefaults()) && isset($route_defaults['_entity_form'])) { - [$entity_type_id, $form_op] = explode('.', $route_defaults['_entity_form']); - $entity = $form_object->getEntity(); - if (($entity->getEntityTypeId() != $entity_type_id) || ($form_object->getOperation() != $form_op)) { - return; - } - } - else { - return; - } - - $entity_type_manager = \Drupal::entityTypeManager(); - if ($entity_type_manager->hasHandler($entity->getEntityTypeId(), 'silverback_autosave')) { - $silverback_autosave_handler = $entity_type_manager->getHandler($entity->getEntityTypeId(), 'silverback_autosave'); - if ($silverback_autosave_handler instanceof AutosaveEntityFormHandlerInterface) { - $silverback_autosave_handler->formAlter($form, $form_state); - } - } -} - -/** - * Implements hook_entity_update(). - */ -function silverback_autosave_entity_update(EntityInterface $entity) { - static $conflict_enabled; - static $autosave_entity_form_storage; - - if (!isset($autosave_entity_form_storage)) { - /** @var \Drupal\silverback_autosave\Storage\AutosaveEntityFormStorageInterface $autosave_entity_form_storage */ - $autosave_entity_form_storage = \Drupal::service('silverback_autosave.entity_form_storage'); - } - - if (($conflict_enabled === FALSE) || (is_null($conflict_enabled) && !($conflict_enabled = \Drupal::moduleHandler()->moduleExists('conflict')))) { - // If conflict management is not available the autosaved entity states have - // to be removed when the entity is saved. - $autosave_entity_form_storage->purgeAutosavedEntityState($entity->getEntityTypeId(), $entity->id()); - } - else { - $entity_type = $entity->getEntityType(); - if ($entity_type->hasHandlerClass('silverback_autosave') && ($class = $entity_type->getHandlerClass('silverback_autosave'))) { - // If conflict is enabled and the entity is saved then delete only the - // current autosave session of the current user. - if ($autosave_session_id = $class::getAutosaveSessionID($entity)) { - $autosave_entity_form_storage->purgeAutosavedEntityState($entity->getEntityTypeId(), $entity->id(), $autosave_session_id); - } - } - } -} - -/** - * Implements hook_entity_update() for the user entity type. - * - * If the permissions of a user are changed, then we delete the autosave states - * belonging to that user. It is possible that new permissions the user is - * losing the ability to access certain fields, but the autosave states contain - * data for them. - */ -function silverback_autosave_user_update(EntityInterface $user) { - /** @var \Drupal\user\UserInterface $user */ - $current_roles = $user->getRoles(); - $original_roles = $user->original->getRoles(); - sort($current_roles); - sort($original_roles); - - if ($current_roles !== $original_roles) { - /** @var \Drupal\silverback_autosave\Storage\AutosaveEntityFormStorageInterface $autosave_entity_form_storage */ - $autosave_entity_form_storage = \Drupal::service('silverback_autosave.entity_form_storage'); - $autosave_entity_form_storage->purgeAutosavedEntitiesStates(NULL, NULL, $user->id()); - } -} diff --git a/packages/drupal/silverback_autosave/silverback_autosave.routing.yml b/packages/drupal/silverback_autosave/silverback_autosave.routing.yml deleted file mode 100644 index ad468d60a..000000000 --- a/packages/drupal/silverback_autosave/silverback_autosave.routing.yml +++ /dev/null @@ -1,7 +0,0 @@ -silverback_autosave.admin_settings: - path: '/admin/config/content/silverback_autosave' - defaults: - _form: '\Drupal\silverback_autosave\Form\AutosaveFormSettingsForm' - _title: 'Silverback autosave settings' - requirements: - _permission: 'administer site configuration' diff --git a/packages/drupal/silverback_autosave/silverback_autosave.services.yml b/packages/drupal/silverback_autosave/silverback_autosave.services.yml deleted file mode 100644 index f09b27af4..000000000 --- a/packages/drupal/silverback_autosave/silverback_autosave.services.yml +++ /dev/null @@ -1,51 +0,0 @@ -services: - silverback_autosave.entity_form_storage: - class: \Drupal\silverback_autosave\Storage\AutosaveEntityFormDatabaseStorage - arguments: [ - '@database', - '@serialization.phpserialize', - '@http_client', - '@logger.channel.default', - ] - - form_validator.silverback_autosave: - public: false - class: \Drupal\silverback_autosave\Form\AutosaveFormValidator - decorates: form_validator - arguments: - [ - '@form_validator.silverback_autosave.inner', - '@request_stack', - '@string_translation', - '@csrf_token', - '@logger.channel.form', - '@form_error_handler', - ] - form_builder.silverback_autosave: - public: false - class: \Drupal\silverback_autosave\Form\AutosaveFormBuilder - decorates: form_builder - # The decorated autosave form builder should be the first applied as it - # will not call further decorators, but directly override the core form - # builder by extending from it and just calling the parent methods. - decoration_priority: 10000 - arguments: - [ - '@form_builder.silverback_autosave.inner', - '@form_validator', - '@form_submitter', - '@form_cache', - '@module_handler', - '@event_dispatcher', - '@request_stack', - '@class_resolver', - '@element_info', - '@theme.manager', - '@?csrf_token', - '@silverback_autosave.entity_form_storage', - ] - form_error_handler.silverback_autosave: - public: false - class: Drupal\silverback_autosave\Form\AutosaveFormErrorHandler - decorates: form_error_handler - arguments: ['@form_error_handler.silverback_autosave.inner'] diff --git a/packages/drupal/silverback_autosave/src/Ajax/OpenAutosaveDisabledDialog.php b/packages/drupal/silverback_autosave/src/Ajax/OpenAutosaveDisabledDialog.php deleted file mode 100644 index 7fad3b04f..000000000 --- a/packages/drupal/silverback_autosave/src/Ajax/OpenAutosaveDisabledDialog.php +++ /dev/null @@ -1,21 +0,0 @@ - 'openAutosaveDisabledDialog'] + parent::render(); - } - -} diff --git a/packages/drupal/silverback_autosave/src/EmptyAlter/Extension/ModuleHandlerEmptyAlter.php b/packages/drupal/silverback_autosave/src/EmptyAlter/Extension/ModuleHandlerEmptyAlter.php deleted file mode 100644 index ce2975c10..000000000 --- a/packages/drupal/silverback_autosave/src/EmptyAlter/Extension/ModuleHandlerEmptyAlter.php +++ /dev/null @@ -1,22 +0,0 @@ -getTriggeringElement(); - if (is_null($triggering_element)) { - $user_input = $form_state->getUserInput(); - $autosave = isset($user_input['_triggering_element_name']) && ($user_input['_triggering_element_name'] == AutosaveFormInterface::AUTOSAVE_ELEMENT_NAME); - } - else { - $autosave = $triggering_element && !empty($triggering_element['#silverback_autosave']); - } - return $autosave; - } - -} diff --git a/packages/drupal/silverback_autosave/src/Form/AutosaveEntityFormHandler.php b/packages/drupal/silverback_autosave/src/Form/AutosaveEntityFormHandler.php deleted file mode 100644 index 562f1b3a2..000000000 --- a/packages/drupal/silverback_autosave/src/Form/AutosaveEntityFormHandler.php +++ /dev/null @@ -1,327 +0,0 @@ -entityTypeId = $entity_type->id(); - $this->entityType = $entity_type; - $this->entityStorage = $entity_type_manager->getStorage($entity_type->id()); - $this->currentUser = $current_user; - $this->autosaveEntityFormStorage = $autosave_entity_form_storage; - $this->time = $time; - $this->dateFormatter = $date_formatter; - $this->configFactory = $config_factory; - $this->keyValueExpirableFactory = $key_value_expirable_factory; - $this->conflictEnabled = $module_handler->moduleExists('conflict'); - } - - /** - * {@inheritdoc} - */ - public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { - return new static( - $entity_type, - $container->get('entity_type.manager'), - $container->get('current_user'), - $container->get('silverback_autosave.entity_form_storage'), - $container->get('datetime.time'), - $container->get('date.formatter'), - $container->get('config.factory'), - $container->get('keyvalue.expirable'), - $container->get('module_handler') - ); - } - - /** - * Performs the needed alterations to the entity form. - * - * @param array $form - * The entity form to be altered to provide the autosave functionality. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - */ - public function formAlter(array &$form, FormStateInterface $form_state) { - $this->traitFormAlter($form, $form_state); - // Flag the entity with the current autosave session ID. - $form['#entity_builders'][] = [static::class, 'entityFormEntityBuild']; - } - - /** - * Ajax callback for autosaving content entity forms. - */ - public function autosaveFormAjax($form, FormStateInterface $form_state) { - $response = $this->traitAutosaveFormAjax($form, $form_state); - $timestamp = $form_state->getTemporaryValue('silverback_autosave_last_autosave_timestamp'); - if ($timestamp == 'entity_saved_meanwhile') { - $input = $form_state->getUserInput(); - // Autosave has run already or this is the first run. - if (is_numeric($input['silverback_autosave_last_autosave_timestamp']) || empty($input['silverback_autosave_last_autosave_timestamp'])) { - // Alert the user about turning off autosave and deleting all the - // autosaved states. - $message = $this->configFactory->get('silverback_autosave.messages') - ->get('entity_saved_in_background_alert_message'); - - $options = [ - 'width' => 'auto', - 'closeOnEscape' => FALSE, - ]; - $response->addCommand(new OpenAutosaveDisabledDialog($this->t('Autosave has been disabled'), $message, $options)); - - // Reset the last autosaved timestamp on the page. - $response->addCommand(new InvokeCommand('input[name="silverback_autosave_last_autosave_timestamp"]', 'attr', ['value', (string) $this->t('Autosave is turned off.')])); - // Stop the autosave submissions. - $attachments = []; - $attachments['drupalSettings']['autosaveForm']['autosaveFormRunning'] = FALSE; - $response->addAttachments($attachments); - } - } - - return $response; - } - - /** - * Entity builder method. - * - * Flags the entity with the autosave form session ID. - * - * @param string $entity_type - * The type of the entity. - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity whose form is being built. - */ - public static function entityFormEntityBuild($entity_type, EntityInterface $entity, array $form, FormStateInterface $form_state) { - if ($silverback_autosave_session_id = static::getAutosaveFormSessionID($form_state)) { - $entity->{static::AUTOSAVE_SESSION_ID} = $silverback_autosave_session_id; - } - } - - /** - * {@inheritdoc} - */ - public function isAutosaveSubmitValid(FormStateInterface $form_state) { - [$form_id, $entity] = $this->getFormIDandEntity($form_state); - - // Check that the entity is still valid. - if ($entity instanceof EntityChangedInterface) { - $changed_time = $entity->getChangedTime(); - - $input = $form_state->getUserInput(); - $changed_form_value = $input['changed'] ?? NULL; - $entity->setChangedTime($changed_form_value ?: $changed_time); - - if (!$this->conflictEnabled && ($unchanged = $this->entityStorage->loadUnchanged($entity->id())) && ($unchanged->getChangedTimeAcrossTranslations() > $entity->getChangedTimeAcrossTranslations())) { - $form_state->setTemporaryValue('silverback_autosave_last_autosave_timestamp', 'entity_saved_meanwhile'); - return FALSE; - } - else { - $entity->setChangedTime($changed_time); - } - } - return TRUE; - } - - /** - * {@inheritdoc} - */ - public function getLastAutosavedFormState(FormStateInterface $form_state, $silverback_autosave_session_id, $uid) { - [$form_id, $entity] = $this->getFormIDandEntity($form_state); - return $this->autosaveEntityFormStorage->getFormState($form_id, $entity->getEntityTypeId(), $entity->id(), $entity->language()->getId(), $uid, $silverback_autosave_session_id); - } - - /** - * {@inheritdoc} - */ - public function storeState(FormStateInterface $form_state, $silverback_autosave_session_id, $autosave_timestamp, $uid) { - [$form_id, $entity] = $this->getFormIDandEntity($form_state); - $this->autosaveEntityFormStorage->storeEntityAndFormState($form_id, $silverback_autosave_session_id, $entity->getEntityTypeId(), $entity->id(), $entity->language()->getId(), $uid, $autosave_timestamp, $entity, $form_state); - } - - /** - * {@inheritdoc} - */ - public function getLastAutosavedTimestamp(FormStateInterface $form_state, $uid) { - [$form_id, $entity] = $this->getFormIDandEntity($form_state); - return $entity->isNew() ? NULL : $this->autosaveEntityFormStorage->getLastAutosavedStateTimestamp($form_id, $entity->getEntityTypeId(), $entity->id(), $entity->language()->getId(), $uid); - } - - /** - * {@inheritdoc} - */ - public function purgeCurrentAutosavedState(FormStateInterface $form_state, $uid) { - [$form_id, $entity] = $this->getFormIDandEntity($form_state); - $this->autosaveEntityFormStorage->purgeAutosavedEntityState($entity->getEntityTypeId(), $entity->id(), $this->getAutosaveFormSessionID($form_state), $form_id, $entity->language()->getId(), $uid); - } - - /** - * {@inheritdoc} - */ - public function purgeAllAutosavedStates(FormStateInterface $form_state, $uid) { - [$form_id, $entity] = $this->getFormIDandEntity($form_state); - $this->autosaveEntityFormStorage->purgeAutosavedEntityState($entity->getEntityTypeId(), $entity->id(), NULL, $form_id, $entity->language()->getId(), $uid); - } - - /** - * {@inheritdoc} - */ - public function isAutosaveEnabled(FormStateInterface $form_state) { - [$form_id, $entity] = $this->getFormIDandEntity($form_state); - $allowed = !$entity->isNew() && !$this->currentUser->isAnonymous(); - return $allowed; - } - - /** - * {@inheritdoc} - */ - public static function getAutosaveSessionID(EntityInterface $entity) { - if (isset($entity->{static::AUTOSAVE_SESSION_ID})) { - return $entity->{static::AUTOSAVE_SESSION_ID}; - } - else { - return NULL; - } - } - - /** - * Retrieves the form ID and the form entity object from the form state. - * - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - * - * @return array - * The first value is the form ID and the second the form entity object. - */ - protected function getFormIDandEntity(FormStateInterface $form_state) { - /** @var \Drupal\Core\Entity\EntityFormInterface $form_object */ - $form_object = $form_state->getFormObject(); - $form_id = $form_object->getFormId(); - $entity = $form_object->getEntity(); - return [$form_id, $entity]; - } - -} diff --git a/packages/drupal/silverback_autosave/src/Form/AutosaveEntityFormHandlerInterface.php b/packages/drupal/silverback_autosave/src/Form/AutosaveEntityFormHandlerInterface.php deleted file mode 100644 index 73533bb0c..000000000 --- a/packages/drupal/silverback_autosave/src/Form/AutosaveEntityFormHandlerInterface.php +++ /dev/null @@ -1,25 +0,0 @@ -isAutosaveEnabled($form_state)) { - return; - } - - $form['#attributes']['class'][] = 'autosave-form'; - $form['#attached']['library'][] = 'silverback_autosave/drupal.silverback_autosave'; - $form['#attached']['drupalSettings']['autosaveForm']['interval'] = $this->configFactory->get('silverback_autosave.settings')->get('interval'); - $form['#attached']['drupalSettings']['autosaveForm']['onlyOnFormChange'] = $this->configFactory->get('silverback_autosave.settings')->get('only_on_form_change'); - $form['#attached']['drupalSettings']['autosaveForm']['notification'] = $this->configFactory->get('silverback_autosave.settings')->get('notification'); - $input = $form_state->getUserInput(); - - $silverback_autosave_session_id = $this->getAutosaveFormSessionID($form_state); - if (!$silverback_autosave_session_id) { - $silverback_autosave_session_id = !empty($input['silverback_autosave_session_id']) ? - $input['silverback_autosave_session_id'] : - $form['#build_id']; - $this->setAutosaveFormSessionID($form_state, $silverback_autosave_session_id); - } - - $form['silverback_autosave_session_id'] = [ - '#type' => 'hidden', - '#value' => $silverback_autosave_session_id, - '#name' => 'silverback_autosave_session_id', - // Form processing and validation requires this value, so ensure the - // submitted form value appears literally, regardless of custom #tree - // and #parents being set elsewhere. - '#parents' => ['silverback_autosave_session_id'], - ]; - - $form[AutosaveFormInterface::AUTOSAVE_ELEMENT_NAME] = [ - '#type' => 'submit', - '#name' => AutosaveFormInterface::AUTOSAVE_ELEMENT_NAME, - '#value' => $this->t('Autosave save'), - '#attributes' => ['class' => ['autosave-form-save', 'visually-hidden']], - '#submit' => [[$this, 'autosaveFormSubmit']], - '#ajax' => [ - 'callback' => [$this, 'autosaveFormAjax'], - // Do not refocus to prevent losing focus of the element the user might - // be currently editing when the autosave submission is triggered. - 'disable-refocus' => TRUE, - 'progress' => FALSE, - ], - '#silverback_autosave' => TRUE, - // Retrieve the "silverback_autosave_session_id" also from the form state as on - // autosave restore the one from the restored state will be present in - // the form state storage and we want to continue using that session for - // the further autosave states after the restoration. - '#silverback_autosave_session_id' => $silverback_autosave_session_id, - ]; - - $form['purge'] = [ - '#type' => 'submit', - '#id' => 'purge-button', - '#name' => 'autosave_form_purge', - '#value' => $this->t('Autosave purge'), - '#limit_validation_errors' => [], - '#attributes' => ['class' => ['autosave-form-purge', 'visually-hidden']], - '#submit' => [[$this, 'autosaveFormPurgeSubmit']], - '#ajax' => [ - 'callback' => [$this, 'autosaveFormPurgeAjax'], - 'event' => 'click', - ], - ]; - - $form['silverback_autosave_last_autosave_timestamp'] = [ - '#type' => 'hidden', - '#name' => 'silverback_autosave_last_autosave_timestamp', - '#value' => $form_state->get('silverback_autosave_last_autosave_timestamp') ?: '', - ]; - } - - /** - * Form submission handler for rejecting autosaved states. - */ - public function autosaveFormPurgeSubmit($form, FormStateInterface $form_state) { - \Drupal::logger('debug')->debug(__METHOD__); - $this->purgeAllAutosavedStates($form_state, $this->currentUser->id()); - } - - /** - * Ajax callback for rejecting autosaved states. - */ - public function autosaveFormPurgeAjax($form, FormStateInterface $form_state) { - \Drupal::logger('debug')->debug(__METHOD__); - return new AjaxResponse(); - } - - /** - * Form submission handler for autosaving forms. - */ - public function autosaveFormSubmit($form, FormStateInterface $form_state) { - // As this processing might take some time we want to prevent that if the - // connection is terminated the user input will be lost. - ignore_user_abort(TRUE); - if (!$this->isAutosaveSubmitValid($form_state)) { - $form_state->disableCache(); - return; - } - // Having an autosave form session id also ensures that after resuming - // editing the new autosaved entities will be saved to the same autosave - // session id. - $silverback_autosave_session_id = $this->getAutosaveFormSessionID($form_state); - $current_user_id = $this->currentUser->id(); - $autosaved_form_state = $this->getLastAutosavedFormState($form_state, $silverback_autosave_session_id, $current_user_id); - // If there is non-autosaved state for this session then we have to put the - // user input into a temporary store and on each autosave submit compare - // against it for changes and after the first change compare with the last - // autosaved state. - if (is_null($autosaved_form_state)) { - if ($initial_user_input = $this->keyValueExpirableFactory->get('silverback_autosave')->get($silverback_autosave_session_id)) { - $autosaved_form_state_input = $initial_user_input; - } - else { - // 6 hours cache life time for forms should be plenty, like the form - // cache. - $expire = 21600; - $this->keyValueExpirableFactory->get('silverback_autosave')->setWithExpire($silverback_autosave_session_id, $form_state->getUserInput(), $expire); - - // This is the first where we cache the user input initially and we are - // done. - $form_state->disableCache(); - return; - } - } - else { - $autosaved_form_state_input = $autosaved_form_state->getUserInput(); - } - - // Subsequent autosaving - compare the user input only. This should be - // sufficient to detect changes in the fields. - $form_state_input = $form_state->getUserInput(); - - $skip_from_comparison_keys = [ - 'form_build_id', - 'form_token', - 'ajax_page_state', - 'silverback_autosave_last_autosave_timestamp', - ]; - foreach ($skip_from_comparison_keys as $skip_from_comparison_key) { - unset($autosaved_form_state_input[$skip_from_comparison_key]); - unset($form_state_input[$skip_from_comparison_key]); - } - - $store = $autosaved_form_state_input != $form_state_input; - - if ($store) { - $autosave_timestamp = $this->time->getRequestTime(); - $form_state->set('silverback_autosave_last_autosave_timestamp', $autosave_timestamp); - $form_state->setTemporaryValue('silverback_autosave_last_autosave_timestamp', $autosave_timestamp); - - $this->storeState($form_state, $silverback_autosave_session_id, $autosave_timestamp, $current_user_id); - $this->keyValueExpirableFactory->get('silverback_autosave')->delete($silverback_autosave_session_id); - } - - // We don't have to cache the form each time an autosave submission is - // triggered, especially when we've skipped the form validation. - $form_state->disableCache(); - } - - /** - * Ajax callback for autosaving forms. - */ - public function autosaveFormAjax($form, FormStateInterface $form_state) { - $response = new AjaxResponse(); - - $timestamp = $form_state->getTemporaryValue('silverback_autosave_last_autosave_timestamp'); - if (is_numeric($timestamp)) { - $response->addCommand(new InvokeCommand('input[name="silverback_autosave_last_autosave_timestamp"]', 'attr', ['value', $timestamp])); - } - - return $response; - } - - /** - * Retrieves the autosave form session ID. - * - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - * - * @return string|null - * The autosave form session ID or NULL if none present yet. - */ - protected static function getAutosaveFormSessionID(FormStateInterface $form_state) { - return $form_state->get('silverback_autosave_session_id'); - } - - /** - * Sets the autosave form session ID into the form state. - * - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - * @param string $silverback_autosave_session_id - * The autosave form session ID. - */ - protected function setAutosaveFormSessionID(FormStateInterface $form_state, $silverback_autosave_session_id) { - $form_state->set('silverback_autosave_session_id', $silverback_autosave_session_id); - } - - /** - * Returns the HTTP method used by the request that is building the form. - * - * @return string - * Can be any valid HTTP method, such as GET, POST, HEAD, etc. - */ - protected function getRequestMethod() { - return \Drupal::requestStack()->getCurrentRequest()->getMethod(); - } - -} diff --git a/packages/drupal/silverback_autosave/src/Form/AutosaveFormBuilder.php b/packages/drupal/silverback_autosave/src/Form/AutosaveFormBuilder.php deleted file mode 100644 index 3c8bd0b7f..000000000 --- a/packages/drupal/silverback_autosave/src/Form/AutosaveFormBuilder.php +++ /dev/null @@ -1,251 +0,0 @@ -formBuilder = $form_builder; - $this->autosaveEntityFormStorage = $autosave_entity_form_storage; - } - - /** - * - */ - public function buildForm($form_id, FormStateInterface &$form_state) { - $form = parent::buildForm($form_id, $form_state); - if ($form_state::hasAnyErrors()) { - // Under circumstances it might happen that the form is submitted but - // returned with validation errors and the form alter hooks are executed - // thus leading to the autosave form alter code being executed as well and - // putting the autosave resume/discard message to the form, which should - // not happen if the form is being returned to the browser with validation - // errors. In order to prevent this we have to add the resume/discard - // message and options only on HTTP GET requests or on POST requests if - // restore or reject submit operations have been performed or in a more - // complex case if the message has not been yet confirmed but other - // AJAX / POST requests are being triggered in the background. As we could - // not detect the last case we still put the form elements into the form, - // but on the client side we will not show the message if the form is - // returned with validation errors. - $form['#attached']['drupalSettings']['autosaveForm']['formHasErrors'] = TRUE; - - // Additionally unset the form elements and settings which might have been - // added, but aren't actually needed. - unset($form['#attached']['drupalSettings']['autosaveForm']['message']); - } - return $form; - } - - /** - * {@inheritdoc} - */ - public function processForm($form_id, &$form, FormStateInterface &$form_state) { - if ($this->isAutosaveTriggered($form_state)) { - // @todo should we add a condition, that the form state is already cached - // in order to stop fully processing the form? - $this->doBuildFormSkip = TRUE; - - // As we'll skip doBuildForm we have to take care of setting the - // triggering element. - $form_state->setTriggeringElement($form[AutosaveFormInterface::AUTOSAVE_ELEMENT_NAME]); - - // Needed to execute the submit handler, as this will not be done if - // duBuildForm is not being executed. - $form_state->setSubmitHandlers($form[AutosaveFormInterface::AUTOSAVE_ELEMENT_NAME]['#submit']); - $form_state->setProcessInput(); - $form_state->setSubmitted(); - } - - $response = parent::processForm($form_id, $form, $form_state); - $this->doBuildFormSkip = FALSE; - - return $response; - } - - /** - * {@inheritdoc} - */ - public function doBuildForm($form_id, &$element, FormStateInterface &$form_state) { - return $this->doBuildFormSkip ? $element : parent::doBuildForm($form_id, $element, $form_state); - } - - /** - * {@inheritdoc} - */ - public function rebuildForm($form_id, FormStateInterface &$form_state, $old_form = NULL) { - $this->restoreAutosavedState($form_id, $form_state); - return parent::rebuildForm($form_id, $form_state, $old_form); - } - - /** - * Restores an autosaved form state. - * - * @param $form_id - * The form id. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - */ - protected function restoreAutosavedState($form_id, FormStateInterface $form_state) { - if (!$form_state->get('silverback_autosave_restored') && ($autosaved_timestamp = $form_state->get('silverback_autosave_state_timestamp'))) { - $form_object = $form_state->getFormObject(); - - // Restore entity form. - if ($form_object instanceof EntityFormInterface) { - $entity = $form_object->getEntity(); - $autosaved_state = $this->autosaveEntityFormStorage->getEntityAndFormState($form_id, $entity->getEntityTypeId(), $entity->id(), $entity->language()->getId(), $this->currentUser()->id(), NULL, $autosaved_timestamp); - - if (is_null($autosaved_state)) { - // @todo Cover the case that the autosaved state has been purged - // meanwhile. - return; - } - - /** @var \Drupal\Core\Entity\EntityInterface $autosaved_entity */ - $autosaved_entity = $autosaved_state['entity']; - /** @var \Drupal\Core\Form\FormStateInterface $autosaved_form_state */ - $autosaved_form_state = $autosaved_state['form_state']; - - // Restore the form with the entity from the autosaved state. - $form_object->setEntity($autosaved_entity); - // Restore the user input. - $current_user_input = $form_state->getUserInput(); - $autosaved_user_input = $autosaved_form_state->getUserInput(); - // We have to rebuild the form and keep the generated form token - // instead of putting the one from the autosaved input, otherwise the - // form builder will set an form state error and, which is going to - // result into an exception, as setting form state errors after the - // validation phase is forbidden. - if (isset($current_user_input['form_token'])) { - $autosaved_user_input['form_token'] = $current_user_input['form_token']; - } - $form_state->setUserInput($autosaved_user_input); - // Recover the form state storage, which is needed to continue from the - // state at which the form was left. - $form_state->setStorage($autosaved_form_state->getStorage()); - - // Flag the form state as being restored from autosave. - $form_state->set('silverback_autosave_restored', TRUE); - } - elseif ($form_object instanceof FormInterface) { - // @todo add support for regular forms. - } - } - } - - /** - * {@inheritdoc} - */ - public function prepareForm($form_id, &$form, FormStateInterface &$form_state) { - $prevent_hooks = FALSE; - - if ($this->isAutosaveTriggered($form_state)) { - // There is no need of generating a new form build id after triggering - // autosave. - $form['#build_id'] = $form_state->getUserInput()['form_build_id']; - - if ($form_state->isCached()) { - $prevent_hooks = TRUE; - } - } - - if ($prevent_hooks) { - // Prevent running hooks. - $module_handler = $this->moduleHandler; - $theme_manager = $this->themeManager; - $this->moduleHandler = new ModuleHandlerEmptyAlter(); - $this->themeManager = new ThemeManagerEmptyAlter(); - } - - parent::prepareForm($form_id, $form, $form_state); - - if ($prevent_hooks) { - $this->moduleHandler = $module_handler; - $this->themeManager = $theme_manager; - } - } - -} diff --git a/packages/drupal/silverback_autosave/src/Form/AutosaveFormErrorHandler.php b/packages/drupal/silverback_autosave/src/Form/AutosaveFormErrorHandler.php deleted file mode 100644 index cd8689c24..000000000 --- a/packages/drupal/silverback_autosave/src/Form/AutosaveFormErrorHandler.php +++ /dev/null @@ -1,43 +0,0 @@ -formErrorHandler = $form_error_handler; - } - - /** - * {@inheritdoc} - */ - public function handleFormErrors(array &$form, FormStateInterface $form_state) { - if ($this->isAutosaveTriggered($form_state)) { - return $this; - } - return $this->formErrorHandler->handleFormErrors($form, $form_state); - } - -} diff --git a/packages/drupal/silverback_autosave/src/Form/AutosaveFormInterface.php b/packages/drupal/silverback_autosave/src/Form/AutosaveFormInterface.php deleted file mode 100644 index d08030316..000000000 --- a/packages/drupal/silverback_autosave/src/Form/AutosaveFormInterface.php +++ /dev/null @@ -1,109 +0,0 @@ -entityTypeManager = $entity_type_manager; - $this->entityTypeBundleInfo = $entity_type_bundle_info; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('entity_type.manager'), - $container->get('entity_type.bundle.info') - ); - } - - /** - * {@inheritdoc} - */ - public function getFormId() { - return 'silverback_autosave_admin_settings'; - } - - /** - * {@inheritdoc} - */ - protected function getEditableConfigNames() { - return [ - 'silverback_autosave.settings', - ]; - } - - /** - * {@inheritdoc} - */ - public function buildForm(array $form, FormStateInterface $form_state) { - $config = $this->config('silverback_autosave.settings'); - - $form['interval'] = [ - '#type' => 'number', - '#title' => $this->t('The interval to use for triggering autosave in milliseconds.'), - '#default_value' => $config->get('interval'), - ]; - $form['only_on_form_change'] = [ - '#type' => 'checkbox', - '#access' => FALSE, - '#title' => $this->t('Run only on form change.') . ' ' . $this->t('(Experimental)'), - '#description' => $this->t('If enabled an autosave submission will only occur if the form changed since the previous autosave submission.'), - '#default_value' => $config->get('only_on_form_change'), - ]; - - $form['active_on'] = [ - '#type' => 'fieldset', - '#title' => $this->t('Active on:'), - '#tree' => TRUE, - ]; - $form['active_on']['content_entity_forms'] = [ - '#type' => 'checkbox', - '#title' => $this->t('Content Entity Forms'), - '#default_value' => $config->get('active_on')['content_entity_forms'], - ]; - - $form['notification'] = [ - '#type' => 'fieldset', - '#title' => $this->t('Notification settings'), - '#description' => $this->t('Display a simple notification every time content is saved'), - '#tree' => TRUE, - ]; - $form['notification']['active'] = [ - '#type' => 'checkbox', - '#title' => $this->t('Active'), - '#default_value' => $config->get('notification')['active'], - ]; - $form['notification']['message'] = [ - '#type' => 'textfield', - '#title' => $this->t('Message'), - '#default_value' => $config->get('notification')['message'], - '#states' => [ - 'visible' => [ - ':input[name="notification[active]"]' => ['checked' => TRUE], - ], - ], - ]; - $form['notification']['delay'] = [ - '#type' => 'number', - '#title' => $this->t('The duration of the notification in milliseconds.'), - '#default_value' => $config->get('notification')['delay'], - '#states' => [ - 'visible' => [ - ':input[name="notification[active]"]' => ['checked' => TRUE], - ], - ], - ]; - - $allowed_content_entity_types = $config->get('allowed_content_entity_types'); - $form['allowed_content_entities'] = [ - '#type' => 'details', - '#open' => !empty($allowed_content_entity_types), - '#title' => $this->t('Allowed Content Entity Forms'), - '#description' => $this->t('In case no entity type is selected then autosave is enabled on all entity forms, otherwise it will be enabled only on the selected ones. Selecting only the entity type will enable all corresponding bundles and selecting only a subset of the bundles will enable autosave only for those bundles and will be disabled for the others.'), - '#tree' => TRUE, - '#states' => [ - 'visible' => [ - ':input[name="active_on[content_entity_forms]"]' => ['checked' => TRUE], - ], - ], - ]; - - /** @var \Drupal\Core\Entity\EntityTypeInterface $entity_type */ - foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) { - if ($entity_type instanceof ContentEntityTypeInterface) { - $entity_type_label = $entity_type->getLabel(); - $bundles_info = $this->entityTypeBundleInfo->getBundleInfo($entity_type_id); - $allowed_bundles = !empty($allowed_content_entity_types[$entity_type_id]['bundles']) ? $allowed_content_entity_types[$entity_type_id]['bundles'] : []; - $bundles = []; - foreach ($bundles_info as $key => $bundle) { - $bundles[$key] = $bundle['label']; - } - - $form['allowed_content_entities'][$entity_type_id]['active'] = [ - '#type' => 'checkbox', - '#title' => $entity_type_label, - '#default_value' => isset($allowed_content_entity_types[$entity_type_id]), - ]; - $form['allowed_content_entities'][$entity_type_id]['bundles'] = [ - '#type' => 'details', - '#open' => !empty($allowed_bundles), - '#title' => $entity_type_label . ' ' . $this->t('bundles'), - '#states' => [ - 'visible' => [ - ':input[name="allowed_content_entities[' . $entity_type_id . '][active]"]' => ['checked' => TRUE], - ], - ], - ]; - $form['allowed_content_entities'][$entity_type_id]['bundles']['selection'] = [ - '#type' => 'checkboxes', - '#default_value' => $allowed_bundles, - '#options' => $bundles, - '#prefix' => '
', - '#suffix' => '
', - ]; - } - } - - return parent::buildForm($form, $form_state); - } - - /** - * {@inheritdoc} - */ - public function submitForm(array &$form, FormStateInterface $form_state) { - /** @var \Drupal\Core\Config\Config $config */ - $config = $this->config('silverback_autosave.settings'); - $allowed_content_entity_types = []; - foreach ($form_state->getValue('allowed_content_entities') as $entity_type_id => $data) { - if (!$data['active']) { - continue; - } - $allowed_bundles = array_filter($data['bundles']['selection']); - $allowed_content_entity_types[$entity_type_id]['bundles'] = $allowed_bundles; - } - $config->set('interval', $form_state->getValue('interval')) - ->set('only_on_form_change', $form_state->getValue('only_on_form_change')) - ->set('active_on', $form_state->getValue('active_on')) - ->set('notification', $form_state->getValue('notification')) - ->set('allowed_content_entity_types', $allowed_content_entity_types) - ->save(); - - parent::submitForm($form, $form_state); - } - -} diff --git a/packages/drupal/silverback_autosave/src/Form/AutosaveFormValidator.php b/packages/drupal/silverback_autosave/src/Form/AutosaveFormValidator.php deleted file mode 100644 index ad0fd6e00..000000000 --- a/packages/drupal/silverback_autosave/src/Form/AutosaveFormValidator.php +++ /dev/null @@ -1,80 +0,0 @@ -formValidator = $form_validator; - parent::__construct($request_stack, $string_translation, $csrf_token, $logger, $form_error_handler); - } - - /** - * {@inheritdoc} - */ - public function validateForm($form_id, &$form, FormStateInterface &$form_state) { - $autosave_submission = $this->isAutosaveTriggered($form_state); - - if ($autosave_submission) { - // On subsequent autosaving we don't need to execute the form validation - // as we are not going to build the intermediate entity. However it might - // happen that between here and the autosave submission handler the - // autosaved state has been purged and therefore we have to check - // explicitly for that there instead of building the intermediate entity. - $form_state->setTemporaryValue('silverback_autosave_form_validation_skipped', TRUE); - } - else { - // We have to execute the validation in the case of autosave submission - // for the very first time as in this case we'll build the intermediate - // entity for comparison and some input values are being prepared in the - // validate functions. This is the case with e.g. autocomplete for entity - // references. - $this->formValidator->validateForm($form_id, $form, $form_state); - } - - // In order for the autosave submit callback to be executed we have to - // clear the errors caused from the validation, otherwise no submit - // callbacks will be executed. - if ($autosave_submission && $form_state::hasAnyErrors()) { - $form_state->clearErrors(); - } - } - -} diff --git a/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormDatabaseStorage.php b/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormDatabaseStorage.php deleted file mode 100644 index d03b89365..000000000 --- a/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormDatabaseStorage.php +++ /dev/null @@ -1,348 +0,0 @@ -connection = $connection; - $this->serializer = $serializer; - $this->httpClient = $httpClient; - $this->logger = $logger; - } - - /** - * {@inheritdoc} - */ - public function storeEntityAndFormState($form_id, $form_session_id, $entity_type_id, $entity_id, $langcode, $uid, $timestamp, EntityInterface $entity, FormStateInterface $form_state) { - $serialized_entity = $this->serializeEntity($entity, $form_state); - $serialized_form_state = $this->serializer->encode([ - 'storage' => $form_state->getStorage(), - 'input' => $form_state->getUserInput(), - ]); - - if (!$entity->isNew()) { - - // @todo FIX This causes new previews to restore the saved one (instead of current form state). - // Purge previous stored states for the entity, - // e.g. when user is navigating away from the page. - // Also, this approach ensures that there will be always - // a stored form state in the db, in case we need to restore. - // $this->purgeAutosavedEntityState($entity_type_id, $entity_id, NULL, $form_id, $langcode, $uid); - $this->connection->insert(static::AUTOSAVE_ENTITY_FORM_TABLE) - ->fields([ - 'form_id', - 'form_session_id', - 'entity_type_id', - 'entity_id', - 'langcode', - 'uid', - 'timestamp', - 'entity', - 'form_state', - ]) - ->values([ - $form_id, - $form_session_id, - $entity_type_id, - $entity_id, - $langcode, - $uid, - $timestamp, - $serialized_entity, - $serialized_form_state, - ]) - ->execute(); - try { - $this->httpClient->post((getenv('PREVIEW_URL') ?: 'http://localhost:8001') . '/__preview', [ - RequestOptions::HEADERS => [ - 'Content-Type' => 'application/json', - ], - RequestOptions::JSON => [ - 'entity_type_id' => $entity_type_id, - 'entity_id' => $entity_id, - 'langcode' => $langcode, - ], - ]); - } catch (GuzzleException $exc) { - $this->logger->critical('Error while to update preview.'); - $this->logger->critical($exc->getMessage()); - } - } - } - - /** - * {@inheritdoc} - */ - public function getEntityAndFormState($form_id, $entity_type_id, $entity_id, $langcode, $uid, $form_session_id = NULL, $autosaved_timestamp = NULL) { - $result = NULL; - $query = $this->connection->select(static::AUTOSAVE_ENTITY_FORM_TABLE, 'cefa') - ->fields('cefa', ['entity', 'form_state', 'timestamp']) - ->orderBy('timestamp', 'DESC') - ->condition('form_id', $form_id); - - if (isset($form_session_id)) { - $query->condition('form_session_id', $form_session_id); - } - - $query->condition('entity_type_id', $entity_type_id) - ->condition('entity_id', $entity_id) - ->condition('langcode', $langcode) - ->condition('uid', $uid); - - if (isset($autosaved_timestamp)) { - $query->condition('timestamp', $autosaved_timestamp); - } - - $data = $query->execute()->fetchAssoc(); - - if ($data) { - // Prepare the entity object. - $result['entity'] = $this->serializer->decode($data['entity']); - // Prepare the form state. - $form_state_data = $this->serializer->decode($data['form_state']); - $result['form_state'] = new FormState(); - $result['form_state']->setStorage($form_state_data['storage']); - $result['form_state']->setUserInput($form_state_data['input']); - $result['form_state']->set('silverback_autosave_state_timestamp', $data['timestamp']); - } - - return $result; - } - - /** - * {@inheritdoc} - */ - public function getEntity($form_id, $entity_type_id, $entity_id, $langcode, $uid, $form_session_id = NULL, $autosaved_timestamp = NULL) { - $entity = NULL; - $query = $this->connection->select(static::AUTOSAVE_ENTITY_FORM_TABLE, 'cefa') - ->fields('cefa', ['entity']) - ->orderBy('timestamp', 'DESC') - ->condition('form_id', $form_id); - - if (isset($form_session_id)) { - $query->condition('form_session_id', $form_session_id); - } - - $query->condition('entity_type_id', $entity_type_id) - ->condition('entity_id', $entity_id) - ->condition('langcode', $langcode) - ->condition('uid', $uid); - - if (isset($autosaved_timestamp)) { - $query->condition('timestamp', $autosaved_timestamp); - } - - $data = $query->execute()->fetchAssoc(); - - if ($data) { - // Prepare the entity object. - $entity = $this->serializer->decode($data['entity']); - } - - return $entity; - } - - /** - * {@inheritdoc} - */ - public function getFormState($form_id, $entity_type_id, $entity_id, $langcode, $uid, $form_session_id = NULL, $autosaved_timestamp = NULL) { - $form_state = NULL; - $query = $this->connection->select(static::AUTOSAVE_ENTITY_FORM_TABLE, 'cefa') - ->fields('cefa', ['form_state', 'timestamp']) - ->orderBy('timestamp', 'DESC') - ->condition('form_id', $form_id); - - if (isset($form_session_id)) { - $query->condition('form_session_id', $form_session_id); - } - - $query->condition('entity_type_id', $entity_type_id) - ->condition('entity_id', $entity_id) - ->condition('langcode', $langcode) - ->condition('uid', $uid); - - if (isset($autosaved_timestamp)) { - $query->condition('timestamp', $autosaved_timestamp); - } - - $data = $query->execute()->fetchAssoc(); - - if ($data) { - // Prepare the form state. - $form_state_data = $this->serializer->decode($data['form_state']); - $form_state = new FormState(); - $form_state->setStorage($form_state_data['storage']); - $form_state->setUserInput($form_state_data['input']); - $form_state->set('silverback_autosave_state_timestamp', $data['timestamp']); - } - - return $form_state; - } - - /** - * {@inheritdoc} - */ - public function hasAutosavedStateForFormState(FormStateInterface $form_state, $uid) { - $result = FALSE; - if (($form_object = $form_state->getFormObject()) && ($form_object instanceof EntityFormInterface) && ($entity = $form_object->getEntity()) && !$entity->isNew()) { - $result = $this->hasAutosavedState($form_object->getFormId(), $entity->getEntityTypeId(), $entity->id(), $entity->language()->getId(), $uid, $form_state->get('silverback_autosave_session_id')); - } - return $result; - } - - /** - * {@inheritdoc} - */ - public function hasAutosavedState($form_id, $entity_type_id, $entity_id, $langcode, $uid, $form_session_id = NULL) { - $query = $this->connection->select(static::AUTOSAVE_ENTITY_FORM_TABLE, 'cefa') - ->condition('form_id', $form_id); - - if (isset($form_session_id)) { - $query->condition('form_session_id', $form_session_id); - } - - $query->condition('entity_type_id', $entity_type_id) - ->condition('entity_id', $entity_id) - ->condition('langcode', $langcode) - ->condition('uid', $uid); - - $count = $query->countQuery() - ->execute() - ->fetchField(); - return (bool) $count; - } - - /** - * {@inheritdoc} - */ - public function getLastAutosavedStateTimestamp($form_id, $entity_type_id, $entity_id, $langcode, $uid) { - $timestamp = $this->connection->select(static::AUTOSAVE_ENTITY_FORM_TABLE, 'cefa') - ->fields('cefa', ['timestamp']) - ->orderBy('timestamp', 'DESC') - ->condition('form_id', $form_id) - ->condition('entity_type_id', $entity_type_id) - ->condition('entity_id', $entity_id) - ->condition('langcode', $langcode) - ->condition('uid', $uid) - ->execute() - ->fetchField(); - - return is_bool($timestamp) ? NULL : $timestamp; - } - - /** - * {@inheritdoc} - */ - public function purgeAutosavedEntityState($entity_type_id, $entity_id, $form_session_id = NULL, $form_id = NULL, $langcode = NULL, $uid = NULL) { - $query = $this->connection->delete(static::AUTOSAVE_ENTITY_FORM_TABLE); - - if (isset($form_session_id)) { - $query->condition('form_session_id', $form_session_id); - } - - if (isset($form_id)) { - $query->condition('form_id', $form_id); - } - - $query->condition('entity_type_id', $entity_type_id) - ->condition('entity_id', $entity_id); - - if (isset($langcode)) { - $query->condition('langcode', $langcode); - } - - if (isset($uid)) { - $query->condition('uid', $uid); - } - - $query->execute(); - } - - /** - * {@inheritdoc} - */ - public function purgeAutosavedEntitiesStates($entity_type_id = NULL, $langcode = NULL, $uid = NULL) { - $query = $this->connection->delete(static::AUTOSAVE_ENTITY_FORM_TABLE); - if (isset($entity_type_id)) { - $query->condition('entity_type_id', $entity_type_id); - } - if (isset($langcode)) { - $query->condition('langcode', $langcode); - } - if (isset($uid)) { - $query->condition('uid', $uid); - } - $query->execute(); - } - - /** - * Returns the serialized entity object. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity object to serialize. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The form state to retrieve meta information about the serialization. - * - * @return string - * The serialized entity. - */ - protected function serializeEntity(EntityInterface $entity, FormStateInterface $form_state) { - if (($form_object = $form_state->getFormObject()) && ($form_object instanceof ContentEntityFormInterface) && method_exists($form_object, 'isEntityDeepSerializationRequired') && $form_object->isEntityDeepSerializationRequired()) { - /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ - if (method_exists($entity, 'setDeepSerialization')) { - $entity->setDeepSerialization(TRUE); - } - } - - return $this->serializer->encode($entity); - } - -} diff --git a/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormStorageInterface.php b/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormStorageInterface.php deleted file mode 100644 index 36cf6b0f7..000000000 --- a/packages/drupal/silverback_autosave/src/Storage/AutosaveEntityFormStorageInterface.php +++ /dev/null @@ -1,203 +0,0 @@ - Date: Thu, 25 Apr 2024 22:14:16 +0200 Subject: [PATCH 81/82] fix: lagoon prod env --- apps/cms/.lagoon.env.prod | 6 +++--- apps/preview/.lagoon.env.prod | 2 +- apps/website/.lagoon.env.prod | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/cms/.lagoon.env.prod b/apps/cms/.lagoon.env.prod index 0309eb662..3e820b07e 100644 --- a/apps/cms/.lagoon.env.prod +++ b/apps/cms/.lagoon.env.prod @@ -1,7 +1,7 @@ PROJECT_NAME=example -PUBLISHER_URL="https://${LAGOON_GIT_BRANCH}-${PROJECT_NAME}.build.amazeelabs.dev" -NETLIFY_URL="https://${LAGOON_GIT_BRANCH}-${PROJECT_NAME}.amazeelabs.dev" -PREVIEW_URL="https://${LAGOON_GIT_BRANCH}-${PROJECT_NAME}.preview.amazeelabs.dev" +PUBLISHER_URL="https://example.build.amazeelabs.dev" +NETLIFY_URL="https://example.amazeelabs.dev" +PREVIEW_URL="https://example.preview.amazeelabs.dev" # Used to set the original client secret. PUBLISHER_OAUTH2_CLIENT_SECRET=REPLACE_ME diff --git a/apps/preview/.lagoon.env.prod b/apps/preview/.lagoon.env.prod index 2c80dc9c1..364cf1a4d 100644 --- a/apps/preview/.lagoon.env.prod +++ b/apps/preview/.lagoon.env.prod @@ -1 +1 @@ -DRUPAL_URL="https://${LAGOON_GIT_BRANCH}-${PROJECT_NAME}.cms.amazeelabs.dev" +DRUPAL_URL="https://example.cms.amazeelabs.dev" diff --git a/apps/website/.lagoon.env.prod b/apps/website/.lagoon.env.prod index 89379ad89..0cd30cc45 100644 --- a/apps/website/.lagoon.env.prod +++ b/apps/website/.lagoon.env.prod @@ -1,7 +1,7 @@ PROJECT_NAME=example DRUPAL_INTERNAL_URL="http://nginx:8080" -DRUPAL_EXTERNAL_URL="https://${LAGOON_GIT_BRANCH}-${PROJECT_NAME}.cms.amazeelabs.dev" -NETLIFY_URL="https://${LAGOON_GIT_BRANCH}-${PROJECT_NAME}.amazeelabs.dev" +DRUPAL_EXTERNAL_URL="https://example.cms.amazeelabs.dev" +NETLIFY_URL="https://example.amazeelabs.dev" # ----------------------------------------------- # Publisher authentication with Drupal (OAuth2). From 07ec3af7fea40ff3abcf0fd61bcc1a8da1bb7bf4 Mon Sep 17 00:00:00 2001 From: Vasile Chindris Date: Mon, 29 Apr 2024 17:25:04 +0300 Subject: [PATCH 82/82] feat(SLB-292): horizontal separator gutenberg block --- .../src/blocks/horizontal-separator.tsx | 21 +++++++++++++++++++ packages/drupal/gutenberg_blocks/src/index.ts | 1 + .../a397ca48-8fad-411e-8901-0eba2feb989c.yml | 2 ++ packages/schema/src/fragments/Page.gql | 1 + .../PageContent/BlockHorizontalSeparator.gql | 5 +++++ packages/schema/src/schema.graphql | 5 +++++ .../BlockHorizontalSeparator.stories.ts | 11 ++++++++++ .../PageContent/BlockHorizontalSeparator.tsx | 5 +++++ .../src/components/Organisms/PageDisplay.tsx | 3 +++ tests/e2e/specs/drupal/blocks.spec.ts | 3 +++ tests/schema/specs/blocks.spec.ts | 6 ++++++ 11 files changed, 63 insertions(+) create mode 100644 packages/drupal/gutenberg_blocks/src/blocks/horizontal-separator.tsx create mode 100644 packages/schema/src/fragments/PageContent/BlockHorizontalSeparator.gql create mode 100644 packages/ui/src/components/Organisms/PageContent/BlockHorizontalSeparator.stories.ts create mode 100644 packages/ui/src/components/Organisms/PageContent/BlockHorizontalSeparator.tsx diff --git a/packages/drupal/gutenberg_blocks/src/blocks/horizontal-separator.tsx b/packages/drupal/gutenberg_blocks/src/blocks/horizontal-separator.tsx new file mode 100644 index 000000000..720094b1f --- /dev/null +++ b/packages/drupal/gutenberg_blocks/src/blocks/horizontal-separator.tsx @@ -0,0 +1,21 @@ +import { registerBlockType } from 'wordpress__blocks'; +import { compose, withState } from 'wordpress__compose'; + +declare const Drupal: { t: (s: string) => string }; + +const { t: __ } = Drupal; + +// @ts-ignore +registerBlockType(`custom/horizontal-separator`, { + title: __('Horizontal separator'), + icon: 'minus', + category: 'text', + attributes: {}, + // @ts-ignore + edit: compose(withState())(() => { + return
; + }), + save() { + return null; + }, +}); diff --git a/packages/drupal/gutenberg_blocks/src/index.ts b/packages/drupal/gutenberg_blocks/src/index.ts index 2763bdad8..2c86b28df 100644 --- a/packages/drupal/gutenberg_blocks/src/index.ts +++ b/packages/drupal/gutenberg_blocks/src/index.ts @@ -9,3 +9,4 @@ import './blocks/image-with-text'; import './filters/list'; import './blocks/cta'; import './blocks/quote'; +import './blocks/horizontal-separator'; diff --git a/packages/drupal/test_content/content/node/a397ca48-8fad-411e-8901-0eba2feb989c.yml b/packages/drupal/test_content/content/node/a397ca48-8fad-411e-8901-0eba2feb989c.yml index ebd66747b..cc7765c07 100644 --- a/packages/drupal/test_content/content/node/a397ca48-8fad-411e-8901-0eba2feb989c.yml +++ b/packages/drupal/test_content/content/node/a397ca48-8fad-411e-8901-0eba2feb989c.yml @@ -55,6 +55,8 @@ default:

A standalone paragraph with markup and link

+ + diff --git a/packages/schema/src/fragments/Page.gql b/packages/schema/src/fragments/Page.gql index cbf2227eb..3ecddb3c9 100644 --- a/packages/schema/src/fragments/Page.gql +++ b/packages/schema/src/fragments/Page.gql @@ -35,6 +35,7 @@ fragment Page on Page { ...BlockCta ...BlockImageWithText ...BlockQuote + ...BlockHorizontalSeparator } metaTags { tag diff --git a/packages/schema/src/fragments/PageContent/BlockHorizontalSeparator.gql b/packages/schema/src/fragments/PageContent/BlockHorizontalSeparator.gql new file mode 100644 index 000000000..ef992a139 --- /dev/null +++ b/packages/schema/src/fragments/PageContent/BlockHorizontalSeparator.gql @@ -0,0 +1,5 @@ +fragment BlockHorizontalSeparator on BlockHorizontalSeparator { + # This is not really used, but until we have other real setting fields, we + # have to provide a dummy one. + markup +} diff --git a/packages/schema/src/schema.graphql b/packages/schema/src/schema.graphql index ca52d0ef3..d367ab9c4 100644 --- a/packages/schema/src/schema.graphql +++ b/packages/schema/src/schema.graphql @@ -206,6 +206,7 @@ union PageContent @resolveEditorBlockType = | BlockCta | BlockImageWithText | BlockQuote + | BlockHorizontalSeparator type BlockForm @type(id: "custom/form") { url: Url @resolveEditorBlockAttribute(key: "formId") @webformIdToUrl(id: "$") @@ -270,6 +271,10 @@ type BlockQuote @type(id: "custom/quote") { image: MediaImage @resolveEditorBlockMedia } +type BlockHorizontalSeparator @type(id: "custom/horizontal-separator") { + markup: Markup! @resolveEditorBlockMarkup +} + input PaginationInput { limit: Int! offset: Int! diff --git a/packages/ui/src/components/Organisms/PageContent/BlockHorizontalSeparator.stories.ts b/packages/ui/src/components/Organisms/PageContent/BlockHorizontalSeparator.stories.ts new file mode 100644 index 000000000..006c3158f --- /dev/null +++ b/packages/ui/src/components/Organisms/PageContent/BlockHorizontalSeparator.stories.ts @@ -0,0 +1,11 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import { BlockHorizontalSeparator } from './BlockHorizontalSeparator'; + +export default { + component: BlockHorizontalSeparator, +} satisfies Meta; + +export const HorizontalSeparator = { + args: {}, +} satisfies StoryObj; diff --git a/packages/ui/src/components/Organisms/PageContent/BlockHorizontalSeparator.tsx b/packages/ui/src/components/Organisms/PageContent/BlockHorizontalSeparator.tsx new file mode 100644 index 000000000..4d5308927 --- /dev/null +++ b/packages/ui/src/components/Organisms/PageContent/BlockHorizontalSeparator.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export function BlockHorizontalSeparator() { + return
; +} diff --git a/packages/ui/src/components/Organisms/PageDisplay.tsx b/packages/ui/src/components/Organisms/PageDisplay.tsx index 86813de03..f5b15c390 100644 --- a/packages/ui/src/components/Organisms/PageDisplay.tsx +++ b/packages/ui/src/components/Organisms/PageDisplay.tsx @@ -6,6 +6,7 @@ import { UnreachableCaseError } from '../../utils/unreachable-case-error'; import { PageTransition } from '../Molecules/PageTransition'; import { BlockCta } from './PageContent/BlockCta'; import { BlockForm } from './PageContent/BlockForm'; +import { BlockHorizontalSeparator } from './PageContent/BlockHorizontalSeparator'; import { BlockMarkup } from './PageContent/BlockMarkup'; import { BlockMedia } from './PageContent/BlockMedia'; import { BlockQuote } from './PageContent/BlockQuote'; @@ -60,6 +61,8 @@ export function PageDisplay(page: PageFragment) { ); case 'BlockQuote': return
; + case 'BlockHorizontalSeparator': + return ; default: throw new UnreachableCaseError(block); } diff --git a/tests/e2e/specs/drupal/blocks.spec.ts b/tests/e2e/specs/drupal/blocks.spec.ts index c3c775333..33be85ba3 100644 --- a/tests/e2e/specs/drupal/blocks.spec.ts +++ b/tests/e2e/specs/drupal/blocks.spec.ts @@ -22,6 +22,9 @@ test('All blocks are rendered', async ({ page }) => { page.locator('a:text("link")[href="/en/architecture"]'), ).toHaveCount(1); + // Horizontal separator. + await expect(page.locator('hr')).toHaveCount(1); + // Image await expect( page.locator( diff --git a/tests/schema/specs/blocks.spec.ts b/tests/schema/specs/blocks.spec.ts index 4730f8d2a..866c376a4 100644 --- a/tests/schema/specs/blocks.spec.ts +++ b/tests/schema/specs/blocks.spec.ts @@ -71,6 +71,9 @@ test('Blocks', async () => { __typename } } + ... on BlockHorizontalSeparator { + __typename + } } } { @@ -106,6 +109,9 @@ test('Blocks', async () => {

A standalone paragraph with markup and link

", }, + { + "__typename": "BlockHorizontalSeparator", + }, { "__typename": "BlockMedia", "caption": "Media image",