From 93646e9ed3687cdc8c9e5486169db73b0c313123 Mon Sep 17 00:00:00 2001 From: Sam Hibberd Date: Fri, 17 May 2024 19:58:39 +0200 Subject: [PATCH 1/7] Allow self realtions on element link fields --- CHANGELOG.md | 5 +++++ composer.json | 2 +- src/templates/types/input/element.twig | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6dd7e5..7dc89cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # Linkit Changelog > One link field to rule them all, built for [Craft 4](http://craftcms.com) +## 4.0.4.2 - 2024-05-17 + +### Changed +- Allow self realtions on element link fields + ## 4.0.4.1 - 2022-10-24 ### Fixed diff --git a/composer.json b/composer.json index 99f6385..9bfa4fb 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "presseddigital/linkit", "description": "One link field to rule them all.", "type": "craft-plugin", - "version": "4.0.4.1", + "version": "4.0.4.2", "keywords": [ "craftcms", "link it", diff --git a/src/templates/types/input/element.twig b/src/templates/types/input/element.twig index e93bb3f..63b3d64 100644 --- a/src/templates/types/input/element.twig +++ b/src/templates/types/input/element.twig @@ -8,6 +8,7 @@ sources: link.sources ?? '*', sourceElementId: element.id, showSiteMenu: true, + allowSelfRelations: true, limit: 1, elements: (currentLink.element ?? false) ? [currentLink.element] : [], criteria: { From 09443bd1168f3bb7018eb99ba8114a257f6c66e2 Mon Sep 17 00:00:00 2001 From: Derrick Grigg Date: Thu, 6 Nov 2025 11:25:23 -0500 Subject: [PATCH 2/7] added cli controller for convert --- composer.json | 4 ++-- src/console/ConvertController.php | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 src/console/ConvertController.php diff --git a/composer.json b/composer.json index 9bfa4fb..93b2d4f 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "presseddigital/linkit", "description": "One link field to rule them all.", "type": "craft-plugin", - "version": "4.0.4.2", + "version": "5.0.0", "keywords": [ "craftcms", "link it", @@ -21,7 +21,7 @@ } ], "require": { - "craftcms/cms": "^4.0.0", + "craftcms/cms": "^5.0.0", "php": "^8.0" }, "autoload": { diff --git a/src/console/ConvertController.php b/src/console/ConvertController.php new file mode 100644 index 0000000..662aee5 --- /dev/null +++ b/src/console/ConvertController.php @@ -0,0 +1,22 @@ + Date: Thu, 6 Nov 2025 18:18:56 -0500 Subject: [PATCH 3/7] fix null error in Phone model and added convert and migrate console controllers --- src/console/ConvertController.php | 22 -- src/console/controllers/ConvertController.php | 200 ++++++++++++++++++ src/console/controllers/MigrateController.php | 175 +++++++++++++++ src/models/Phone.php | 2 +- 4 files changed, 376 insertions(+), 23 deletions(-) delete mode 100644 src/console/ConvertController.php create mode 100644 src/console/controllers/ConvertController.php create mode 100644 src/console/controllers/MigrateController.php diff --git a/src/console/ConvertController.php b/src/console/ConvertController.php deleted file mode 100644 index 662aee5..0000000 --- a/src/console/ConvertController.php +++ /dev/null @@ -1,22 +0,0 @@ -from('{{%fields}}') + ->where(['type' => LinkitField::class]) + ->all(); + + // Loop through each field and migrate settings in this pass + foreach ($fields as $field) { + echo "Preparing to migrate field “{$field['handle']}” ({$field['uid']}) settings.\n"; + + echo " > Field “{$field['handle']}” settings migrated.\n\n"; + $linkItSettings = json_decode($field['settings'], true); + $settings = $this->convertSettings($linkItSettings); + + // Update the field type and settings directly in the database + Db::update( + '{{%fields}}', + [ + 'type' => Link::class, + 'settings' => json_encode($settings, JSON_UNESCAPED_SLASHES) + ], + ['uid' => $field['uid']] + ); + + echo " > Field {$field['handle']} successfully updated.\n"; + + // Rebuild project config for this field + $projectConfig = Craft::$app->getProjectConfig(); + $fieldsService = Craft::$app->getFields(); + + // Refresh fields cache so we get the updated field + $fieldsService->refreshFields(); + + // Get the updated field object + $updatedField = $fieldsService->getFieldByUid($field['uid']); + + if ($updatedField) { + // Rebuild the project config for this field + $projectConfig->set( + "fields.{$field['uid']}", + $fieldsService->createFieldConfig($updatedField) + ); + + echo " > Project config rebuilt for field {$field['handle']}.\n"; + } + + echo PHP_EOL; + } + return ExitCode::OK; + + } + + + /** + * Convert LinkIt settings structure to native Link field settings structure + * + * @param array $linkitSettings + * @return array + */ + private function convertSettings(array $linkitSettings): array + { + $typeMapping = [ + 'presseddigital\\linkit\\models\\Entry' => 'entry', + 'presseddigital\\linkit\\models\\Asset' => 'asset', + 'presseddigital\\linkit\\models\\Category' => 'category', + 'presseddigital\\linkit\\models\\User' => 'user', + 'presseddigital\\linkit\\models\\Email' => 'email', + 'presseddigital\\linkit\\models\\Phone' => 'tel', + 'presseddigital\\linkit\\models\\Url' => 'url', + // Social media types merge into URL + 'presseddigital\\linkit\\models\\Twitter' => 'url', + 'presseddigital\\linkit\\models\\Facebook' => 'url', + 'presseddigital\\linkit\\models\\Instagram' => 'url', + 'presseddigital\\linkit\\models\\LinkedIn' => 'url', + ]; + + $types = []; + $typeSettings = []; + $advancedFields = []; + + // Add target if allowTarget was enabled + if (!empty($linkitSettings['allowTarget'])) { + $advancedFields[] = 'target'; + } + + // Process each type from the old structure + if (isset($linkitSettings['types']) && is_array($linkitSettings['types'])) { + foreach ($linkitSettings['types'] as $oldTypeClass => $typeConfig) { + // Skip if not enabled + if (empty($typeConfig['enabled']) || $typeConfig['enabled'] === "") { + continue; + } + + // Map to new type name + if (!isset($typeMapping[$oldTypeClass])) { + continue; + } + + $newTypeName = $typeMapping[$oldTypeClass]; + + // Add to types array (avoiding duplicates for merged social types) + if (!in_array($newTypeName, $types)) { + $types[] = $newTypeName; + } + + // Configure type-specific settings + switch ($newTypeName) { + case 'entry': + $typeSettings['entry'] = [ + 'sources' => $typeConfig['sources'] ?? '*', + 'showUnpermittedSections' => '1', + 'showUnpermittedEntries' => '1', + ]; + break; + + case 'asset': + $typeSettings['asset'] = [ + 'sources' => $typeConfig['sources'] ?? '*', + 'allowedKinds' => '*', + 'showUnpermittedVolumes' => '1', + 'showUnpermittedFiles' => '1', + ]; + break; + + case 'category': + $typeSettings['category'] = [ + 'sources' => $typeConfig['sources'] ?? '*', + ]; + break; + + case 'url': + // Merge URL settings from Url type and social media types + if (!isset($typeSettings['url'])) { + $typeSettings['url'] = [ + 'allowRootRelativeUrls' => '0', + 'allowAnchors' => '0', + 'allowCustomSchemes' => '0', + ]; + } + + // If this is the main URL type, map its specific settings + if ($oldTypeClass === 'presseddigital\\linkit\\models\\Url') { + if (!empty($typeConfig['allowPaths'])) { + $typeSettings['url']['allowRootRelativeUrls'] = '1'; + } + if (!empty($typeConfig['allowHash'])) { + $typeSettings['url']['allowAnchors'] = '1'; + } + // Allow custom schemes if mailto or alias enabled + if (!empty($typeConfig['allowMailto']) || !empty($typeConfig['allowAlias'])) { + $typeSettings['url']['allowCustomSchemes'] = '1'; + } + } + break; + } + } + } + + // Build the new settings structure + $newSettings = [ + 'advancedFields' => $advancedFields, + 'fullGraphqlData' => true, + 'maxLength' => 255, + 'showLabelField' => !empty($linkitSettings['allowCustomText']), + 'typeSettings' => $typeSettings, + 'types' => $types, + ]; + + return $newSettings; + } + +} \ No newline at end of file diff --git a/src/console/controllers/MigrateController.php b/src/console/controllers/MigrateController.php new file mode 100644 index 0000000..39ed8e2 --- /dev/null +++ b/src/console/controllers/MigrateController.php @@ -0,0 +1,175 @@ +from('{{%fields}}') + ->where(['type' => LinkitField::class]) + ->orWwhere(['type' => Link::class]) +// ->orWhere(['type' => LinkitField::class]) + ->all(); + + foreach ($fields as $field) { + echo "Preparing to migrate field '{$field['handle']}' ({$field['uid']}) content.\n"; + + $fieldLayouts = (new Query()) + ->from('{{%fieldlayouts}}') + ->where(['like', 'config', $field['uid']]) + ->all(); + + foreach($fieldLayouts as $fieldLayout) { + $element = $this->findField(json_decode($fieldLayout['config'], true), $field['uid']); + + if ($element){ + $contentEntries = (new Query()) + ->from('{{%elements_sites}}') + ->where(['like', 'content', $element['uid']]) + ->all(); + + if (count($contentEntries) < 1) { + echo "> No content to migrate for field '{$field['handle']}'\n"; + continue; + } + + foreach($contentEntries as $contentEntry) { + $newContent = $this->convertContent($contentEntry['content'], $contentEntry['siteId'], $element['uid'] ); + if ($newContent !== false){ + echo " > Migrated content for element #{$contentEntry['elementId']}\n"; + } else { + echo " > Unable to convert content for element #{$contentEntry['elementId']}\n"; + } + } + } + } + echo "> Field '{$field['handle']}' content migrated.\n\n"; + } + + return ExitCode::OK; + } + + private function findField($data, $fieldUid){ + $foundElement = false; + + foreach ($data['tabs'] as &$tab) { + foreach ($tab['elements'] as &$element) { + if (isset($element['fieldUid']) && $element['fieldUid'] === $fieldUid) { + $foundElement = &$element; + break 2; // Break out of loop + } + } + } + + return $foundElement; + } + + /** + * Convert LinkIt content format to native Link field format + * + * @param string $contentJson The JSON string from the content column + * @param string $fieldUid The field UID to update + * @return string The converted JSON string + */ + private function convertContent(string $contentJson, int $siteId, string $fieldUid): string|false + { + $content = json_decode($contentJson, true); + + if (!$content || !isset($content[$fieldUid])) { + return false; + } + + $linkitData = $content[$fieldUid]; + + // If it's already been converted, return + if (isset($linkitData['type']) && str_starts_with($linkitData['type'], 'presseddigital') === false){ + echo ' > already converted this content' . PHP_EOL; + return false; + } + + // If it's not valid LinkIt data, return + if (!$linkitData || !isset($linkitData['type']) || !isset($linkitData['value'])) { + return false; + } + + $typeMapping = [ + 'presseddigital\\linkit\\models\\Entry' => 'entry', + 'presseddigital\\linkit\\models\\Asset' => 'asset', + 'presseddigital\\linkit\\models\\Category' => 'category', + 'presseddigital\\linkit\\models\\User' => 'user', + 'presseddigital\\linkit\\models\\Email' => 'email', + 'presseddigital\\linkit\\models\\Phone' => 'tel', + 'presseddigital\\linkit\\models\\Url' => 'url', + 'presseddigital\\linkit\\models\\Twitter' => 'url', + 'presseddigital\\linkit\\models\\Facebook' => 'url', + 'presseddigital\\linkit\\models\\Instagram' => 'url', + 'presseddigital\\linkit\\models\\LinkedIn' => 'url', + ]; + + // Map the type + $oldType = $linkitData['type']; + $newType = $typeMapping[$oldType] ?? 'url'; + + // Build the new Link field structure + $newLinkData = [ + 'type' => $newType, + 'label' => $linkitData['customText'] ?? null, + 'value' => $this->convertValueForType($newType, $linkitData['value'], $siteId), + ]; + + // Add target if present + if (!empty($linkitData['target'])) { + $newLinkData['target'] = $linkitData['target']; + } + + // Update the content with the new format + $content[$fieldUid] = $newLinkData; + + return json_encode($content); + } + + /** + * Convert value based on type - element types need special reference format + * + * @param string $type The new type (entry, asset, category, etc.) + * @param mixed $value The original value + * @return string The converted value + */ + private function convertValueForType(string $type, $value, int $siteId): string + { + // Element types need the special {type:id@siteId:url} format + switch ($type) { + case 'entry': + return "{{entry:{$value}@{$siteId}:url}}"; + case 'asset': + return "{{asset:{$value}@{$siteId}:url}}"; + case 'category': + return "{{category:{$value}@{$siteId}:url}}"; + case 'user': + return "{{user:{$value}@{$siteId}:url}}"; + case 'email': + return "mailto:{$value}"; + case 'tel': + return "tel:{$value}"; + default: + // For URL and other types, return as-is + return (string) $value; + } + } +} \ No newline at end of file diff --git a/src/models/Phone.php b/src/models/Phone.php index 8249de0..dfafd52 100644 --- a/src/models/Phone.php +++ b/src/models/Phone.php @@ -32,7 +32,7 @@ public static function defaultPlaceholder(): string public function getUrl(): string { - return (string) 'tel:' . StringHelper::stripWhitespace($this->value); + return (string) 'tel:' . StringHelper::stripWhitespace($this->value ?? ''); } /** From b1b6386cf0c81ce8e711b88465b3e472af114197 Mon Sep 17 00:00:00 2001 From: Derrick Grigg Date: Fri, 7 Nov 2025 15:05:23 -0500 Subject: [PATCH 4/7] additional updates for the linkit conversion and migration processes --- src/console/controllers/ConvertController.php | 16 +++++----- src/console/controllers/MigrateController.php | 30 ++++++++++++------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/console/controllers/ConvertController.php b/src/console/controllers/ConvertController.php index a31c89c..fbee45d 100644 --- a/src/console/controllers/ConvertController.php +++ b/src/console/controllers/ConvertController.php @@ -7,6 +7,7 @@ use craft\db\Query; use craft\fields\Link; use craft\helpers\App; +use craft\helpers\Console; use craft\helpers\Db; use yii\console\ExitCode; use presseddigital\linkit\fields\LinkitField; @@ -24,7 +25,7 @@ public function actionIndex(): int { App::maxPowerCaptain(); - echo LinkitField::class; + $this->stdout("Convert Linkit to native Link field.\n"); // Get all LinkIt fields $fields = (new Query()) @@ -34,9 +35,7 @@ public function actionIndex(): int // Loop through each field and migrate settings in this pass foreach ($fields as $field) { - echo "Preparing to migrate field “{$field['handle']}” ({$field['uid']}) settings.\n"; - - echo " > Field “{$field['handle']}” settings migrated.\n\n"; + $this->stdout("\nPreparing to migrate field '{$field['handle']}' ({$field['uid']}) settings.\n"); $linkItSettings = json_decode($field['settings'], true); $settings = $this->convertSettings($linkItSettings); @@ -50,7 +49,7 @@ public function actionIndex(): int ['uid' => $field['uid']] ); - echo " > Field {$field['handle']} successfully updated.\n"; + $this->stdout(" > Field {$field['handle']} successfully updated.\n"); // Rebuild project config for this field $projectConfig = Craft::$app->getProjectConfig(); @@ -69,11 +68,12 @@ public function actionIndex(): int $fieldsService->createFieldConfig($updatedField) ); - echo " > Project config rebuilt for field {$field['handle']}.\n"; + $this->stdout(" > Project config rebuilt for field {$field['handle']}.\n"); } - - echo PHP_EOL; } + $this->stdout("\nConversion of LinkIt fields to Link complete.\n", Console::FG_GREEN, Console::BOLD ); + $this->stdout("Commit your project config changes, and run `craft up` on other environments for the changes to take effect.\n"); + $this->stdout("Run 'craft linkit/migrate' in this and other environments to migrate the content.\n\n"); return ExitCode::OK; } diff --git a/src/console/controllers/MigrateController.php b/src/console/controllers/MigrateController.php index 39ed8e2..f23ba48 100644 --- a/src/console/controllers/MigrateController.php +++ b/src/console/controllers/MigrateController.php @@ -10,6 +10,7 @@ use craft\helpers\Db; use presseddigital\linkit\fields\LinkitField; use yii\console\ExitCode; +use craft\helpers\Console; class MigrateController extends Controller { @@ -22,13 +23,13 @@ public function actionIndex(): int { $fields = (new Query()) ->from('{{%fields}}') - ->where(['type' => LinkitField::class]) - ->orWwhere(['type' => Link::class]) +// ->where(['type' => LinkitField::class]) + ->where(['type' => Link::class]) // ->orWhere(['type' => LinkitField::class]) ->all(); foreach ($fields as $field) { - echo "Preparing to migrate field '{$field['handle']}' ({$field['uid']}) content.\n"; + $this->stdout("\nPreparing to migrate field '{$field['handle']}' ({$field['uid']}) content.\n"); $fieldLayouts = (new Query()) ->from('{{%fieldlayouts}}') @@ -45,21 +46,30 @@ public function actionIndex(): int ->all(); if (count($contentEntries) < 1) { - echo "> No content to migrate for field '{$field['handle']}'\n"; + $this->stdout(" > No content to migrate for field '{$field['handle']}'\n", Console::FG_YELLOW); continue; } foreach($contentEntries as $contentEntry) { + $this->stdout("existing content\n"); + $this->stdout($contentEntry['content'] . PHP_EOL); $newContent = $this->convertContent($contentEntry['content'], $contentEntry['siteId'], $element['uid'] ); if ($newContent !== false){ - echo " > Migrated content for element #{$contentEntry['elementId']}\n"; +// $this->stdout($newContent . PHP_EOL); + Db::update('{{%elements_sites}}', + ['content' => $newContent], + [ + 'elementId' => $contentEntry['elementId'], + 'siteId' => $contentEntry['siteId'] + ]); + $this->stdout(" > Migrated content for element #{$contentEntry['elementId']}\n"); } else { - echo " > Unable to convert content for element #{$contentEntry['elementId']}\n"; + $this->stdout(" > Unable to convert content for element #{$contentEntry['elementId']}\n", Console::FG_RED); } } } } - echo "> Field '{$field['handle']}' content migrated.\n\n"; + $this->stdout(" > Field '{$field['handle']}' content migrated.\n", Console::FG_GREEN); } return ExitCode::OK; @@ -87,7 +97,7 @@ private function findField($data, $fieldUid){ * @param string $fieldUid The field UID to update * @return string The converted JSON string */ - private function convertContent(string $contentJson, int $siteId, string $fieldUid): string|false + private function convertContent(string $contentJson, int $siteId, string $fieldUid): array|false { $content = json_decode($contentJson, true); @@ -99,7 +109,6 @@ private function convertContent(string $contentJson, int $siteId, string $fieldU // If it's already been converted, return if (isset($linkitData['type']) && str_starts_with($linkitData['type'], 'presseddigital') === false){ - echo ' > already converted this content' . PHP_EOL; return false; } @@ -141,7 +150,8 @@ private function convertContent(string $contentJson, int $siteId, string $fieldU // Update the content with the new format $content[$fieldUid] = $newLinkData; - return json_encode($content); +// return json_encode($content, JSON_UNESCAPED_SLASHES ); + return $content; } /** From 670c90888255645976fc6d163dbb86e9c64a2282 Mon Sep 17 00:00:00 2001 From: Derrick Grigg Date: Fri, 7 Nov 2025 15:07:30 -0500 Subject: [PATCH 5/7] cleaned up output message --- src/console/controllers/MigrateController.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/console/controllers/MigrateController.php b/src/console/controllers/MigrateController.php index f23ba48..a1f9ccb 100644 --- a/src/console/controllers/MigrateController.php +++ b/src/console/controllers/MigrateController.php @@ -51,11 +51,8 @@ public function actionIndex(): int } foreach($contentEntries as $contentEntry) { - $this->stdout("existing content\n"); - $this->stdout($contentEntry['content'] . PHP_EOL); $newContent = $this->convertContent($contentEntry['content'], $contentEntry['siteId'], $element['uid'] ); if ($newContent !== false){ -// $this->stdout($newContent . PHP_EOL); Db::update('{{%elements_sites}}', ['content' => $newContent], [ @@ -72,6 +69,8 @@ public function actionIndex(): int $this->stdout(" > Field '{$field['handle']}' content migrated.\n", Console::FG_GREEN); } + $this->stdout("\nLinkIt content conversion is complete.\n", Console::FG_GREEN); + return ExitCode::OK; } From 1413bfd8509c83039c31836fe6c27f40b0c22305 Mon Sep 17 00:00:00 2001 From: Derrick Grigg Date: Fri, 7 Nov 2025 15:14:45 -0500 Subject: [PATCH 6/7] removed unnecessary lines --- src/console/controllers/MigrateController.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/console/controllers/MigrateController.php b/src/console/controllers/MigrateController.php index a1f9ccb..8191eef 100644 --- a/src/console/controllers/MigrateController.php +++ b/src/console/controllers/MigrateController.php @@ -23,9 +23,7 @@ public function actionIndex(): int { $fields = (new Query()) ->from('{{%fields}}') -// ->where(['type' => LinkitField::class]) ->where(['type' => Link::class]) -// ->orWhere(['type' => LinkitField::class]) ->all(); foreach ($fields as $field) { From 37b619ac978797be6d0dfa1cf7af1914b7379a5c Mon Sep 17 00:00:00 2001 From: Derrick Grigg Date: Mon, 10 Nov 2025 10:38:13 -0500 Subject: [PATCH 7/7] fix: cleaned up code commenting --- src/console/controllers/ConvertController.php | 4 +--- src/console/controllers/MigrateController.php | 10 +++++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/console/controllers/ConvertController.php b/src/console/controllers/ConvertController.php index fbee45d..bd19301 100644 --- a/src/console/controllers/ConvertController.php +++ b/src/console/controllers/ConvertController.php @@ -14,11 +14,9 @@ class ConvertController extends Controller { -// public $defaultAction = 'convert'; - /** - * Convert all LinkIt field to native Craft Link fields + * Convert all LinkIt fields to native Craft Link fields * @return int */ public function actionIndex(): int diff --git a/src/console/controllers/MigrateController.php b/src/console/controllers/MigrateController.php index 8191eef..b050202 100644 --- a/src/console/controllers/MigrateController.php +++ b/src/console/controllers/MigrateController.php @@ -15,7 +15,7 @@ class MigrateController extends Controller { /** - * Convert all LinkIt field to native Craft Link fields + * Migrate all LinkIt field content to native Craft Link fields * @return int */ @@ -72,6 +72,14 @@ public function actionIndex(): int return ExitCode::OK; } + /** + * Find and return the matching field within the included $data + * + * @param $data The JSON data to find the field in + * @param $fieldUid the field UID to find + * @return false|mixed The found field or false if not found + */ + private function findField($data, $fieldUid){ $foundElement = false;