diff --git a/composer.json b/composer.json
index 2467c479..0469cf58 100644
--- a/composer.json
+++ b/composer.json
@@ -1,7 +1,7 @@
{
"name": "smartling/wordpress-connector",
"license": "GPL-2.0-or-later",
- "version": "5.7.0",
+ "version": "5.7.3",
"description": "",
"type": "wordpress-plugin",
"repositories": [
diff --git a/inc/Smartling/Helpers/WordpressContentTypeHelper.php b/inc/Smartling/Helpers/WordpressContentTypeHelper.php
index b1eaff96..5f2ee768 100644
--- a/inc/Smartling/Helpers/WordpressContentTypeHelper.php
+++ b/inc/Smartling/Helpers/WordpressContentTypeHelper.php
@@ -9,10 +9,6 @@
use Smartling\Exception\SmartlingDirectRunRuntimeException;
use Smartling\Submissions\SubmissionEntity;
-/**
- * Class WordpressContentTypeHelper
- * @package Smartling\Helpers
- */
class WordpressContentTypeHelper
{
/**
@@ -144,39 +140,51 @@ public static function getBaseTypeByContentType($contentType)
return $ctHandler->getBaseType();
}
- public static function getEditUrl(SubmissionEntity $submission)
+ public static function getTargetEditUrl(SubmissionEntity $submission)
{
- /**
- * @var ContentTypeAbstract $ctHandler
- */
- $ctHandler = static::getContentTypeManager()->getHandler($submission->getContentType());
+ return static::buildEditUrl($submission, $submission->getTargetBlogId(), $submission->getTargetId());
+ }
+
+ public static function getSourceEditUrl(SubmissionEntity $submission): string
+ {
+ return static::buildEditUrl($submission, $submission->getSourceBlogId(), $submission->getSourceId());
+ }
+
+ private static function buildEditUrl(SubmissionEntity $submission, int $blogId, int $contentId): string
+ {
+ try {
+ $ctHandler = static::getContentTypeManager()->getHandler($submission->getContentType());
+ } catch (\Exception $e) {
+ Bootstrap::getLogger()->warning(sprintf('%s (submissionId=%d)', $e->getMessage(), $submission->getId()));
+
+ return '';
+ }
if ($ctHandler instanceof ContentTypeAbstract) {
- $tail = '';
switch ($ctHandler->getBaseType()) {
case 'post':
- $tail = vsprintf('/post.php?post=%s&action=edit', [$submission->getTargetId()]);
+ $tail = sprintf('/post.php?post=%s&action=edit', $contentId);
break;
case 'taxonomy':
- $tail = sprintf('/term.php?taxonomy=%s&tag_ID=%s', $submission->getContentType(), $submission->getTargetId());
+ $tail = sprintf('/term.php?taxonomy=%s&tag_ID=%s', $submission->getContentType(), $contentId);
break;
default:
return '';
}
- return get_admin_url($submission->getTargetBlogId(), $tail);
- } else {
- Bootstrap::getLogger()->warning(
- vsprintf(
- 'Requested edit URI for unknown content-type \'%s\'',
- [
- $submission->getContentType(),
- ]
- )
- );
-
- return '';
+ return get_admin_url($blogId, $tail);
}
+ Bootstrap::getLogger()->warning(
+ vsprintf(
+ 'Requested edit URI for unknown content-type \'%s\'',
+ [
+ $submission->getContentType(),
+ ]
+ )
+ );
+
+ return '';
+
}
-}
\ No newline at end of file
+}
diff --git a/inc/Smartling/WP/Controller/ConfigurationProfileFormController.php b/inc/Smartling/WP/Controller/ConfigurationProfileFormController.php
index 81835595..511755a7 100644
--- a/inc/Smartling/WP/Controller/ConfigurationProfileFormController.php
+++ b/inc/Smartling/WP/Controller/ConfigurationProfileFormController.php
@@ -230,18 +230,23 @@ public function save(): void
$profile->setSourceLocale($locale);
}
+ $sourceBlogId = $profile->getSourceLocale()->getBlogId();
+
$usedTargetLocales = [];
if (array_key_exists('targetLocales', $settings)) {
$locales = [];
- foreach ($settings['targetLocales'] as $blogId => $settings) {
+ foreach ($settings['targetLocales'] as $blogId => $targetLocaleSettings) {
+ if ((int)$blogId === $sourceBlogId) {
+ continue;
+ }
try {
$tLocale = new TargetLocale();
$tLocale->setBlogId($blogId);
$tLocale->setLabel($this->siteHelper->getBlogLabelById($this->localizationPluginProxy, $blogId));
- $enabled = 'on' === $settings['enabled'];
- $tLocale->setEnabled(array_key_exists('enabled', $settings) && $enabled);
- $smartlingLocale = array_key_exists('target', $settings) ? $settings['target'] : -1;
+ $enabled = 'on' === $targetLocaleSettings['enabled'];
+ $tLocale->setEnabled(array_key_exists('enabled', $targetLocaleSettings) && $enabled);
+ $smartlingLocale = array_key_exists('target', $targetLocaleSettings) ? $targetLocaleSettings['target'] : -1;
$tLocale->setSmartlingLocale($smartlingLocale);
if ($smartlingLocale !== -1 && $enabled) {
$usedTargetLocales[] = $smartlingLocale;
@@ -269,7 +274,8 @@ protected function renderLocales(
string $displayName,
int $blogId,
string $smartlingName,
- bool $enabled,
+ bool $checked,
+ bool $disabled,
): string {
$parts = [];
@@ -279,24 +285,34 @@ protected function renderLocales(
'name' => sprintf('smartling_settings[targetLocales][%s][enabled]', $blogId),
];
- if (true === $enabled) {
+ if (true === $checked) {
$checkboxProperties['checked'] = 'checked';
}
+ if (true === $disabled) {
+ $checkboxProperties['disabled'] = 'disabled';
+ }
+
$parts[] = HtmlTagGeneratorHelper::tag('input', '', $checkboxProperties);
$parts[] = HtmlTagGeneratorHelper::tag('span', htmlspecialchars($displayName));
$parts = [
HtmlTagGeneratorHelper::tag('label', implode('', $parts), ['class' => 'radio-label']),
];
+ $targetLocaleProperties = [
+ 'name' => sprintf('smartling_settings[targetLocales][%s][target]', $blogId),
+ ];
+
+ if (true === $disabled) {
+ $targetLocaleProperties['disabled'] = 'disabled';
+ }
+
if (0 === count($locales)) {
$sLocale = HtmlTagGeneratorHelper::tag(
'input',
'',
- [
- 'name' => sprintf('smartling_settings[targetLocales][%s][target]', $blogId),
- 'type' => 'text',
- ]);
+ $targetLocaleProperties + ['type' => 'text'],
+ );
} else {
$sLocale = HtmlTagGeneratorHelper::tag(
'select',
@@ -304,9 +320,8 @@ protected function renderLocales(
$smartlingName,
$locales
),
- [
- 'name' => sprintf('smartling_settings[targetLocales][%s][target]', $blogId),
- ]);
+ $targetLocaleProperties,
+ );
}
$parts = [
diff --git a/inc/Smartling/WP/Table/SubmissionTableWidget.php b/inc/Smartling/WP/Table/SubmissionTableWidget.php
index f9354669..a8175b51 100644
--- a/inc/Smartling/WP/Table/SubmissionTableWidget.php
+++ b/inc/Smartling/WP/Table/SubmissionTableWidget.php
@@ -135,6 +135,24 @@ public function column_cb($item): string
);
}
+ public static function buildSourceTitleCell(string $escapedTitle, string $sourceEditUrl): string
+ {
+ if ($sourceEditUrl === '') {
+ return $escapedTitle;
+ }
+
+ return HtmlTagGeneratorHelper::tag('a', $escapedTitle, ['href' => $sourceEditUrl]);
+ }
+
+ public static function buildTargetLocaleCell(string $blogLabel, string $targetEditUrl): string
+ {
+ if ($targetEditUrl === '') {
+ return $blogLabel;
+ }
+
+ return HtmlTagGeneratorHelper::tag('a', $blogLabel, ['href' => $targetEditUrl]);
+ }
+
public function get_columns(): array
{
$columns = $this->submissionManager->getColumnsLabels();
@@ -398,7 +416,10 @@ public function prepare_items(): void
$fileName = htmlentities($row[SubmissionEntity::FIELD_FILE_URI]);
$row[SubmissionEntity::FIELD_FILE_URI] = $fileName;
- $row[SubmissionEntity::FIELD_SOURCE_TITLE] = htmlentities($row[SubmissionEntity::FIELD_SOURCE_TITLE]);
+ $row[SubmissionEntity::FIELD_SOURCE_TITLE] = static::buildSourceTitleCell(
+ htmlentities($row[SubmissionEntity::FIELD_SOURCE_TITLE]),
+ 0 !== $element->getSourceId() ? WordpressContentTypeHelper::getSourceEditUrl($element) : ''
+ );
$row[SubmissionEntity::FIELD_CONTENT_TYPE] = WordpressContentTypeHelper::getLocalizedContentType($row[SubmissionEntity::FIELD_CONTENT_TYPE]);
$row[SubmissionEntity::FIELD_SUBMISSION_DATE] = $this->sqlToReadableDate($row[SubmissionEntity::FIELD_SUBMISSION_DATE]);
$row[SubmissionEntity::FIELD_APPLIED_DATE] = $this->sqlToReadableDate($row[SubmissionEntity::FIELD_APPLIED_DATE]);
@@ -407,7 +428,10 @@ public function prepare_items(): void
} catch (BlogNotFoundException $e) {
$blogLabel = "*blog id {$row[SubmissionEntity::FIELD_TARGET_BLOG_ID]} not found*";
}
- $row[SubmissionEntity::FIELD_TARGET_LOCALE] = $blogLabel;
+ $row[SubmissionEntity::FIELD_TARGET_LOCALE] = static::buildTargetLocaleCell(
+ htmlentities($blogLabel),
+ 0 !== $element->getTargetId() ? WordpressContentTypeHelper::getTargetEditUrl($element) : ''
+ );
$row[SubmissionEntity::VIRTUAL_FIELD_JOB_LINK] = $jobInfo->getJobName() === '' ? '' : "getProjectUid()}/account-jobs/?filename=$fileName\">" . esc_html($jobInfo->getJobName()) . '';
$flagBlockParts = [];
diff --git a/inc/Smartling/WP/View/ConfigurationProfileForm.php b/inc/Smartling/WP/View/ConfigurationProfileForm.php
index 542b4cee..ae1ac4e7 100644
--- a/inc/Smartling/WP/View/ConfigurationProfileForm.php
+++ b/inc/Smartling/WP/View/ConfigurationProfileForm.php
@@ -473,13 +473,8 @@
getTargetLocales();
$supportedLocales = $this->api->getSupportedLocales($profile);
+ $currentSourceBlogId = $profile->getSourceLocale()->getBlogId();
foreach ($locales as $blogId => $label) {
- if ($blogId === $profile->getSourceLocale()
- ->getBlogId()
- ) {
- continue;
- }
-
$smartlingLocale = '';
$enabled = false;
@@ -490,10 +485,12 @@
break;
}
}
+
+ $isSourceLocaleRow = $blogId === $currentSourceBlogId;
?>
-
- = $this->renderLocales($supportedLocales, $label, $blogId, $smartlingLocale, $enabled) ?>
+
+ = $this->renderLocales($supportedLocales, $label, $blogId, $smartlingLocale, $enabled, $isSourceLocaleRow) ?>
getIsCloned() || 1 === $item->hasLocks());
if (0 !== (int) $item->getTargetId()) {
- $editUrl = WordpressContentTypeHelper::getEditUrl($item);
+ $editUrl = WordpressContentTypeHelper::getTargetEditUrl($item);
}
/**
diff --git a/inc/Smartling/WP/View/taxonomy-based-content-type.php b/inc/Smartling/WP/View/taxonomy-based-content-type.php
index 081800c5..91eead4b 100644
--- a/inc/Smartling/WP/View/taxonomy-based-content-type.php
+++ b/inc/Smartling/WP/View/taxonomy-based-content-type.php
@@ -97,7 +97,9 @@
$percent = $item->getCompletionPercentage();
$status = $item->getStatusColor();
$statusFlags = $item->getStatusFlags();
- $editUrl = WordpressContentTypeHelper::getEditUrl($item);
+ if (0 !== $item->getTargetId()) {
+ $editUrl = WordpressContentTypeHelper::getTargetEditUrl($item);
+ }
$enabled = !(1 === $item->getIsCloned() || 1 === $item->getIsLocked());
break;
}
diff --git a/js/configuration-profile-form.js b/js/configuration-profile-form.js
index ef049c8f..6305cd5e 100644
--- a/js/configuration-profile-form.js
+++ b/js/configuration-profile-form.js
@@ -11,6 +11,22 @@
});
$('#smartling-configuration-profile-form').validate()
}
+
+ const syncTargetLocaleRows = function (sourceLocaleSelect) {
+ const sourceBlogId = String(sourceLocaleSelect.value);
+ $('#target-locale-block tr.target-locale-row').each(function () {
+ const row = $(this);
+ const isSource = String(row.data('blog-id')) === sourceBlogId;
+ row.toggleClass('hidden', isSource);
+ row.find('input, select').prop('disabled', isSource);
+ });
+ };
+
+ $('#default-locales, #default-locales-new').each(function () {
+ syncTargetLocaleRows(this);
+ }).on('change', function () {
+ syncTargetLocaleRows(this);
+ });
$('a.toggleExpert').on('click', function () {
$('.toggleExpert').removeClass('hidden');
$('a.toggleExpert').addClass('hidden');
diff --git a/readme.txt b/readme.txt
index 87132a16..7975f286 100755
--- a/readme.txt
+++ b/readme.txt
@@ -4,7 +4,7 @@ Tags: translation, localization, multilingual, internationalization, smartling
Requires at least: 5.5
Tested up to: 7.0
Requires PHP: 8.0
-Stable tag: 5.7.0
+Stable tag: 5.7.3
License: GPLv2 or later
Translate content in WordPress quickly and seamlessly with Smartling, the industry-leading Translation Management System.
@@ -62,6 +62,10 @@ Additional information on the Smartling Connector for WordPress can be found [he
3. Track translation status within WordPress from the Submissions Board. View overall progress of submitted translation requests as well as resend updated content.
== Changelog ==
+= 5.7.3 =
+* Fixed possible misconfiguration where target locales were being saved as a duplicate of a newly changed source locale
+* Added links to the source and target content (if available) on the Translation Progress screen
+
= 5.7.0 =
* Reworked upload queue, added a live-refreshing upload queue count with visual feedback on change
diff --git a/smartling-connector.php b/smartling-connector.php
index bddaadd5..81d70c2f 100755
--- a/smartling-connector.php
+++ b/smartling-connector.php
@@ -11,7 +11,7 @@
* Plugin Name: Smartling Connector
* Plugin URI: https://www.smartling.com/products/automate/integrations/wordpress/
* Description: Integrate your WordPress site with Smartling to upload your content and download translations.
- * Version: 5.7.0
+ * Version: 5.7.3
* Author: Smartling
* Author URI: https://www.smartling.com
* License: GPL-2.0+
diff --git a/tests/Smartling/Helpers/WordpressContentTypeHelperTest.php b/tests/Smartling/Helpers/WordpressContentTypeHelperTest.php
new file mode 100644
index 00000000..8ae74483
--- /dev/null
+++ b/tests/Smartling/Helpers/WordpressContentTypeHelperTest.php
@@ -0,0 +1,186 @@
+originalManager = Bootstrap::getContainer()->get('content-type-descriptor-manager');
+ }
+
+ protected function tearDown(): void
+ {
+ Bootstrap::getContainer()->set('content-type-descriptor-manager', $this->originalManager);
+ parent::tearDown();
+ }
+
+ private function registerContentTypeHandler(?string $baseType): void
+ {
+ $manager = $this->createMock(ContentTypeManager::class);
+
+ if ($baseType === null) {
+ // A handler that implements the interface but not ContentTypeAbstract,
+ // matching the "unknown/unregistered content type" branch in production code.
+ $manager->method('getHandler')->willReturn($this->createMock(ContentTypeInterface::class));
+ } else {
+ $handler = $this->createMock(ContentTypeAbstract::class);
+ $handler->method('getBaseType')->willReturn($baseType);
+ $manager->method('getHandler')->willReturn($handler);
+ }
+
+ Bootstrap::getContainer()->set('content-type-descriptor-manager', $manager);
+ }
+
+ /**
+ * ContentTypeManager::getHandler() throws (rather than returning some sentinel
+ * value) when the requested content type was never registered as a descriptor -
+ * e.g. a historical submission row whose custom-post-type integration has since
+ * been removed/deactivated. buildEditUrl() must not let that exception escape.
+ */
+ private function registerThrowingContentTypeManager(): void
+ {
+ $manager = $this->createMock(ContentTypeManager::class);
+ $manager->method('getHandler')->willThrowException(
+ new SmartlingInvalidFactoryArgumentException("Requested descriptor for 'sovos_product' that doesn't exists.")
+ );
+
+ Bootstrap::getContainer()->set('content-type-descriptor-manager', $manager);
+ }
+
+ public function testGetSourceEditUrlReturnsEmptyStringWhenContentTypeManagerThrows(): void
+ {
+ $this->registerThrowingContentTypeManager();
+
+ $submission = (new SubmissionEntity())
+ ->setContentType('sovos_product')
+ ->setSourceBlogId(1)
+ ->setSourceId(1);
+
+ $this->assertSame('', WordpressContentTypeHelper::getSourceEditUrl($submission));
+ }
+
+ public function testGetEditUrlReturnsEmptyStringWhenContentTypeManagerThrows(): void
+ {
+ $this->registerThrowingContentTypeManager();
+
+ $submission = (new SubmissionEntity())
+ ->setContentType('sovos_product')
+ ->setTargetBlogId(1)
+ ->setTargetId(1);
+
+ $this->assertSame('', WordpressContentTypeHelper::getTargetEditUrl($submission));
+ }
+
+ public function testGetSourceEditUrlBuildsPostEditLink(): void
+ {
+ $this->registerContentTypeHandler('post');
+
+ $submission = (new SubmissionEntity())
+ ->setContentType('post')
+ ->setSourceBlogId(5)
+ ->setSourceId(42);
+
+ $this->assertSame(
+ 'https://blog-5.test/post.php?post=42&action=edit',
+ WordpressContentTypeHelper::getSourceEditUrl($submission)
+ );
+ }
+
+ public function testGetSourceEditUrlBuildsAttachmentEditLinkUsingPostPhp(): void
+ {
+ // Attachments share the 'post' base type, so they must resolve through the
+ // same post.php edit screen as regular posts, not upload.php.
+ $this->registerContentTypeHandler('post');
+
+ $submission = (new SubmissionEntity())
+ ->setContentType('attachment')
+ ->setSourceBlogId(5)
+ ->setSourceId(99);
+
+ $this->assertSame(
+ 'https://blog-5.test/post.php?post=99&action=edit',
+ WordpressContentTypeHelper::getSourceEditUrl($submission)
+ );
+ }
+
+ public function testGetSourceEditUrlBuildsTaxonomyEditLink(): void
+ {
+ $this->registerContentTypeHandler('taxonomy');
+
+ $submission = (new SubmissionEntity())
+ ->setContentType('category')
+ ->setSourceBlogId(7)
+ ->setSourceId(13);
+
+ $this->assertSame(
+ 'https://blog-7.test/term.php?taxonomy=category&tag_ID=13',
+ WordpressContentTypeHelper::getSourceEditUrl($submission)
+ );
+ }
+
+ public function testGetSourceEditUrlReturnsEmptyStringForUnsupportedBaseType(): void
+ {
+ $this->registerContentTypeHandler('virtual');
+
+ $submission = (new SubmissionEntity())
+ ->setContentType('menu')
+ ->setSourceBlogId(1)
+ ->setSourceId(1);
+
+ $this->assertSame('', WordpressContentTypeHelper::getSourceEditUrl($submission));
+ }
+
+ public function testGetSourceEditUrlReturnsEmptyStringForUnknownContentType(): void
+ {
+ $this->registerContentTypeHandler(null);
+
+ $submission = (new SubmissionEntity())
+ ->setContentType('does-not-exist')
+ ->setSourceBlogId(1)
+ ->setSourceId(1);
+
+ $this->assertSame('', WordpressContentTypeHelper::getSourceEditUrl($submission));
+ }
+
+ public function testGetEditUrlStillBuildsTargetEditLink(): void
+ {
+ // Regression check: refactoring getEditUrl() to share code with
+ // getSourceEditUrl() must not change its existing target-link behavior.
+ $this->registerContentTypeHandler('post');
+
+ $submission = (new SubmissionEntity())
+ ->setContentType('post')
+ ->setTargetBlogId(9)
+ ->setTargetId(101);
+
+ $this->assertSame(
+ 'https://blog-9.test/post.php?post=101&action=edit',
+ WordpressContentTypeHelper::getTargetEditUrl($submission)
+ );
+ }
+ }
+}
diff --git a/tests/Smartling/WP/Controller/ConfigurationProfileFormControllerTest.php b/tests/Smartling/WP/Controller/ConfigurationProfileFormControllerTest.php
new file mode 100644
index 00000000..35040051
--- /dev/null
+++ b/tests/Smartling/WP/Controller/ConfigurationProfileFormControllerTest.php
@@ -0,0 +1,165 @@
+storedRequest = $_REQUEST;
+ }
+
+ protected function tearDown(): void
+ {
+ parent::tearDown();
+ $_REQUEST = $this->storedRequest;
+ }
+
+ private function createController(SettingsManager $settingsManager, SiteHelper $siteHelper): ConfigurationProfileFormController
+ {
+ return new ConfigurationProfileFormController(
+ $this->createMock(ApiWrapperInterface::class),
+ $this->createMock(LocalizationPluginProxyInterface::class),
+ $this->createMock(PluginInfo::class),
+ $settingsManager,
+ $siteHelper,
+ $this->createMock(SubmissionManager::class),
+ $this->createMock(Cache::class),
+ );
+ }
+
+ public function testSaveRemovesTargetLocaleMatchingNewSourceLocale(): void
+ {
+ $profile = new ConfigurationProfileEntity();
+ $profile->setId(1);
+
+ $siteHelper = $this->createMock(SiteHelper::class);
+ $siteHelper->method('getBlogLabelById')->willReturnCallback(
+ static fn(LocalizationPluginProxyInterface $proxy, int $blogId) => 'blog-' . $blogId
+ );
+
+ $settingsManager = $this->createMock(SettingsManager::class);
+ $settingsManager->method('getEntityById')->with(1)->willReturn([$profile]);
+ $settingsManager->expects($this->once())
+ ->method('storeEntity')
+ ->with($this->callback(function (ConfigurationProfileEntity $savedProfile) {
+ $targetBlogIds = array_map(static fn($locale) => $locale->getBlogId(), $savedProfile->getTargetLocales());
+ // Target locale for blogId 3 (== new source locale) must be dropped,
+ // while the unrelated target locale for blogId 4 must survive.
+ return $savedProfile->getSourceLocale()->getBlogId() === 3
+ && !in_array(3, $targetBlogIds, true)
+ && in_array(4, $targetBlogIds, true)
+ && count($targetBlogIds) === 1;
+ }))
+ ->willReturnArgument(0);
+
+ $controller = $this->createController($settingsManager, $siteHelper);
+
+ $_REQUEST['smartling_settings'] = [
+ 'id' => 1,
+ 'defaultLocale' => '3',
+ 'targetLocales' => [
+ 3 => ['enabled' => 'on', 'target' => 'fr-FR'],
+ 4 => ['enabled' => 'on', 'target' => 'de-DE'],
+ ],
+ ];
+
+ $controller->save();
+ }
+
+ public function testSaveKeepsNonCollidingTargetLocalesAndStillDetectsDuplicateSmartlingLocales(): void
+ {
+ $profile = new ConfigurationProfileEntity();
+ $profile->setId(1);
+
+ $siteHelper = $this->createMock(SiteHelper::class);
+ $siteHelper->method('getBlogLabelById')->willReturnCallback(
+ static fn(LocalizationPluginProxyInterface $proxy, int $blogId) => 'blog-' . $blogId
+ );
+
+ $settingsManager = $this->createMock(SettingsManager::class);
+ $settingsManager->method('getEntityById')->with(1)->willReturn([$profile]);
+ // Duplicate Smartling locale codes among the remaining (non-source) targets
+ // must still block the save, same as before this change.
+ $settingsManager->expects($this->never())->method('storeEntity');
+
+ $controller = $this->createController($settingsManager, $siteHelper);
+
+ $_REQUEST['smartling_settings'] = [
+ 'id' => 1,
+ 'defaultLocale' => '3',
+ 'targetLocales' => [
+ 3 => ['enabled' => 'on', 'target' => 'fr-FR'],
+ 4 => ['enabled' => 'on', 'target' => 'de-DE'],
+ 5 => ['enabled' => 'on', 'target' => 'de-DE'],
+ ],
+ ];
+
+ $controller->save();
+ }
+
+ public function testRenderLocalesDisablesInputsForSourceLocaleRow(): void
+ {
+ $controller = $this->createController(
+ $this->createMock(SettingsManager::class),
+ $this->createMock(SiteHelper::class),
+ );
+
+ $method = new \ReflectionMethod(ConfigurationProfileFormController::class, 'renderLocales');
+ // PHP 8.0 (this project's target) still requires setAccessible() to invoke a
+ // protected method via reflection; it only became a no-op starting PHP 8.1.
+ $method->setAccessible(true);
+
+ $html = $method->invoke($controller, ['en-US' => 'English'], 'French', 3, 'fr-FR', true, true);
+
+ $this->assertStringContainsString('disabled="disabled"', $html);
+ }
+
+ public function testRenderLocalesDoesNotDisableInputsForRegularRow(): void
+ {
+ $controller = $this->createController(
+ $this->createMock(SettingsManager::class),
+ $this->createMock(SiteHelper::class),
+ );
+
+ $method = new \ReflectionMethod(ConfigurationProfileFormController::class, 'renderLocales');
+ $method->setAccessible(true);
+
+ $html = $method->invoke($controller, ['en-US' => 'English'], 'French', 3, 'fr-FR', true, false);
+
+ $this->assertStringNotContainsString('disabled="disabled"', $html);
+ }
+ }
+}
diff --git a/tests/Smartling/WP/Table/SubmissionTableWidgetTest.php b/tests/Smartling/WP/Table/SubmissionTableWidgetTest.php
index 67cc2ced..6503d2c7 100644
--- a/tests/Smartling/WP/Table/SubmissionTableWidgetTest.php
+++ b/tests/Smartling/WP/Table/SubmissionTableWidgetTest.php
@@ -113,6 +113,48 @@ public function testProcessBulkActionIgnoresNonArraySubmissionPayload(): void
$x->processBulkAction();
}
+ /**
+ * The Title column must link to the source content's WP edit screen when a source
+ * edit URL is available.
+ */
+ public function testBuildSourceTitleCellWrapsTitleInLinkWhenUrlAvailable(): void
+ {
+ $this->assertSame(
+ 'My Title',
+ SubmissionTableWidget::buildSourceTitleCell('My Title', 'https://example.com/wp-admin/post.php?post=1&action=edit')
+ );
+ }
+
+ /**
+ * With no source edit URL (e.g. unsupported content type), the Title column must
+ * fall back to plain text instead of rendering a dead/empty link.
+ */
+ public function testBuildSourceTitleCellReturnsPlainTextWhenUrlMissing(): void
+ {
+ $this->assertSame('My Title', SubmissionTableWidget::buildSourceTitleCell('My Title', ''));
+ }
+
+ /**
+ * The Locale column must link to the target content's WP edit screen when a target
+ * edit URL is available (e.g. translation already applied).
+ */
+ public function testBuildTargetLocaleCellWrapsLabelInLinkWhenUrlAvailable(): void
+ {
+ $this->assertSame(
+ 'German',
+ SubmissionTableWidget::buildTargetLocaleCell('German', 'https://de.example.com/wp-admin/post.php?post=2&action=edit')
+ );
+ }
+
+ /**
+ * With no target edit URL (e.g. translation not yet applied, target_id === 0), the
+ * Locale column must fall back to plain text instead of rendering a dead link.
+ */
+ public function testBuildTargetLocaleCellReturnsPlainTextWhenUrlMissing(): void
+ {
+ $this->assertSame('German', SubmissionTableWidget::buildTargetLocaleCell('German', ''));
+ }
+
private function buildWidget(
ApiWrapperInterface $apiWrapper,
SettingsManager $settingsManager,