diff --git a/frontend/src/client/i18n.ts b/frontend/src/client/i18n.ts index 0a312fe..a611f77 100644 --- a/frontend/src/client/i18n.ts +++ b/frontend/src/client/i18n.ts @@ -1116,6 +1116,9 @@ const translations: Record> = { "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> = { "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…", diff --git a/frontend/src/client/projects-detail.ts b/frontend/src/client/projects-detail.ts index 042d85a..6edd358 100644 --- a/frontend/src/client/projects-detail.ts +++ b/frontend/src/client/projects-detail.ts @@ -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); diff --git a/frontend/src/i18n-keys.ts b/frontend/src/i18n-keys.ts index 355b102..d3da53d 100644 --- a/frontend/src/i18n-keys.ts +++ b/frontend/src/i18n-keys.ts @@ -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" diff --git a/frontend/src/projects-detail.tsx b/frontend/src/projects-detail.tsx index 9993389..ade942f 100644 --- a/frontend/src/projects-detail.tsx +++ b/frontend/src/projects-detail.tsx @@ -109,6 +109,10 @@ export function renderProjectsDetail(): string {
+
diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index 0681501..5809fc0 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -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;