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,