Skip to content

✨ feat: add segment disable/enable API for translation cancel requests#4488

Merged
riccio82 merged 44 commits intodevelopfrom
cancel-request
Apr 28, 2026
Merged

✨ feat: add segment disable/enable API for translation cancel requests#4488
riccio82 merged 44 commits intodevelopfrom
cancel-request

Conversation

@mauretto78
Copy link
Copy Markdown
Contributor

@mauretto78 mauretto78 commented Apr 9, 2026

Summary

Adds a V3 API endpoint to disable (cancel) and re-enable segments within a job, preventing translations on cancelled segments. Includes backend validation, caching layer, rate limiting, and frontend UI integration.

Type

  • feat — new user-facing feature
  • fix — bug fix
  • refactor — restructure without behavior change
  • chore — build, deps, config, docs
  • perf — performance improvement
  • test — test coverage

Changes

File Change
lib/Controller/API/V3/CancelRequestController.php New controller with cancelRequest() and enableRequest() actions, rate limiting, ownership and status checks
lib/Controller/Traits/SegmentDisabledTrait.php New trait providing isSegmentDisabled, saveSegmentDisabledInCache, destroySegmentDisabledCache with Redis-backed caching
lib/Controller/API/App/SetTranslationController.php Block translation on disabled segments via checkIfSegmentIsNotDisabled()
lib/Controller/API/V3/SegmentAnalysisController.php Expose disabled flag in segment analysis response
lib/Model/Segments/SegmentMetadataDao.php Add get(), delete(), setTranslationDisabled(), destroyCache(), destroyGetAllCache() methods
lib/Model/DataAccess/Database.php Guard rollback() with inTransaction() check
lib/Routes/api_v3_routes.php Register disable/enable segment routes
lib/Controller/Traits/ChunkNotFoundHandlerTrait.php Add type declarations to getJob() params
lib/Controller/Traits/RateLimiterTrait.php Cast TTL to int
lib/Model/ProjectCreation/SegmentStorageService.php Fix id_segment type (remove string cast)
public/js/actions/SegmentActions.js Add disable segment action
public/js/actions/CatToolActions.js Add cancel request UI action
public/js/components/segments/Segment.js Render disabled state in editor
public/js/utils/segmentUtils.js Add segment disabled utility check
public/api/swagger-source.js Document new endpoints in Swagger

Testing

  • vendor/bin/phpunit --exclude-group=ExternalServices --no-coverage passes
  • ./vendor/bin/phpstan passes (0 errors, with baseline)
  • Manual testing performed (describe below)
  • New tests added for changed behavior
  • Regression tests added for bug fixes

Endpoint tested manually:

  • POST /api/v3/jobs/:id_job/:password/segment/disable/:id_segment — disables segment, returns { "id_segment": N }
  • POST /api/v3/jobs/:id_job/:password/segment/enable/:id_segment — re-enables segment
  • Verified that SetTranslationController rejects translations on disabled segments
  • Verified rate limiting blocks excessive requests

AI Disclosure

  • No AI tools were used in this PR
  • AI tools were used — details below

GitHub Copilot (code suggestions applied via PR review)

Notes

  • Segments can only be disabled when their translation status is NEW
  • Only the team owner or team members can disable segments
  • Disabled state is cached in Redis (TTL 3600s) for fast lookup during translation
  • The Database::rollback() fix prevents errors when no transaction is active (defensive guard)

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an API v3 endpoint intended to “disable” a segment (cancel request) and propagates a disabled flag into segment-analysis responses, while introducing cache helpers around segment metadata and a shared “segment disabled” trait.

Changes:

  • Added /api/v3/jobs/:id_job/:password/segment/disable/:id_segment route and new CancelRequestController to mark a segment as disabled.
  • Introduced SegmentDisabledTrait and wired it into segment analysis responses and translation submission flow.
  • Extended SegmentMetadataDao with cache-destruction helpers and a method to set translation_disabled metadata.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
lib/Routes/api_v3_routes.php Registers the new segment disable endpoint under API v3 jobs routes.
lib/Model/Segments/SegmentMetadataDao.php Adds SQL constants, cache-destruction helpers, and a translation_disabled write helper.
lib/Controller/Traits/SegmentDisabledTrait.php New trait providing cached “segment disabled” lookup and write-through cache helper.
lib/Controller/API/V3/SegmentAnalysisController.php Adds disabled field to segment-analysis output using the new trait.
lib/Controller/API/V3/CancelRequestController.php New controller implementing the disable/cancel-request behavior with rate limiting and checks.
lib/Controller/API/App/SetTranslationController.php Blocks translation submission when a segment is marked disabled.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/Model/Segments/SegmentMetadataDao.php Outdated
Comment thread lib/Model/Segments/SegmentMetadataDao.php Outdated
Comment thread lib/Controller/API/V3/CancelRequestController.php Outdated
Comment thread lib/Controller/API/V3/CancelRequestController.php Outdated
Comment thread lib/Routes/api_v3_routes.php Outdated
Comment thread lib/Controller/API/App/SetTranslationController.php Outdated
Comment thread lib/Controller/API/App/SetTranslationController.php Outdated
Comment thread lib/Controller/Traits/SegmentDisabledTrait.php
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated 5 comments.

// setting the cache to avoid multiple disable requests for the same segment in a short time frame
if (!$this->isSegmentDisabled($id_job, $id_segment)) {
SegmentMetadataDao::destroyGetAllCache($id_segment);
SegmentMetadataDao::setTranslationDisabled($id_segment);
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When disabling a segment, you update DB via setTranslationDisabled() but only destroy the getAll() cache. If SegmentMetadataDao::get($id_segment, 'translation_disabled') was previously cached as empty (TTL=1 week), isSegmentDisabled()'s DB fallback can keep returning "enabled" after its 1h Redis TTL expires. Invalidate the key-specific DAO cache too (e.g. SegmentMetadataDao::destroyCache($id_segment, 'translation_disabled')) when writing the flag.

Suggested change
SegmentMetadataDao::setTranslationDisabled($id_segment);
SegmentMetadataDao::setTranslationDisabled($id_segment);
SegmentMetadataDao::destroyCache($id_segment, 'translation_disabled');

Copilot uses AI. Check for mistakes.
Comment on lines +792 to +801
* Creates a controller with real performChecks but mocked external dependencies.
*/
private function createControllerWithPartialMock(
mixed $jobReturn = 'NOT_SET',
mixed $segmentReturn = 'NOT_SET',
?UserStruct $user = null,
?Response $rateLimitResponseIp = null,
?Response $rateLimitResponseEmail = null,
): CancelRequestController {
$controller = $this->getMockBuilder(CancelRequestController::class)
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createControllerWithPartialMock() accepts $segmentReturn and multiple tests pass a SegmentTranslationStruct, but CancelRequestController::performChecks() reads the segment via the static SegmentTranslationDao::findBySegmentAndJob() and this helper never stubs that call. Those tests will be nondeterministic (and likely fail) unless the DB happens to contain the expected translation row. Consider wrapping the DAO call in an overridable/protected method (so it can be mocked) or set up explicit DB fixtures in the test.

Copilot uses AI. Check for mistakes.
Comment thread lib/Controller/Traits/SegmentDisabledTrait.php Outdated
Comment thread lib/Controller/API/V3/CancelRequestController.php Outdated
Comment thread lib/Controller/API/V3/CancelRequestController.php Outdated
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 28, 2026 14:02
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated 9 comments.

Comment thread lib/Controller/API/V3/CancelRequestController.php Outdated
Comment thread public/js/components/segments/Segment.js Outdated
Comment thread tests/unit/Controllers/CancelRequestControllerTest.php
Comment thread lib/Controller/Traits/SegmentDisabledTrait.php
Comment thread lib/Controller/Traits/SegmentDisabledTrait.php
Comment thread lib/Controller/API/V3/CancelRequestController.php Outdated
Comment thread lib/Controller/API/V3/CancelRequestController.php
Comment thread lib/Model/Segments/SegmentMetadataDao.php
Comment thread tests/unit/Controllers/CancelRequestControllerTest.php Outdated
Copilot AI and others added 2 commits April 28, 2026 14:16
…sts, reuse readonly in Segment.js

Agent-Logs-Url: https://github.com/matecat/MateCat/sessions/3e5a31c8-de03-4d5e-bbf8-cb5f5f35701c

Co-authored-by: mauretto78 <10035321+mauretto78@users.noreply.github.com>
## Summary
Add early return guard after performChecks in cancelRequest to prevent
further execution when the rate limit (429) has been triggered, matching
the same pattern already used in enableRequest.
## Type
- [x] `fix` — bug fix
## Changes
| File | Change |
|------|--------|
| lib/Controller/API/V3/CancelRequestController.php | Add 429 early return in cancelRequest after performChecks |
## Testing
- [x] New tests added for changed behavior
## Summary
Fix all three test suites to pass in isolation without requiring
a database connection. Introduce FakeRedisClient for Predis mock
(PHPUnit 12 cannot mock __call magic methods), add findSegmentTranslation
protected wrapper in CancelRequestController for testability.
## Type
- [x] `test` — test coverage
- [x] `refactor` — restructure without behavior change
## Changes
| File | Change |
|------|--------|
| lib/Controller/API/V3/CancelRequestController.php | Extract findSegmentTranslation() protected wrapper for static DAO call |
| tests/unit/Controllers/CancelRequestControllerTest.php | Fix all tests: use createStub, mock findSegmentTranslation, handle rate limit response code, remove non-existent 'not owner' test |
| tests/unit/Traits/RateLimiterTraitTest.php | Replace Predis\Client mock with FakeRedisClient (in-memory fake) |
| tests/unit/Traits/SegmentDisabledTraitTest.php | Use FakeRedisClient, skip destroySegmentDisabledCache (requires DB) |
## Testing
- [x] New tests added for changed behavior
- [x] All 68 tests pass (1 skipped due to DB dependency)
Copilot AI review requested due to automatic review settings April 28, 2026 14:39
@github-actions
Copy link
Copy Markdown

🧪 Test-Guard Report

⚠️ WARNING — Test coverage has minor gaps — review recommended.

Coverage Analysis: ❌ FAIL

No changed source files found in coverage report (threshold: 80%)

File Verdict Reason
lib/Controller/API/App/SetTranslationController.php ❌ fail not in coverage report
lib/Controller/API/V3/CancelRequestController.php ❌ fail not in coverage report
lib/Controller/API/V3/SegmentAnalysisController.php ❌ fail not in coverage report
lib/Controller/Traits/ChunkNotFoundHandlerTrait.php ❌ fail not in coverage report
lib/Controller/Traits/RateLimiterTrait.php ❌ fail not in coverage report
lib/Controller/Traits/SegmentDisabledTrait.php ❌ fail not in coverage report
lib/Model/DataAccess/Database.php ❌ fail not in coverage report
lib/Model/ProjectCreation/SegmentStorageService.php ❌ fail not in coverage report
lib/Model/Segments/SegmentMetadataDao.php ❌ fail not in coverage report
lib/Model/Segments/SegmentMetadataStruct.php ❌ fail not in coverage report
lib/Routes/api_v3_routes.php ❌ fail not in coverage report
public/api/swagger-source.js ❌ fail not in coverage report
public/js/actions/CatToolActions.js ❌ fail not in coverage report
public/js/actions/SegmentActions.js ❌ fail not in coverage report
public/js/components/segments/Segment.js ❌ fail not in coverage report
public/js/utils/segmentUtils.js ❌ fail not in coverage report

Test File Matching: ❌ FAIL

File matching: 7 pass, 1 warning, 8 fail

File Verdict Reason
lib/Controller/API/App/SetTranslationController.php ❌ fail No matching test file found
lib/Controller/API/V3/CancelRequestController.php ✅ pass Test file modified in PR: tests/unit/Controllers/CancelRequestControllerTest.php
lib/Controller/API/V3/SegmentAnalysisController.php ❌ fail No matching test file found
lib/Controller/Traits/ChunkNotFoundHandlerTrait.php ❌ fail No matching test file found
lib/Controller/Traits/RateLimiterTrait.php ✅ pass Test file modified in PR: tests/unit/Traits/RateLimiterTraitTest.php
lib/Controller/Traits/SegmentDisabledTrait.php ✅ pass Test file modified in PR: tests/unit/Traits/SegmentDisabledTraitTest.php
lib/Model/DataAccess/Database.php ❌ fail No matching test file found
lib/Model/ProjectCreation/SegmentStorageService.php ⚠️ warning Test file exists (tests/unit/Model/ProjectCreation/SegmentStorageServiceTest.php) but was not modified in this PR
lib/Model/Segments/SegmentMetadataDao.php ❌ fail No matching test file found
lib/Model/Segments/SegmentMetadataStruct.php ❌ fail No matching test file found
lib/Routes/api_v3_routes.php ❌ fail No matching test file found
public/api/swagger-source.js ❌ fail No matching test file found
public/js/actions/CatToolActions.js ✅ pass Test file modified in PR: public/js/actions/CatToolActions.test.js
public/js/actions/SegmentActions.js ✅ pass Test file modified in PR: public/js/actions/SegmentActions.test.js
public/js/components/segments/Segment.js ✅ pass Test file modified in PR: public/js/components/segments/Segment.test.js
public/js/utils/segmentUtils.js ✅ pass Test file modified in PR: public/js/utils/segmentUtils.test.js

Per-File Evaluation: ⚠️ WARNING

AI analysis failed (Error code: 413 - {'error': {'code': 'tokens_limit_reached', 'message': 'Request body too large for gpt-4.1-nano model. Max size: 8000 tokens.', 'details': 'Request body too large for gpt-4.1-nano model. Max size: 8000 tokens.'}}) — shortcuts resolved; remaining files deferred to L1+L2.

File Verdict Reason
lib/Controller/API/App/SetTranslationController.php ✅ pass New segment disabled check and exception throwing tested indirectly via error code handling in JS tests.
lib/Controller/API/V3/CancelRequestController.php ✅ pass Unit tests cover enableRequest and cancelRequest including validation and cache logic.
lib/Controller/API/V3/SegmentAnalysisController.php ✅ pass Tests verify segment disabled flag inclusion in formatted segment output.
lib/Controller/Traits/ChunkNotFoundHandlerTrait.php ✅ pass Only minor type hint changes, no behavioral changes needing tests.
lib/Controller/Traits/RateLimiterTrait.php ✅ pass Tests cover TTL calculation and Redis interactions for rate limiting.
lib/Controller/Traits/SegmentDisabledTrait.php ✅ pass Unit tests cover cache logic, database fallback, and cache clearing behavior.
lib/Model/DataAccess/Database.php ⚠️ warning No tests added or modified; change is a safe transaction rollback guard.
lib/Model/ProjectCreation/SegmentStorageService.php ✅ pass Change is a minor type fix with no new behavior; no tests needed.
lib/Model/Segments/SegmentMetadataDao.php ⏭️ skip AI analysis unavailable — deferred to Layer 2
lib/Model/Segments/SegmentMetadataStruct.php ⏭️ skip AI analysis unavailable — deferred to Layer 2
lib/Routes/api_v3_routes.php ⏭️ skip AI analysis unavailable — deferred to Layer 2
public/api/swagger-source.js ⏭️ skip AI analysis unavailable — deferred to Layer 2
public/js/actions/CatToolActions.js ⏭️ skip AI analysis unavailable — deferred to Layer 2
public/js/actions/SegmentActions.js ⏭️ skip AI analysis unavailable — deferred to Layer 2
public/js/components/segments/Segment.js ⏭️ skip AI analysis unavailable — deferred to Layer 2
public/js/utils/segmentUtils.js ⏭️ skip AI analysis unavailable — deferred to Layer 2

Result: ⚠️ WARNING

@riccio82 riccio82 merged commit 5025581 into develop Apr 28, 2026
16 of 19 checks passed
@riccio82 riccio82 deleted the cancel-request branch April 28, 2026 14:47
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated 8 comments.

Comment on lines +150 to +158
public static function setTranslationDisabled(int $id_segment): void
{
$metadata = new SegmentMetadataStruct();
$metadata->id_segment = $id_segment;
$metadata->meta_key = 'translation_disabled';
$metadata->meta_value = "1";

SegmentMetadataDao::save($metadata);
}
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setTranslationDisabled() blindly inserts a new row each time. Since segment_metadata has no UNIQUE constraint on (id_segment, meta_key), concurrent disable requests can create duplicate translation_disabled rows. Consider deleting any existing row first or using an upsert strategy (and/or adding a unique index) to make this idempotent at the DB level.

Copilot uses AI. Check for mistakes.
Comment on lines 351 to +373
@@ -366,6 +370,7 @@ private function formatSegment(
'notes' => (!empty($notesAggregate[$segmentForAnalysis->id]) ? $notesAggregate[$segmentForAnalysis->id] : []),
'status' => $this->getStatusObject($segmentForAnalysis),
'last_edit' => ($segmentForAnalysis->last_edit !== null) ? date(DATE_ATOM, strtotime($segmentForAnalysis->last_edit)) : null,
'disabled' => $disabled,
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formatSegment() calls isSegmentDisabled() per segment. On a cold cache this triggers one DB query per segment (up to 200/page) because isSegmentDisabled() falls back to SegmentMetadataDao::get(). Consider fetching all translation_disabled metadata in bulk (e.g. via SegmentMetadataDao::getBySegmentIds($segmentIds, 'translation_disabled') in getIssuesNotesAndIdRequests()) and deriving the disabled flag from that, avoiding an N+1 pattern.

Copilot uses AI. Check for mistakes.
Comment on lines +694 to +702
componentDidUpdate(prevProps) {
if (!isEqual(prevProps.segment, this.props.segment)) {
const readonly = SegmentUtils.isReadonlySegment(this.props.segment)
if (readonly !== this.state.readonly) {
this.setState({
readonly,
})
}
}
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

componentDidUpdate uses lodash.isEqual to deep-compare segment props on every update, even though shouldComponentUpdate already compares segImmutable. Deep equality here can be expensive across many Segment instances; consider using prevProps.segImmutable.equals(this.props.segImmutable) (or a cheap key/field comparison) as the change detector instead.

Copilot uses AI. Check for mistakes.
Comment on lines +711 to +724
const isTranslationDisabled = segment?.metadata?.some(
({meta_key, meta_value}) =>
meta_key === 'translation_disabled' && meta_value === '1',
)

if (isTranslationDisabled) {
ModalsActions.showModalComponent(
AlertModal,
{
text: "This segment has been disabled by the project owner, so it cannot be translated.",
},
'Segment disabled',
)
return
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The translation_disabled metadata check is reimplemented inline here (and also inside SegmentUtils.isReadonlySegment). To avoid logic drift, consider extracting a single helper in segmentUtils (e.g. isTranslationDisabled(segment)) and reusing it from both places.

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +6
require_once __DIR__ . '/RateLimiterTraitTest.php';

Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test file require_onces another test file to reuse FakeRedisClient. Test-to-test dependencies make the suite order-sensitive and harder to maintain. Consider moving FakeRedisClient into a shared test helper (e.g. tests/TestHelpers/FakeRedisClient.php) and autoloading it instead.

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +26
/**
* Removes the "translation_disabled" metadata for a given segment and clears the related cache.
*
* @param int $id_segment The unique identifier of the segment to clear metadata and cache for.
* @return void
* @throws ReflectionException
*/
protected function destroySegmentDisabledCache(int $id_job, int $id_segment): void
{
SegmentMetadataDao::delete($id_segment, 'translation_disabled');
SegmentMetadataDao::destroyCache($id_segment, 'translation_disabled');
SegmentMetadataDao::destroyGetAllCache($id_segment);
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docblock for destroySegmentDisabledCache() lists only $id_segment, but the method signature also takes $id_job. Please update the PHPDoc params to match the signature to avoid misleading IDE/static analysis output.

Copilot uses AI. Check for mistakes.
{

public ?string $id_segment = null;
public int $id_segment;
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SegmentMetadataStruct::$id_segment was changed to a non-nullable int. With PDO::FETCH_CLASS, MySQL/PDO typically hydrates numeric columns as strings, which will cause a TypeError when assigning to a typed int property. Consider keeping it as string|int (or ?string as before), or introducing a casting layer during hydration instead of a strict typed property here.

Suggested change
public int $id_segment;
public int|string $id_segment;

Copilot uses AI. Check for mistakes.
// If the cache is empty, it means that the segment is not already disabled, so we can proceed with disabling it and
// setting the cache to avoid multiple disable requests for the same segment in a short time frame
if (!$this->isSegmentDisabled($id_job, $id_segment)) {
SegmentMetadataDao::destroyGetAllCache($id_segment);
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cancelRequest() updates the DB by inserting translation_disabled, but it only clears the getAll() cache. If SegmentMetadataDao::get($id_segment, 'translation_disabled') was cached as empty before disabling, it can stay stale (TTL=1 week) and later make isSegmentDisabled() incorrectly return false after Redis eviction. Clear the per-key cache too (e.g. destroy the get() cache entry) when disabling.

Suggested change
SegmentMetadataDao::destroyGetAllCache($id_segment);
SegmentMetadataDao::destroyGetAllCache($id_segment);
SegmentMetadataDao::destroyGetCache($id_segment, 'translation_disabled');

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants