Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
3d6d980
[T2586]: basic regex validation phone nubmer in my2_new_sponsorship_w…
EliasKeller Aug 18, 2025
cb65fcb
[T2586]: basic regex validation email in my2_new_sponsorship_wizard.js
EliasKeller Aug 18, 2025
6158473
[T2586] FEAT: add event (onBlur) binding for the email and phone numb…
EliasKeller Aug 18, 2025
5da7fec
[T2586] FEAT: started to build a centralized field validation in util…
EliasKeller Aug 19, 2025
beabe75
[T2586] FEAT: included Selection and made field validation configurable
EliasKeller Aug 19, 2025
dc82b83
[T2586] REFACTOR: added comments where needed
EliasKeller Aug 19, 2025
b6c74a2
[T2586] refactoring
EliasKeller Aug 19, 2025
e26d963
[T2586] refactoring
EliasKeller Aug 19, 2025
e4f0d8d
[T2586] pre-commit
EliasKeller Aug 19, 2025
adf0afc
[T2586]: basic regex validation phone nubmer in my2_new_sponsorship_w…
EliasKeller Aug 18, 2025
1514502
[T2586]: basic regex validation email in my2_new_sponsorship_wizard.js
EliasKeller Aug 18, 2025
eb120c0
[T2586] FEAT: add event (onBlur) binding for the email and phone numb…
EliasKeller Aug 18, 2025
2e81ff6
[T2586] FEAT: started to build a centralized field validation in util…
EliasKeller Aug 19, 2025
9295535
[T2586] FEAT: included Selection and made field validation configurable
EliasKeller Aug 19, 2025
c0b461d
[T2586] REFACTOR: added comments where needed
EliasKeller Aug 19, 2025
7772e95
[T2586] refactoring
EliasKeller Aug 19, 2025
ed809ed
[T2586] refactoring
EliasKeller Aug 19, 2025
b246757
Run pre-commit formatting
Aug 26, 2025
0733524
Merge remote-tracking branch 'origin/T2586-phone-number-check-in-spon…
EliasKeller Sep 2, 2025
8717a7e
merging
EliasKeller Dec 16, 2025
972253d
Merge pull request #229 from CompassionCH/T2586-phone-number-check-in…
EliasKeller Dec 16, 2025
fda444e
[T2591] FEAT: make validation available globally for my_compassion 2.0
EliasKeller Dec 17, 2025
c55696d
[T2591] FEAT: validate user settings
EliasKeller Dec 22, 2025
026fcd4
[T2591] FEAT: append backend error to specific field
EliasKeller Dec 23, 2025
b6ebcf3
[T2591] REFACTOR: pre-commit
EliasKeller Dec 23, 2025
5931ada
[T2591] REFACTOR: remove unnecessary css invalid class specified only…
EliasKeller Dec 23, 2025
3ed155c
[T2591] REFACTOR: kabab case to camel cas in data DOM object
EliasKeller Dec 23, 2025
3e31652
[T2591] REFACTOR: pre-commit
EliasKeller Dec 23, 2025
4ca9cd9
[T2591] REFACTOR: pre-commit
EliasKeller Dec 23, 2025
94e881f
[T2902] REFACTOR: adapting FormField to display client-side validatio…
Danielgergely Jan 21, 2026
a6725e0
[T2902] REFACTOR: form validation for settings page complete
Danielgergely Jan 22, 2026
e739e1f
[T2902] FEATURE: loading spinner added
Danielgergely Jan 22, 2026
cedf2a1
[T2902] FEATURE: loading spinner working in both desktop and mobile view
Danielgergely Jan 23, 2026
9c00a0e
[T2902] STYLE: pre-commit changes
Danielgergely Jan 23, 2026
f6c9b22
[T2092] STYLE: pre-commit 2
Danielgergely Jan 23, 2026
c97fd3d
[T2902] FIX: Gemini code assist suggestions
Danielgergely Jan 23, 2026
37f2137
Merge branch 'T2591-extend-usage-of-form-field-validator' into T2902-…
Danielgergely Jan 28, 2026
418db7d
Merge branch '14.0' into T2902-profile-settingspage
Danielgergely Feb 6, 2026
26a6763
[T2902] REFACTOR: final form validation settings
Danielgergely Feb 10, 2026
6f88447
[T2902] REFACTOR: unification and ux improvements
Danielgergely Feb 11, 2026
55782ca
[T2904] FIX: GCA suggestions
Danielgergely Feb 11, 2026
8991106
[T2902] REFACTOR: re-added user reference to user settings page
Danielgergely Feb 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions my_compassion/static/src/css/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,16 @@ h1 {
outline: none; /* Remove default outline */
box-shadow: 0 0 10px rgba(74, 166, 246, 0.5); /* Glow effect */
}

/*
Global form validation styles
Applied to all form fields marked as invalid (`.is-invalid`)
within `.form-field-component` across the entire MyCompassion app.
*/
.form-field-component .is-invalid {
border: 1px solid var(--mid-orange);
border-left: 5px solid var(--mid-orange);
/* override bootstrap invalid style */
background-image: none;
padding-right: 0.75rem;
}
14 changes: 0 additions & 14 deletions my_compassion/static/src/css/my2_new_sponsorship_wizard.css
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,6 @@
border: 0.65rem solid;
}

.new-sponsorship-wizard-form input {
width: 100%;
height: 2.6rem;
border-radius: 5px;
border: 1px solid var(--mid-grey);
padding-left: 10px;
padding-right: 10px;
}

.new-sponsorship-wizard-form input[type="checkbox"] {
width: 1.4rem;
height: 1.4rem;
Expand Down Expand Up @@ -90,11 +81,6 @@
margin-right: auto;
}

.new-sponsorship-wizard-form .is-invalid {
border: 1px solid var(--mid-orange);
border-left: 5px solid var(--mid-orange);
}

.thank-you-child-profile {
max-width: 350px;
}
Expand Down
26 changes: 26 additions & 0 deletions my_compassion/static/src/css/user_settings.css
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,29 @@
border-top-right-radius: 25px;
border-bottom-right-radius: 25px;
}

/* Spinner styling */
#user-settings-loader {
background: rgba(255, 255, 255, 0.5);
position: absolute;
width: 100%;
top: 0;
height: fit-content;
display: flex;
}

.spinner-rotate {
animation: custom-spin 1.5s infinite linear !important;
margin: auto;
}

@keyframes custom-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
30 changes: 6 additions & 24 deletions my_compassion/static/src/js/my2_new_sponsorship_wizard.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,33 +107,15 @@ document.addEventListener("DOMContentLoaded", function (event) {
*/
_validateForm: function () {
var isValid = true;
// Remove previous error messages and styles
this.$(".input-invalid-hint").remove();
this.$("input.is-invalid").removeClass("is-invalid");

// Find all required inputs within the current step that are visible
this.$("input[required]:visible, select[required]:visible").each(function () {
var $input = $(this);
if (!$input.val()) {

this.$(".form-field-component:visible").each(function () {
var fieldWidget = $(this).data("widget");

if (fieldWidget && !fieldWidget.validate()) {
isValid = false;
// Add the 'is-invalid' class
$input.addClass("is-invalid");

// Add a small text hint above the input field
var $errorHint = $(
'<div class="input-invalid-hint text-mid-orange tiny-text mb-1">This field is required.</div>'
);
var $select_container = $input.parent(".SelectComponent");

if ($select_container.length > 0) {
// If the input is a select component, place the hint before the container
$select_container.before($errorHint);
} else {
// Otherwise, it's a standard input, so place the hint before the input itself
$input.before($errorHint);
}
}
});

return isValid;
},

Expand Down
151 changes: 92 additions & 59 deletions my_compassion/static/src/js/my2_user_settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,6 @@ document.addEventListener("DOMContentLoaded", () => {
* Main setup function to initialize all event listeners.
*/
function initializeUserSettings() {
// Hides the error messages of the select components
document.querySelectorAll(".invalid-hint").forEach((hint) => {
hint.style.display = "none";
});

initTabNavigation();
initCommunicationSettings();
initAgreementsForm();
Expand Down Expand Up @@ -149,28 +144,6 @@ document.addEventListener("DOMContentLoaded", () => {
});
}

/**
* Attaches a standardized event listener to an agreement checkbox.
* When checked, it calls a specific RPC route.
*/

function attachAgreementListener(checkbox, route) {
checkbox.addEventListener("change", function () {
// Only proceed if the checkbox is being checked
if (!this.checked) return;

rpc.query({ route, params: {} })
.then(() => {
window.location.reload();
})
.catch((err) => {
console.error("RPC Error:", err);
this.checked = false; // Revert the checkbox state on error
Dialog.alert(null, "Could not save your confirmation. Please try again.");
});
});
}

/**
* Initializes the privacy checkbox.
*/
Expand Down Expand Up @@ -289,67 +262,124 @@ document.addEventListener("DOMContentLoaded", () => {

let originalValues = {};

const getFieldWidget = (fieldName) => {
const input = form.querySelector(`[name="${fieldName}"]`);
if (!input) return null;

return $(input).closest(".form-field-component").data("widget");
};

function toggleLoader(isLoading) {
const loader = document.getElementById("user-settings-loader");

if (isLoading) {
loader?.classList.remove("d-none");
} else {
loader?.classList.add("d-none");
}
}

const showBackendErrors = (errors) => {
for (const fieldName in errors) {
const widget = getFieldWidget(fieldName);
if (widget) {
widget.showError(errors[fieldName]);
} else {
console.warn(`No widget found for field ${fieldName} to display error: ${errors[fieldName]}`);
}
}
};

const clearErrors = () => {
form.querySelectorAll(".is-invalid").forEach((input) => {
input.classList.remove("is-invalid");
});
form.querySelectorAll(".invalid-hint").forEach((hint) => {
hint.style.display = "none";
fields.forEach((field) => {
const widget = getFieldWidget(field);
if (widget) {
widget.clearError();
}
});
};

const showErrors = (errors) => {
for (const fieldName in errors) {
const input = form.querySelector(`[name="${fieldName}"]`);
if (input) {
input.classList.add("is-invalid");
const container = input.closest(".form-field-container");
if (container) {
const hintEl = container.querySelector(".invalid-hint");
if (hintEl) {
hintEl.textContent = errors[fieldName];
hintEl.style.display = "block";
}
const validateForm = () => {
let isValid = true;

fields.forEach((field) => {
const widget = getFieldWidget(field);
if (widget) {
if (!widget.validate()) {
isValid = false;
}
}
}
});

return isValid;
};

const storeOriginalValues = () => {
fields.forEach((field) => {
const input = form.querySelector(`[name="${field}"]`);
if (input) originalValues[field] = input.value;
if (input) originalValues[field] = input.type === "checkbox" ? input.checked : input.value;
});
};

const restoreOriginalValues = () => {
fields.forEach((field) => {
const input = form.querySelector(`[name="${field}"]`);
if (input && originalValues[field] !== undefined) {
input.value = originalValues[field];
if (input.type === "checkbox") {
input.checked = originalValues[field];
} else {
input.value = originalValues[field];
}
}
});
};

editButton.addEventListener("click", () => {
storeOriginalValues();
const anyValueUpdated = () => {
return fields.some((field) => {
const input = form.querySelector(`[name="${field}"]`);
if (input) {
const valueToCheck = input.type === "checkbox" ? input.checked : input.value;
return valueToCheck !== originalValues[field];
}
});
};

function toggleEdit(isEditing) {
clearErrors();
form.classList.add("is-editing");
form.classList.toggle("is-editing", isEditing);
}

editButton.addEventListener("click", (e) => {
e.preventDefault();
storeOriginalValues();
toggleEdit(true);
});

cancelButton.addEventListener("click", () => {
cancelButton.addEventListener("click", (e) => {
e.preventDefault();
restoreOriginalValues();
clearErrors();
form.classList.remove("is-editing");
toggleEdit(false);
});

saveButton.addEventListener("click", () => {
saveButton.addEventListener("click", (e) => {
e.preventDefault();
clearErrors();

if (!validateForm()) return;
toggleLoader(true);

if (!anyValueUpdated()) {
toggleLoader(false);
toggleEdit(false);
return;
}

const payload = {};
fields.forEach((field) => {
for (const field of fields) {
const input = form.querySelector(`[name="${field}"]`);
if (input) payload[field] = input.value;
});
if (!input || input.offsetParent === null) continue; // Skip hidden/missing inputs
payload[field] = input.type === "checkbox" ? input.checked : input.value;
}

rpc.query({ route: endpoint, params: payload })
.then((response) => {
Expand All @@ -362,16 +392,19 @@ document.addEventListener("DOMContentLoaded", () => {
displayEl.textContent =
input.tagName === "SELECT" ? input.options[input.selectedIndex].text : input.value;
});
form.classList.remove("is-editing");
toggleEdit(false);
toggleLoader(false);
} else {
if (response.errors) {
showErrors(response.errors);
showBackendErrors(response.errors);
}
toggleLoader(false);
}
})
.catch((err) => {
console.error("RPC Error:", err);
Dialog.alert(null, "An unexpected error occurred. Please try again later.");
toggleLoader(false);
});
});
}
Expand Down
Loading