fix(t-paliad-141): project team-add autocomplete + invite-new-user inline flow
Root cause: `.collab-suggestions` had `display: none` in CSS but no JS site
ever toggled it back on. Suggestions rendered into a permanently hidden div.
Bug originated when the akten-collab-* pattern was renamed and copied for
project team-add and partner-units member-add — the original akten-neu.ts
toggled `style.display`, but the copies relied on innerHTML alone.
Fix: switch to content-driven visibility — `.collab-suggestions:not(:empty)
{ display: block }`. No JS changes needed at consumer sites; fixes all three
broken pickers (project team-add, project parent picker, partner-units member-
add) at once. Added missing styling for `.collab-suggestion` items (padding,
hover, separators) — they were unstyled even when visible.
Plus: invite-new-user inline affordance on project /team. When the typed
query matches zero existing users, a "Benutzer nicht gefunden? Einladen"
row appears below the dropdown. Click opens the existing global invite modal
(sidebar-invite-btn → /api/invite) and pre-fills the email if the query
looks like one. No new backend, no new modal — reuses what /admin/team and
the sidebar already use.
This commit is contained in:
@@ -1116,6 +1116,9 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"projects.detail.team.confirm_remove": "Mitglied entfernen?",
|
||||
"projects.detail.team.empty": "Noch keine Teammitglieder.",
|
||||
"projects.detail.team.error.user_required": "Benutzer ausw\u00e4hlen",
|
||||
"projects.detail.team.invite.hint": "Benutzer nicht gefunden?",
|
||||
"projects.detail.team.invite.hint_email": "Niemand mit dieser E-Mail.",
|
||||
"projects.detail.team.invite.cta": "Einladen",
|
||||
"projects.view.tree": "Baumansicht",
|
||||
"projects.tree.toggle": "Aufklappen / Zuklappen",
|
||||
"projects.tree.loading": "Baum wird geladen\u2026",
|
||||
@@ -2792,6 +2795,9 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"projects.detail.team.confirm_remove": "Remove member?",
|
||||
"projects.detail.team.empty": "No team members yet.",
|
||||
"projects.detail.team.error.user_required": "Select a user",
|
||||
"projects.detail.team.invite.hint": "User not found?",
|
||||
"projects.detail.team.invite.hint_email": "No one with that email.",
|
||||
"projects.detail.team.invite.cta": "Invite",
|
||||
"projects.view.tree": "Tree view",
|
||||
"projects.tree.toggle": "Expand / collapse",
|
||||
"projects.tree.loading": "Loading tree…",
|
||||
|
||||
@@ -1383,8 +1383,24 @@ function initTeamForm(id: string) {
|
||||
const sugs = document.getElementById("team-user-suggestions") as HTMLDivElement | null;
|
||||
const msg = document.getElementById("team-msg") as HTMLParagraphElement | null;
|
||||
const role = document.getElementById("team-role") as HTMLSelectElement | null;
|
||||
const inviteHint = document.getElementById("team-user-invite-hint") as HTMLDivElement | null;
|
||||
const inviteHintText = document.getElementById("team-user-invite-hint-text") as HTMLSpanElement | null;
|
||||
const inviteBtn = document.getElementById("team-user-invite-btn") as HTMLButtonElement | null;
|
||||
if (!addBtn || !form || !cancel || !input || !hidden || !sugs || !msg || !role) return;
|
||||
|
||||
const hideInviteHint = () => {
|
||||
if (inviteHint) inviteHint.style.display = "none";
|
||||
};
|
||||
const showInviteHint = (q: string) => {
|
||||
if (!inviteHint || !inviteHintText) return;
|
||||
const looksLikeEmail = /@/.test(q) && /\./.test(q.split("@")[1] || "");
|
||||
inviteHintText.textContent = looksLikeEmail
|
||||
? t("projects.detail.team.invite.hint_email") || "Niemand mit dieser E-Mail."
|
||||
: t("projects.detail.team.invite.hint") || "Benutzer nicht gefunden?";
|
||||
inviteHint.dataset.email = looksLikeEmail ? q : "";
|
||||
inviteHint.style.display = "";
|
||||
};
|
||||
|
||||
addBtn.addEventListener("click", () => {
|
||||
form.style.display = "";
|
||||
addBtn.style.display = "none";
|
||||
@@ -1396,18 +1412,21 @@ function initTeamForm(id: string) {
|
||||
input.value = "";
|
||||
hidden.value = "";
|
||||
sugs.innerHTML = "";
|
||||
hideInviteHint();
|
||||
msg.textContent = "";
|
||||
});
|
||||
|
||||
input.addEventListener("input", () => {
|
||||
const q = input.value.trim().toLowerCase();
|
||||
const q = input.value.trim();
|
||||
const lc = q.toLowerCase();
|
||||
hidden.value = "";
|
||||
if (!q) {
|
||||
sugs.innerHTML = "";
|
||||
hideInviteHint();
|
||||
return;
|
||||
}
|
||||
const matches = userOptions
|
||||
.filter((u) => (u.display_name + " " + u.email).toLowerCase().includes(q))
|
||||
.filter((u) => (u.display_name + " " + u.email).toLowerCase().includes(lc))
|
||||
.slice(0, 8);
|
||||
sugs.innerHTML = matches
|
||||
.map(
|
||||
@@ -1422,8 +1441,29 @@ function initTeamForm(id: string) {
|
||||
hidden.value = el.dataset.id!;
|
||||
input.value = el.dataset.label!;
|
||||
sugs.innerHTML = "";
|
||||
hideInviteHint();
|
||||
});
|
||||
});
|
||||
|
||||
if (matches.length === 0) {
|
||||
showInviteHint(q);
|
||||
} else {
|
||||
hideInviteHint();
|
||||
}
|
||||
});
|
||||
|
||||
inviteBtn?.addEventListener("click", () => {
|
||||
const sidebarBtn = document.getElementById("sidebar-invite-btn") as HTMLButtonElement | null;
|
||||
if (!sidebarBtn) return;
|
||||
sidebarBtn.click();
|
||||
const prefill = inviteHint?.dataset.email || "";
|
||||
if (prefill) {
|
||||
const inviteEmail = document.getElementById("invite-email") as HTMLInputElement | null;
|
||||
if (inviteEmail) {
|
||||
inviteEmail.value = prefill;
|
||||
inviteEmail.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
@@ -1446,6 +1486,7 @@ function initTeamForm(id: string) {
|
||||
input.value = "";
|
||||
hidden.value = "";
|
||||
sugs.innerHTML = "";
|
||||
hideInviteHint();
|
||||
form.style.display = "none";
|
||||
addBtn.style.display = "";
|
||||
await loadTeam(id);
|
||||
|
||||
@@ -1447,6 +1447,9 @@ export type I18nKey =
|
||||
| "projects.detail.team.form.role"
|
||||
| "projects.detail.team.form.submit"
|
||||
| "projects.detail.team.form.user"
|
||||
| "projects.detail.team.invite.cta"
|
||||
| "projects.detail.team.invite.hint"
|
||||
| "projects.detail.team.invite.hint_email"
|
||||
| "projects.detail.team.remove"
|
||||
| "projects.detail.title"
|
||||
| "projects.detail.verlauf.empty"
|
||||
|
||||
@@ -109,6 +109,10 @@ export function renderProjectsDetail(): string {
|
||||
<input type="text" id="team-user-input" placeholder="Name oder E-Mail..." autocomplete="off" />
|
||||
<input type="hidden" id="team-user-id" />
|
||||
<div id="team-user-suggestions" className="collab-suggestions" />
|
||||
<div id="team-user-invite-hint" className="collab-invite-hint" style="display:none">
|
||||
<span id="team-user-invite-hint-text" data-i18n="projects.detail.team.invite.hint">Benutzer nicht gefunden?</span>
|
||||
<button type="button" className="btn-secondary btn-small" id="team-user-invite-btn" data-i18n="projects.detail.team.invite.cta">Einladen</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-field">
|
||||
<label htmlFor="team-role" data-i18n="projects.detail.team.form.role">Rolle</label>
|
||||
|
||||
@@ -5872,6 +5872,61 @@ input[type="range"]::-moz-range-thumb {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Visibility is content-driven: when innerHTML is "" the div has no children
|
||||
and stays hidden; the moment a consumer renders <.collab-suggestion> rows
|
||||
the dropdown shows. Keeps the JS sites (project team-add, project parent
|
||||
picker, partner-units member-add) from each having to toggle display. */
|
||||
.collab-suggestions:not(:empty) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.collab-suggestion {
|
||||
display: block;
|
||||
padding: 0.5rem 0.75rem;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.collab-suggestion:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.collab-suggestion:hover,
|
||||
.collab-suggestion.is-active {
|
||||
background: var(--color-bg-lime-tint);
|
||||
}
|
||||
|
||||
.collab-suggestion strong {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.collab-suggestion .form-hint {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
|
||||
/* Inline "invite this user instead" affordance shown beneath an empty
|
||||
.collab-suggestions when the typed query has no matches. */
|
||||
.collab-invite-hint {
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--color-surface-alt, var(--color-bg-lime-tint));
|
||||
border: 1px dashed var(--color-border);
|
||||
border-radius: var(--radius);
|
||||
font-size: 0.85rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.collab-invite-hint button {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.entity-suggestion {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user