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
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:
parent
3372025458
commit
9d5905e689
@ -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[]')
|
||||||
|
|||||||
@ -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 }};
|
||||||
|
|
||||||
document.querySelector('form').addEventListener('submit', function(e) {
|
(function() {
|
||||||
var html = quill.root.innerHTML;
|
var qc = document.getElementById('quill-editor');
|
||||||
if (html === '<p><br></p>' || quill.getText().trim() === '') {
|
quill.on('text-change', function() { qc && qc.classList.remove('field-error'); });
|
||||||
e.preventDefault();
|
document.querySelector('form').addEventListener('submit', function(e) {
|
||||||
alert('Wpisz treść ogłoszenia.');
|
var html = quill.root.innerHTML;
|
||||||
quill.focus();
|
var empty = (html === '<p><br></p>' || quill.getText().trim() === '');
|
||||||
return;
|
if (empty) {
|
||||||
}
|
e.preventDefault();
|
||||||
document.getElementById('description').value = html;
|
qc && qc.classList.add('field-error');
|
||||||
});
|
qc && qc.scrollIntoView({behavior: 'smooth', block: 'center'});
|
||||||
|
quill.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
qc && qc.classList.remove('field-error');
|
||||||
|
document.getElementById('description').value = html;
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
function toggleDeleteAttachment(attId) {
|
function toggleDeleteAttachment(attId) {
|
||||||
var el = document.getElementById('existing-' + attId);
|
var el = document.getElementById('existing-' + attId);
|
||||||
|
|||||||
@ -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).
|
||||||
document.querySelector('form').addEventListener('submit', function(e) {
|
(function() {
|
||||||
var html = quill.root.innerHTML;
|
var qc = document.getElementById('quill-editor');
|
||||||
if (html === '<p><br></p>' || quill.getText().trim() === '') {
|
// Restore from server-rendered textarea (e.g. after POST validation error)
|
||||||
e.preventDefault();
|
var initialDesc = document.getElementById('description').value;
|
||||||
alert('Wpisz treść ogłoszenia.');
|
if (initialDesc) { quill.root.innerHTML = initialDesc; }
|
||||||
quill.focus();
|
// Clear error highlight as soon as user starts typing
|
||||||
return;
|
quill.on('text-change', function() { qc && qc.classList.remove('field-error'); });
|
||||||
}
|
|
||||||
document.getElementById('description').value = html;
|
document.querySelector('form').addEventListener('submit', function(e) {
|
||||||
});
|
var html = quill.root.innerHTML;
|
||||||
|
var empty = (html === '<p><br></p>' || quill.getText().trim() === '');
|
||||||
|
if (empty) {
|
||||||
|
e.preventDefault();
|
||||||
|
qc && qc.classList.add('field-error');
|
||||||
|
qc && qc.scrollIntoView({behavior: 'smooth', block: 'center'});
|
||||||
|
quill.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
qc && qc.classList.remove('field-error');
|
||||||
|
document.getElementById('description').value = html;
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
const dropzone = document.getElementById('dropzone');
|
const dropzone = document.getElementById('dropzone');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user