Invite people after review, then finish the Rule, with login before inviting and Invite your members on Profile.
This commit is contained in:
1 parent
88bfccd1e7
commit
70ceb73104
31 files changed
+1388
-401
No files matched your search
@@ -1,34 +1,84 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import MultiSelect from "../../../../components/controls/MultiSelect";
|
||||
import Alert from "../../../../components/modals/Alert";
|
||||
import type { ChipOption } from "../../../../components/controls/MultiSelect/MultiSelect.types";
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
type ClipboardEvent,
|
||||
} from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import Button from "../../../../components/buttons/Button";
|
||||
import InlineTextButton from "../../../../components/buttons/InlineTextButton";
|
||||
import TextInput from "../../../../components/controls/TextInput";
|
||||
import Icon from "../../../../components/asset/icon";
|
||||
import { ASSETS, getAssetPath } from "../../../../../lib/assetUtils";
|
||||
import { useTranslation } from "../../../../contexts/MessagesContext";
|
||||
import { fetchAuthSession } from "../../../../../lib/create/api";
|
||||
import { membershipInvitePreview } from "../../../../../lib/create/membershipInvitePreview";
|
||||
import {
|
||||
INVITE_EMAIL_PATTERN,
|
||||
formatInviteCountWord,
|
||||
parseInvitePaste,
|
||||
} from "../../../../../lib/create/parseInviteEmails";
|
||||
import { MAX_STAKEHOLDER_EMAILS } from "../../../../../lib/create/stakeholderLimits";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import { MembershipInvitePreviewCard } from "../../components/MembershipInvitePreviewCard";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
|
||||
import {
|
||||
CREATE_FLOW_MANAGE_STAKEHOLDERS_QUERY,
|
||||
CREATE_FLOW_MANAGE_STAKEHOLDERS_VALUE,
|
||||
} from "../../utils/flowSteps";
|
||||
import { createFlowStepPath } from "../../utils/createFlowPaths";
|
||||
import { PublishedStakeholdersManagePanel } from "./PublishedStakeholdersManagePanel";
|
||||
|
||||
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
const VISIBLE_WHEN_COLLAPSED = 6;
|
||||
|
||||
function emailsToChipOptions(emails: string[]): ChipOption[] {
|
||||
return emails.map((email) => ({
|
||||
id: email,
|
||||
label: email,
|
||||
state: "selected" as const,
|
||||
}));
|
||||
function InvitePersonRow({
|
||||
email,
|
||||
youLabel,
|
||||
removeAria,
|
||||
onRemove,
|
||||
}: {
|
||||
email: string;
|
||||
youLabel?: string;
|
||||
removeAria: string;
|
||||
onRemove?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex w-full items-center gap-[var(--space-300)] border-b border-[var(--color-border-default-secondary)] py-[var(--space-200)]">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-[var(--space-200)]">
|
||||
<p className="truncate text-medium-paragraph text-[var(--color-content-default-primary)]">
|
||||
{email}
|
||||
</p>
|
||||
{youLabel ? (
|
||||
<p className="shrink-0 text-small-label text-[var(--color-content-default-tertiary)]">
|
||||
{youLabel}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
{onRemove ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRemove}
|
||||
aria-label={removeAria}
|
||||
className="flex size-[30px] shrink-0 items-center justify-center rounded-full bg-[var(--color-surface-default-secondary)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-invert-primary)]"
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element -- icon asset */}
|
||||
<img
|
||||
src={getAssetPath(ASSETS.ICON_CLOSE)}
|
||||
alt=""
|
||||
width={14}
|
||||
height={14}
|
||||
className="size-[14px]"
|
||||
/>
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ConfirmStakeholdersScreen() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { state, updateState, markCreateFlowInteraction } = useCreateFlow();
|
||||
const t = useTranslation("create.reviewAndComplete.confirmStakeholders");
|
||||
@@ -37,163 +87,271 @@ export function ConfirmStakeholdersScreen() {
|
||||
searchParams?.get(CREATE_FLOW_MANAGE_STAKEHOLDERS_QUERY) ===
|
||||
CREATE_FLOW_MANAGE_STAKEHOLDERS_VALUE;
|
||||
const editingPublishedRuleId = state.editingPublishedRuleId?.trim() ?? "";
|
||||
const managePublishedMode =
|
||||
manageStakeholdersIntent && editingPublishedRuleId.length > 0;
|
||||
|
||||
const [ownerEmail, setOwnerEmail] = useState<string | null>(null);
|
||||
const [draftEmail, setDraftEmail] = useState("");
|
||||
const [fieldError, setFieldError] = useState<string | null>(null);
|
||||
const [pasteResult, setPasteResult] = useState<string | null>(null);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!manageStakeholdersIntent) return;
|
||||
if (editingPublishedRuleId.length > 0) return;
|
||||
router.replace(createFlowStepPath("edit-rule"));
|
||||
}, [
|
||||
manageStakeholdersIntent,
|
||||
editingPublishedRuleId.length,
|
||||
router,
|
||||
]);
|
||||
let cancelled = false;
|
||||
void fetchAuthSession().then(({ user }) => {
|
||||
if (cancelled) return;
|
||||
const email = user?.email?.trim().toLowerCase() ?? "";
|
||||
setOwnerEmail(email.length > 0 ? email : null);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const persistedKey = (state.stakeholderEmails ?? []).join("\0");
|
||||
const [toastDismissed, setToastDismissed] = useState(false);
|
||||
const [chipError, setChipError] = useState<string | null>(null);
|
||||
const [stakeholderOptions, setStakeholderOptions] = useState<ChipOption[]>(
|
||||
() => emailsToChipOptions(state.stakeholderEmails ?? []),
|
||||
const invitees = useMemo(() => {
|
||||
const raw = state.stakeholderEmails ?? [];
|
||||
const owner = ownerEmail;
|
||||
return raw
|
||||
.map((email) => email.trim().toLowerCase())
|
||||
.filter((email) => email.length > 0 && email !== owner);
|
||||
}, [state.stakeholderEmails, ownerEmail]);
|
||||
|
||||
const rows = useMemo(() => {
|
||||
const list: Array<{ email: string; isYou: boolean }> = [];
|
||||
if (ownerEmail) {
|
||||
list.push({ email: ownerEmail, isYou: true });
|
||||
}
|
||||
for (const email of invitees) {
|
||||
list.push({ email, isYou: false });
|
||||
}
|
||||
return list;
|
||||
}, [ownerEmail, invitees]);
|
||||
|
||||
const visibleRows = expanded
|
||||
? rows
|
||||
: rows.slice(0, VISIBLE_WHEN_COLLAPSED);
|
||||
const hiddenCount = Math.max(0, rows.length - visibleRows.length);
|
||||
|
||||
const preview = membershipInvitePreview(state);
|
||||
|
||||
const countLine =
|
||||
rows.length === 1
|
||||
? t("countOne")
|
||||
: t("countMany").replace("{count}", String(rows.length));
|
||||
|
||||
const persistInvitees = useCallback(
|
||||
(next: string[]) => {
|
||||
updateState({ stakeholderEmails: next });
|
||||
},
|
||||
[updateState],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setStakeholderOptions((prev) => {
|
||||
const inFlight = prev.filter((c) => c.state === "custom");
|
||||
const nextPersisted = emailsToChipOptions(state.stakeholderEmails ?? []);
|
||||
return [...nextPersisted, ...inFlight];
|
||||
});
|
||||
}, [persistedKey]);
|
||||
const addOne = useCallback(
|
||||
(raw: string): boolean => {
|
||||
const trimmed = raw.trim().toLowerCase();
|
||||
if (!INVITE_EMAIL_PATTERN.test(trimmed)) {
|
||||
setFieldError(t("invalidEmail"));
|
||||
return false;
|
||||
}
|
||||
if (ownerEmail && trimmed === ownerEmail) {
|
||||
setFieldError(t("duplicateEmail"));
|
||||
return false;
|
||||
}
|
||||
if (invitees.includes(trimmed)) {
|
||||
setFieldError(t("duplicateEmail"));
|
||||
return false;
|
||||
}
|
||||
if (invitees.length >= MAX_STAKEHOLDER_EMAILS) {
|
||||
setFieldError(t("maxStakeholders"));
|
||||
return false;
|
||||
}
|
||||
persistInvitees([...invitees, trimmed]);
|
||||
return true;
|
||||
},
|
||||
[invitees, ownerEmail, persistInvitees, t],
|
||||
);
|
||||
|
||||
const handleAddStakeholder = () => {
|
||||
const handleAdd = () => {
|
||||
markCreateFlowInteraction();
|
||||
setChipError(null);
|
||||
const confirmed = state.stakeholderEmails ?? [];
|
||||
const customCount = stakeholderOptions.filter(
|
||||
(o) => o.state === "custom",
|
||||
).length;
|
||||
if (confirmed.length + customCount >= MAX_STAKEHOLDER_EMAILS) {
|
||||
setChipError(t("maxStakeholders"));
|
||||
return;
|
||||
setPasteResult(null);
|
||||
if (addOne(draftEmail)) {
|
||||
setDraftEmail("");
|
||||
setFieldError(null);
|
||||
}
|
||||
setStakeholderOptions((prev) => [
|
||||
...prev,
|
||||
{ id: crypto.randomUUID(), label: "", state: "custom" },
|
||||
]);
|
||||
};
|
||||
|
||||
const handleCustomChipConfirm = (chipId: string, value: string) => {
|
||||
const handlePaste = (event: ClipboardEvent<HTMLDivElement>) => {
|
||||
const text = event.clipboardData.getData("text");
|
||||
const tokens = text
|
||||
.split(/[\s,;]+/)
|
||||
.map((token) => token.trim())
|
||||
.filter((token) => token.length > 0);
|
||||
if (tokens.length <= 1) return;
|
||||
|
||||
event.preventDefault();
|
||||
markCreateFlowInteraction();
|
||||
setChipError(null);
|
||||
const trimmed = value.trim().toLowerCase();
|
||||
if (!EMAIL_PATTERN.test(trimmed)) {
|
||||
setChipError(t("invalidEmail"));
|
||||
return;
|
||||
setFieldError(null);
|
||||
|
||||
const existing = ownerEmail ? [ownerEmail, ...invitees] : [...invitees];
|
||||
const parsed = parseInvitePaste(text, existing, MAX_STAKEHOLDER_EMAILS);
|
||||
if (parsed.added.length > 0) {
|
||||
persistInvitees([...invitees, ...parsed.added]);
|
||||
}
|
||||
const current = state.stakeholderEmails ?? [];
|
||||
if (current.includes(trimmed)) {
|
||||
setChipError(t("duplicateEmail"));
|
||||
setStakeholderOptions((prev) => prev.filter((opt) => opt.id !== chipId));
|
||||
return;
|
||||
setDraftEmail(parsed.invalidRemainder);
|
||||
if (parsed.invalidRemainder.length > 0) {
|
||||
setFieldError(t("invalidEmail"));
|
||||
}
|
||||
if (current.length >= MAX_STAKEHOLDER_EMAILS) {
|
||||
setChipError(t("maxStakeholders"));
|
||||
setStakeholderOptions((prev) => prev.filter((opt) => opt.id !== chipId));
|
||||
return;
|
||||
|
||||
const parts: string[] = [];
|
||||
if (parsed.added.length > 0) {
|
||||
const people =
|
||||
parsed.added.length === 1 ? t("peopleNounOne") : t("peopleNounMany");
|
||||
parts.push(
|
||||
t("pasteAdded")
|
||||
.replace("{count}", formatInviteCountWord(parsed.added.length))
|
||||
.replace("{people}", people),
|
||||
);
|
||||
}
|
||||
setStakeholderOptions((prev) =>
|
||||
prev.map((opt) =>
|
||||
opt.id === chipId
|
||||
? { id: trimmed, label: trimmed, state: "selected" as const }
|
||||
: opt,
|
||||
),
|
||||
);
|
||||
updateState({ stakeholderEmails: [...current, trimmed] });
|
||||
const invalidCount = parsed.invalidRemainder
|
||||
.split(",")
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item.length > 0).length;
|
||||
if (invalidCount === 1) {
|
||||
parts.push(t("pasteInvalidOne"));
|
||||
} else if (invalidCount > 1) {
|
||||
parts.push(
|
||||
t("pasteInvalidMany").replace(
|
||||
"{count}",
|
||||
formatInviteCountWord(invalidCount),
|
||||
),
|
||||
);
|
||||
}
|
||||
setPasteResult(parts.length > 0 ? parts.join(" ") : null);
|
||||
};
|
||||
|
||||
const handleCustomChipClose = (chipId: string) => {
|
||||
const handleRemove = (email: string) => {
|
||||
markCreateFlowInteraction();
|
||||
setChipError(null);
|
||||
setStakeholderOptions((prev) => prev.filter((opt) => opt.id !== chipId));
|
||||
setPasteResult(null);
|
||||
persistInvitees(invitees.filter((item) => item !== email));
|
||||
};
|
||||
|
||||
const handleChipClick = (chipId: string) => {
|
||||
markCreateFlowInteraction();
|
||||
setChipError(null);
|
||||
setStakeholderOptions((prev) => prev.filter((opt) => opt.id !== chipId));
|
||||
updateState({
|
||||
stakeholderEmails: (state.stakeholderEmails ?? []).filter(
|
||||
(e) => e !== chipId,
|
||||
),
|
||||
});
|
||||
};
|
||||
const header = (
|
||||
<div className="flex w-full min-w-0 flex-col gap-[var(--space-800)]">
|
||||
<CreateFlowHeaderLockup
|
||||
title={t("title")}
|
||||
description={t("description")}
|
||||
justification="left"
|
||||
/>
|
||||
{preview ? (
|
||||
<MembershipInvitePreviewCard
|
||||
preview={preview}
|
||||
fromYourRuleLabel={t("fromYourRule")}
|
||||
whoBelongsLabel={t("whoBelongsLabel")}
|
||||
joiningLabel={t("joiningLabel")}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (managePublishedMode) {
|
||||
if (manageStakeholdersIntent) {
|
||||
return (
|
||||
<CreateFlowTwoColumnSelectShell
|
||||
header={
|
||||
<CreateFlowHeaderLockup
|
||||
title={t("managePublished.lockupTitle")}
|
||||
description={t("managePublished.lockupDescription")}
|
||||
justification="left"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<PublishedStakeholdersManagePanel ruleId={editingPublishedRuleId} />
|
||||
<CreateFlowTwoColumnSelectShell header={header} lgVerticalAlign="start">
|
||||
{editingPublishedRuleId.length > 0 ? (
|
||||
<PublishedStakeholdersManagePanel ruleId={editingPublishedRuleId} />
|
||||
) : null}
|
||||
</CreateFlowTwoColumnSelectShell>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateFlowTwoColumnSelectShell
|
||||
header={
|
||||
<CreateFlowHeaderLockup
|
||||
title={t("title")}
|
||||
description={t("description")}
|
||||
justification="left"
|
||||
/>
|
||||
}
|
||||
>
|
||||
{chipError ? (
|
||||
<p
|
||||
className="text-small-paragraph text-[var(--color-border-default-utility-negative)]"
|
||||
role="alert"
|
||||
>
|
||||
{chipError}
|
||||
<CreateFlowTwoColumnSelectShell header={header} lgVerticalAlign="start">
|
||||
<div className="flex w-full min-w-0 flex-col gap-[var(--space-600)]">
|
||||
<div
|
||||
className="flex w-full items-start gap-[var(--space-200)]"
|
||||
onPasteCapture={handlePaste}
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<TextInput
|
||||
id="invite-member-email"
|
||||
type="email"
|
||||
inputSize="small"
|
||||
showHelpIcon={false}
|
||||
label={t("emailLabel")}
|
||||
placeholder={t("emailPlaceholder")}
|
||||
value={draftEmail}
|
||||
autoComplete="email"
|
||||
autoFocus
|
||||
error={Boolean(fieldError)}
|
||||
textHint={fieldError || false}
|
||||
onChange={(event) => {
|
||||
setDraftEmail(event.target.value);
|
||||
setFieldError(null);
|
||||
setPasteResult(null);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter") return;
|
||||
event.preventDefault();
|
||||
handleAdd();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="shrink-0 pt-[29px]">
|
||||
<Button
|
||||
type="button"
|
||||
size="small"
|
||||
buttonType="outline"
|
||||
palette="default"
|
||||
onClick={handleAdd}
|
||||
>
|
||||
{t("add")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{pasteResult ? (
|
||||
<div className="flex items-center gap-2" role="status">
|
||||
<Icon name="warning" size={24} />
|
||||
<p className="text-medium-label text-[var(--color-content-default-primary)]">
|
||||
{pasteResult}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{rows.length > 0 ? (
|
||||
<div className="flex w-full flex-col border-t border-[var(--color-border-default-secondary)]">
|
||||
{visibleRows.map((row) => (
|
||||
<InvitePersonRow
|
||||
key={row.isYou ? `you:${row.email}` : row.email}
|
||||
email={row.email}
|
||||
youLabel={row.isYou ? t("you") : undefined}
|
||||
removeAria={t("removeAria").replace("{email}", row.email)}
|
||||
onRemove={
|
||||
row.isYou ? undefined : () => handleRemove(row.email)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{hiddenCount > 0 ? (
|
||||
<div className="pt-[var(--space-300)] pb-[var(--space-100)]">
|
||||
<InlineTextButton
|
||||
onClick={() => {
|
||||
markCreateFlowInteraction();
|
||||
setExpanded(true);
|
||||
}}
|
||||
>
|
||||
{t("showMore").replace("{count}", String(hiddenCount))}
|
||||
</InlineTextButton>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{rows.length > 0 ? (
|
||||
<p className="text-medium-label text-[var(--color-content-default-primary)]">
|
||||
{countLine}
|
||||
</p>
|
||||
) : null}
|
||||
<MultiSelect
|
||||
formHeader={false}
|
||||
showHelpIcon={false}
|
||||
size="s"
|
||||
options={stakeholderOptions}
|
||||
onChipClick={handleChipClick}
|
||||
onAddClick={handleAddStakeholder}
|
||||
onCustomChipConfirm={handleCustomChipConfirm}
|
||||
onCustomChipClose={handleCustomChipClose}
|
||||
addButton
|
||||
addButtonText={t("addStakeholder")}
|
||||
/>
|
||||
</CreateFlowTwoColumnSelectShell>
|
||||
|
||||
{!toastDismissed && (
|
||||
<div
|
||||
className="fixed bottom-[5.25rem] left-1/2 z-10 w-[min(640px,calc(100%-2.5rem))] max-w-[640px] -translate-x-1/2 md:bottom-[5.5rem]"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<Alert
|
||||
type="banner"
|
||||
status="positive"
|
||||
title={t("draftToastTitle")}
|
||||
hasLeadingIcon={false}
|
||||
hasBodyText={false}
|
||||
onClose={() => setToastDismissed(true)}
|
||||
className="w-full !px-[var(--space-600)] !py-[var(--space-400)] md:!py-4"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
<p className="text-small-paragraph text-[var(--color-content-default-secondary)]">
|
||||
{t("privacy")}
|
||||
</p>
|
||||
</div>
|
||||
</CreateFlowTwoColumnSelectShell>
|
||||
);
|
||||
}
|
||||
@@ -129,28 +129,28 @@ export function PublishedStakeholdersManagePanel({
|
||||
{t("managePublished.empty")}
|
||||
</p>
|
||||
) : (
|
||||
<ul className="flex flex-col gap-3">
|
||||
<ul className="flex w-full flex-col border-t border-[var(--color-border-default-secondary)]">
|
||||
{items.map((row) => (
|
||||
<li
|
||||
key={row.id}
|
||||
className="flex flex-col gap-2 rounded-lg bg-black/5 px-3 py-3 md:flex-row md:items-center md:justify-between"
|
||||
className="flex items-center gap-[var(--space-300)] border-b border-[var(--color-border-default-secondary)] py-[var(--space-200)]"
|
||||
>
|
||||
<div className="flex min-w-0 flex-col gap-1">
|
||||
<span className="truncate text-sm font-medium text-[var(--color-content-default-primary)] md:text-base">
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
||||
<span className="truncate text-medium-paragraph text-[var(--color-content-default-primary)]">
|
||||
{row.email}
|
||||
</span>
|
||||
<span className="text-x-small-paragraph text-[var(--color-content-default-tertiary)] md:text-sm">
|
||||
<span className="text-x-small-paragraph text-[var(--color-content-default-tertiary)]">
|
||||
{row.status === "pending"
|
||||
? t("managePublished.pending")
|
||||
: t("managePublished.accepted")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className="flex shrink-0 flex-wrap justify-end gap-2">
|
||||
{row.status === "pending" ? (
|
||||
<Button
|
||||
type="button"
|
||||
size="small"
|
||||
buttonType="outline"
|
||||
buttonType="ghost"
|
||||
palette="default"
|
||||
disabled={busyId === row.id}
|
||||
onClick={() => void handleResend(row.id)}
|
||||
@@ -165,7 +165,7 @@ export function PublishedStakeholdersManagePanel({
|
||||
<Button
|
||||
type="button"
|
||||
size="small"
|
||||
buttonType="outline"
|
||||
buttonType="ghost"
|
||||
palette="default"
|
||||
disabled={busyId === row.id}
|
||||
onClick={() => void handleRemove(row.id)}
|
||||
@@ -182,7 +182,7 @@ export function PublishedStakeholdersManagePanel({
|
||||
</ul>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-end md:gap-3">
|
||||
<div className="flex items-start gap-[var(--space-200)]">
|
||||
<div className="min-w-0 flex-1">
|
||||
<TextInput
|
||||
id="published-stakeholder-email"
|
||||
@@ -201,17 +201,18 @@ export function PublishedStakeholdersManagePanel({
|
||||
autoComplete="email"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
size="small"
|
||||
buttonType="filled"
|
||||
palette="default"
|
||||
className="md:mb-[2px]"
|
||||
disabled={addBusy || items === null}
|
||||
onClick={() => void handleAdd()}
|
||||
>
|
||||
{t("managePublished.addInvite")}
|
||||
</Button>
|
||||
<div className="shrink-0 pt-[29px]">
|
||||
<Button
|
||||
type="button"
|
||||
size="small"
|
||||
buttonType="outline"
|
||||
palette="default"
|
||||
disabled={addBusy || items === null}
|
||||
onClick={() => void handleAdd()}
|
||||
>
|
||||
{t("managePublished.addInvite")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user