fix: add org — domain no longer required, dropdown hides on blur, member bubbles UX
This commit is contained in:
parent
6e50974faf
commit
5c17bf5980
|
|
@ -1299,11 +1299,8 @@ func (h *Handlers) CreateOrg(w http.ResponseWriter, r *http.Request) {
|
|||
ErrorResponse(w, http.StatusBadRequest, "missing_fields", "Organization name required")
|
||||
return
|
||||
}
|
||||
if len(req.Domains) == 0 {
|
||||
ErrorResponse(w, http.StatusBadRequest, "missing_fields", "At least one domain required")
|
||||
return
|
||||
}
|
||||
// Validate domains are not empty strings
|
||||
// Domains are optional — used for invite validation only
|
||||
// Validate domains are not empty strings (if provided)
|
||||
for _, d := range req.Domains {
|
||||
if strings.TrimSpace(d) == "" {
|
||||
ErrorResponse(w, http.StatusBadRequest, "invalid_domains", "Empty domain not allowed")
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@
|
|||
<div class="relative">
|
||||
<label class="block text-sm font-medium text-[#94a3b8] mb-2">Organization Name</label>
|
||||
<input type="text" id="addOrgName" placeholder="Start typing to search or create..." autocomplete="off"
|
||||
oninput="orgNameSearch(this.value)"
|
||||
oninput="orgNameSearch(this.value)" onblur="orgNameBlur()"
|
||||
class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c] text-sm">
|
||||
<div id="orgNameDropdown" class="hidden absolute left-0 right-0 top-full mt-1 bg-[#0d1f3c] border border-white/[0.12] rounded-lg z-10 max-h-48 overflow-y-auto shadow-xl"></div>
|
||||
<input type="hidden" id="addOrgId" value="">
|
||||
|
|
@ -862,17 +862,16 @@
|
|||
|
||||
// ---- Add Org Modal ----
|
||||
let orgSearchTimeout;
|
||||
let memberCount = 0;
|
||||
let pendingMembers = []; // array of {name, email, title}
|
||||
|
||||
function openAddOrgModal() {
|
||||
document.getElementById('addOrgModal').classList.remove('hidden');
|
||||
document.getElementById('addOrgError').classList.add('hidden');
|
||||
document.getElementById('addOrgName').value = '';
|
||||
document.getElementById('addOrgId').value = '';
|
||||
document.getElementById('memberRows').innerHTML = '';
|
||||
memberCount = 0;
|
||||
pendingMembers = [];
|
||||
renderMemberBubbles();
|
||||
setOrgType('buyer', document.querySelector('[data-type="buyer"]'));
|
||||
addMemberRow();
|
||||
loadGlobalOrgs();
|
||||
}
|
||||
|
||||
|
|
@ -904,8 +903,7 @@
|
|||
function orgNameSearch(q) {
|
||||
clearTimeout(orgSearchTimeout);
|
||||
const dd = document.getElementById('orgNameDropdown');
|
||||
const orgId = document.getElementById('addOrgId');
|
||||
orgId.value = '';
|
||||
document.getElementById('addOrgId').value = '';
|
||||
if (!q) { dd.classList.add('hidden'); return; }
|
||||
orgSearchTimeout = setTimeout(() => {
|
||||
const matches = allGlobalOrgs.filter(o => {
|
||||
|
|
@ -913,43 +911,122 @@
|
|||
return (d.name || '').toLowerCase().includes(q.toLowerCase());
|
||||
});
|
||||
if (!matches.length) {
|
||||
dd.innerHTML = `<div class="px-4 py-3 text-sm text-[#94a3b8]">No match — will create "<strong class="text-white">${escHtml(q)}</strong>" as new org</div>`;
|
||||
} else {
|
||||
// No match — just hide, the typed name will be used as-is on submit
|
||||
dd.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
dd.innerHTML = matches.map(o => {
|
||||
const d = parseData(o.data_text);
|
||||
const name = d.name || 'Unnamed';
|
||||
return `<div onclick="selectOrg('${o.entry_id}','${escHtml(name).replace(/'/g,"\'")}','${d.role||''}',this)"
|
||||
return `<div onclick="selectOrg('${o.entry_id}','${name.replace(/'/g,"\'")}','${d.role||''}')"
|
||||
class="px-4 py-3 text-sm text-white hover:bg-white/[0.06] cursor-pointer flex items-center gap-3">
|
||||
<span class="font-medium">${escHtml(name)}</span>
|
||||
<span class="text-xs text-[#475569] capitalize">${d.role || ''}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
dd.classList.remove('hidden');
|
||||
}, 200);
|
||||
}
|
||||
|
||||
function selectOrg(orgId, orgName, role, el) {
|
||||
function orgNameBlur() {
|
||||
// Hide dropdown shortly after blur (allow click to register first)
|
||||
setTimeout(() => document.getElementById('orgNameDropdown').classList.add('hidden'), 200);
|
||||
}
|
||||
|
||||
function selectOrg(orgId, orgName, role) {
|
||||
document.getElementById('addOrgId').value = orgId;
|
||||
document.getElementById('addOrgName').value = orgName;
|
||||
if (role) setOrgType(role, document.querySelector(`[data-type="${role}"]`));
|
||||
document.getElementById('orgNameDropdown').classList.add('hidden');
|
||||
// Load members of this org for suggestions
|
||||
loadOrgMemberSuggestions(orgId);
|
||||
}
|
||||
|
||||
function addMemberRow() {
|
||||
const idx = memberCount++;
|
||||
const row = document.createElement('div');
|
||||
row.className = 'grid grid-cols-[1fr_1fr_1fr_auto] gap-2 items-center';
|
||||
row.id = 'member-row-' + idx;
|
||||
row.innerHTML = `
|
||||
<input type="text" placeholder="Name" id="mname-${idx}"
|
||||
let orgMemberSuggestions = [];
|
||||
async function loadOrgMemberSuggestions(orgId) {
|
||||
orgMemberSuggestions = [];
|
||||
if (!orgId) return;
|
||||
// Get members from existing deal_org entries that reference this org
|
||||
try {
|
||||
const res = await fetchAPI('/api/orgs/' + orgId);
|
||||
const org = await res.json();
|
||||
if (org && org.members) orgMemberSuggestions = org.members;
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
// ---- Member bubble system ----
|
||||
function renderMemberBubbles() {
|
||||
const container = document.getElementById('memberRows');
|
||||
const bubbles = pendingMembers.map((m, i) =>
|
||||
`<div class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-[#c9a84c]/15 border border-[#c9a84c]/30 rounded-full text-sm text-white">
|
||||
<span class="w-5 h-5 rounded-full bg-[#c9a84c]/30 flex items-center justify-center text-xs font-semibold text-[#c9a84c]">${(m.name||'?')[0].toUpperCase()}</span>
|
||||
<span class="font-medium">${escHtml(m.name)}</span>
|
||||
${m.title ? `<span class="text-[#94a3b8] text-xs">· ${escHtml(m.title)}</span>` : ''}
|
||||
<button onclick="removeMember(${i})" class="text-[#94a3b8] hover:text-red-400 transition leading-none ml-1">×</button>
|
||||
</div>`
|
||||
).join('');
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="flex flex-wrap gap-2 mb-2">${bubbles}</div>
|
||||
<div class="grid grid-cols-[1fr_1fr_1fr_auto] gap-2 items-center" id="member-input-row">
|
||||
<div class="relative">
|
||||
<input type="text" id="new-mname" placeholder="Name" autocomplete="off"
|
||||
oninput="memberNameSearch(this.value)"
|
||||
class="w-full px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] text-xs focus:outline-none focus:border-[#c9a84c]">
|
||||
<div id="memberNameDropdown" class="hidden absolute left-0 right-0 top-full mt-1 bg-[#0d1f3c] border border-white/[0.12] rounded-lg z-20 max-h-32 overflow-y-auto shadow-xl"></div>
|
||||
</div>
|
||||
<input type="email" id="new-memail" placeholder="Email"
|
||||
class="px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] text-xs focus:outline-none focus:border-[#c9a84c]">
|
||||
<input type="email" placeholder="Email" id="memail-${idx}"
|
||||
<input type="text" id="new-mtitle" placeholder="Title"
|
||||
onkeydown="if(event.key==='Enter'){event.preventDefault();commitMember();}"
|
||||
class="px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] text-xs focus:outline-none focus:border-[#c9a84c]">
|
||||
<input type="text" placeholder="Title" id="mtitle-${idx}"
|
||||
class="px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] text-xs focus:outline-none focus:border-[#c9a84c]">
|
||||
<button onclick="document.getElementById('member-row-${idx}').remove()" class="text-[#475569] hover:text-red-400 transition text-lg leading-none">×</button>`;
|
||||
document.getElementById('memberRows').appendChild(row);
|
||||
<button onclick="commitMember()" class="w-7 h-7 rounded-full bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] text-lg font-bold flex items-center justify-center leading-none transition">+</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function memberNameSearch(q) {
|
||||
const dd = document.getElementById('memberNameDropdown');
|
||||
if (!q || !orgMemberSuggestions.length) { dd.classList.add('hidden'); return; }
|
||||
const matches = orgMemberSuggestions.filter(m =>
|
||||
(m.name || '').toLowerCase().includes(q.toLowerCase()) ||
|
||||
(m.email || '').toLowerCase().includes(q.toLowerCase())
|
||||
);
|
||||
if (!matches.length) { dd.classList.add('hidden'); return; }
|
||||
dd.innerHTML = matches.map(m =>
|
||||
`<div onclick="selectMemberSuggestion(${JSON.stringify(JSON.stringify(m))})"
|
||||
class="px-3 py-2 text-xs text-white hover:bg-white/[0.06] cursor-pointer">
|
||||
<span class="font-medium">${escHtml(m.name)}</span>
|
||||
${m.email ? `<span class="text-[#475569] ml-1">${escHtml(m.email)}</span>` : ''}
|
||||
</div>`
|
||||
).join('');
|
||||
dd.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function selectMemberSuggestion(mJson) {
|
||||
const m = JSON.parse(mJson);
|
||||
document.getElementById('new-mname').value = m.name || '';
|
||||
document.getElementById('new-memail').value = m.email || '';
|
||||
document.getElementById('new-mtitle').value = m.title || '';
|
||||
document.getElementById('memberNameDropdown').classList.add('hidden');
|
||||
}
|
||||
|
||||
function commitMember() {
|
||||
const name = (document.getElementById('new-mname')?.value || '').trim();
|
||||
const email = (document.getElementById('new-memail')?.value || '').trim();
|
||||
const title = (document.getElementById('new-mtitle')?.value || '').trim();
|
||||
if (!name && !email) return;
|
||||
pendingMembers.push({ name: name || email, email, title });
|
||||
// Add to org suggestions for future use
|
||||
if (!orgMemberSuggestions.find(m => m.email === email)) {
|
||||
orgMemberSuggestions.push({ name, email, title });
|
||||
}
|
||||
renderMemberBubbles();
|
||||
setTimeout(() => document.getElementById('new-mname')?.focus(), 50);
|
||||
}
|
||||
|
||||
function removeMember(idx) {
|
||||
pendingMembers.splice(idx, 1);
|
||||
renderMemberBubbles();
|
||||
}
|
||||
|
||||
async function submitAddOrg() {
|
||||
|
|
@ -960,15 +1037,12 @@
|
|||
const btn = document.getElementById('addOrgSubmitBtn');
|
||||
if (!name) { errEl.textContent = 'Organization name is required'; errEl.classList.remove('hidden'); return; }
|
||||
|
||||
// Collect members
|
||||
const members = [];
|
||||
document.querySelectorAll('#memberRows > div').forEach(row => {
|
||||
const idx = row.id.replace('member-row-', '');
|
||||
const n = document.getElementById('mname-' + idx)?.value.trim();
|
||||
const e = document.getElementById('memail-' + idx)?.value.trim();
|
||||
const t = document.getElementById('mtitle-' + idx)?.value.trim();
|
||||
if (n || e) members.push({ name: n || '', email: e || '', title: t || '' });
|
||||
});
|
||||
// Collect members — commit any in-progress input row first
|
||||
const inProgName = document.getElementById('new-mname')?.value.trim();
|
||||
const inProgEmail = document.getElementById('new-memail')?.value.trim();
|
||||
const inProgTitle = document.getElementById('new-mtitle')?.value.trim();
|
||||
if (inProgName || inProgEmail) pendingMembers.push({ name: inProgName || inProgEmail, email: inProgEmail, title: inProgTitle });
|
||||
const members = [...pendingMembers];
|
||||
|
||||
const perms = {
|
||||
download: document.getElementById('permDownload').value,
|
||||
|
|
|
|||
Loading…
Reference in New Issue