feat(freelancer-edit): add LinkedIn/Upwork/Fiverr fields; save the whole record

Two defects, one file. (1) The edit form was missing the three profile links the
application form collects. Added as optional type=url fields pre-filled from the
record, with the application form's own labels and placeholders, under the LIVE
wire keys verbatim: "Linkedin" (capital L), "upwork", "fiverr" — casing never
normalised. (2) The save built its payload field-by-field, so Image and the three
links were absent from the body and a freelancer saving an edit wiped their own
photo and links. The payload now starts from the loaded record (read-modify-write)
and overwrites only the fields this form owns; Image rides through untouched. An
empty optional link on a record that never had one is left as the record's own
value — no invented empty strings. 'has' stays first, method stays PATCH.

No second URL check: the only url predicate is the directory card's isLink().
This commit is contained in:
webmaster committed 2026-11-03 18:51:51 +00:00
1 parent 7dfd5aec9b
commit 5cf58ffa8a
1 file changed
+63 -4
+63 -4
View File
@@ -182,6 +182,11 @@
description: ["description", "Description"], description: ["description", "Description"],
Country: ["Country", "country"], Country: ["Country", "country"],
OCA_link: ["OCA_link", "oca_link"], 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"], functional: ["functional"], system: ["system"], modules: ["modules"],
integration: ["integration"], ai: ["ai"], integration: ["integration"], ai: ["ai"],
english: ["english"], german: ["german"], spanish: ["spanish"] english: ["english"], german: ["german"], spanish: ["spanish"]
@@ -407,6 +412,26 @@
return wrap; 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) { function rangeField(fieldset, name, labelText, value) {
var row = el("div", "row"); var row = el("div", "row");
var label = el("label", "", labelText); var label = el("label", "", labelText);
@@ -534,6 +559,17 @@
form.appendChild(textField("edOcaLink", "OCA member profile link", readField(record, "OCA_link"), 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/…")); 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 // 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. // editing text (an upload path, a base64 blob, a size limit). Shown, not editable.
var photoWrap = el("div", "field"); var photoWrap = el("div", "field");
@@ -580,10 +616,20 @@
save.disabled = true; save.disabled = true;
line.className = "status"; line.className = "status";
line.textContent = "Saving…"; line.textContent = "Saving…";
// The body shape: the credential (the SAME "has" key the delete sends, one // The body shape: READ-MODIFY-WRITE. Start from the record the webhook returned and
// credential path for both) + the record's id + every field this form owns. // overwrite ONLY the fields this form owns. A field-by-field payload is what deletes a
// Method is C.saveMethod ("PATCH"). // freelancer's own photo and links on save — a partial payload wipes what it leaves
var payload = { has: hash, id: rec && rec.id !== undefined ? rec.id : undefined }; // 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.Name = document.getElementById("edName").value.trim();
payload.Focus = document.getElementById("edFocus").value.trim(); payload.Focus = document.getElementById("edFocus").value.trim();
payload.description = document.getElementById("edDescription").value.trim(); payload.description = document.getElementById("edDescription").value.trim();
@@ -591,6 +637,19 @@
payload.OCA_link = document.getElementById("edOcaLink").value.trim(); payload.OCA_link = document.getElementById("edOcaLink").value.trim();
C.focusAxes.forEach(function (axis) { payload[axis] = num(document.getElementById("ed-" + axis).value, 0); }); 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); }); 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, { fetch(C.webhook, {
method: C.saveMethod, method: C.saveMethod,