diff --git a/freelancer-edit/index.html b/freelancer-edit/index.html
index 2a3a69c..cb90872 100644
--- a/freelancer-edit/index.html
+++ b/freelancer-edit/index.html
@@ -182,6 +182,11 @@
description: ["description", "Description"],
Country: ["Country", "country"],
OCA_link: ["OCA_link", "oca_link"],
+ // The three profile links. The primary name is the LIVE wire key, casing and all —
+ // "Linkedin", "upwork", "fiverr" are deliberately mixed and must never be normalised
+ // (a normalised key is the silent-drop class that ate odoo_versions). A same-word
+ // casing variant is the identical fallback, the way Name/name already works.
+ Linkedin: ["Linkedin", "linkedin"], upwork: ["upwork", "Upwork"], fiverr: ["fiverr", "Fiverr"],
functional: ["functional"], system: ["system"], modules: ["modules"],
integration: ["integration"], ai: ["ai"],
english: ["english"], german: ["german"], spanish: ["spanish"]
@@ -407,6 +412,26 @@
return wrap;
}
+ // An optional profile link. A real `type="url"` input with the application form's own
+ // label and placeholder, so the two forms ask for a link the same way. No URL check
+ // lives here: the value is passed through untouched, and the ONLY url predicate in the
+ // fleet is the directory card's isLink() (a prefix allowlist) — one check, not two.
+ function linkField(id, name, labelText, value, placeholder) {
+ var wrap = el("div", "field");
+ var label = el("label", "", labelText);
+ label.setAttribute("for", id);
+ label.appendChild(el("span", "hint", " (optional)"));
+ var input = el("input");
+ input.type = "url";
+ input.id = id;
+ input.name = name;
+ input.value = value == null ? "" : String(value);
+ input.placeholder = placeholder;
+ wrap.appendChild(label);
+ wrap.appendChild(input);
+ return wrap;
+ }
+
function rangeField(fieldset, name, labelText, value) {
var row = el("div", "row");
var label = el("label", "", labelText);
@@ -534,6 +559,17 @@
form.appendChild(textField("edOcaLink", "OCA member profile link", readField(record, "OCA_link"),
0, "Your OCA profile URL, if you have one: https://apps.odoo-community.org/community/…"));
+ // The three profile links — parity with the application form. Keys are the LIVE wire
+ // keys verbatim ("Linkedin" capital L, "upwork" and "fiverr" lower) and the labels and
+ // placeholders are the application form's own. Each is optional and pre-filled from the
+ // record.
+ form.appendChild(linkField("edLinkedin", "Linkedin", "LinkedIn URL",
+ readField(record, "Linkedin"), "https://www.linkedin.com/in/…"));
+ form.appendChild(linkField("edUpwork", "upwork", "Upwork URL",
+ readField(record, "upwork"), "https://www.upwork.com/freelancers/…"));
+ form.appendChild(linkField("edFiverr", "fiverr", "Fiverr URL",
+ readField(record, "fiverr"), "https://www.fiverr.com/…"));
+
// The image is READ-ONLY on purpose: replacing a photo is a different problem from
// editing text (an upload path, a base64 blob, a size limit). Shown, not editable.
var photoWrap = el("div", "field");
@@ -580,10 +616,20 @@
save.disabled = true;
line.className = "status";
line.textContent = "Saving…";
- // The body shape: the credential (the SAME "has" key the delete sends, one
- // credential path for both) + the record's id + every field this form owns.
- // Method is C.saveMethod ("PATCH").
- var payload = { has: hash, id: rec && rec.id !== undefined ? rec.id : undefined };
+ // The body shape: READ-MODIFY-WRITE. Start from the record the webhook returned and
+ // overwrite ONLY the fields this form owns. A field-by-field payload is what deletes a
+ // freelancer's own photo and links on save — a partial payload wipes what it leaves
+ // out, so the whole record goes back and only the edited keys are replaced. The
+ // credential rides first: "has" is the SAME key the delete sends (one credential path
+ // for both), then the record's id. Method is C.saveMethod ("PATCH").
+ var payload = { has: hash };
+ if (rec) {
+ Object.keys(rec).forEach(function (k) {
+ if (k === "has") return; // never let the record's own "has" shadow the credential
+ payload[k] = rec[k]; // Image, the links, createdAt/updatedAt and everything else
+ });
+ }
+ payload.id = rec && rec.id !== undefined ? rec.id : undefined;
payload.Name = document.getElementById("edName").value.trim();
payload.Focus = document.getElementById("edFocus").value.trim();
payload.description = document.getElementById("edDescription").value.trim();
@@ -591,6 +637,19 @@
payload.OCA_link = document.getElementById("edOcaLink").value.trim();
C.focusAxes.forEach(function (axis) { payload[axis] = num(document.getElementById("ed-" + axis).value, 0); });
C.langs.forEach(function (pair) { payload[pair[0]] = num(document.getElementById("ed-" + pair[0]).value, 0); });
+ // The three links, read from their inputs. Only a key we actually have a value for is
+ // touched: an empty optional field must NOT invent an empty string on a record that
+ // never had one — the record's own value (null, or absent) is already in the payload
+ // and is left as it is. A link the record DID carry and the owner cleared is a
+ // deliberate clear and sends "".
+ [["edLinkedin", "Linkedin"], ["edUpwork", "upwork"], ["edFiverr", "fiverr"]].forEach(function (pair) {
+ var input = document.getElementById(pair[0]);
+ if (!input) return;
+ var v = input.value.trim();
+ var had = rec && rec[pair[1]] !== undefined && rec[pair[1]] !== null && rec[pair[1]] !== "";
+ if (v) payload[pair[1]] = v;
+ else if (had) payload[pair[1]] = "";
+ });
fetch(C.webhook, {
method: C.saveMethod,