mAi: #75 - team view mailto: link for non-admin members
t-paliad-244 / m/paliad#75. Both "E-Mail an Auswahl senden" actions on /team (filter-bar + bottom selection footer) now branch on canBroadcast(): - Admin path keeps the in-app compose modal (POST /api/team/broadcast). - Non-admin path renders a native <a href="mailto:..."> with the recipient list pre-filled, comma-joined and URL-encoded via buildMailtoHref (already exported from broadcast.ts). Filter-bar button used to hide for non-admins; it now shows as the mailto: anchor and its href refreshes on every filter change so the link always matches what's visible. Empty visible set disables the affordance visually (aria-disabled + pointer-events:none) so a click can't open an empty composer. Bottom selection footer mirrors the same shape. No new i18n keys, no backend changes, admin compose flow untouched.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { initI18n, onLangChange, t, tDyn } from "./i18n";
|
import { initI18n, onLangChange, t, tDyn } from "./i18n";
|
||||||
import { initSidebar } from "./sidebar";
|
import { initSidebar } from "./sidebar";
|
||||||
import { openBroadcastModal, firstName, type BroadcastRecipient } from "./broadcast";
|
import { openBroadcastModal, firstName, buildMailtoHref, type BroadcastRecipient } from "./broadcast";
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -341,28 +341,64 @@ function buildProjectFilter() {
|
|||||||
function buildBroadcastButton() {
|
function buildBroadcastButton() {
|
||||||
const wrap = document.getElementById("team-broadcast-wrap");
|
const wrap = document.getElementById("team-broadcast-wrap");
|
||||||
if (!wrap) return;
|
if (!wrap) return;
|
||||||
if (!canBroadcast()) {
|
// Wait for /api/me so the affordance never flickers between admin (form)
|
||||||
|
// and non-admin (mailto) on initial paint. canBroadcast() already returns
|
||||||
|
// false when me is null but we'd briefly render the mailto anchor before
|
||||||
|
// the admin form, which is visually jarring.
|
||||||
|
if (!me) {
|
||||||
wrap.innerHTML = "";
|
wrap.innerHTML = "";
|
||||||
wrap.style.display = "none";
|
wrap.style.display = "none";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
wrap.style.display = "";
|
wrap.style.display = "";
|
||||||
wrap.innerHTML = `
|
const label = esc(t("team.broadcast.button") || "E-Mail an Auswahl");
|
||||||
<button type="button" class="btn btn-primary" id="team-broadcast-btn">
|
const counter = `<span class="team-broadcast-count" id="team-broadcast-count">0</span>`;
|
||||||
${esc(t("team.broadcast.button") || "E-Mail an Auswahl")} <span class="team-broadcast-count" id="team-broadcast-count">0</span>
|
if (canBroadcast()) {
|
||||||
</button>
|
// Admin path (global_admin or project-lead-of-selected): opens the
|
||||||
`;
|
// in-app compose modal that POSTs to /api/team/broadcast.
|
||||||
document.getElementById("team-broadcast-btn")?.addEventListener("click", () => onBroadcastClick());
|
wrap.innerHTML = `
|
||||||
|
<button type="button" class="btn btn-primary" id="team-broadcast-btn">
|
||||||
|
${label} ${counter}
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
document.getElementById("team-broadcast-btn")?.addEventListener("click", () => onBroadcastClick());
|
||||||
|
} else {
|
||||||
|
// Non-admin path (t-paliad-244): native mailto: anchor pre-filled with
|
||||||
|
// the current filter set. href is refreshed in updateBroadcastButton()
|
||||||
|
// whenever filters change so the link always reflects what's visible.
|
||||||
|
wrap.innerHTML = `
|
||||||
|
<a class="btn btn-primary" id="team-broadcast-btn" href="mailto:">
|
||||||
|
${label} ${counter}
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateBroadcastButton() {
|
function updateBroadcastButton() {
|
||||||
buildBroadcastButton();
|
buildBroadcastButton();
|
||||||
|
const recipients = displayedRecipients();
|
||||||
const countEl = document.getElementById("team-broadcast-count");
|
const countEl = document.getElementById("team-broadcast-count");
|
||||||
if (countEl) {
|
if (countEl) countEl.textContent = String(recipients.length);
|
||||||
const n = displayedRecipients().length;
|
const btn = document.getElementById("team-broadcast-btn");
|
||||||
countEl.textContent = String(n);
|
if (!btn) return;
|
||||||
const btn = document.getElementById("team-broadcast-btn") as HTMLButtonElement | null;
|
if (btn.tagName === "BUTTON") {
|
||||||
if (btn) btn.disabled = n === 0;
|
(btn as HTMLButtonElement).disabled = recipients.length === 0;
|
||||||
|
} else {
|
||||||
|
// Anchor (non-admin): regenerate the mailto: href against the current
|
||||||
|
// visible recipients, and disable the affordance when empty so a click
|
||||||
|
// doesn't open an empty mail composer.
|
||||||
|
const a = btn as HTMLAnchorElement;
|
||||||
|
if (recipients.length === 0) {
|
||||||
|
a.setAttribute("href", "mailto:");
|
||||||
|
a.setAttribute("aria-disabled", "true");
|
||||||
|
a.style.pointerEvents = "none";
|
||||||
|
a.style.opacity = "0.5";
|
||||||
|
} else {
|
||||||
|
a.setAttribute("href", buildMailtoHref(recipients));
|
||||||
|
a.removeAttribute("aria-disabled");
|
||||||
|
a.style.pointerEvents = "";
|
||||||
|
a.style.opacity = "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -673,14 +709,21 @@ function renderSelectionFooter(): void {
|
|||||||
"{n}",
|
"{n}",
|
||||||
String(n),
|
String(n),
|
||||||
);
|
);
|
||||||
|
const sendLabel = esc(t("team.selection.send") || "E-Mail an Auswahl");
|
||||||
|
// t-paliad-244: mirror buildBroadcastButton() so the bottom send button
|
||||||
|
// behaves the same as the filter-bar one. Admin (canBroadcast) opens the
|
||||||
|
// compose modal; non-admin gets a native mailto: anchor pre-filled with
|
||||||
|
// the explicit selection.
|
||||||
|
const adminPath = canBroadcast();
|
||||||
|
const sendAction = adminPath
|
||||||
|
? `<button type="button" class="btn-primary" id="team-selection-send">${sendLabel}</button>`
|
||||||
|
: `<a class="btn-primary" id="team-selection-send" href="${buildMailtoHref(selectedRecipients())}">${sendLabel}</a>`;
|
||||||
footer.innerHTML = `
|
footer.innerHTML = `
|
||||||
<span class="team-selection-count">${esc(countLabel)}</span>
|
<span class="team-selection-count">${esc(countLabel)}</span>
|
||||||
<button type="button" class="btn-secondary btn-small" id="team-selection-clear">
|
<button type="button" class="btn-secondary btn-small" id="team-selection-clear">
|
||||||
${esc(t("team.selection.clear") || "Auswahl aufheben")}
|
${esc(t("team.selection.clear") || "Auswahl aufheben")}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn-primary" id="team-selection-send">
|
${sendAction}
|
||||||
${esc(t("team.selection.send") || "E-Mail an Auswahl")}
|
|
||||||
</button>
|
|
||||||
`;
|
`;
|
||||||
footer.style.display = "";
|
footer.style.display = "";
|
||||||
document.body.classList.add("team-has-selection");
|
document.body.classList.add("team-has-selection");
|
||||||
@@ -691,9 +734,12 @@ function renderSelectionFooter(): void {
|
|||||||
syncMasterCheckbox();
|
syncMasterCheckbox();
|
||||||
renderSelectionFooter();
|
renderSelectionFooter();
|
||||||
});
|
});
|
||||||
document.getElementById("team-selection-send")?.addEventListener("click", () => {
|
if (adminPath) {
|
||||||
onBroadcastFromSelection();
|
document.getElementById("team-selection-send")?.addEventListener("click", () => {
|
||||||
});
|
onBroadcastFromSelection();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Anchor path has no click handler — native href open is the action.
|
||||||
}
|
}
|
||||||
|
|
||||||
// selectedRecipients maps the explicit selection Set into the
|
// selectedRecipients maps the explicit selection Set into the
|
||||||
|
|||||||
Reference in New Issue
Block a user