fix(classifieds): preserve form values + red border on missing fields
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions

Previously when server validation failed (e.g. missing required field),
the whole form re-rendered with all values cleared — user had to retype
everything. Also Quill empty-content showed an alert dialog.

Now:
- Server-side: form_data + missing_fields passed to template; values
  re-populate inputs, missing fields get .field-error class (red border)
- Quill empty: red border on the editor container instead of alert,
  cleared as soon as user starts typing
- Other required fields (radio, select, title): same .field-error
  treatment plus :invalid CSS for live HTML5 feedback

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-04-14 13:33:18 +02:00
parent 3372025458
commit 9d5905e689
3 changed files with 86 additions and 38 deletions

View File

@ -93,7 +93,13 @@ def new():
if not listing_type or not category or not title or not description: if not listing_type or not category or not title or not description:
flash('Wszystkie wymagane pola muszą być wypełnione.', 'error') flash('Wszystkie wymagane pola muszą być wypełnione.', 'error')
return render_template('classifieds/new.html') return render_template('classifieds/new.html', form_data=request.form,
missing_fields={
'listing_type': not listing_type,
'category': not category,
'title': not title,
'description': not description,
})
db = SessionLocal() db = SessionLocal()
try: try:
@ -276,7 +282,11 @@ def edit(classified_id):
if not classified.title or not classified.description: if not classified.title or not classified.description:
flash('Tytuł i opis są wymagane.', 'error') flash('Tytuł i opis są wymagane.', 'error')
return render_template('classifieds/edit.html', classified=classified) return render_template('classifieds/edit.html', classified=classified,
missing_fields={
'title': not classified.title,
'description': not classified.description,
})
# Handle deleted attachments # Handle deleted attachments
delete_ids = request.form.getlist('delete_attachments[]') delete_ids = request.form.getlist('delete_attachments[]')

View File

@ -51,6 +51,11 @@
.quill-container .ql-toolbar { border-top-left-radius: var(--radius); border-top-right-radius: var(--radius); } .quill-container .ql-toolbar { border-top-left-radius: var(--radius); border-top-right-radius: var(--radius); }
.quill-container .ql-container { border-bottom-left-radius: var(--radius); border-bottom-right-radius: var(--radius); font-size: var(--font-size-base); } .quill-container .ql-container { border-bottom-left-radius: var(--radius); border-bottom-right-radius: var(--radius); font-size: var(--font-size-base); }
.quill-container .ql-editor { min-height: 150px; } .quill-container .ql-editor { min-height: 150px; }
.field-error, input.field-error, .quill-container.field-error {
border: 2px solid #dc2626 !important;
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.15);
}
:invalid:not(:focus):not(:placeholder-shown) { border: 2px solid #dc2626; }
/* Existing attachments */ /* Existing attachments */
.existing-attachment { position: relative; border: 1px solid var(--border); border-radius: var(--radius); padding: var(--spacing-xs); background: var(--surface); } .existing-attachment { position: relative; border: 1px solid var(--border); border-radius: var(--radius); padding: var(--spacing-xs); background: var(--surface); }
@ -114,12 +119,12 @@
<div class="form-group"> <div class="form-group">
<label for="title">Tytuł ogłoszenia *</label> <label for="title">Tytuł ogłoszenia *</label>
<input type="text" id="title" name="title" required maxlength="255" value="{{ classified.title }}"> <input type="text" id="title" name="title" required maxlength="255" value="{{ classified.title }}" class="{% if missing_fields and missing_fields.title %}field-error{% endif %}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Opis *</label> <label>Opis *</label>
<div id="quill-editor" class="quill-container"></div> <div id="quill-editor" class="quill-container{% if missing_fields and missing_fields.description %} field-error{% endif %}"></div>
<textarea id="description" name="description" style="display:none;"></textarea> <textarea id="description" name="description" style="display:none;"></textarea>
</div> </div>
@ -185,16 +190,23 @@ var quill = new Quill('#quill-editor', {
}); });
quill.root.innerHTML = {{ classified.description|tojson }}; quill.root.innerHTML = {{ classified.description|tojson }};
(function() {
var qc = document.getElementById('quill-editor');
quill.on('text-change', function() { qc && qc.classList.remove('field-error'); });
document.querySelector('form').addEventListener('submit', function(e) { document.querySelector('form').addEventListener('submit', function(e) {
var html = quill.root.innerHTML; var html = quill.root.innerHTML;
if (html === '<p><br></p>' || quill.getText().trim() === '') { var empty = (html === '<p><br></p>' || quill.getText().trim() === '');
if (empty) {
e.preventDefault(); e.preventDefault();
alert('Wpisz treść ogłoszenia.'); qc && qc.classList.add('field-error');
qc && qc.scrollIntoView({behavior: 'smooth', block: 'center'});
quill.focus(); quill.focus();
return; return;
} }
qc && qc.classList.remove('field-error');
document.getElementById('description').value = html; document.getElementById('description').value = html;
}); });
})();
function toggleDeleteAttachment(attId) { function toggleDeleteAttachment(attId) {
var el = document.getElementById('existing-' + attId); var el = document.getElementById('existing-' + attId);

View File

@ -25,6 +25,20 @@
.quill-container .ql-editor { .quill-container .ql-editor {
min-height: 150px; min-height: 150px;
} }
/* Highlight required fields that failed validation. Applied by server
on POST validation error and by client JS on Quill empty submit. */
.field-error,
input.field-error,
select.field-error,
.type-selector.field-error,
.quill-container.field-error {
border: 2px solid #dc2626 !important;
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.15);
}
:invalid:not(:focus):not(:placeholder-shown),
select:invalid:not(:focus) {
border: 2px solid #dc2626;
}
.form-container { .form-container {
max-width: 700px; max-width: 700px;
margin: 0 auto; margin: 0 auto;
@ -279,9 +293,9 @@
<div class="form-group"> <div class="form-group">
<label>Typ ogłoszenia *</label> <label>Typ ogłoszenia *</label>
<div class="type-selector"> <div class="type-selector{% if missing_fields and missing_fields.listing_type %} field-error{% endif %}">
<div class="type-option"> <div class="type-option">
<input type="radio" id="type_szukam" name="listing_type" value="szukam" required> <input type="radio" id="type_szukam" name="listing_type" value="szukam" required {% if form_data and form_data.get('listing_type') == 'szukam' %}checked{% endif %}>
<label for="type_szukam"> <label for="type_szukam">
<span class="type-icon">🔍</span> <span class="type-icon">🔍</span>
<strong>Szukam</strong> <strong>Szukam</strong>
@ -289,7 +303,7 @@
</label> </label>
</div> </div>
<div class="type-option"> <div class="type-option">
<input type="radio" id="type_oferuje" name="listing_type" value="oferuje" required> <input type="radio" id="type_oferuje" name="listing_type" value="oferuje" required {% if form_data and form_data.get('listing_type') == 'oferuje' %}checked{% endif %}>
<label for="type_oferuje"> <label for="type_oferuje">
<span class="type-icon"></span> <span class="type-icon"></span>
<strong>Oferuję</strong> <strong>Oferuję</strong>
@ -301,36 +315,36 @@
<div class="form-group"> <div class="form-group">
<label for="category">Kategoria *</label> <label for="category">Kategoria *</label>
<select id="category" name="category" required> <select id="category" name="category" required class="{% if missing_fields and missing_fields.category %}field-error{% endif %}">
<option value="">Wybierz kategorię...</option> <option value="">Wybierz kategorię...</option>
<option value="uslugi">Usługi profesjonalne</option> <option value="uslugi" {% if form_data and form_data.get('category') == 'uslugi' %}selected{% endif %}>Usługi profesjonalne</option>
<option value="produkty">Produkty, materiały</option> <option value="produkty" {% if form_data and form_data.get('category') == 'produkty' %}selected{% endif %}>Produkty, materiały</option>
<option value="wspolpraca">Propozycje współpracy</option> <option value="wspolpraca" {% if form_data and form_data.get('category') == 'wspolpraca' %}selected{% endif %}>Propozycje współpracy</option>
<option value="praca">Oferty pracy, zlecenia</option> <option value="praca" {% if form_data and form_data.get('category') == 'praca' %}selected{% endif %}>Oferty pracy, zlecenia</option>
<option value="inne">Inne</option> <option value="inne" {% if form_data and form_data.get('category') == 'inne' %}selected{% endif %}>Inne</option>
</select> </select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="title">Tytuł ogłoszenia *</label> <label for="title">Tytuł ogłoszenia *</label>
<input type="text" id="title" name="title" required maxlength="255" placeholder="np. Szukam firmy do wykonania strony www"> <input type="text" id="title" name="title" required maxlength="255" placeholder="np. Szukam firmy do wykonania strony www" value="{{ form_data.get('title', '') if form_data else '' }}" class="{% if missing_fields and missing_fields.title %}field-error{% endif %}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Opis *</label> <label>Opis *</label>
<div id="quill-editor" class="quill-container"></div> <div id="quill-editor" class="quill-container{% if missing_fields and missing_fields.description %} field-error{% endif %}"></div>
<textarea id="description" name="description" style="display:none;"></textarea> <textarea id="description" name="description" style="display:none;">{{ form_data.get('description', '') if form_data else '' }}</textarea>
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-group"> <div class="form-group">
<label for="budget_info">Budżet / Cena</label> <label for="budget_info">Budżet / Cena</label>
<input type="text" id="budget_info" name="budget_info" maxlength="255" placeholder="np. 5000-10000 PLN lub 'do negocjacji'"> <input type="text" id="budget_info" name="budget_info" maxlength="255" placeholder="np. 5000-10000 PLN lub 'do negocjacji'" value="{{ form_data.get('budget_info', '') if form_data else '' }}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="location_info">Lokalizacja</label> <label for="location_info">Lokalizacja</label>
<input type="text" id="location_info" name="location_info" maxlength="255" placeholder="np. Wejherowo, Cala Polska, Online"> <input type="text" id="location_info" name="location_info" maxlength="255" placeholder="np. Wejherowo, Cala Polska, Online" value="{{ form_data.get('location_info', '') if form_data else '' }}">
</div> </div>
</div> </div>
@ -371,16 +385,28 @@ var quill = new Quill('#quill-editor', {
// Sync Quill content to hidden textarea on form submit + validate non-empty. // Sync Quill content to hidden textarea on form submit + validate non-empty.
// Note: hidden textarea cannot use `required` (browser cannot show validation // Note: hidden textarea cannot use `required` (browser cannot show validation
// UI on display:none fields, which silently blocks submit). // UI on display:none fields, which silently blocks submit).
(function() {
var qc = document.getElementById('quill-editor');
// Restore from server-rendered textarea (e.g. after POST validation error)
var initialDesc = document.getElementById('description').value;
if (initialDesc) { quill.root.innerHTML = initialDesc; }
// Clear error highlight as soon as user starts typing
quill.on('text-change', function() { qc && qc.classList.remove('field-error'); });
document.querySelector('form').addEventListener('submit', function(e) { document.querySelector('form').addEventListener('submit', function(e) {
var html = quill.root.innerHTML; var html = quill.root.innerHTML;
if (html === '<p><br></p>' || quill.getText().trim() === '') { var empty = (html === '<p><br></p>' || quill.getText().trim() === '');
if (empty) {
e.preventDefault(); e.preventDefault();
alert('Wpisz treść ogłoszenia.'); qc && qc.classList.add('field-error');
qc && qc.scrollIntoView({behavior: 'smooth', block: 'center'});
quill.focus(); quill.focus();
return; return;
} }
qc && qc.classList.remove('field-error');
document.getElementById('description').value = html; document.getElementById('description').value = html;
}); });
})();
(function() { (function() {
const dropzone = document.getElementById('dropzone'); const dropzone = document.getElementById('dropzone');