Skip to content

Commit f454256

Browse files
authored
fix: Resolve race condition in cache directory creation (#499)
* chore: Remove version declaration from docker-compose.yml - Removed the version declaration from the docker-compose.yml file to streamline the configuration. This change does not affect the functionality of the services defined in the file. * chore: Update .gitignore to include documentation and demo directories - Added .docs and .demo to the .gitignore file to prevent tracking of documentation and demo files in the repository. * fix: Improve cache directory creation in CleanTranslationCacheListener - Enhanced the directory creation logic to throw a RuntimeException if the cache directory cannot be created, ensuring better error handling and robustness in the CleanTranslationCacheListener class. * feat: Enhance translation grid initialization and URL filter handling - Refactored grid initialization to ensure it runs after the DOM is fully loaded, with a small delay for better reliability. - Implemented URL filter parsing from both query strings and hash fragments, allowing for dynamic filter application on the grid. - Added functionality to update the URL with current filter values without reloading the page, improving user experience. - Updated links in the overview template to maintain locale context when navigating to the translation grid.
1 parent 88e3cac commit f454256

5 files changed

Lines changed: 130 additions & 9 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@ composer.lock
44

55
Tests/app/tmp/
66
.phpunit.result.cache
7+
8+
.docs
9+
.demo

EventDispatcher/CleanTranslationCacheListener.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ private function isCacheExpired()
5959
$cache_file = strtr($cache_file, '/', '\\');
6060
$cache_dir = strtr($cache_dir, '/', '\\');
6161
}
62-
if (!\is_dir($cache_dir)) {
63-
\mkdir($cache_dir);
62+
if (!\is_dir($cache_dir) && !\mkdir($cache_dir, 0777, true) && !\is_dir($cache_dir)) {
63+
throw new \RuntimeException(sprintf('Directory "%s" was not created', $cache_dir));
6464
}
6565
if (!\file_exists($cache_file)) {
6666
\touch($cache_file);

Resources/public/js/translation.js

Lines changed: 123 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,15 @@ const TranslationManager = (() => {
1414

1515
const debounceTimeouts = {};
1616

17-
document.addEventListener('DOMContentLoaded', function () {
18-
reloadGrid();
17+
const initializeGrid = () => {
18+
// Small delay to ensure DOM is fully ready
19+
setTimeout(() => {
20+
// Read filter parameters from URL (query string or hash) and apply them
21+
parseUrlFilters().then((hasFilters) => {
22+
// Reload grid with filters applied
23+
reloadGrid();
24+
});
25+
}, 100);
1926

2027
document.querySelectorAll('.input-sm').forEach(input => {
2128
input.addEventListener('keyup', function() {
@@ -26,11 +33,22 @@ const TranslationManager = (() => {
2633
_currentPage = 1;
2734
_order = 'id';
2835
_direction = 'asc';
36+
// Update URL with current filter values
37+
updateUrlWithFilters();
38+
// Reload grid
2939
reloadGrid();
3040
}, 200);
3141
});
3242
});
33-
});
43+
};
44+
45+
// Check if DOM is already loaded
46+
if (document.readyState === 'loading') {
47+
document.addEventListener('DOMContentLoaded', initializeGrid);
48+
} else {
49+
// DOM is already loaded, execute immediately
50+
initializeGrid();
51+
}
3452
};
3553

3654
const sharedMessage = {
@@ -393,6 +411,108 @@ const TranslationManager = (() => {
393411

394412
const getMaxPageNumber = (total) => Math.ceil(total / translationCfg.maxPageNumber);
395413

414+
const parseUrlFilters = () =>
415+
{
416+
return new Promise((resolve) => {
417+
let hasFilters = false;
418+
const filterPromises = [];
419+
420+
// First, try to parse from query string (?filter[_domain]=validators)
421+
const queryString = window.location.search;
422+
let params = null;
423+
424+
if (queryString) {
425+
params = new URLSearchParams(queryString.substring(1)); // Remove '?'
426+
} else {
427+
// Fallback to hash fragment (#!?filter[_domain]=validators)
428+
const hash = window.location.hash;
429+
if (hash && hash.startsWith('#!?')) {
430+
params = new URLSearchParams(hash.substring(3)); // Remove '#!?'
431+
}
432+
}
433+
434+
if (!params) {
435+
resolve(false);
436+
return;
437+
}
438+
439+
// Process filter parameters
440+
params.forEach((value, key) => {
441+
if (key.startsWith('filter[') && key.endsWith(']')) {
442+
// Extract column name from filter[column]
443+
const column = key.substring(7, key.length - 1);
444+
// Map column names to input IDs (e.g., _domain -> __domain)
445+
// Note: locales keep their original name (en, es, etc.)
446+
let inputId = column;
447+
if (column === '_domain') {
448+
inputId = '__domain';
449+
}
450+
// _key and locales use their column name as input ID
451+
452+
// Wait for input to be available
453+
const filterPromise = new Promise((filterResolve) => {
454+
const trySetFilter = (attempts = 0) => {
455+
const input = document.getElementById(inputId);
456+
if (input) {
457+
input.value = decodeURIComponent(value);
458+
hasFilters = true;
459+
filterResolve();
460+
} else if (attempts < 10) {
461+
// Retry up to 10 times with 50ms delay
462+
setTimeout(() => trySetFilter(attempts + 1), 50);
463+
} else {
464+
// Give up after 10 attempts
465+
filterResolve();
466+
}
467+
};
468+
trySetFilter();
469+
});
470+
471+
filterPromises.push(filterPromise);
472+
}
473+
});
474+
475+
// Wait for all filters to be applied
476+
if (filterPromises.length > 0) {
477+
Promise.all(filterPromises).then(() => {
478+
resolve(hasFilters);
479+
});
480+
} else {
481+
resolve(false);
482+
}
483+
});
484+
};
485+
486+
const updateUrlWithFilters = () =>
487+
{
488+
const params = new URLSearchParams();
489+
let hasFilters = false;
490+
491+
document.querySelectorAll('.table input').forEach(input => {
492+
const column = input.getAttribute('id');
493+
const filterValue = input.value.trim();
494+
if (filterValue !== '') {
495+
// Map input IDs back to column names for URL
496+
// Note: locales (en, es, etc.) and _key keep their input ID as column name
497+
let urlColumn = column;
498+
if (column === '__domain') {
499+
urlColumn = '_domain';
500+
}
501+
// For other columns (locales, _key), use the input ID directly
502+
params.set(`filter[${urlColumn}]`, filterValue);
503+
hasFilters = true;
504+
}
505+
});
506+
507+
// Update URL without reloading the page
508+
if (window.history && window.history.replaceState) {
509+
const newUrl = hasFilters
510+
? window.location.pathname + '?' + params.toString()
511+
: window.location.pathname;
512+
window.history.replaceState(null, '', newUrl);
513+
}
514+
};
515+
396516
const addFilteredValuesToParams = (params) =>
397517
{
398518
let search = false;

Resources/views/Translation/overview.html.twig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
<h1>
1717
{{ 'overview.page_title'|trans }}
1818
<div class="pull-right">
19-
<a href="{{ path('lexik_translation_grid') }}" role="button" class="btn btn-primary">
19+
<a href="{{ path('lexik_translation_grid', {'_locale': app.request.attributes.get('_locale')|default(app.request.locale|default('en'))}) }}" role="button" class="btn btn-primary">
2020
<span class="glyphicon glyphicon-th"></span>
2121
{{ 'overview.show_grid'|trans }}
2222
</a>
@@ -49,7 +49,7 @@
4949
<tbody>
5050
{% for domain in domains %}
5151
<tr columns="columns">
52-
<td><a href="{{ path('lexik_translation_grid') }}#!?filter[_domain]={{ domain | url_encode }}">{{ domain }}</a></td>
52+
<td><a href="{{ path('lexik_translation_grid', {'_locale': app.request.attributes.get('_locale')|default(app.request.locale|default('en'))}) }}?filter[_domain]={{ domain | url_encode }}">{{ domain }}</a></td>
5353
{% for locale in locales %}
5454
<td class="text-center">
5555
<span class="text {{ stats[domain][locale]['completed'] == 100 ? 'text-success' : 'text-danger' }}">

docker-compose.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
version: '3.8'
2-
31
networks:
42
lexik_translation_network:
53
driver: bridge

0 commit comments

Comments
 (0)