From 281ee464b9b6ebc1149fa7148ba3fe4d00c2cc29 Mon Sep 17 00:00:00 2001 From: Chris Penny Date: Tue, 28 Feb 2023 07:59:37 +1300 Subject: [PATCH 01/18] Update dependencies. Move to Github Actions --- .github/workflows/main.yml | 12 ++++++++++++ .scrutinizer.yml | 12 ------------ .travis.yml | 39 -------------------------------------- composer.json | 10 ++++------ 4 files changed, 16 insertions(+), 57 deletions(-) create mode 100644 .github/workflows/main.yml delete mode 100644 .scrutinizer.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..2e6906d0 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,12 @@ +name: CI + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + ci: + uses: silverstripe/gha-ci/.github/workflows/ci.yml@v1 + with: + js: false diff --git a/.scrutinizer.yml b/.scrutinizer.yml deleted file mode 100644 index cef78936..00000000 --- a/.scrutinizer.yml +++ /dev/null @@ -1,12 +0,0 @@ -inherit: true - -tools: - external_code_coverage: false - -checks: - php: - code_rating: true - duplication: true - -filter: - paths: [code/*, tests/*] diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 187d7832..00000000 --- a/.travis.yml +++ /dev/null @@ -1,39 +0,0 @@ -version: ~> 1.0 - -import: - - silverstripe/silverstripe-travis-shared:config/provision/standard.yml - -jobs: - include: - - php: 7.3 - env: - - DB=MYSQL - - REQUIRE_INSTALLER="$REQUIRE_RECIPE 4.x-dev" - - PHPUNIT_TEST=1 - - PHPCS_TEST=1 - - PHPSTAN_TEST=1 - - php: 7.3 - env: - - DB=PGSQL - - REQUIRE_INSTALLER="$REQUIRE_RECIPE 4.x-dev" - - PHPUNIT_TEST=1 - - COW_TEST=1 - - php: 7.4 - env: - - DB=MYSQL - - PDO=1 - - REQUIRE_INSTALLER="$REQUIRE_RECIPE 4.x-dev" - - PHPUNIT_COVERAGE_TEST=1 - - php: 7.4 - env: - - DB=MYSQL - - REQUIRE_INSTALLER="$REQUIRE_RECIPE 4.x-dev" - - PHPUNIT_TEST=1 - - REQUIRE_GRAPHQL="4.x-dev" - - php: 8.0 - env: - - DB=MYSQL - - REQUIRE_INSTALLER="$REQUIRE_RECIPE 4.x-dev" - - PHPUNIT_TEST=1 - - REQUIRE_GRAPHQL="4.x-dev" - - COMPOSER_INSTALL_ARG=--ignore-platform-reqs diff --git a/composer.json b/composer.json index e8c1087e..22b2655e 100644 --- a/composer.json +++ b/composer.json @@ -3,14 +3,12 @@ "description": "Add advanced link functionality to Silverstripe.", "type": "silverstripe-vendormodule", "require": { - "silverstripe/admin": "^1.8", - "silverstripe/cms": "^4.8", - "silverstripe/asset-admin": "^1.8", - "silverstripe/graphql": "^4.x-dev", - "silverstripe/vendor-plugin": "^1" + "php": "^7.4 || ^8", + "silverstripe/cms": "^4.11", + "silverstripe/graphql": "^4" }, "require-dev": { - "sminnee/phpunit": "^5.7", + "silverstripe/recipe-testing": "^2", "squizlabs/php_codesniffer": "^3" }, "license": "BSD-3-Clause", From f1c7ba8d8c1ffe33717ba92e700c661fa4e96756 Mon Sep 17 00:00:00 2001 From: Chris Penny Date: Tue, 28 Feb 2023 08:08:19 +1300 Subject: [PATCH 02/18] Update namespace from Link to LinkField --- README.md | 6 +++--- _config/config.yml | 8 ++++---- _config/types.yml | 10 +++++----- _graphql/queries.yml | 4 ++-- composer.json | 8 ++++---- src/Extensions/AjaxField.php | 2 +- src/Extensions/LeftAndMain.php | 4 ++-- src/Extensions/ModalController.php | 6 +++--- src/Form/FormFactory.php | 4 ++-- src/Form/JsonField.php | 2 +- src/Form/LinkField.php | 2 +- src/GraphQL/LinkDescriptionResolver.php | 4 ++-- src/GraphQL/LinkTypeResolver.php | 6 +++--- src/JsonData.php | 2 +- src/Models/EmailLink.php | 2 +- src/Models/ExternalLink.php | 2 +- src/Models/FileLink.php | 4 ++-- src/Models/Link.php | 8 ++++---- src/Models/SiteTreeLink.php | 2 +- src/ORM/DBJson.php | 2 +- src/ORM/DBLink.php | 6 +++--- src/Type/Registry.php | 2 +- src/Type/Type.php | 4 ++-- tests/php/LinkModelTest.php | 4 ++-- tests/php/LinkModelTest.yml | 2 +- 25 files changed, 53 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 5d822803..9e27aebc 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,9 @@ You may need to add the repository URL into your `composer.json` via the `reposi ```php Date: Tue, 28 Feb 2023 08:31:34 +1300 Subject: [PATCH 03/18] Add Linkable migration task. Linting --- README.md | 9 + _config/types.yml | 3 + docs/en/linkable-migration.md | 115 ++++++ src/Extensions/AjaxField.php | 1 - src/Extensions/ModalController.php | 9 +- src/Form/JsonField.php | 3 + src/GraphQL/LinkDescriptionResolver.php | 1 - src/JsonData.php | 3 +- src/Models/EmailLink.php | 4 +- src/Models/ExternalLink.php | 6 +- src/Models/FileLink.php | 9 +- src/Models/Link.php | 11 +- src/Models/PhoneLink.php | 27 ++ src/Models/SiteTreeLink.php | 12 +- src/ORM/DBJson.php | 1 + src/ORM/DBLink.php | 2 + src/Tasks/LinkableMigrationTask.php | 469 ++++++++++++++++++++++++ src/Type/Registry.php | 8 +- 18 files changed, 660 insertions(+), 33 deletions(-) create mode 100644 docs/en/linkable-migration.md create mode 100644 src/Models/PhoneLink.php create mode 100644 src/Tasks/LinkableMigrationTask.php diff --git a/README.md b/README.md index 9e27aebc..9d766f8a 100644 --- a/README.md +++ b/README.md @@ -57,3 +57,12 @@ class Page extends SiteTree } } ``` + +## Migrating from Shae Dawson's Linkable module + +https://github.com/sheadawson/silverstripe-linkable + +Shae Dawson's Linkable module was a much loved, and much used module. It is, unfortunately, no longer maintained. We +have provided some steps and tasks that we hope can be used to migrate your project from Linkable to LinkField. + +* [Migraiton docs](docs/en/linkable-migration.md) diff --git a/_config/types.yml b/_config/types.yml index f8941cbd..29b5dcbb 100644 --- a/_config/types.yml +++ b/_config/types.yml @@ -16,3 +16,6 @@ SilverStripe\LinkField\Type\Registry: email: classname: SilverStripe\LinkField\Models\EmailLink enabled: true + phone: + classname: SilverStripe\LinkField\Models\PhoneLink + enabled: true diff --git a/docs/en/linkable-migration.md b/docs/en/linkable-migration.md new file mode 100644 index 00000000..bf98b892 --- /dev/null +++ b/docs/en/linkable-migration.md @@ -0,0 +1,115 @@ +# Instructions + +## Preamble + +This migration process covers shifting data from the `Linkable` tables to the appropriate `LinkField` tables. + +This does not cover usages of `EmbeddedObject` (at least, not at this time). + +**Versioned:** If you have `Versioned` `Linkable`, then the expectation is that you will also `Version` `LinkField`. If +you have not `Versioned` `Linkable`, then the expectation is that you will **not** `Version` `LinkField`. + +## Install Silvesrtripe Linkfield + +Install the Silverstripe Linkfield module: + +```bash +$ composer require silverstripe/linkfield 1.x-dev +``` + +Or if you would like the (experimental) GraphQL 4 version: + +```bash +$ composer require silverstripe/linkfield 2.x-dev +``` + +Optionally, you can also remove the Linkable module (though, you might find it useful to keep around as a reference +while you are upgrading your code). + +Do this step at whatever point makes sense to you. + +```bash +$ composer remove sheadawson/silverstripe-linkable +``` + +## Replace app usages + +You should review how you are using the original `Link` model and `LinkField`, but if you don't have any customisations, +then replacing the old with the new **might** be quite simple. + +If you have used imports (`use` statements), then your first step might just be to search for `use [old];` and replace +with `use [new];` (since the class name references have not changed at all). + +Old: `Sheadawson\Linkable\Models\Link` +New: `SilverStripe\LinkField\Models\Link` + +Old: `Sheadawson\Linkable\Forms\LinkField` +New: `SilverStripe\LinkField\Form\LinkField` + +If you have extensions, new fields, etc, then your replacements might need to be a bit more considered. + +The other key (less easy to automate) thing that you'll need to update is that the old `LinkField` required you to +specify the related field with `ID` appended, whereas the new `LinkField` requires you to specify the field without +`ID` appended. EG. + +Old: `LinkField::create('MyLinkID')` +New: `LinkField::create('MyLink')` + +Search for instances of `LinkField::create` and `new LinkField`, and hopefully that should give you all of the places +where you need to update field name references. + +### Configuration + +Be sure to check how the old module classes are referenced in config `yml` files (eg: `app/_config`). Update +appropriately. + +### Populate module + +If you use the populate module, you will not be able to simply "replace" the namespace. Fixture definitions for the +new Linkfield module are quite different. There are entirely different models for different link types, whereas before +it was just a DB field to specify the type. + +## Replace template usages + +Before: You might have had references to `$LinkURL` or `$Link.LinkURL`. +After: These would need to be updated to `$URL` or `$Link.URL` respectively. + +Before: `$OpenInNewWindow` or `$Link.OpenInNewWindow`. +After: `$OpenInNew` or `$Link.OpenInew` respectively. + +Before: `$Link.TargetAttr` or `$TargetAttr` would output the appropriate `target="xx"`. +After: There is no direct replacement. + +This is an area where you should spend some decent effort to make sure each implementation is outputting as you expect +it to. There may be more "handy" methods that Linkable provided that no longer exist (that we haven't covered above). + +## Table structures + +It's important to understand that we are going from a single table in Linkable to multiple tables in LinkField. + +**Before:** We had 1 table with all data, and one of the field in there specified the type of the Link. +**Now:** We have 1 table for each type of Link, with a base `Link` table for all record. + +## Specify any custom configuration + +Have a look at `LinkableMigrationTask`. There are some configuration properties defined in there: + +- `$link_mapping` +- `$email_mapping` +- `$external_mapping` +- `$file_mapping` +- `$phone_mapping` +- `$sitetree_mapping` + +Each of these specifies how an original field from the `LinkableLink` table will map to one of the new LinkField tables. + +If you previously had some custom fields that needed to be available across **all** Link types, then you're (probably) +going to add this as an extension on the (base) `Link` class. This is going to mean that the new fields will be added +to the `LinkField_Link` table. This means that you need to update the configuration for `$link_mapping` so that we +correctly migrate those field values into the `LinkField_Link` table. + +If you had/have a field that you only want displayed on (say) SiteTree links, then you would want to add that extension +to `SiteTreeLink`. This would create new fields in the `LinkField_SiteTreeLink` table, which will mean you need to +also update the config for `$sitetree_mapping`. + +It's important that you get the correct mappings to the correct tables. diff --git a/src/Extensions/AjaxField.php b/src/Extensions/AjaxField.php index 057c194d..7764c65c 100644 --- a/src/Extensions/AjaxField.php +++ b/src/Extensions/AjaxField.php @@ -14,7 +14,6 @@ */ class AjaxField extends Extension { - public function updateLink(&$link, $action) { /** @var FormField $owner */ diff --git a/src/Extensions/ModalController.php b/src/Extensions/ModalController.php index b91e9c03..80ae178d 100644 --- a/src/Extensions/ModalController.php +++ b/src/Extensions/ModalController.php @@ -18,13 +18,13 @@ */ class ModalController extends Extension { - private static $url_handlers = [ + private static array $url_handlers = [ 'editorAnchorLink/$ItemID' => 'editorAnchorLink', // Matches LeftAndMain::methodSchema args ]; - private static $allowed_actions = array( + private static array $allowed_actions = [ 'DynamicLink', - ); + ]; /** * Builds and returns the external link form @@ -58,6 +58,7 @@ public function DynamicLink() private function getContext(): array { $linkTypeKey = $this->getOwner()->controller->getRequest()->getVar('key'); + if (empty($linkTypeKey)) { throw new HTTPResponse_Exception(sprintf('key is required', __CLASS__), 400); } @@ -84,8 +85,10 @@ private function getData(): array { $data = []; $dataString = $this->getOwner()->controller->getRequest()->getVar('data'); + if ($dataString) { $parsedData = json_decode($dataString, true); + if (json_last_error() === JSON_ERROR_NONE) { $data = $parsedData; } else { diff --git a/src/Form/JsonField.php b/src/Form/JsonField.php index 9f7257c9..2b945c7e 100644 --- a/src/Form/JsonField.php +++ b/src/Form/JsonField.php @@ -4,6 +4,7 @@ use InvalidArgumentException; use SilverStripe\Forms\FormField; +use SilverStripe\LinkField\JsonData; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObjectInterface; @@ -34,6 +35,7 @@ public function saveInto(DataObjectInterface $record) { // Check required relation details are available $fieldname = $this->getName(); + if (!$fieldname) { return $this; } @@ -45,6 +47,7 @@ public function saveInto(DataObjectInterface $record) /** @var JsonData|DataObject $jsonDataObject */ $jsonDataObjectID = $record->{"{$fieldname}ID"}; + if ($jsonDataObjectID && $jsonDataObject = $record->$fieldname) { if ($value) { $jsonDataObject = $jsonDataObject->setData($value); diff --git a/src/GraphQL/LinkDescriptionResolver.php b/src/GraphQL/LinkDescriptionResolver.php index f9c1f77f..f72a5e98 100644 --- a/src/GraphQL/LinkDescriptionResolver.php +++ b/src/GraphQL/LinkDescriptionResolver.php @@ -24,7 +24,6 @@ public static function resolve($obj, $args = [], $context = [], ?ResolveInfo $in return ['description' => '']; } - return ['description' => $type->generateLinkDescription($data)]; } } diff --git a/src/JsonData.php b/src/JsonData.php index 3d42e7e4..89ba7201 100644 --- a/src/JsonData.php +++ b/src/JsonData.php @@ -5,11 +5,10 @@ use JsonSerializable; /** - * An object that can be serialize and deserialize to JSON. + * An object that can be serialized and deserialized to JSON. */ interface JsonData extends JsonSerializable { - /** * @param array|JsonData $data * @return $this diff --git a/src/Models/EmailLink.php b/src/Models/EmailLink.php index e229fe76..53eb158e 100644 --- a/src/Models/EmailLink.php +++ b/src/Models/EmailLink.php @@ -11,12 +11,10 @@ */ class EmailLink extends Link { - - private static $db = [ + private static array $db = [ 'Email' => 'Varchar(255)' ]; - public function generateLinkDescription(array $data): string { return isset($data['Email']) ? $data['Email'] : ''; diff --git a/src/Models/ExternalLink.php b/src/Models/ExternalLink.php index a3ff62b4..c817e969 100644 --- a/src/Models/ExternalLink.php +++ b/src/Models/ExternalLink.php @@ -3,18 +3,16 @@ namespace SilverStripe\LinkField\Models; /** - * An link to an external URL. + * A link to an external URL. * * @property string $ExternalUrl */ class ExternalLink extends Link { - - private static $db = [ + private static array $db = [ 'ExternalUrl' => 'Varchar' ]; - public function generateLinkDescription(array $data): string { return isset($data['ExternalUrl']) ? $data['ExternalUrl'] : ''; diff --git a/src/Models/FileLink.php b/src/Models/FileLink.php index 7a813eb2..25931ff9 100644 --- a/src/Models/FileLink.php +++ b/src/Models/FileLink.php @@ -3,8 +3,6 @@ namespace SilverStripe\LinkField\Models; use SilverStripe\Assets\File; -use SilverStripe\i18n\i18n; -use SilverStripe\LinkField\Type\Type; /** * A link to a File track in asset-admin @@ -13,12 +11,10 @@ */ class FileLink extends Link { - - private static $has_one = [ + private static array $has_one = [ 'File' => File::class ]; - public function generateLinkDescription(array $data): string { if (empty($data['FileID'])) { @@ -26,6 +22,7 @@ public function generateLinkDescription(array $data): string } $file = File::get()->byID($data['FileID']); + return $file ? $file->getFilename() : ''; } @@ -34,7 +31,7 @@ public function LinkTypeHandlerName(): string return 'InsertMediaModal'; } - public function getURL() + public function getURL(): string { return $this->File ? $this->File->getURL() : ''; } diff --git a/src/Models/Link.php b/src/Models/Link.php index b4896ffd..2dd6c1c5 100644 --- a/src/Models/Link.php +++ b/src/Models/Link.php @@ -20,13 +20,11 @@ */ class Link extends DataObject implements JsonData, Type { - - private static $db = [ + private static array $db = [ 'Title' => 'Varchar', 'OpenInNew' => 'Boolean' ]; - public function defineLinkTypeRequirements() { Requirements::add_i18n_javascript('silverstripe/linkfield:client/lang', false, true); @@ -54,11 +52,11 @@ public function scaffoldLinkFields(array $data): FieldList return $this->getCMSFields(); } - function setData($data): JsonData { if (is_string($data)) { $data = json_decode($data, true); + if (json_last_error() !== JSON_ERROR_NONE) { throw new InvalidArgumentException(sprintf( '%s: Decoding json string failred with "%s"', @@ -79,11 +77,13 @@ function setData($data): JsonData } $type = Registry::singleton()->byKey($data['typeKey']); + if (empty($type)) { throw new InvalidArgumentException(sprintf('%s: %s is not a registered Link Type.', __CLASS__, $data['typeKey'])); } $jsonData = $this; + if ($this->ClassName !== get_class($type)) { if ($this->isInDB()) { $jsonData = $this->newClassInstance(get_class($type)); @@ -104,6 +104,7 @@ function setData($data): JsonData public function jsonSerialize() { $typeKey = Registry::singleton()->keyByClassName(static::class); + if (empty($typeKey)) { return []; } @@ -120,11 +121,13 @@ public function jsonSerialize() public function loadLinkData(array $data): JsonData { $link = new static(); + foreach ($data as $key => $value) { if ($link->hasField($key)) { $link->setField($key, $value); } } + return $link; } diff --git a/src/Models/PhoneLink.php b/src/Models/PhoneLink.php new file mode 100644 index 00000000..e545417a --- /dev/null +++ b/src/Models/PhoneLink.php @@ -0,0 +1,27 @@ + 'Varchar(255)' + ]; + + public function generateLinkDescription(array $data): string + { + return isset($data['Phone']) ? $data['Phone'] : ''; + } + + public function getURL(): string + { + return $this->Phone ? sprintf('tel:%s', $this->Phone) : ''; + } +} diff --git a/src/Models/SiteTreeLink.php b/src/Models/SiteTreeLink.php index eda33133..519b5e11 100644 --- a/src/Models/SiteTreeLink.php +++ b/src/Models/SiteTreeLink.php @@ -4,26 +4,26 @@ use SilverStripe\CMS\Forms\AnchorSelectorField; use SilverStripe\CMS\Model\SiteTree; +use SilverStripe\Forms\FieldList; use SilverStripe\Forms\TreeDropdownField; /** - * A link to a Page in the CMS. + * A link to a Page in the CMS + * * @property SiteTree $Page * @property int $PageID * @property string $Anchor */ class SiteTreeLink extends Link { - - private static $db = [ + private static array $db = [ 'Anchor' => 'Varchar' ]; - private static $has_one = [ + private static array $has_one = [ 'Page' => SiteTree::class ]; - public function generateLinkDescription(array $data): string { if (empty($data['PageID'])) { @@ -35,7 +35,7 @@ public function generateLinkDescription(array $data): string return $page ? $page->URLSegment : ''; } - public function getCMSFields() + public function getCMSFields(): FieldList { $fields = parent::getCMSFields(); diff --git a/src/ORM/DBJson.php b/src/ORM/DBJson.php index 17ed7325..4b6de834 100644 --- a/src/ORM/DBJson.php +++ b/src/ORM/DBJson.php @@ -3,6 +3,7 @@ namespace SilverStripe\LinkField\ORM; use SilverStripe\Core\Config\Config; +use SilverStripe\LinkField\JsonData; use SilverStripe\ORM\DB; use SilverStripe\ORM\FieldType\DBField; diff --git a/src/ORM/DBLink.php b/src/ORM/DBLink.php index d715b9a6..a5889434 100644 --- a/src/ORM/DBLink.php +++ b/src/ORM/DBLink.php @@ -19,8 +19,10 @@ class DBLink extends DBJson public function forTemplate() { $value = $this->getValue(); + if ($value) { $type = Registry::singleton()->byKey($value['typeKey']); + if ($type) { return $type->loadLinkData($value)->forTemplate(); } diff --git a/src/Tasks/LinkableMigrationTask.php b/src/Tasks/LinkableMigrationTask.php new file mode 100644 index 00000000..2d454cb7 --- /dev/null +++ b/src/Tasks/LinkableMigrationTask.php @@ -0,0 +1,469 @@ + 'LinkField_Link', + self::TABLE_LIVE => 'LinkField_Link_Live', + self::TABLE_VERSIONS => 'LinkField_Link_Versions', + ]; + + protected const TABLE_MAP_EMAIL_LINK = [ + self::TABLE_BASE => 'LinkField_EmailLink', + self::TABLE_LIVE => 'LinkField_EmailLink_Live', + self::TABLE_VERSIONS => 'LinkField_EmailLink_Versions', + ]; + + protected const TABLE_MAP_EXTERNAL_LINK = [ + self::TABLE_BASE => 'LinkField_ExternalLink', + self::TABLE_LIVE => 'LinkField_ExternalLink_Live', + self::TABLE_VERSIONS => 'LinkField_ExternalLink_Versions', + ]; + + protected const TABLE_MAP_FILE_LINK = [ + self::TABLE_BASE => 'LinkField_FileLink', + self::TABLE_LIVE => 'LinkField_FileLink_Live', + self::TABLE_VERSIONS => 'LinkField_FileLink_Versions', + ]; + + protected const TABLE_MAP_PHONE_LINK = [ + self::TABLE_BASE => 'LinkField_PhoneLink', + self::TABLE_LIVE => 'LinkField_PhoneLink_Live', + self::TABLE_VERSIONS => 'LinkField_PhoneLink_Versions', + ]; + + protected const TABLE_MAP_SITE_TREE_LINK = [ + self::TABLE_BASE => 'LinkField_SiteTreeLink', + self::TABLE_LIVE => 'LinkField_SiteTreeLink_Live', + self::TABLE_VERSIONS => 'LinkField_SiteTreeLink_Versions', + ]; + + /** + * @config + * @var string[] + */ + private static $versions_mapping_global = [ + 'RecordID' => 'RecordID', + 'Version' => 'Version', + ]; + + /** + * @config + * @var string[] + */ + private static $versions_mapping_base_only = [ + 'WasPublished' => 'WasPublished', + 'WasDeleted' => 'WasDeleted', + 'WasDraft' => 'WasDraft', + 'AuthorID' => 'AuthorID', + 'PublisherID' => 'PublisherID', + ]; + + /** + * LinkableLink field => LinkField_Link field + * + * @config + * @var string[] + */ + private static $link_mapping = [ + 'ID' => 'ID', + 'LastEdited' => 'LastEdited', + 'Created' => 'Created', + 'Title' => 'Title', + 'OpenInNewWindow' => 'OpenInNew', + ]; + + /** + * LinkableLink field => LinkField_EmailLink field + * + * @config + * @var string[] + */ + private static $email_mapping = [ + 'ID' => 'ID', + 'Email' => 'Email', + ]; + + /** + * LinkableLink field => LinkField_ExternalLink field + * + * @config + * @var string[] + */ + private static $external_mapping = [ + 'ID' => 'ID', + 'URL' => 'ExternalUrl', + ]; + + /** + * LinkableLink field => LinkField_FileLink field + * + * @config + * @var string[] + */ + private static $file_mapping = [ + 'ID' => 'ID', + 'FileID' => 'FileID', + ]; + + /** + * LinkableLink field => LinkField_PhoneLink field + * + * @config + * @var string[] + */ + private static $phone_mapping = [ + 'ID' => 'ID', + 'Phone' => 'Phone', + ]; + + /** + * LinkableLink field => LinkField_SiteTreeLink field + * + * @config + * @var string[] + */ + private static $sitetree_mapping = [ + 'ID' => 'ID', + 'SiteTreeID' => 'PageID', + 'Anchor' => 'Anchor', + ]; + + /** + * @var string + */ + private static $segment = 'linkable-migration-task'; + + /** + * @var string + */ + protected $title = 'Linkable Migration Task'; + + /** + * @var string + */ + protected $description = 'Truncate LinkField records and migrate from Linkable records'; + + /** + * @param HTTPRequest $request + * @return void + * @throws Exception + */ + public function run($request): void + { + // Check that we have matching Versioned states between Linkable and LinkField + if (!$this->versionedStatusMatches()) { + throw new Exception( + 'Linkable and LinkField do not have matching Versioned applications. Make sure that both are' + . ' either un-Versioned or Versioned' + ); + } + + // If we're un-Versioned then it's just going to be the base table + $tables = [ + self::TABLE_BASE, + ]; + + // Since we passed the versionedStatusMatches() step, then we can just check if Link is Versioned, and we can + // safely assume that the Linkable Versioned tables also exist + if (Link::singleton()->hasExtension(Versioned::class)) { + // Add the _Live and _Versions tables to the list of things we need to copy + $tables[] = self::TABLE_LIVE; + $tables[] = self::TABLE_VERSIONS; + } + + // We expect your LinkField tables to be completely clear before migration is kicked off + $this->truncateLinkFieldTables(); + + foreach ($tables as $table) { + // Grab any/all records from the desired table (base, live, versions) + $linkableResults = SQLSelect::create('*', $table)->execute(); + + // Nothing to see here + if ($linkableResults->numRecords() === 0) { + echo sprintf("Nothing to process for `%s`\r\n", $table); + + continue; + } + + echo sprintf("Processing `%s`\r\n", $table); + + // Loop through each DB record + foreach ($linkableResults as $linkableData) { + // We now need to determine what type of Link the original Linkable record was, because we're going to + // have to process each of those slightly differently + switch ($linkableData['Type']) { + case 'Email': + $this->insertEmail($linkableData, $table); + + break; + case 'URL': + $this->insertExternal($linkableData, $table); + + break; + case 'File': + $this->insertFile($linkableData, $table); + + break; + case 'Phone': + $this->insertPhone($linkableData, $table); + + break; + case 'SiteTree': + $this->insertSiteTree($linkableData, $table); + + break; + } + } + + echo sprintf("%d records inserted, finished processing `%s`\r\n", DB::affected_rows(), $table); + } + } + + /** + * Check to see if there is the existence of a _Live table for Linkable (indicating that it was Versioned) + * @return bool + */ + protected function versionedStatusMatches(): bool + { + $wasVersioned = DB::query('SHOW TABLES LIKE \'LinkableLink_Live\';')->numRecords() > 0; + $isVersioned = Link::singleton()->hasExtension(Versioned::class); + + return $wasVersioned === $isVersioned; + } + + /** + * We expect your LinkField tables to be completely clear before migration is kicked off + * This method will delete all data in the new tables providing a clear start and the ability + * to repeat this dev task + * + * @return void + */ + protected function truncateLinkFieldTables(): void + { + $tables = [ + 'LinkField_Link', + 'LinkField_EmailLink', + 'LinkField_ExternalLink', + 'LinkField_FileLink', + 'LinkField_PhoneLink', + 'LinkField_SiteTreeLink', + ]; + $versioned = [ + '_Live', + '_Versions', + ]; + $isVersioned = Link::singleton()->hasExtension(Versioned::class); + + foreach ($tables as $table) { + DB::get_conn()->clearTable($table); + + if (!$isVersioned) { + continue; + } + + foreach ($versioned as $tableSuffix) { + DB::get_conn()->clearTable($table . $tableSuffix); + } + } + } + + /** + * Create assignments from the old field values to the new fields based on provided configuration + * + * @param array $config + * @param array $linkableData + * @param string $originTable + * @return array + */ + protected function getAssignmentsForMapping(array $config, array $linkableData, string $originTable): array + { + // If we're processing the _Versions table, then we need to add all the Version table field assignments + if ($originTable === self::TABLE_VERSIONS) { + $config += $this->config()->get('versions_mapping_global'); + } + + // We're now going to start assigning values to the new fields (as you've specified in your config) + $assignments = []; + + // Loop through each config + foreach ($config as $oldField => $newField) { + // Assign the new field to equal whatever value was in the original record (based on the old field name) + $assignments[$newField] = $linkableData[$oldField]; + } + + return $assignments; + } + + /** + * Create new generic link record based on provided data + * + * @param string $className + * @param array $linkableData + * @param string $originTable + * @return void + */ + protected function insertLink(string $className, array $linkableData, string $originTable): void + { + $config = $this->config()->get('link_mapping'); + + // If we're processing the _Versions table, then we need to add all the Version table field assignments that are + // specifically for the base record (such as all the "WasPublished", "WasDraft", etc fields) + if ($originTable === self::TABLE_VERSIONS) { + $config += $this->config()->get('versions_mapping_global'); + } + + // These assignments are based on our config + $assignments = $this->getAssignmentsForMapping( + $config, + $linkableData, + $originTable + ); + // We also need to add ClassName for the base table, and this is not configurable + $assignments['ClassName'] = $className; + + // Find out what the corresponding table is for the origin table + $newTable = self::TABLE_MAP_LINK[$originTable]; + + // Insert our new record + SQLInsert::create($newTable, $assignments)->execute(); + } + + /** + * Insert new record for email type link + * + * @param array $linkableData + * @param string $originTable + * @return void + */ + protected function insertEmail(array $linkableData, string $originTable): void + { + // Insert the base record for this EmailLink + $this->insertLink(EmailLink::class, $linkableData, $originTable); + + $newTable = self::TABLE_MAP_EMAIL_LINK[$originTable]; + + $assignments = $this->getAssignmentsForMapping( + $this->config()->get('email_mapping'), + $linkableData, + $originTable + ); + + SQLInsert::create($newTable, $assignments)->execute(); + } + + /** + * Insert new record for external type link + * + * @param array $linkableData + * @param string $originTable + * @return void + */ + protected function insertExternal(array $linkableData, string $originTable): void + { + // Insert the base record for this ExternalLink + $this->insertLink(ExternalLink::class, $linkableData, $originTable); + + $newTable = self::TABLE_MAP_EXTERNAL_LINK[$originTable]; + + $assignments = $this->getAssignmentsForMapping( + $this->config()->get('external_mapping'), + $linkableData, + $originTable + ); + + SQLInsert::create($newTable, $assignments)->execute(); + } + + /** + * Insert new record for file type link + * + * @param array $linkableData + * @param string $originTable + * @return void + */ + protected function insertFile(array $linkableData, string $originTable): void + { + // Insert the base record for this FileLink + $this->insertLink(FileLink::class, $linkableData, $originTable); + + $newTable = self::TABLE_MAP_FILE_LINK[$originTable]; + + $assignments = $this->getAssignmentsForMapping( + $this->config()->get('file_mapping'), + $linkableData, + $originTable + ); + + SQLInsert::create($newTable, $assignments)->execute(); + } + + /** + * Insert new record for phone type link + * + * @param array $linkableData + * @param string $originTable + * @return void + */ + protected function insertPhone(array $linkableData, string $originTable): void + { + // Insert the base record for this PhoneLink + $this->insertLink(PhoneLink::class, $linkableData, $originTable); + + $newTable = self::TABLE_MAP_PHONE_LINK[$originTable]; + + $assignments = $this->getAssignmentsForMapping( + $this->config()->get('phone_mapping'), + $linkableData, + $originTable + ); + + SQLInsert::create($newTable, $assignments)->execute(); + } + + /** + * Insert new record for site tree (internal) type link + * + * @param array $linkableData + * @param string $originTable + * @return void + */ + protected function insertSiteTree(array $linkableData, string $originTable): void + { + // Insert the base record for this SiteTreeLink + $this->insertLink(SiteTreeLink::class, $linkableData, $originTable); + + $newTable = self::TABLE_MAP_SITE_TREE_LINK[$originTable]; + + $assignments = $this->getAssignmentsForMapping( + $this->config()->get('sitetree_mapping'), + $linkableData, + $originTable + ); + + SQLInsert::create($newTable, $assignments)->execute(); + } +} diff --git a/src/Type/Registry.php b/src/Type/Registry.php index 7486b5bd..8762fa6c 100644 --- a/src/Type/Registry.php +++ b/src/Type/Registry.php @@ -3,6 +3,7 @@ namespace SilverStripe\LinkField\Type; use InvalidArgumentException; +use LogicException; use SilverStripe\Core\Config\Configurable; use SilverStripe\Core\Extensible; use SilverStripe\Core\Injector\Injectable; @@ -19,7 +20,6 @@ class Registry private static $types = []; - /** * Find the matching LinkType by its key or null it can't be found. * @param string $key @@ -30,6 +30,7 @@ public function byKey(string $key): ?Type { /** @var array $types */ $typeDefinitions = self::config()->get('types'); + if (empty($typeDefinitions[$key])) { return null; } @@ -86,14 +87,14 @@ public function init() private function definitionToType(array $def): Type { if (empty($def['classname'])) { - throw new \LogicException(sprintf('%s: All types should reference a valid classname', __CLASS__)); + throw new LogicException(sprintf('%s: All types should reference a valid classname', __CLASS__)); } /** @var Type $type */ $type = Injector::inst()->get($def['classname']); if (!$type instanceof Type) { - throw new \LogicException(sprintf('%s: %s is not a valid link type', __CLASS__, $def['classname'])); + throw new LogicException(sprintf('%s: %s is not a valid link type', __CLASS__, $def['classname'])); } return $type; @@ -102,6 +103,7 @@ private function definitionToType(array $def): Type public function keyByClassName(string $classname): ?string { $typeDefinitions = self::config()->get('types'); + foreach ($typeDefinitions as $key => $def) { if ($def['classname'] === $classname) { return $key; From ecae0fb40155fea9e9d21a5428dc564859c345f2 Mon Sep 17 00:00:00 2001 From: Chris Penny Date: Tue, 28 Feb 2023 08:40:12 +1300 Subject: [PATCH 04/18] Update template namespace. Bring across some 1 features --- phpcs.xml.dist | 5 ++ phpunit.xml.dist | 20 +++++--- src/Models/EmailLink.php | 3 +- src/Models/ExternalLink.php | 2 + src/Models/FileLink.php | 2 + src/Models/Link.php | 2 + src/Models/SiteTreeLink.php | 49 ++++++++++++++++++- .../{Link => LinkField}/Form/JsonField.ss | 0 .../{Link => LinkField}/Models/Link.ss | 0 9 files changed, 73 insertions(+), 10 deletions(-) rename templates/SilverStripe/{Link => LinkField}/Form/JsonField.ss (100%) rename templates/SilverStripe/{Link => LinkField}/Models/Link.ss (100%) diff --git a/phpcs.xml.dist b/phpcs.xml.dist index f4615578..980215a8 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -5,6 +5,11 @@ src tests + + + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 1d2d9665..2dbbe210 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,10 +1,16 @@ - - - tests/php - + + + tests + + + + + src/ + + tests/ + + + diff --git a/src/Models/EmailLink.php b/src/Models/EmailLink.php index 53eb158e..370e5f76 100644 --- a/src/Models/EmailLink.php +++ b/src/Models/EmailLink.php @@ -3,6 +3,7 @@ namespace SilverStripe\LinkField\Models; use SilverStripe\Forms\EmailField; +use SilverStripe\Forms\FieldList; /** * A link to an Email address. @@ -20,7 +21,7 @@ public function generateLinkDescription(array $data): string return isset($data['Email']) ? $data['Email'] : ''; } - public function getCMSFields() + public function getCMSFields(): FieldList { $fields = parent::getCMSFields(); diff --git a/src/Models/ExternalLink.php b/src/Models/ExternalLink.php index c817e969..90c8e96a 100644 --- a/src/Models/ExternalLink.php +++ b/src/Models/ExternalLink.php @@ -9,6 +9,8 @@ */ class ExternalLink extends Link { + private static string $table_name = 'LinkField_ExternalLink'; + private static array $db = [ 'ExternalUrl' => 'Varchar' ]; diff --git a/src/Models/FileLink.php b/src/Models/FileLink.php index 25931ff9..ae44b89c 100644 --- a/src/Models/FileLink.php +++ b/src/Models/FileLink.php @@ -11,6 +11,8 @@ */ class FileLink extends Link { + private static string $table_name = 'LinkField_FileLink'; + private static array $has_one = [ 'File' => File::class ]; diff --git a/src/Models/Link.php b/src/Models/Link.php index 2dd6c1c5..234fa9dd 100644 --- a/src/Models/Link.php +++ b/src/Models/Link.php @@ -20,6 +20,8 @@ */ class Link extends DataObject implements JsonData, Type { + private static $table_name = 'LinkField_Link'; + private static array $db = [ 'Title' => 'Varchar', 'OpenInNew' => 'Boolean' diff --git a/src/Models/SiteTreeLink.php b/src/Models/SiteTreeLink.php index 519b5e11..21d6ceeb 100644 --- a/src/Models/SiteTreeLink.php +++ b/src/Models/SiteTreeLink.php @@ -16,6 +16,8 @@ */ class SiteTreeLink extends Link { + private static string $table_name = 'LinkField_SiteTreeLink'; + private static array $db = [ 'Anchor' => 'Varchar' ]; @@ -32,7 +34,11 @@ public function generateLinkDescription(array $data): string $page = SiteTree::get()->byID($data['PageID']); - return $page ? $page->URLSegment : ''; + if (!$page || !$page->exists()) { + return ''; + } + + return $page->URLSegment ?: ''; } public function getCMSFields(): FieldList @@ -58,12 +64,51 @@ public function getCMSFields(): FieldList return $fields; } - public function getURL() + public function onBeforeWrite(): void + { + parent::onBeforeWrite(); + + $this->populateTitle(); + } + + public function getURL(): string { $url = $this->Page ? $this->Page->Link() : ''; + if ($this->Anchor) { $url .= '#' . $this->Anchor; } + return $url; } + + protected function populateTitle(): void + { + $title = $this->getTitleFromPage(); + $this->extend('updateGetTitleFromPage', $title); + $this->Title = $title; + } + + /** + * Try to populate link title from page title in case we don't have a title yet + * + * @return string|null + */ + protected function getTitleFromPage(): ?string + { + if ($this->Title) { + // If we already have a title, we can just bail out without any changes + return $this->Title; + } + + $page = $this->Page; + + if (!$page || !$page->exists()) { + // We don't have a page to fall back to + return null; + } + + // Use page title as a default value in case CMS user didn't provide the title + return $page->Title; + } } diff --git a/templates/SilverStripe/Link/Form/JsonField.ss b/templates/SilverStripe/LinkField/Form/JsonField.ss similarity index 100% rename from templates/SilverStripe/Link/Form/JsonField.ss rename to templates/SilverStripe/LinkField/Form/JsonField.ss diff --git a/templates/SilverStripe/Link/Models/Link.ss b/templates/SilverStripe/LinkField/Models/Link.ss similarity index 100% rename from templates/SilverStripe/Link/Models/Link.ss rename to templates/SilverStripe/LinkField/Models/Link.ss From aed169bd409c2feed2f954b338176c4e2f04b9e6 Mon Sep 17 00:00:00 2001 From: Chris Penny Date: Tue, 28 Feb 2023 09:02:16 +1300 Subject: [PATCH 05/18] Add test cov. Update Link and FileLink based on 1 --- src/Models/FileLink.php | 1 + src/Models/Link.php | 102 +++++++++++++++++- .../SilverStripe/LinkField/Models/Link.ss | 2 +- tests/php/LinkModelTest.php | 21 ---- tests/php/LinkModelTest.yml | 3 - tests/php/Models/LinkTest.php | 85 +++++++++++++++ tests/php/Models/LinkTest.yml | 11 ++ 7 files changed, 199 insertions(+), 26 deletions(-) delete mode 100644 tests/php/LinkModelTest.php delete mode 100644 tests/php/LinkModelTest.yml create mode 100644 tests/php/Models/LinkTest.php create mode 100644 tests/php/Models/LinkTest.yml diff --git a/src/Models/FileLink.php b/src/Models/FileLink.php index ae44b89c..84c9951f 100644 --- a/src/Models/FileLink.php +++ b/src/Models/FileLink.php @@ -6,6 +6,7 @@ /** * A link to a File track in asset-admin + * * @property File $File * @property int $FileID */ diff --git a/src/Models/Link.php b/src/Models/Link.php index 234fa9dd..9b1c4085 100644 --- a/src/Models/Link.php +++ b/src/Models/Link.php @@ -3,8 +3,13 @@ namespace SilverStripe\LinkField\Models; use InvalidArgumentException; +use ReflectionException; +use SilverStripe\Core\ClassInfo; use SilverStripe\Core\Injector\Injector; +use SilverStripe\Forms\CompositeValidator; +use SilverStripe\Forms\DropdownField; use SilverStripe\Forms\FieldList; +use SilverStripe\Forms\RequiredFields; use SilverStripe\LinkField\JsonData; use SilverStripe\LinkField\Type\Registry; use SilverStripe\LinkField\Type\Type; @@ -13,7 +18,8 @@ use SilverStripe\View\Requirements; /** - * A Link Data Object. This class should be subclass and you should never directly interact with a plain Link instance. + * A Link Data Object. This class should be a subclass, and you should never directly interact with a plain Link + * instance * * @property string $Title * @property bool $OpenInNew @@ -27,6 +33,13 @@ class Link extends DataObject implements JsonData, Type 'OpenInNew' => 'Boolean' ]; + /** + * In-memory only property used to change link type + * This case is relevant for CMS edit form which doesn't use React driven UI + * This is a workaround as changing the ClassName directly is not fully supported in the GridField admin + */ + private ?string $linkType = null; + public function defineLinkTypeRequirements() { Requirements::add_i18n_javascript('silverstripe/linkfield:client/lang', false, true); @@ -54,6 +67,71 @@ public function scaffoldLinkFields(array $data): FieldList return $this->getCMSFields(); } + /** + * @return FieldList + * @throws ReflectionException + */ + public function getCMSFields(): FieldList + { + $fields = parent::getCMSFields(); + $linkTypes = $this->getLinkTypes(); + + if (static::class === self::class) { + // Add a link type selection field for generic links + $fields->addFieldsToTab( + 'Root.Main', + [ + $linkTypeField = DropdownField::create('LinkType', 'Link Type', $linkTypes), + ], + 'Title' + ); + + $linkTypeField->setEmptyString('-- select type --'); + } + + return $fields; + } + + /** + * @return CompositeValidator + */ + public function getCMSCompositeValidator(): CompositeValidator + { + $validator = parent::getCMSCompositeValidator(); + + if (static::class === self::class) { + // Make Link type mandatory for generic links + $validator->addValidator(RequiredFields::create([ + 'LinkType', + ])); + } + + return $validator; + } + + /** + * Form hook defined in @see Form::saveInto() + * We use this to work with an in-memory only field + * + * @param $value + */ + public function saveLinkType($value) + { + $this->linkType = $value; + } + + public function onBeforeWrite(): void + { + // Detect link type change and update the class accordingly + if ($this->linkType && DataObject::singleton($this->linkType) instanceof Link) { + $this->setClassName($this->linkType); + $this->populateDefaults(); + $this->forceChange(); + } + + parent::onBeforeWrite(); + } + function setData($data): JsonData { if (is_string($data)) { @@ -103,6 +181,7 @@ function setData($data): JsonData return $jsonData; } + #[\ReturnTypeWillChange] public function jsonSerialize() { $typeKey = Registry::singleton()->keyByClassName(static::class); @@ -145,4 +224,25 @@ public function forTemplate() { return $this->renderWith([self::class]); } + + /** + * Get all link types except the generic one + * + * @throws ReflectionException + */ + private function getLinkTypes(): array + { + $classes = ClassInfo::subclassesFor(self::class); + $types = []; + + foreach ($classes as $class) { + if ($class === self::class) { + continue; + } + + $types[$class] = ClassInfo::shortName($class); + } + + return $types; + } } diff --git a/templates/SilverStripe/LinkField/Models/Link.ss b/templates/SilverStripe/LinkField/Models/Link.ss index 5296e16d..7b95b36b 100644 --- a/templates/SilverStripe/LinkField/Models/Link.ss +++ b/templates/SilverStripe/LinkField/Models/Link.ss @@ -1 +1 @@ -target="_blank"<% end_if %>>$Title +target="_blank" rel="noopener noreferrer"<% end_if %>>$Title diff --git a/tests/php/LinkModelTest.php b/tests/php/LinkModelTest.php deleted file mode 100644 index 4c11f1ec..00000000 --- a/tests/php/LinkModelTest.php +++ /dev/null @@ -1,21 +0,0 @@ -objFromFixture(Link::class, 'link-1'); - - $this->assertEquals('FormBuilderModal', $model->LinkTypeHandlerName()); - } -} diff --git a/tests/php/LinkModelTest.yml b/tests/php/LinkModelTest.yml deleted file mode 100644 index dcbaa451..00000000 --- a/tests/php/LinkModelTest.yml +++ /dev/null @@ -1,3 +0,0 @@ -SilverStripe\LinkField\Models\Link: - link-1: - Title: Link1 diff --git a/tests/php/Models/LinkTest.php b/tests/php/Models/LinkTest.php new file mode 100644 index 00000000..c5a2d7d0 --- /dev/null +++ b/tests/php/Models/LinkTest.php @@ -0,0 +1,85 @@ +objFromFixture(Link::class, 'link-1'); + + $this->assertEquals('FormBuilderModal', $model->LinkTypeHandlerName()); + } + + /** + * @throws ValidationException + */ + public function testSiteTreeLinkTitleFallback(): void + { + /** @var SiteTreeLink $model */ + $model = $this->objFromFixture(SiteTreeLink::class, 'page-link-1'); + + $this->assertEquals('PageLink1', $model->Title, 'We expect to get a default Link title'); + + /** @var SiteTree $page */ + $page = $this->objFromFixture(SiteTree::class, 'page-1'); + + $model->PageID = $page->ID; + $model->Title = ''; + $model->write(); + + $this->assertEquals($page->Title, $model->Title, 'We expect to get the linked Page title'); + + $customTitle = 'My custom title'; + $model->Title = $customTitle; + $model->write(); + + $this->assertEquals($customTitle, $model->Title, 'We expect to get the custom title not page title'); + } + + /** + * @param string $class + * @param bool $expected + * @throws ReflectionException + * @dataProvider linkTypeProvider + */ + public function testLinkType(string $class, bool $expected): void + { + /** @var Link $model */ + $model = DataObject::singleton($class); + $fields = $model->getCMSFields(); + $linkTypeField = $fields->fieldByName('Root.Main.LinkType'); + $expected + ? $this->assertNotNull($linkTypeField, 'We expect to a find link type field') + : $this->assertNull($linkTypeField, 'We do not expect to a find link type field'); + } + + public function linkTypeProvider(): array + { + return [ + [EmailLink::class, false], + [ExternalLink::class, false], + [FileLink::class, false], + [PhoneLink::class, false], + [SiteTreeLink::class, false], + [Link::class, true], + ]; + } +} diff --git a/tests/php/Models/LinkTest.yml b/tests/php/Models/LinkTest.yml new file mode 100644 index 00000000..92e0f883 --- /dev/null +++ b/tests/php/Models/LinkTest.yml @@ -0,0 +1,11 @@ +SilverStripe\LinkField\Models\Link: + link-1: + Title: Link1 + +SilverStripe\LinkField\Models\SiteTreeLink: + page-link-1: + Title: PageLink1 + +SilverStripe\CMS\Model\SiteTree: + page-1: + Title: Page1 From f813cf6678a075f46e6bfc01e145981622c740be Mon Sep 17 00:00:00 2001 From: Chris Penny Date: Tue, 28 Feb 2023 12:02:49 +1300 Subject: [PATCH 06/18] Add missing table_name config --- src/Models/EmailLink.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Models/EmailLink.php b/src/Models/EmailLink.php index 370e5f76..47d1aaf2 100644 --- a/src/Models/EmailLink.php +++ b/src/Models/EmailLink.php @@ -12,6 +12,8 @@ */ class EmailLink extends Link { + private static string $table_name = 'LinkField_EmailLink'; + private static array $db = [ 'Email' => 'Varchar(255)' ]; From ec1e6df9270c92be1bd2abd5819265d811cf998d Mon Sep 17 00:00:00 2001 From: Chris Penny Date: Tue, 28 Feb 2023 12:35:03 +1300 Subject: [PATCH 07/18] Update docs regarding migration task --- docs/en/linkable-migration.md | 32 +++++++++++++++++ src/Tasks/LinkableMigrationTask.php | 53 +++++------------------------ 2 files changed, 41 insertions(+), 44 deletions(-) diff --git a/docs/en/linkable-migration.md b/docs/en/linkable-migration.md index bf98b892..bf372e21 100644 --- a/docs/en/linkable-migration.md +++ b/docs/en/linkable-migration.md @@ -113,3 +113,35 @@ to `SiteTreeLink`. This would create new fields in the `LinkField_SiteTreeLink` also update the config for `$sitetree_mapping`. It's important that you get the correct mappings to the correct tables. + +### Linkable `has_one` to one of your other DataObjects + +An example for the above [Specify any custom configuration](#specify-any-custom-configuration) would be that if one +of your DataObjects `has_many` `Link`. This would require there to be a `has_one` on `Link` back to your DataObject. + +Let's say that our `Page` `has_many` `Link`: +```php +class Page extends SiteTree +{ + private static array $has_many = [ + 'Links' => Link::class, + ]; +} +``` + +This would require a corresponding `has_one` on `Link`: +```yaml +Sheadawson\Linkable\Models\Link: + has_one: + ParentPage: Page +``` + +If we inspect the `LinkableLink` table, we'll see that there is a field called `ParentPageID`. We need to tell the +migration task about this field, and where it needs to migrate to. + +Assuming you keep the same relationship name, you'll want to add the following `$link_mapping` configuration: +```yaml +SilverStripe\LinkField\Tasks\LinkableMigrationTask: + link_mapping: + ParentPageID: ParentPageID +``` diff --git a/src/Tasks/LinkableMigrationTask.php b/src/Tasks/LinkableMigrationTask.php index 2d454cb7..95f6ddd8 100644 --- a/src/Tasks/LinkableMigrationTask.php +++ b/src/Tasks/LinkableMigrationTask.php @@ -61,20 +61,12 @@ class LinkableMigrationTask extends BuildTask self::TABLE_VERSIONS => 'LinkField_SiteTreeLink_Versions', ]; - /** - * @config - * @var string[] - */ - private static $versions_mapping_global = [ + private static array $versions_mapping_global = [ 'RecordID' => 'RecordID', 'Version' => 'Version', ]; - /** - * @config - * @var string[] - */ - private static $versions_mapping_base_only = [ + private static array $versions_mapping_base_only = [ 'WasPublished' => 'WasPublished', 'WasDeleted' => 'WasDeleted', 'WasDraft' => 'WasDraft', @@ -84,11 +76,8 @@ class LinkableMigrationTask extends BuildTask /** * LinkableLink field => LinkField_Link field - * - * @config - * @var string[] */ - private static $link_mapping = [ + private static array $link_mapping = [ 'ID' => 'ID', 'LastEdited' => 'LastEdited', 'Created' => 'Created', @@ -98,73 +87,49 @@ class LinkableMigrationTask extends BuildTask /** * LinkableLink field => LinkField_EmailLink field - * - * @config - * @var string[] */ - private static $email_mapping = [ + private static array $email_mapping = [ 'ID' => 'ID', 'Email' => 'Email', ]; /** * LinkableLink field => LinkField_ExternalLink field - * - * @config - * @var string[] */ - private static $external_mapping = [ + private static array $external_mapping = [ 'ID' => 'ID', 'URL' => 'ExternalUrl', ]; /** * LinkableLink field => LinkField_FileLink field - * - * @config - * @var string[] */ - private static $file_mapping = [ + private static array $file_mapping = [ 'ID' => 'ID', 'FileID' => 'FileID', ]; /** * LinkableLink field => LinkField_PhoneLink field - * - * @config - * @var string[] */ - private static $phone_mapping = [ + private static array $phone_mapping = [ 'ID' => 'ID', 'Phone' => 'Phone', ]; /** * LinkableLink field => LinkField_SiteTreeLink field - * - * @config - * @var string[] */ - private static $sitetree_mapping = [ + private static array $sitetree_mapping = [ 'ID' => 'ID', 'SiteTreeID' => 'PageID', 'Anchor' => 'Anchor', ]; - /** - * @var string - */ private static $segment = 'linkable-migration-task'; - /** - * @var string - */ protected $title = 'Linkable Migration Task'; - /** - * @var string - */ protected $description = 'Truncate LinkField records and migrate from Linkable records'; /** @@ -239,7 +204,7 @@ public function run($request): void } } - echo sprintf("%d records inserted, finished processing `%s`\r\n", DB::affected_rows(), $table); + echo sprintf("%d records inserted, finished processing `%s`\r\n", count($linkableResults), $table); } } From 9ead3b9e21b0146dabfd26d788ac5c57d3e3c176 Mon Sep 17 00:00:00 2001 From: Chris Penny Date: Tue, 28 Feb 2023 12:55:03 +1300 Subject: [PATCH 08/18] Fix for dev task count --- src/Tasks/LinkableMigrationTask.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tasks/LinkableMigrationTask.php b/src/Tasks/LinkableMigrationTask.php index 95f6ddd8..311ac24e 100644 --- a/src/Tasks/LinkableMigrationTask.php +++ b/src/Tasks/LinkableMigrationTask.php @@ -204,7 +204,7 @@ public function run($request): void } } - echo sprintf("%d records inserted, finished processing `%s`\r\n", count($linkableResults), $table); + echo sprintf("%d records inserted, finished processing `%s`\r\n", $linkableResults->numRecords(), $table); } } From 7d9dd9cd180fda0dc69248725851d647cd31facf Mon Sep 17 00:00:00 2001 From: Chris Penny Date: Wed, 1 Mar 2023 08:42:23 +1300 Subject: [PATCH 09/18] Update README --- README.md | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 9d766f8a..928785fe 100644 --- a/README.md +++ b/README.md @@ -6,26 +6,13 @@ Experimental module looking at how we could implement a link field and a link da Installation via composer. -### Stable version (GraphQL v3) +### GraphQL v4 - Silverstripe 4 -`composer require silverstripe/linkfield 1.x-dev` +`composer require silverstripe/linkfield` -### Experimental version (GraphQL v4) +### GraphQL v3 - Silverstripe 4 -`composer require silverstripe/linkfield 2.x-dev` - -### Known issues - -You may need to add the repository URL into your `composer.json` via the `repositories` field (example below). - -```json -"repositories": { - "silverstripe/linkfield": { - "type": "git", - "url": "https://github.com/silverstripe/silverstripe-linkfield.git" - } -}, -``` +`composer require silverstripe/linkfield:^1` ## Sample usage From 9e2774f4b3f46c6b1c90ac35b9e14cb0702cae12 Mon Sep 17 00:00:00 2001 From: Chris Penny Date: Wed, 1 Mar 2023 08:47:35 +1300 Subject: [PATCH 10/18] Update README --- README.md | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 928785fe..a460ecca 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ # Silverstripe link module -Experimental module looking at how we could implement a link field and a link data object. +This module provides a Link model and CMS interface for managing different types of links. Including: + +* Emails +* External links +* Links to pages within the CMS +* Links to assets within the CMS +* Phone numbers ## Installation @@ -25,11 +31,11 @@ use SilverStripe\LinkField\LinkField; class Page extends SiteTree { - private static $db = [ + private static array $db = [ 'DbLink' => DBLink::class ]; - private static $has_one = [ + private static array $has_one = [ 'HasOneLink' => Link::class, ]; @@ -37,8 +43,13 @@ class Page extends SiteTree { $fields = parent::getCMSFields(); - $fields->insertBefore('Title', LinkField::create('HasOneLink')); - $fields->insertBefore('Title', LinkField::create('DbLink')); + $fields->addFieldsToTab( + 'Root.Main', + [ + LinkField::create('HasOneLink'), + LinkField::create('DbLink'), + ] + ) return $fields; } From a531cdda96b137d7802a55ce95e0a1acabd50d61 Mon Sep 17 00:00:00 2001 From: Chris Penny Date: Wed, 1 Mar 2023 08:51:17 +1300 Subject: [PATCH 11/18] Fix composer.json namespace references --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 081affe9..487f21a8 100644 --- a/composer.json +++ b/composer.json @@ -29,12 +29,12 @@ ] }, "scripts": { - "lint": "phpcs LinkField/ tests/php/", - "lint-clean": "phpcbf LinkField/ tests/php/" + "lint": "phpcs src/ tests/php/", + "lint-clean": "phpcbf src/ tests/php/" }, "autoload": { "psr-4": { - "SilverStripe\\LinkField\\": "LinkField/", + "SilverStripe\\LinkField\\": "src/", "SilverStripe\\LinkField\\Tests\\": "tests/php/" } }, From 71c50537d1e7299d39105453efbbd68b38e2f94b Mon Sep 17 00:00:00 2001 From: Chris Penny Date: Thu, 2 Mar 2023 07:55:16 +1300 Subject: [PATCH 12/18] Enable JS test. Ignore linting. Add sample-test.js --- .eslintignore | 3 +-- .github/workflows/main.yml | 2 -- client/src/tests/sample-test.js | 8 ++++++++ phpunit.xml.dist | 23 ++++++++++++----------- 4 files changed, 21 insertions(+), 15 deletions(-) create mode 100644 client/src/tests/sample-test.js diff --git a/.eslintignore b/.eslintignore index 81943c37..684bec4c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1 @@ -client/dist/ -client/lang/ +client/ diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2e6906d0..2004851c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,5 +8,3 @@ on: jobs: ci: uses: silverstripe/gha-ci/.github/workflows/ci.yml@v1 - with: - js: false diff --git a/client/src/tests/sample-test.js b/client/src/tests/sample-test.js new file mode 100644 index 00000000..d6511e67 --- /dev/null +++ b/client/src/tests/sample-test.js @@ -0,0 +1,8 @@ +/* global jest, describe, it, expect */ + +describe('sample tests', () => { + it('sample test', () => { + const css = 'sample css'; + expect(css).toBe('sample css'); + }); +}); diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 2dbbe210..2fdf2b19 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,16 +1,17 @@ - - + + + + + src/ + + + tests/ + + - tests + tests/ - - - src/ - - tests/ - - - From 129cc25a6ec6687af71e63b38baf187383583cab Mon Sep 17 00:00:00 2001 From: Chris Penny Date: Thu, 2 Mar 2023 08:02:22 +1300 Subject: [PATCH 13/18] Update dist files --- client/dist/js/bundle.js | 1137 +-------------------------------- client/dist/styles/bundle.css | 3 +- 2 files changed, 2 insertions(+), 1138 deletions(-) diff --git a/client/dist/js/bundle.js b/client/dist/js/bundle.js index 49fabd9f..5329a554 100644 --- a/client/dist/js/bundle.js +++ b/client/dist/js/bundle.js @@ -1,1136 +1 @@ -/******/ (function(modules) { // webpackBootstrap -/******/ // The module cache -/******/ var installedModules = {}; -/******/ -/******/ // The require function -/******/ function __webpack_require__(moduleId) { -/******/ -/******/ // Check if module is in cache -/******/ if(installedModules[moduleId]) { -/******/ return installedModules[moduleId].exports; -/******/ } -/******/ // Create a new module (and put it into the cache) -/******/ var module = installedModules[moduleId] = { -/******/ i: moduleId, -/******/ l: false, -/******/ exports: {} -/******/ }; -/******/ -/******/ // Execute the module function -/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); -/******/ -/******/ // Flag the module as loaded -/******/ module.l = true; -/******/ -/******/ // Return the exports of the module -/******/ return module.exports; -/******/ } -/******/ -/******/ -/******/ // expose the modules object (__webpack_modules__) -/******/ __webpack_require__.m = modules; -/******/ -/******/ // expose the module cache -/******/ __webpack_require__.c = installedModules; -/******/ -/******/ // identity function for calling harmony imports with the correct context -/******/ __webpack_require__.i = function(value) { return value; }; -/******/ -/******/ // define getter function for harmony exports -/******/ __webpack_require__.d = function(exports, name, getter) { -/******/ if(!__webpack_require__.o(exports, name)) { -/******/ Object.defineProperty(exports, name, { -/******/ configurable: false, -/******/ enumerable: true, -/******/ get: getter -/******/ }); -/******/ } -/******/ }; -/******/ -/******/ // getDefaultExport function for compatibility with non-harmony modules -/******/ __webpack_require__.n = function(module) { -/******/ var getter = module && module.__esModule ? -/******/ function getDefault() { return module['default']; } : -/******/ function getModuleExports() { return module; }; -/******/ __webpack_require__.d(getter, 'a', getter); -/******/ return getter; -/******/ }; -/******/ -/******/ // Object.prototype.hasOwnProperty.call -/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; -/******/ -/******/ // __webpack_public_path__ -/******/ __webpack_require__.p = ""; -/******/ -/******/ // Load entry module and return exports -/******/ return __webpack_require__(__webpack_require__.s = "./client/src/bundles/bundle.js"); -/******/ }) -/************************************************************************/ -/******/ ({ - -/***/ "./client/src/boot/index.js": -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - - -var _Config = __webpack_require__(6); - -var _Config2 = _interopRequireDefault(_Config); - -var _registerReducers = __webpack_require__("./client/src/boot/registerReducers.js"); - -var _registerReducers2 = _interopRequireDefault(_registerReducers); - -var _registerComponents = __webpack_require__("./client/src/boot/registerComponents.js"); - -var _registerComponents2 = _interopRequireDefault(_registerComponents); - -var _registerQueries = __webpack_require__("./client/src/boot/registerQueries.js"); - -var _registerQueries2 = _interopRequireDefault(_registerQueries); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -document.addEventListener('DOMContentLoaded', function () { - (0, _registerComponents2.default)(); - - (0, _registerQueries2.default)(); - - (0, _registerReducers2.default)(); -}); - -/***/ }), - -/***/ "./client/src/boot/registerComponents.js": -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _Injector = __webpack_require__(0); - -var _Injector2 = _interopRequireDefault(_Injector); - -var _LinkPicker = __webpack_require__("./client/src/components/LinkPicker/LinkPicker.js"); - -var _LinkPicker2 = _interopRequireDefault(_LinkPicker); - -var _LinkField = __webpack_require__("./client/src/components/LinkField/LinkField.js"); - -var _LinkField2 = _interopRequireDefault(_LinkField); - -var _LinkModal = __webpack_require__("./client/src/components/LinkModal/LinkModal.js"); - -var _LinkModal2 = _interopRequireDefault(_LinkModal); - -var _FileLinkModal = __webpack_require__("./client/src/components/LinkModal/FileLinkModal.js"); - -var _FileLinkModal2 = _interopRequireDefault(_FileLinkModal); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -var registerComponents = function registerComponents() { - _Injector2.default.component.registerMany({ - LinkPicker: _LinkPicker2.default, - LinkField: _LinkField2.default, - 'LinkModal.FormBuilderModal': _LinkModal2.default, - 'LinkModal.InsertMediaModal': _FileLinkModal2.default - }); -}; - -exports.default = registerComponents; - -/***/ }), - -/***/ "./client/src/boot/registerQueries.js": -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _Injector = __webpack_require__(0); - -var _Injector2 = _interopRequireDefault(_Injector); - -var _readLinkTypes = __webpack_require__("./client/src/state/linkTypes/readLinkTypes.js"); - -var _readLinkTypes2 = _interopRequireDefault(_readLinkTypes); - -var _readLinkDescription = __webpack_require__("./client/src/state/linkDescription/readLinkDescription.js"); - -var _readLinkDescription2 = _interopRequireDefault(_readLinkDescription); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -var registerQueries = function registerQueries() { - _Injector2.default.query.register('readLinkTypes', _readLinkTypes2.default); - _Injector2.default.query.register('readLinkDescription', _readLinkDescription2.default); -}; -exports.default = registerQueries; - -/***/ }), - -/***/ "./client/src/boot/registerReducers.js": -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _Injector = __webpack_require__(0); - -var _Injector2 = _interopRequireDefault(_Injector); - -var _redux = __webpack_require__(8); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -var registerReducers = function registerReducers() {}; - -exports.default = registerReducers; - -/***/ }), - -/***/ "./client/src/bundles/bundle.js": -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - - -__webpack_require__("./client/src/boot/index.js"); -__webpack_require__("./client/src/entwine/JsonField.js"); - -/***/ }), - -/***/ "./client/src/components/LinkField/LinkField.js": -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; - -var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); - -var _i18n = __webpack_require__(3); - -var _i18n2 = _interopRequireDefault(_i18n); - -var _react = __webpack_require__(1); - -var _react2 = _interopRequireDefault(_react); - -var _reactRedux = __webpack_require__(7); - -var _redux = __webpack_require__(8); - -var _reactApollo = __webpack_require__(13); - -var _Injector = __webpack_require__(0); - -var _FieldHolder = __webpack_require__(9); - -var _FieldHolder2 = _interopRequireDefault(_FieldHolder); - -var _propTypes = __webpack_require__(2); - -var _propTypes2 = _interopRequireDefault(_propTypes); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } - -var LinkField = function LinkField(_ref) { - var id = _ref.id, - loading = _ref.loading, - Loading = _ref.Loading, - data = _ref.data, - LinkPicker = _ref.LinkPicker, - onChange = _ref.onChange, - types = _ref.types, - linkDescription = _ref.linkDescription, - props = _objectWithoutProperties(_ref, ['id', 'loading', 'Loading', 'data', 'LinkPicker', 'onChange', 'types', 'linkDescription']); - - if (loading) { - return _react2.default.createElement(Loading, null); - } - - var _useState = (0, _react.useState)(false), - _useState2 = _slicedToArray(_useState, 2), - editing = _useState2[0], - setEditing = _useState2[1]; - - var _useState3 = (0, _react.useState)(''), - _useState4 = _slicedToArray(_useState3, 2), - newTypeKey = _useState4[0], - setNewTypeKey = _useState4[1]; - - var onClear = function onClear(event) { - typeof onChange === 'function' && onChange(event, { id: id, value: {} }); - }; - - var typeKey = data.typeKey; - - var type = types[typeKey]; - var modalType = newTypeKey ? types[newTypeKey] : type; - - var linkProps = { - title: data ? data.Title : '', - link: type ? { type: type, title: data.Title, description: linkDescription } : undefined, - onEdit: function onEdit() { - setEditing(true); - }, - onClear: onClear, - onSelect: function onSelect(key) { - setNewTypeKey(key); - setEditing(true); - }, - types: Object.values(types) - }; - - var onModalSubmit = function onModalSubmit(data, action, submitFn) { - console.dir({ data: data, action: action, submitFn: submitFn, onChange: onChange }); - - var SecurityID = data.SecurityID, - action_insert = data.action_insert, - value = _objectWithoutProperties(data, ['SecurityID', 'action_insert']); - - typeof onChange === 'function' && onChange(event, { id: id, value: value }); - setEditing(false); - setNewTypeKey(''); - return Promise.resolve(); - }; - - var modalProps = { - type: modalType, - editing: editing, - onSubmit: onModalSubmit, - onClosed: function onClosed() { - setEditing(false); - }, - data: data - }; - - var handlerName = modalType ? modalType.handlerName : 'FormBuilderModal'; - var LinkModal = (0, _Injector.loadComponent)('LinkModal.' + handlerName); - - return _react2.default.createElement( - _react.Fragment, - null, - _react2.default.createElement(LinkPicker, linkProps), - _react2.default.createElement(LinkModal, modalProps) - ); -}; - -var stringifyData = function stringifyData(Component) { - return function (_ref2) { - var data = _ref2.data, - value = _ref2.value, - props = _objectWithoutProperties(_ref2, ['data', 'value']); - - var dataValue = value || data; - if (typeof dataValue === 'string') { - dataValue = JSON.parse(dataValue); - } - return _react2.default.createElement(Component, _extends({ dataStr: JSON.stringify(dataValue) }, props, { data: dataValue })); - }; -}; - -exports.default = (0, _redux.compose)((0, _Injector.inject)(['LinkPicker', 'Loading']), (0, _Injector.injectGraphql)('readLinkTypes'), stringifyData, (0, _Injector.injectGraphql)('readLinkDescription'), _reactApollo.withApollo, _FieldHolder2.default)(LinkField); - -/***/ }), - -/***/ "./client/src/components/LinkModal/FileLinkModal.js": -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; - -var _i18n = __webpack_require__(3); - -var _i18n2 = _interopRequireDefault(_i18n); - -var _react = __webpack_require__(1); - -var _react2 = _interopRequireDefault(_react); - -var _InsertMediaModal = __webpack_require__(11); - -var _InsertMediaModal2 = _interopRequireDefault(_InsertMediaModal); - -var _reactRedux = __webpack_require__(7); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } - -var FileLinkModal = function FileLinkModal(_ref) { - var type = _ref.type, - editing = _ref.editing, - data = _ref.data, - actions = _ref.actions, - onSubmit = _ref.onSubmit, - props = _objectWithoutProperties(_ref, ['type', 'editing', 'data', 'actions', 'onSubmit']); - - if (!type) { - return false; - } - - (0, _react.useEffect)(function () { - if (editing) { - actions.initModal(); - } else { - actions.reset(); - } - }, [editing]); - - var attrs = data ? { - ID: data.FileID, - Description: data.Title, - TargetBlank: data.OpenInNew ? true : false - } : {}; - - var onInsert = function onInsert(_ref2) { - var ID = _ref2.ID, - Description = _ref2.Description, - TargetBlank = _ref2.TargetBlank; - - return onSubmit({ - FileID: ID, - Title: Description, - OpenInNew: TargetBlank, - typeKey: type.key - }, '', function () {}); - }; - - return _react2.default.createElement(_InsertMediaModal2.default, _extends({ - isOpen: editing, - type: 'insert-link', - title: false, - bodyClassName: 'modal__dialog', - className: 'insert-link__dialog-wrapper--internal', - fileAttributes: attrs, - onInsert: onInsert - }, props)); -}; - -function mapStateToProps() { - return {}; -} - -function mapDispatchToProps(dispatch) { - return { - actions: { - initModal: function initModal() { - return dispatch({ - type: 'INIT_FORM_SCHEMA_STACK', - payload: { formSchema: { type: 'insert-link', nextType: 'admin' } } - }); - }, - reset: function reset() { - return dispatch({ type: 'RESET' }); - } - } - }; -} - -exports.default = (0, _reactRedux.connect)(mapStateToProps, mapDispatchToProps)(FileLinkModal); - -/***/ }), - -/***/ "./client/src/components/LinkModal/LinkModal.js": -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; - -var _i18n = __webpack_require__(3); - -var _i18n2 = _interopRequireDefault(_i18n); - -var _react = __webpack_require__(1); - -var _react2 = _interopRequireDefault(_react); - -var _propTypes = __webpack_require__(2); - -var _propTypes2 = _interopRequireDefault(_propTypes); - -var _FormBuilderModal = __webpack_require__(10); - -var _FormBuilderModal2 = _interopRequireDefault(_FormBuilderModal); - -var _url = __webpack_require__(12); - -var _url2 = _interopRequireDefault(_url); - -var _qs = __webpack_require__(16); - -var _qs2 = _interopRequireDefault(_qs); - -var _Config = __webpack_require__(6); - -var _Config2 = _interopRequireDefault(_Config); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } - -var leftAndMain = 'SilverStripe\\Admin\\LeftAndMain'; - -var buildSchemaUrl = function buildSchemaUrl(key, data) { - var schemaUrl = _Config2.default.getSection(leftAndMain).form.DynamicLink.schemaUrl; - - var parsedURL = _url2.default.parse(schemaUrl); - var parsedQs = _qs2.default.parse(parsedURL.query); - parsedQs.key = key; - if (data) { - parsedQs.data = JSON.stringify(data); - } - return _url2.default.format(_extends({}, parsedURL, { search: _qs2.default.stringify(parsedQs) })); -}; - -var LinkModal = function LinkModal(_ref) { - var type = _ref.type, - editing = _ref.editing, - data = _ref.data, - props = _objectWithoutProperties(_ref, ['type', 'editing', 'data']); - - if (!type) { - return false; - } - - return _react2.default.createElement(_FormBuilderModal2.default, _extends({ - title: type.title, - isOpen: editing, - schemaUrl: buildSchemaUrl(type.key, data), - identifier: 'Link.EditingLinkInfo' - }, props)); -}; - -exports.default = LinkModal; - -/***/ }), - -/***/ "./client/src/components/LinkPicker/LinkPicker.js": -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.Component = undefined; - -var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; - -var _i18n = __webpack_require__(3); - -var _i18n2 = _interopRequireDefault(_i18n); - -var _react = __webpack_require__(1); - -var _react2 = _interopRequireDefault(_react); - -var _Injector = __webpack_require__(0); - -var _propTypes = __webpack_require__(2); - -var _propTypes2 = _interopRequireDefault(_propTypes); - -var _reactstrap = __webpack_require__(4); - -var _classnames = __webpack_require__(5); - -var _classnames2 = _interopRequireDefault(_classnames); - -var _LinkPickerMenu = __webpack_require__("./client/src/components/LinkPicker/LinkPickerMenu.js"); - -var _LinkPickerMenu2 = _interopRequireDefault(_LinkPickerMenu); - -var _LinkPickerTitle = __webpack_require__("./client/src/components/LinkPicker/LinkPickerTitle.js"); - -var _LinkPickerTitle2 = _interopRequireDefault(_LinkPickerTitle); - -var _LinkType = __webpack_require__("./client/src/types/LinkType.js"); - -var _LinkType2 = _interopRequireDefault(_LinkType); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -var LinkPicker = function LinkPicker(_ref) { - var types = _ref.types, - onSelect = _ref.onSelect, - link = _ref.link, - onEdit = _ref.onEdit, - onClear = _ref.onClear; - return _react2.default.createElement( - 'div', - { - className: (0, _classnames2.default)('link-picker', 'form-control', { 'link-picker--selected': link }) }, - link === undefined && _react2.default.createElement(_LinkPickerMenu2.default, { types: types, onSelect: onSelect }), - link && _react2.default.createElement(_LinkPickerTitle2.default, _extends({}, link, { onClear: onClear, onClick: function onClick() { - return link && onEdit && onEdit(link); - } })) - ); -}; - -LinkPicker.propTypes = _extends({}, _LinkPickerMenu2.default.propTypes, { - link: _propTypes2.default.shape(_LinkPickerTitle2.default.propTypes), - onEdit: _propTypes2.default.func, - onClear: _propTypes2.default.func -}); - -exports.Component = LinkPicker; -exports.default = LinkPicker; - -/***/ }), - -/***/ "./client/src/components/LinkPicker/LinkPickerMenu.js": -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); - -var _i18n = __webpack_require__(3); - -var _i18n2 = _interopRequireDefault(_i18n); - -var _react = __webpack_require__(1); - -var _react2 = _interopRequireDefault(_react); - -var _Injector = __webpack_require__(0); - -var _propTypes = __webpack_require__(2); - -var _propTypes2 = _interopRequireDefault(_propTypes); - -var _reactstrap = __webpack_require__(4); - -var _classnames = __webpack_require__(5); - -var _classnames2 = _interopRequireDefault(_classnames); - -var _LinkType = __webpack_require__("./client/src/types/LinkType.js"); - -var _LinkType2 = _interopRequireDefault(_LinkType); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -var LinkPickerMenu = function LinkPickerMenu(_ref) { - var types = _ref.types, - onSelect = _ref.onSelect; - - var _useState = (0, _react.useState)(false), - _useState2 = _slicedToArray(_useState, 2), - isOpen = _useState2[0], - setIsOpen = _useState2[1]; - - var toggle = function toggle() { - return setIsOpen(function (prevState) { - return !prevState; - }); - }; - - return _react2.default.createElement( - _reactstrap.Dropdown, - { - isOpen: isOpen, - toggle: toggle, - className: 'link-picker__menu' - }, - _react2.default.createElement( - _reactstrap.DropdownToggle, - { className: 'link-picker__menu-toggle font-icon-link', caret: true }, - _i18n2.default._t('Link.ADD_LINK', 'Add Link') - ), - _react2.default.createElement( - _reactstrap.DropdownMenu, - null, - types.map(function (_ref2) { - var key = _ref2.key, - title = _ref2.title; - return _react2.default.createElement( - _reactstrap.DropdownItem, - { key: key, onClick: function onClick() { - return onSelect(key); - } }, - title - ); - }) - ) - ); -}; - -LinkPickerMenu.propTypes = { - types: _propTypes2.default.arrayOf(_LinkType2.default).isRequired, - onSelect: _propTypes2.default.func.isRequired -}; - -exports.default = LinkPickerMenu; - -/***/ }), - -/***/ "./client/src/components/LinkPicker/LinkPickerTitle.js": -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _i18n = __webpack_require__(3); - -var _i18n2 = _interopRequireDefault(_i18n); - -var _react = __webpack_require__(1); - -var _react2 = _interopRequireDefault(_react); - -var _Injector = __webpack_require__(0); - -var _propTypes = __webpack_require__(2); - -var _propTypes2 = _interopRequireDefault(_propTypes); - -var _classnames = __webpack_require__(5); - -var _classnames2 = _interopRequireDefault(_classnames); - -var _LinkType = __webpack_require__("./client/src/types/LinkType.js"); - -var _LinkType2 = _interopRequireDefault(_LinkType); - -var _reactstrap = __webpack_require__(4); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -var stopPropagation = function stopPropagation(fn) { - return function (e) { - console.log('trying to stop propagation'); - e.nativeEvent.stopImmediatePropagation(); - e.preventDefault(); - e.nativeEvent.preventDefault(); - e.stopPropagation(); - fn && fn(); - }; -}; - -var LinkPickerTitle = function LinkPickerTitle(_ref) { - var title = _ref.title, - type = _ref.type, - description = _ref.description, - onClear = _ref.onClear, - onClick = _ref.onClick; - return _react2.default.createElement( - _reactstrap.Button, - { className: 'link-picker__link font-icon-link', color: 'secondary', onClick: stopPropagation(onClick) }, - _react2.default.createElement( - 'div', - { className: 'link-picker__link-detail' }, - _react2.default.createElement( - 'div', - { className: 'link-picker__title' }, - title - ), - _react2.default.createElement( - 'small', - { className: 'link-picker__type' }, - type.title, - ':\xA0', - _react2.default.createElement( - 'span', - { className: 'link-picker__url' }, - description - ) - ) - ), - _react2.default.createElement( - _reactstrap.Button, - { className: 'link-picker__clear', color: 'link', onClick: stopPropagation(onClear) }, - _i18n2.default._t('Link.CLEAR', 'Clear') - ) - ); -}; - -LinkPickerTitle.propTypes = { - title: _propTypes2.default.string.isRequired, - type: _LinkType2.default, - description: _propTypes2.default.string, - onClear: _propTypes2.default.func, - onClick: _propTypes2.default.func -}; - -exports.default = LinkPickerTitle; - -/***/ }), - -/***/ "./client/src/entwine/JsonField.js": -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - - -var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; - -var _jquery = __webpack_require__(15); - -var _jquery2 = _interopRequireDefault(_jquery); - -var _react = __webpack_require__(1); - -var _react2 = _interopRequireDefault(_react); - -var _reactDom = __webpack_require__(14); - -var _reactDom2 = _interopRequireDefault(_reactDom); - -var _Injector = __webpack_require__(0); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -_jquery2.default.entwine('ss', function ($) { - $('.js-injector-boot .entwine-jsonfield').entwine({ - - Component: null, - - onmatch: function onmatch() { - var cmsContent = this.closest('.cms-content').attr('id'); - var context = cmsContent ? { context: cmsContent } : {}; - - var schemaComponent = this.data('schema-component'); - var ReactField = (0, _Injector.loadComponent)(schemaComponent, context); - - this.setComponent(ReactField); - this._super(); - this.refresh(); - }, - refresh: function refresh() { - var props = this.getProps(); - var ReactField = this.getComponent(); - _reactDom2.default.render(_react2.default.createElement(ReactField, _extends({}, props, { noHolder: true })), this[0]); - }, - handleChange: function handleChange(event, _ref) { - var id = _ref.id, - value = _ref.value; - - var fieldID = $(this).data('field-id'); - $('#' + fieldID).val(JSON.stringify(value)).trigger('change'); - this.refresh(); - }, - getProps: function getProps() { - var fieldID = $(this).data('field-id'); - var dataStr = $('#' + fieldID).val(); - var value = dataStr ? JSON.parse(dataStr) : undefined; - - return { - id: fieldID, - value: value, - onChange: this.handleChange.bind(this) - }; - }, - onunmatch: function onunmatch() { - _reactDom2.default.unmountComponentAtNode(this[0]); - } - }); -}); - -/***/ }), - -/***/ "./client/src/state/linkDescription/readLinkDescription.js": -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _Injector = __webpack_require__(0); - -var apolloConfig = { - props: function props(_props) { - var _props$data = _props.data, - error = _props$data.error, - readLinkDescription = _props$data.readLinkDescription, - networkLoading = _props$data.loading; - - var errors = error && error.graphQLErrors && error.graphQLErrors.map(function (graphQLError) { - return graphQLError.message; - }); - var linkDescription = readLinkDescription ? readLinkDescription.description : ''; - - return { - loading: networkLoading, - linkDescription: linkDescription, - graphQLErrors: errors - }; - } -}; - -var READ = _Injector.graphqlTemplates.READ; - -var query = { - apolloConfig: apolloConfig, - templateName: READ, - pluralName: 'LinkDescription', - pagination: false, - params: { - dataStr: 'String!' - }, - args: { - root: { - dataStr: 'dataStr' - } - }, - fields: ['description'] -}; -exports.default = query; - -/***/ }), - -/***/ "./client/src/state/linkTypes/readLinkTypes.js": -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; - -var _Injector = __webpack_require__(0); - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - -var apolloConfig = { - props: function props(_props) { - var _props$data = _props.data, - error = _props$data.error, - readLinkTypes = _props$data.readLinkTypes, - networkLoading = _props$data.loading; - - var errors = error && error.graphQLErrors && error.graphQLErrors.map(function (graphQLError) { - return graphQLError.message; - }); - - var types = readLinkTypes ? readLinkTypes.reduce(function (accumulator, type) { - return _extends({}, accumulator, _defineProperty({}, type.key, type)); - }, {}) : {}; - - return { - loading: networkLoading, - types: types, - graphQLErrors: errors - }; - } -}; - -var READ = _Injector.graphqlTemplates.READ; - -var query = { - apolloConfig: apolloConfig, - templateName: READ, - pluralName: 'LinkTypes', - pagination: false, - params: { - keys: '[ID]' - }, - args: { - root: { - keys: 'keys' - } - }, - fields: ['key', 'title', 'handlerName'] -}; -exports.default = query; - -/***/ }), - -/***/ "./client/src/types/LinkType.js": -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _propTypes = __webpack_require__(2); - -var _propTypes2 = _interopRequireDefault(_propTypes); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -var LinkType = _propTypes2.default.shape({ - key: _propTypes2.default.string.isRequired, - title: _propTypes2.default.string.isRequired -}); - -exports.default = LinkType; - -/***/ }), - -/***/ 0: -/***/ (function(module, exports) { - -module.exports = Injector; - -/***/ }), - -/***/ 1: -/***/ (function(module, exports) { - -module.exports = React; - -/***/ }), - -/***/ 10: -/***/ (function(module, exports) { - -module.exports = FormBuilderModal; - -/***/ }), - -/***/ 11: -/***/ (function(module, exports) { - -module.exports = InsertMediaModal; - -/***/ }), - -/***/ 12: -/***/ (function(module, exports) { - -module.exports = NodeUrl; - -/***/ }), - -/***/ 13: -/***/ (function(module, exports) { - -module.exports = ReactApollo; - -/***/ }), - -/***/ 14: -/***/ (function(module, exports) { - -module.exports = ReactDom; - -/***/ }), - -/***/ 15: -/***/ (function(module, exports) { - -module.exports = jQuery; - -/***/ }), - -/***/ 16: -/***/ (function(module, exports) { - -module.exports = qs; - -/***/ }), - -/***/ 2: -/***/ (function(module, exports) { - -module.exports = PropTypes; - -/***/ }), - -/***/ 3: -/***/ (function(module, exports) { - -module.exports = i18n; - -/***/ }), - -/***/ 4: -/***/ (function(module, exports) { - -module.exports = Reactstrap; - -/***/ }), - -/***/ 5: -/***/ (function(module, exports) { - -module.exports = classnames; - -/***/ }), - -/***/ 6: -/***/ (function(module, exports) { - -module.exports = Config; - -/***/ }), - -/***/ 7: -/***/ (function(module, exports) { - -module.exports = ReactRedux; - -/***/ }), - -/***/ 8: -/***/ (function(module, exports) { - -module.exports = Redux; - -/***/ }), - -/***/ 9: -/***/ (function(module, exports) { - -module.exports = FieldHolder; - -/***/ }) - -/******/ }); -//# sourceMappingURL=bundle.js.map \ No newline at end of file +!function(e){function t(r){if(n[r])return n[r].exports;var i=n[r]={i:r,l:!1,exports:{}};return e[r].call(i.exports,i,i.exports,t),i.l=!0,i.exports}var n={};t.m=e,t.c=n,t.i=function(e){return e},t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:r})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s="./client/src/bundles/bundle.js")}({"./client/src/boot/index.js":function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}var i=n(6),o=(r(i),n("./client/src/boot/registerReducers.js")),a=r(o),l=n("./client/src/boot/registerComponents.js"),c=r(l),u=n("./client/src/boot/registerQueries.js"),s=r(u);document.addEventListener("DOMContentLoaded",function(){(0,c.default)(),(0,s.default)(),(0,a.default)()})},"./client/src/boot/registerComponents.js":function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var i=n(0),o=r(i),a=n("./client/src/components/LinkPicker/LinkPicker.js"),l=r(a),c=n("./client/src/components/LinkField/LinkField.js"),u=r(c),s=n("./client/src/components/LinkModal/LinkModal.js"),d=r(s),f=n("./client/src/components/LinkModal/FileLinkModal.js"),p=r(f),y=function(){o.default.component.registerMany({LinkPicker:l.default,LinkField:u.default,"LinkModal.FormBuilderModal":d.default,"LinkModal.InsertMediaModal":p.default})};t.default=y},"./client/src/boot/registerQueries.js":function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var i=n(0),o=r(i),a=n("./client/src/state/linkTypes/readLinkTypes.js"),l=r(a),c=n("./client/src/state/linkDescription/readLinkDescription.js"),u=r(c),s=function(){o.default.query.register("readLinkTypes",l.default),o.default.query.register("readLinkDescription",u.default)};t.default=s},"./client/src/boot/registerReducers.js":function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(0),i=(function(e){e&&e.__esModule}(r),n(8),function(){});t.default=i},"./client/src/bundles/bundle.js":function(e,t,n){"use strict";n("./client/src/boot/index.js"),n("./client/src/entwine/JsonField.js")},"./client/src/components/LinkField/LinkField.js":function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function i(e,t){var n={};for(var r in e)t.indexOf(r)>=0||Object.prototype.hasOwnProperty.call(e,r)&&(n[r]=e[r]);return n}Object.defineProperty(t,"__esModule",{value:!0});var o=Object.assign||function(e){for(var t=1;t=0||Object.prototype.hasOwnProperty.call(e,r)&&(n[r]=e[r]);return n}function o(){return{}}function a(e){return{actions:{initModal:function(){return e({type:"INIT_FORM_SCHEMA_STACK",payload:{formSchema:{type:"insert-link",nextType:"admin"}}})},reset:function(){return e({type:"RESET"})}}}}Object.defineProperty(t,"__esModule",{value:!0});var l=Object.assign||function(e){for(var t=1;t=0||Object.prototype.hasOwnProperty.call(e,r)&&(n[r]=e[r]);return n}Object.defineProperty(t,"__esModule",{value:!0});var o=Object.assign||function(e){for(var t=1;t Date: Thu, 2 Mar 2023 08:02:30 +1300 Subject: [PATCH 14/18] Use table_name in Linkable migration task --- src/Tasks/LinkableMigrationTask.php | 64 ++++++++++++++++++----------- 1 file changed, 40 insertions(+), 24 deletions(-) diff --git a/src/Tasks/LinkableMigrationTask.php b/src/Tasks/LinkableMigrationTask.php index 311ac24e..7024d326 100644 --- a/src/Tasks/LinkableMigrationTask.php +++ b/src/Tasks/LinkableMigrationTask.php @@ -11,6 +11,7 @@ use SilverStripe\LinkField\Models\Link; use SilverStripe\LinkField\Models\PhoneLink; use SilverStripe\LinkField\Models\SiteTreeLink; +use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DB; use SilverStripe\ORM\Queries\SQLInsert; use SilverStripe\ORM\Queries\SQLSelect; @@ -26,39 +27,39 @@ class LinkableMigrationTask extends BuildTask protected const TABLE_VERSIONS = 'LinkableLink_Versions'; protected const TABLE_MAP_LINK = [ - self::TABLE_BASE => 'LinkField_Link', - self::TABLE_LIVE => 'LinkField_Link_Live', - self::TABLE_VERSIONS => 'LinkField_Link_Versions', + self::TABLE_BASE => '%s', + self::TABLE_LIVE => '%s_Live', + self::TABLE_VERSIONS => '%s_Versions', ]; protected const TABLE_MAP_EMAIL_LINK = [ - self::TABLE_BASE => 'LinkField_EmailLink', - self::TABLE_LIVE => 'LinkField_EmailLink_Live', - self::TABLE_VERSIONS => 'LinkField_EmailLink_Versions', + self::TABLE_BASE => '%s', + self::TABLE_LIVE => '%s_Live', + self::TABLE_VERSIONS => '%s_Versions', ]; protected const TABLE_MAP_EXTERNAL_LINK = [ - self::TABLE_BASE => 'LinkField_ExternalLink', - self::TABLE_LIVE => 'LinkField_ExternalLink_Live', - self::TABLE_VERSIONS => 'LinkField_ExternalLink_Versions', + self::TABLE_BASE => '%s', + self::TABLE_LIVE => '%s_Live', + self::TABLE_VERSIONS => '%s_Versions', ]; protected const TABLE_MAP_FILE_LINK = [ - self::TABLE_BASE => 'LinkField_FileLink', - self::TABLE_LIVE => 'LinkField_FileLink_Live', - self::TABLE_VERSIONS => 'LinkField_FileLink_Versions', + self::TABLE_BASE => '%s', + self::TABLE_LIVE => '%s_Live', + self::TABLE_VERSIONS => '%s_Versions', ]; protected const TABLE_MAP_PHONE_LINK = [ - self::TABLE_BASE => 'LinkField_PhoneLink', - self::TABLE_LIVE => 'LinkField_PhoneLink_Live', - self::TABLE_VERSIONS => 'LinkField_PhoneLink_Versions', + self::TABLE_BASE => '%s', + self::TABLE_LIVE => '%s_Live', + self::TABLE_VERSIONS => '%s_Versions', ]; protected const TABLE_MAP_SITE_TREE_LINK = [ - self::TABLE_BASE => 'LinkField_SiteTreeLink', - self::TABLE_LIVE => 'LinkField_SiteTreeLink_Live', - self::TABLE_VERSIONS => 'LinkField_SiteTreeLink_Versions', + self::TABLE_BASE => '%s', + self::TABLE_LIVE => '%s_Live', + self::TABLE_VERSIONS => '%s_Versions', ]; private static array $versions_mapping_global = [ @@ -311,7 +312,7 @@ protected function insertLink(string $className, array $linkableData, string $or $assignments['ClassName'] = $className; // Find out what the corresponding table is for the origin table - $newTable = self::TABLE_MAP_LINK[$originTable]; + $newTable = sprintf(self::TABLE_MAP_LINK[$originTable], DataObject::getSchema()->tableName(Link::class)); // Insert our new record SQLInsert::create($newTable, $assignments)->execute(); @@ -329,7 +330,10 @@ protected function insertEmail(array $linkableData, string $originTable): void // Insert the base record for this EmailLink $this->insertLink(EmailLink::class, $linkableData, $originTable); - $newTable = self::TABLE_MAP_EMAIL_LINK[$originTable]; + $newTable = sprintf( + self::TABLE_MAP_EMAIL_LINK[$originTable], + DataObject::getSchema()->tableName(EmailLink::class) + ); $assignments = $this->getAssignmentsForMapping( $this->config()->get('email_mapping'), @@ -352,7 +356,10 @@ protected function insertExternal(array $linkableData, string $originTable): voi // Insert the base record for this ExternalLink $this->insertLink(ExternalLink::class, $linkableData, $originTable); - $newTable = self::TABLE_MAP_EXTERNAL_LINK[$originTable]; + $newTable = sprintf( + self::TABLE_MAP_EXTERNAL_LINK[$originTable], + DataObject::getSchema()->tableName(ExternalLink::class) + ); $assignments = $this->getAssignmentsForMapping( $this->config()->get('external_mapping'), @@ -375,7 +382,10 @@ protected function insertFile(array $linkableData, string $originTable): void // Insert the base record for this FileLink $this->insertLink(FileLink::class, $linkableData, $originTable); - $newTable = self::TABLE_MAP_FILE_LINK[$originTable]; + $newTable = sprintf( + self::TABLE_MAP_FILE_LINK[$originTable], + DataObject::getSchema()->tableName(FileLink::class) + ); $assignments = $this->getAssignmentsForMapping( $this->config()->get('file_mapping'), @@ -398,7 +408,10 @@ protected function insertPhone(array $linkableData, string $originTable): void // Insert the base record for this PhoneLink $this->insertLink(PhoneLink::class, $linkableData, $originTable); - $newTable = self::TABLE_MAP_PHONE_LINK[$originTable]; + $newTable = sprintf( + self::TABLE_MAP_PHONE_LINK[$originTable], + DataObject::getSchema()->tableName(PhoneLink::class) + ); $assignments = $this->getAssignmentsForMapping( $this->config()->get('phone_mapping'), @@ -421,7 +434,10 @@ protected function insertSiteTree(array $linkableData, string $originTable): voi // Insert the base record for this SiteTreeLink $this->insertLink(SiteTreeLink::class, $linkableData, $originTable); - $newTable = self::TABLE_MAP_SITE_TREE_LINK[$originTable]; + $newTable = sprintf( + self::TABLE_MAP_SITE_TREE_LINK[$originTable], + DataObject::getSchema()->tableName(SiteTreeLink::class) + ); $assignments = $this->getAssignmentsForMapping( $this->config()->get('sitetree_mapping'), From 51cd5746f908576c1ecf165e751836b6362c566a Mon Sep 17 00:00:00 2001 From: Chris Penny Date: Thu, 2 Mar 2023 08:26:23 +1300 Subject: [PATCH 15/18] Linting --- src/GraphQL/LinkDescriptionResolver.php | 5 ++++- src/GraphQL/LinkTypeResolver.php | 3 ++- src/Models/EmailLink.php | 2 +- src/Models/ExternalLink.php | 2 +- src/Models/FileLink.php | 2 +- src/Models/Link.php | 2 +- src/Models/PhoneLink.php | 2 +- src/Models/SiteTreeLink.php | 4 ++-- 8 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/GraphQL/LinkDescriptionResolver.php b/src/GraphQL/LinkDescriptionResolver.php index f72a5e98..fa029822 100644 --- a/src/GraphQL/LinkDescriptionResolver.php +++ b/src/GraphQL/LinkDescriptionResolver.php @@ -3,6 +3,7 @@ namespace SilverStripe\LinkField\GraphQL; use GraphQL\Type\Definition\ResolveInfo; +use InvalidArgumentException; use SilverStripe\GraphQL\Schema\DataObject\Resolver; use SilverStripe\LinkField\Type\Registry; @@ -11,8 +12,9 @@ class LinkDescriptionResolver extends Resolver public static function resolve($obj, $args = [], $context = [], ?ResolveInfo $info = null) { $data = json_decode($args['dataStr'], true); + if (json_last_error() !== JSON_ERROR_NONE) { - throw new \InvalidArgumentException('data must be a valid JSON string'); + throw new InvalidArgumentException('data must be a valid JSON string'); } if (empty($data['typeKey'])) { @@ -20,6 +22,7 @@ public static function resolve($obj, $args = [], $context = [], ?ResolveInfo $in } $type = Registry::singleton()->byKey($data['typeKey']); + if (empty($type)) { return ['description' => '']; } diff --git a/src/GraphQL/LinkTypeResolver.php b/src/GraphQL/LinkTypeResolver.php index 79df46a8..a61f28f1 100644 --- a/src/GraphQL/LinkTypeResolver.php +++ b/src/GraphQL/LinkTypeResolver.php @@ -3,6 +3,7 @@ namespace SilverStripe\LinkField\GraphQL; use GraphQL\Type\Definition\ResolveInfo; +use InvalidArgumentException; use SilverStripe\GraphQL\Schema\DataObject\Resolver; use SilverStripe\LinkField\Type\Registry; use SilverStripe\LinkField\Type\Type; @@ -12,7 +13,7 @@ class LinkTypeResolver extends Resolver public static function resolve($obj, $args = [], $context = [], ?ResolveInfo $info = null) { if (isset($args['keys']) && !is_array($args['keys'])) { - throw new \InvalidArgumentException('If `keys` is provdied, it must be an array'); + throw new InvalidArgumentException('If `keys` is provdied, it must be an array'); } $types = Registry::singleton()->list(); diff --git a/src/Models/EmailLink.php b/src/Models/EmailLink.php index 47d1aaf2..601855f3 100644 --- a/src/Models/EmailLink.php +++ b/src/Models/EmailLink.php @@ -15,7 +15,7 @@ class EmailLink extends Link private static string $table_name = 'LinkField_EmailLink'; private static array $db = [ - 'Email' => 'Varchar(255)' + 'Email' => 'Varchar(255)', ]; public function generateLinkDescription(array $data): string diff --git a/src/Models/ExternalLink.php b/src/Models/ExternalLink.php index 90c8e96a..cf9f1b04 100644 --- a/src/Models/ExternalLink.php +++ b/src/Models/ExternalLink.php @@ -12,7 +12,7 @@ class ExternalLink extends Link private static string $table_name = 'LinkField_ExternalLink'; private static array $db = [ - 'ExternalUrl' => 'Varchar' + 'ExternalUrl' => 'Varchar', ]; public function generateLinkDescription(array $data): string diff --git a/src/Models/FileLink.php b/src/Models/FileLink.php index 84c9951f..0b0d99ae 100644 --- a/src/Models/FileLink.php +++ b/src/Models/FileLink.php @@ -15,7 +15,7 @@ class FileLink extends Link private static string $table_name = 'LinkField_FileLink'; private static array $has_one = [ - 'File' => File::class + 'File' => File::class, ]; public function generateLinkDescription(array $data): string diff --git a/src/Models/Link.php b/src/Models/Link.php index 9b1c4085..3dd88933 100644 --- a/src/Models/Link.php +++ b/src/Models/Link.php @@ -30,7 +30,7 @@ class Link extends DataObject implements JsonData, Type private static array $db = [ 'Title' => 'Varchar', - 'OpenInNew' => 'Boolean' + 'OpenInNew' => 'Boolean', ]; /** diff --git a/src/Models/PhoneLink.php b/src/Models/PhoneLink.php index e545417a..855e3b1c 100644 --- a/src/Models/PhoneLink.php +++ b/src/Models/PhoneLink.php @@ -12,7 +12,7 @@ class PhoneLink extends Link private static string $table_name = 'LinkField_PhoneLink'; private static array $db = [ - 'Phone' => 'Varchar(255)' + 'Phone' => 'Varchar(255)', ]; public function generateLinkDescription(array $data): string diff --git a/src/Models/SiteTreeLink.php b/src/Models/SiteTreeLink.php index 21d6ceeb..8a63aa69 100644 --- a/src/Models/SiteTreeLink.php +++ b/src/Models/SiteTreeLink.php @@ -19,11 +19,11 @@ class SiteTreeLink extends Link private static string $table_name = 'LinkField_SiteTreeLink'; private static array $db = [ - 'Anchor' => 'Varchar' + 'Anchor' => 'Varchar', ]; private static array $has_one = [ - 'Page' => SiteTree::class + 'Page' => SiteTree::class, ]; public function generateLinkDescription(array $data): string From 14ddd5f8bba34c7d2e3ce143a3203b1e2d1d47e7 Mon Sep 17 00:00:00 2001 From: Chris Penny Date: Thu, 2 Mar 2023 10:19:40 +1300 Subject: [PATCH 16/18] Move eslint ignore to each file --- .eslintignore | 3 ++- client/src/boot/index.js | 1 + client/src/boot/registerComponents.js | 1 + client/src/boot/registerQueries.js | 1 + client/src/boot/registerReducers.js | 1 + client/src/components/LinkField/LinkField.js | 1 + client/src/components/LinkModal/FileLinkModal.js | 1 + client/src/components/LinkModal/LinkModal.js | 1 + client/src/components/LinkPicker/LinkPicker.js | 1 + client/src/components/LinkPicker/LinkPickerMenu.js | 1 + client/src/components/LinkPicker/LinkPickerTitle.js | 1 + client/src/components/LinkPicker/tests/LinkPicker-story.js | 1 + client/src/entwine/JsonField.js | 1 + client/src/state/linkDescription/readLinkDescription.js | 1 + client/src/state/linkTypes/readLinkTypes.js | 1 + client/src/tests/sample-test.js | 1 + client/src/types/LinkType.js | 1 + 17 files changed, 18 insertions(+), 1 deletion(-) diff --git a/.eslintignore b/.eslintignore index 684bec4c..81943c37 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ -client/ +client/dist/ +client/lang/ diff --git a/client/src/boot/index.js b/client/src/boot/index.js index ddffa8c5..4d7bb2a5 100644 --- a/client/src/boot/index.js +++ b/client/src/boot/index.js @@ -1,4 +1,5 @@ /* global document */ +/* eslint-disable */ import Config from 'lib/Config'; import registerReducers from './registerReducers'; import registerComponents from './registerComponents'; diff --git a/client/src/boot/registerComponents.js b/client/src/boot/registerComponents.js index 2d744b6d..1fd6283b 100644 --- a/client/src/boot/registerComponents.js +++ b/client/src/boot/registerComponents.js @@ -1,3 +1,4 @@ +/* eslint-disable */ import Injector from 'lib/Injector'; import LinkPicker from 'components/LinkPicker/LinkPicker'; import LinkField from 'components/LinkField/LinkField'; diff --git a/client/src/boot/registerQueries.js b/client/src/boot/registerQueries.js index b08b44c4..1d68cfa3 100644 --- a/client/src/boot/registerQueries.js +++ b/client/src/boot/registerQueries.js @@ -1,3 +1,4 @@ +/* eslint-disable */ import Injector from 'lib/Injector'; import readLinkTypes from 'state/linkTypes/readLinkTypes'; import readLinkDescription from 'state/linkDescription/readLinkDescription'; diff --git a/client/src/boot/registerReducers.js b/client/src/boot/registerReducers.js index 052ea8c2..1abd586a 100644 --- a/client/src/boot/registerReducers.js +++ b/client/src/boot/registerReducers.js @@ -1,3 +1,4 @@ +/* eslint-disable */ import Injector from 'lib/Injector'; import { combineReducers } from 'redux'; // import gallery from 'state/gallery/GalleryReducer'; diff --git a/client/src/components/LinkField/LinkField.js b/client/src/components/LinkField/LinkField.js index a5173eaa..5a71dd41 100644 --- a/client/src/components/LinkField/LinkField.js +++ b/client/src/components/LinkField/LinkField.js @@ -1,3 +1,4 @@ +/* eslint-disable */ import i18n from 'i18n'; import React, { Component, Fragment, useState } from 'react'; import { connect } from 'react-redux'; diff --git a/client/src/components/LinkModal/FileLinkModal.js b/client/src/components/LinkModal/FileLinkModal.js index 5313e6f5..a5d7631f 100644 --- a/client/src/components/LinkModal/FileLinkModal.js +++ b/client/src/components/LinkModal/FileLinkModal.js @@ -1,4 +1,5 @@ /* global tinymce, editorIdentifier, ss */ +/* eslint-disable */ import i18n from 'i18n'; import React, {useEffect} from 'react'; import InsertMediaModal from 'containers/InsertMediaModal/InsertMediaModal'; diff --git a/client/src/components/LinkModal/LinkModal.js b/client/src/components/LinkModal/LinkModal.js index 84948346..7e5ce86d 100644 --- a/client/src/components/LinkModal/LinkModal.js +++ b/client/src/components/LinkModal/LinkModal.js @@ -1,3 +1,4 @@ +/* eslint-disable */ import i18n from 'i18n'; import React from 'react'; import PropTypes from 'prop-types'; diff --git a/client/src/components/LinkPicker/LinkPicker.js b/client/src/components/LinkPicker/LinkPicker.js index 3ec8eab0..98aab6ec 100644 --- a/client/src/components/LinkPicker/LinkPicker.js +++ b/client/src/components/LinkPicker/LinkPicker.js @@ -1,3 +1,4 @@ +/* eslint-disable */ import i18n from 'i18n'; import React from 'react'; import { inject } from 'lib/Injector'; diff --git a/client/src/components/LinkPicker/LinkPickerMenu.js b/client/src/components/LinkPicker/LinkPickerMenu.js index 3a1ac555..036f8392 100644 --- a/client/src/components/LinkPicker/LinkPickerMenu.js +++ b/client/src/components/LinkPicker/LinkPickerMenu.js @@ -1,3 +1,4 @@ +/* eslint-disable */ import i18n from 'i18n'; import React, {useState, setState} from 'react'; import { inject } from 'lib/Injector'; diff --git a/client/src/components/LinkPicker/LinkPickerTitle.js b/client/src/components/LinkPicker/LinkPickerTitle.js index 38229d0f..7777709e 100644 --- a/client/src/components/LinkPicker/LinkPickerTitle.js +++ b/client/src/components/LinkPicker/LinkPickerTitle.js @@ -1,3 +1,4 @@ +/* eslint-disable */ import i18n from 'i18n'; import React from 'react'; import { inject } from 'lib/Injector'; diff --git a/client/src/components/LinkPicker/tests/LinkPicker-story.js b/client/src/components/LinkPicker/tests/LinkPicker-story.js index d8c78766..95f1050e 100644 --- a/client/src/components/LinkPicker/tests/LinkPicker-story.js +++ b/client/src/components/LinkPicker/tests/LinkPicker-story.js @@ -1,3 +1,4 @@ +/* eslint-disable */ import React from 'react'; // eslint-disable-next-line import/no-extraneous-dependencies import { storiesOf } from '@storybook/react'; diff --git a/client/src/entwine/JsonField.js b/client/src/entwine/JsonField.js index f429e811..35d29c4b 100644 --- a/client/src/entwine/JsonField.js +++ b/client/src/entwine/JsonField.js @@ -1,4 +1,5 @@ /* global ss */ +/* eslint-disable */ import jQuery from 'jquery'; import React from 'react'; import ReactDOM from 'react-dom'; diff --git a/client/src/state/linkDescription/readLinkDescription.js b/client/src/state/linkDescription/readLinkDescription.js index df31484f..2301bd26 100644 --- a/client/src/state/linkDescription/readLinkDescription.js +++ b/client/src/state/linkDescription/readLinkDescription.js @@ -1,3 +1,4 @@ +/* eslint-disable */ import { graphqlTemplates } from 'lib/Injector'; const apolloConfig = { diff --git a/client/src/state/linkTypes/readLinkTypes.js b/client/src/state/linkTypes/readLinkTypes.js index 1a2621da..1bb0bb7e 100644 --- a/client/src/state/linkTypes/readLinkTypes.js +++ b/client/src/state/linkTypes/readLinkTypes.js @@ -1,3 +1,4 @@ +/* eslint-disable */ import { graphqlTemplates } from 'lib/Injector'; const apolloConfig = { diff --git a/client/src/tests/sample-test.js b/client/src/tests/sample-test.js index d6511e67..47bc3714 100644 --- a/client/src/tests/sample-test.js +++ b/client/src/tests/sample-test.js @@ -1,4 +1,5 @@ /* global jest, describe, it, expect */ +/* eslint-disable */ describe('sample tests', () => { it('sample test', () => { diff --git a/client/src/types/LinkType.js b/client/src/types/LinkType.js index b8656a58..c086ec78 100644 --- a/client/src/types/LinkType.js +++ b/client/src/types/LinkType.js @@ -1,3 +1,4 @@ +/* eslint-disable */ import PropTypes from 'prop-types'; const LinkType = PropTypes.shape({ From b6ff5706acd48a9cd48612f0c14d0b2d0c52de12 Mon Sep 17 00:00:00 2001 From: Chris Penny Date: Thu, 2 Mar 2023 13:09:55 +1300 Subject: [PATCH 17/18] Update LinkableMigrationTask to allow for separate query param field --- docs/en/linkable-migration.md | 47 +++++++++++++ src/Models/SiteTreeLink.php | 10 ++- src/Tasks/LinkableMigrationTask.php | 102 +++++++++++++++++++++++++++- 3 files changed, 155 insertions(+), 4 deletions(-) diff --git a/docs/en/linkable-migration.md b/docs/en/linkable-migration.md index bf372e21..7ba7b571 100644 --- a/docs/en/linkable-migration.md +++ b/docs/en/linkable-migration.md @@ -9,6 +9,10 @@ This does not cover usages of `EmbeddedObject` (at least, not at this time). **Versioned:** If you have `Versioned` `Linkable`, then the expectation is that you will also `Version` `LinkField`. If you have not `Versioned` `Linkable`, then the expectation is that you will **not** `Version` `LinkField`. +**No support for internal links with query params (GET params):** Please be aware that Linkfield does not support +internal links with query params (`?`) out of the box, and therefor the migration task will **remove** any query +params that are present in the Linkable's `Anchor` field. + ## Install Silvesrtripe Linkfield Install the Silverstripe Linkfield module: @@ -145,3 +149,46 @@ SilverStripe\LinkField\Tasks\LinkableMigrationTask: link_mapping: ParentPageID: ParentPageID ``` + +## Adding support for internal links with query params + +No official support is provided, but you can achieve this through adding your own extensions. + +Add a new field to `SiteTreeLink` to store your query params, EG: + +```php +class SiteTreeLinkExtension extends DataExtension +{ + private static array $db = [ + 'QueryParams' => 'Varchar', + ]; +} +``` + +An extension point called `updateGetURLBeforeAnchor` is available: + +```php +class SiteTreeLinkExtension extends DataExtension +{ + ... + + public function updateGetURLBeforeAnchor(&$url): void + { + // Assumes that you save your QueryParams within prepending the ?, so we append it here + $url .= sprintf('?%s', $this->owner->QueryParams); + } +} +``` + +If you also plan to use the `LinkableMigrationTask`, then there is a configuration that you can enable to tell us where +you would like the query params from the old `AnchorLink` to be migrated. + +Please note: The migration task assumes that you will store your query params without prepending the `?` (following +the same paradigm as our `Anchor` field). + +EG: + +```yaml +SilverStripe\LinkField\Tasks\LinkableMigrationTask: + sitetree_query_params_to: QueryParams +``` diff --git a/src/Models/SiteTreeLink.php b/src/Models/SiteTreeLink.php index 8a63aa69..a6ee8ce6 100644 --- a/src/Models/SiteTreeLink.php +++ b/src/Models/SiteTreeLink.php @@ -10,9 +10,9 @@ /** * A link to a Page in the CMS * - * @property SiteTree $Page * @property int $PageID * @property string $Anchor + * @method SiteTree Page() */ class SiteTreeLink extends Link { @@ -32,6 +32,7 @@ public function generateLinkDescription(array $data): string return ''; } + /** @var SiteTree $page */ $page = SiteTree::get()->byID($data['PageID']); if (!$page || !$page->exists()) { @@ -59,6 +60,7 @@ public function getCMSFields(): FieldList $fields->insertAfter( 'PageID', AnchorSelectorField::create('Anchor') + ->setDescription('Do not prepend "#". EG: "option1=value&option2=value2"') ); return $fields; @@ -73,7 +75,9 @@ public function onBeforeWrite(): void public function getURL(): string { - $url = $this->Page ? $this->Page->Link() : ''; + $url = $this->Page() ? $this->Page()->Link() : ''; + + $this->extend('updateGetURLBeforeAnchor', $url); if ($this->Anchor) { $url .= '#' . $this->Anchor; @@ -101,7 +105,7 @@ protected function getTitleFromPage(): ?string return $this->Title; } - $page = $this->Page; + $page = $this->Page(); if (!$page || !$page->exists()) { // We don't have a page to fall back to diff --git a/src/Tasks/LinkableMigrationTask.php b/src/Tasks/LinkableMigrationTask.php index 7024d326..2a08d432 100644 --- a/src/Tasks/LinkableMigrationTask.php +++ b/src/Tasks/LinkableMigrationTask.php @@ -18,6 +18,10 @@ use SilverStripe\Versioned\Versioned; /** + * This migration task is provided without the promise that it will follow semver and without promising official support + * and maintenance. We have, however, made our absolute best effort to check that it works. It is a development task, + * and as such we expect you to test this locally before running it on any production environments + * * @codeCoverageIgnore */ class LinkableMigrationTask extends BuildTask @@ -124,9 +128,29 @@ class LinkableMigrationTask extends BuildTask private static array $sitetree_mapping = [ 'ID' => 'ID', 'SiteTreeID' => 'PageID', - 'Anchor' => 'Anchor', + // @see insertSiteTree() for the migration of the Anchor field ]; + /** + * @see insertSiteTree() for the migration of the Anchor field + */ + private static string $sitetree_anchor_from = 'Anchor'; + + /** + * @see insertSiteTree() for the migration of the Anchor field + */ + private static string $sitetree_anchor_to = 'Anchor'; + + /** + * @see insertSiteTree() for the migration of the Anchor field + */ + private static ?string $sitetree_query_params_from = 'Anchor'; + + /** + * @see insertSiteTree() for the migration of the Anchor field + */ + private static ?string $sitetree_query_params_to = null; + private static $segment = 'linkable-migration-task'; protected $title = 'Linkable Migration Task'; @@ -445,6 +469,82 @@ protected function insertSiteTree(array $linkableData, string $originTable): voi $originTable ); + // Special case for the Anchor field. Linkable supports query params and/or anchors, but the Linkfield module + // only supports anchors. Linkable also requires that you prepend the #, and Linkfield requires you to *not* + $anchorFrom = $this->config()->get('sitetree_anchor_from'); + $anchorTo = $this->config()->get('sitetree_anchor_to'); + + $assignments[$anchorTo] = $this->getAnchorString($linkableData[$anchorFrom]); + + // Linkable supports adding query params and anchors together in the Anchor field. This module does not. If you + // would like to add support for query params, then you will need to have created (probably through an + // extension) a new and separate field (EG: QueryParams) on SiteTreeLink. You can then update the config for + // $sitetree_query_params_to to the name of the field you created (EG: QueryParams) + $queryParamsFrom = $this->config()->get('sitetree_query_params_from'); + $queryParamsTo = $this->config()->get('sitetree_query_params_to'); + + if ($queryParamsFrom && $queryParamsTo) { + $assignments[$queryParamsTo] = $this->getQueryString($linkableData[$queryParamsFrom]); + } + SQLInsert::create($newTable, $assignments)->execute(); } + + protected function getAnchorString(?string $originalAnchor): ?string + { + if (!$originalAnchor) { + return null; + } + + // We know that Linkable requires users to include a hash (#) for any anchors that they want. If we don't find + // a hash then there is no anchor here + if (!str_contains($originalAnchor, '#')) { + return null; + } + + $firstChar = $originalAnchor[0] ?? null; + + // Linkable supported query params (?) and anchors (#) in the same Anchor field. We know that query params must + // be provided before anchor, so if the first char is an anchor then we can just trim that and return; + if ($firstChar === '#') { + return ltrim($originalAnchor, '#'); + } + + // The only remaining possibility is that there is a string before the hash + // Explode the string at the first #, and we would expect there to always be exactly 2 parts + $parts = explode('#', $originalAnchor, 2); + + // return the second part + return $parts[1]; + } + + /** + * This method is not used (out of the box) as part of the migration process. This has been provided so that if + * you have a need for it, you can extend this class and access it + */ + protected function getQueryString(?string $originalAnchor): ?string + { + if (!$originalAnchor) { + return null; + } + + // We know that Linkable requires users to include a ? for any query params that they want. If we don't find + // a ? then there are no query params here + if (!str_contains($originalAnchor, '?')) { + return null; + } + + // Linkable supported query params (?) and anchors (#) in the same Anchor field. We know that query params must + // be provided before anchor, so if there are no anchors in the string, then we can just trim the ? and return + if (!str_contains($originalAnchor, '#')) { + return ltrim($originalAnchor, '?'); + } + + // The only remaining possibility is that there are query params followed by an anchor + // Explode the string at the first #, and we would expect there to always be exactly 2 parts + $parts = explode('#', $originalAnchor, 2); + + // return the first part (the query params) after trimming the prepended ? + return ltrim($parts[1], '?'); + } } From 271ea2899d750b6093e60efcfe94709881c703dc Mon Sep 17 00:00:00 2001 From: Chris Penny Date: Thu, 2 Mar 2023 13:16:26 +1300 Subject: [PATCH 18/18] Update README to warn about old table names --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index a460ecca..b8d79420 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,14 @@ class Page extends SiteTree } ``` +## Migrating from Version `1.0.0` or `dev-master` + +Please be aware that in early versions of this module (and in untagged `dev-master`) there were no table names defined +for our `Link` classes. These have now all been defined, which may mean that you need to rename your old tables, or +migrate the data across. + +EG: `SilverStripe_LinkField_Models_Link` needs to be migrated to `LinkField_Link`. + ## Migrating from Shae Dawson's Linkable module https://github.com/sheadawson/silverstripe-linkable