Files
zeekayandHanzo Dev f77d5010d8 ee: delete the relocated upstream commercial code and its paywall
Papermark dual-licenses: AGPL-3.0 for the tree, a separate commercial
licence for ee/. We hold the first and not the second, so none of the
second may live here.

ee/ was already absent from main, but it was never deleted. 98f0da7fa
("ci: amd64-only build") renamed 150 files out of ee/ byte-identical
into features/, lib/billing/legacy/ (ee/stripe), lib/billing/limits/
(ee/limits), and flattened app/(ee)/ into app/ — putting commercially
licensed code under our AGPL root LICENSE. A rename is not a deletion.
Every path that commit moved is now gone, edited-since or not.

Deleted, with their routes and UI entry points: features/ai and
app/api/ai, features/workflows and app/api/{workflows,workflow-entry},
features/conversations and the Q&A/FAQ surface, features/security/sso
with app/api/auth/saml + app/api/scim + lib/jackson.ts (we use Hanzo
IAM OIDC natively), features/dataroom-invitations, features/templates,
features/access-notifications, features/conversions, features/
permissions. Five of these were gated by lib/featureFlags, which reads
@vercel/edge-config and returns false for every flag when EDGE_CONFIG
is unset — every non-Vercel deployment we run. Those flags are gone too.

Deleted the commercial gating outright rather than reimplementing it:
lib/billing/legacy (ee/stripe) and lib/billing/limits (ee/limits), the
upgrade modals and banners, UpgradeButton, FeaturePreview, PlanBadge,
the trial and blocking modals, /settings/upgrade, /settings/billing and
the billing checkout/seat/pause/cancel API. usePlan() no longer returns
capability booleans — the plan tag describes provisioning, it is not a
permission — and useLimits() is gone with every quota check it fed.
Subscription pause went the same way. Also dropped the npm dependencies
that existed only for the removed features (@boxyhq/saml-jackson, the
@ai-sdk packages, openai, streamdown, use-stick-to-bottom,
@trigger.dev/python) and the Jackson-specific next.config webpack shims.

Rewritten as original Hanzo code where a capability was load-bearing:
- lib/utils/get-file-size-limits.ts — one UPLOAD_LIMITS set plus
  getFileSizeLimit(contentType), env-overridable. Operational limits,
  not commercial ones, so the tus and browser-upload paths still refuse
  oversized files without consulting a plan matrix.
- components/links/link-sheet/permissions-sheet.tsx — a flat, depth
  annotated item list with view/download switches over the same
  /permission-groups API. Two states, not three. ItemPermission now has
  a single definition in lib/types.ts.
- app/api/links/[id]/upload/route.ts — visitor uploads, built around one
  authorize() helper shared by GET and POST, and a notification debounce
  keyed per (link, viewer) instead of listing and cancelling runs.

There is now exactly one link sheet: the ee DataroomLinkSheet duplicated
the AGPL LinkSheet, which already handled DATAROOM_LINK. Replaced
upstream's Omit<LinkType, "WORKFLOW_LINK"> — Omit on a string union
yields an object type and never narrowed anything — with a real union.

Verified: the S3/tus upload+download harness passes 13/13 against MinIO
after the deletions, and tsc goes from 257 pre-existing errors on
origin/main to 192 with zero files worse than baseline.

Reasoning recorded in LLM.md. LICENSE untouched.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-03 20:19:21 -07:00

216 lines
6.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from "react";
import { useTeam } from "@/context/team-context";
import { toast } from "sonner";
import { mutate } from "swr";
import { useAnalytics } from "@/lib/analytics";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export function AddViewerModal({
dataroomId,
open,
setOpen,
children,
}: {
dataroomId: string;
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
children?: React.ReactNode;
}) {
const [emails, setEmails] = useState<string[]>([]);
const [inputValue, setInputValue] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const teamInfo = useTeam();
const analytics = useAnalytics();
// Email validation regex pattern
const validateEmail = (email: string) => {
return email.match(
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
);
};
// const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// const val = e.target.value;
// setInputValue(val);
// if (val.endsWith(",")) {
// const newEmail = val.slice(0, -1).trim(); // Remove the comma and trim whitespace
// if (validateEmail(newEmail) && !emails.includes(newEmail)) {
// setEmails([...emails, newEmail]); // Add the new email if it's valid and not already in the list
// setInputValue(""); // Reset input field
// }
// }
// };
// const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// const inputValue = e.target.value;
// setInputValue(inputValue);
// // Split the input value by commas to support pasting comma-separated emails
// const potentialEmails = inputValue
// .split(",")
// .map((email) => email.trim())
// .filter((email) => email);
// const newEmails: string[] = [];
// potentialEmails.forEach((email) => {
// // Check if the email is valid and not already included
// if (validateEmail(email) && !emails.includes(email)) {
// newEmails.push(email);
// }
// });
// // If there are new valid emails, update the state
// if (newEmails.length > 0) {
// setEmails([...emails, ...newEmails]);
// setInputValue(""); // Reset input field only if new emails were added
// }
// };
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const inputValue = e.target.value;
setInputValue(inputValue);
};
const handleInputBlurOrComma = () => {
// Split the current input value by commas in case of pasting multiple emails
const potentialEmails = inputValue
.split(",")
.map((email) => email.trim().toLowerCase())
.filter((email) => email);
const newEmails = potentialEmails.filter((email) => {
// Validate each email and check it's not already in the list
return validateEmail(email) && !emails.includes(email);
});
if (newEmails.length > 0) {
setEmails([...emails, ...newEmails]); // Add new valid emails to the list
setInputValue(""); // Clear input field
}
};
const removeEmail = (index: number) => {
setEmails(emails.filter((_, i) => i !== index));
};
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
event.stopPropagation();
if (emails.length === 0) return;
if (emails.length > 5) {
toast.error(
"You can only send invitations to a maximum of 5 emails at a time.",
);
return;
}
setLoading(true);
// POST request with multiple emails
const response = await fetch(
`/api/teams/${teamInfo?.currentTeam?.id}/datarooms/${dataroomId}/users`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
emails: emails,
}),
},
);
if (!response.ok) {
const error = await response.json();
setLoading(false);
setOpen(false);
toast.error(error.message || "Failed to send invitations.");
return;
}
analytics.capture("Dataroom View Invitation Sent", {
inviteeCount: emails.length,
teamId: teamInfo?.currentTeam?.id,
dataroomId: dataroomId,
});
mutate(
`/api/teams/${teamInfo?.currentTeam?.id}/datarooms/${dataroomId}/viewers`,
);
toast.success("Invitation emails have been sent!");
setOpen(false);
setLoading(false);
setEmails([]); // Reset emails state
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader className="text-start">
<DialogTitle>Invite Visitors</DialogTitle>
<DialogDescription>
Enter email addresses, separated by commas.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit}>
<Label htmlFor="email" className="opacity-80">
Emails
</Label>
<div className="flex flex-wrap gap-2 py-2 text-sm">
{emails.map((email, index) => (
<div
key={index}
className="flex items-center gap-2 rounded bg-gray-100 px-2 py-1"
>
{email}
<button type="button" onClick={() => removeEmail(index)}>
×
</button>
</div>
))}
<Input
id="email"
value={inputValue}
placeholder="[email protected]"
className="flex-1"
onChange={handleInputChange}
onBlur={handleInputBlurOrComma} // Handle when input loses focus
onKeyDown={(e) => {
if (e.key === ",") {
e.preventDefault(); // Prevent the comma from being added to the input
handleInputBlurOrComma(); // Act as if the input lost focus to process the email
}
}}
/>
</div>
<DialogFooter>
<Button type="submit" className="mt-8 h-9 w-full" loading={loading}>
{loading ? "Sending emails..." : "Add members"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}