Files
zeekay 9dff9e667e rip: enterprise dirs + paywalls — full AGPL, no commercial-license code
Drop the dual-licensed `ee/` tree entirely. All features previously
locked behind the Hanzo Dataroom Commercial License are now part of the
AGPL-3.0 core and freely available — custom domains, branded viewing,
advanced analytics, watermarks, access controls, Q&A, AI vector stores,
workflows, SAML SSO, dataroom invitations, conversations, templates.

Layout changes:
* `ee/features/{ai,conversations,workflows,permissions,storage,templates,
  dataroom-invitations,access-notifications,security,conversions}/` →
  `features/<name>/` (AGPL).
* `ee/limits/` → `lib/billing/limits/` with every plan resolving to an
  unlimited profile (no users/links/documents/domains/datarooms cap,
  conversations and watermarks always on).
* `ee/stripe/` → `lib/billing/legacy/` (already a compat shim over
  `@hanzoai/commerce`; renewal-reminder webhook is now a no-op).
* `ee/features/billing/cancellation/` deleted. Pause/unpause/cancel/
  retention routes now compile against `lib/billing/cancellation.ts`
  stubs that return 410 Gone; `isTeamPaused`/`isTeamPausedById` in
  `lib/billing/paused.ts` always report active. `CancellationModal` is
  a no-op shim in `components/billing/`.
* `app/(ee)/api/*` route group folded into `app/api/*` (SAML auth,
  SCIM, AI chat, workflows, FAQ, link-upload).

Build: `npm run build` green.
2026-06-07 11:46:48 -07:00

293 lines
9.0 KiB
TypeScript

import React, { useEffect, useState } from "react";
import {
editFAQFormSchema,
faqParamSchema,
} from "@/features/conversations/lib/schemas/faq";
import { BookOpen, Check, FileText, Link2 } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { PublishedFAQ } from "../../pages/faq-overview";
interface EditFAQModalProps {
faq: PublishedFAQ;
dataroomId: string;
teamId: string;
isOpen: boolean;
onClose: () => void;
onSuccess: () => void;
}
interface EditFAQFormData {
editedQuestion: string;
answer: string;
visibilityMode: "PUBLIC_DATAROOM" | "PUBLIC_LINK" | "PUBLIC_DOCUMENT";
}
const visibilityOptions = [
{
value: "PUBLIC_DATAROOM" as const,
label: "Entire Dataroom",
description: "Visible to all dataroom visitors",
icon: BookOpen,
},
{
value: "PUBLIC_LINK" as const,
label: "Specific Link",
description: "Visible only to visitors from this link",
icon: Link2,
},
{
value: "PUBLIC_DOCUMENT" as const,
label: "Specific Document",
description: "Visible only when viewing this document",
icon: FileText,
},
];
export function EditFAQModal({
faq,
dataroomId,
teamId,
isOpen,
onClose,
onSuccess,
}: EditFAQModalProps) {
const [isUpdating, setIsUpdating] = useState(false);
const [formData, setFormData] = useState<EditFAQFormData>({
editedQuestion: faq.editedQuestion,
answer: faq.answer,
visibilityMode: faq.visibilityMode,
});
// Update form data when FAQ changes
useEffect(() => {
if (faq) {
setFormData({
editedQuestion: faq.editedQuestion,
answer: faq.answer,
visibilityMode: faq.visibilityMode,
});
}
}, [faq]);
const handleInputChange = (field: keyof EditFAQFormData, value: string) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
const handleUpdate = async (e: React.FormEvent) => {
e.preventDefault();
try {
// Validate API parameters
const paramValidation = faqParamSchema.safeParse({
teamId,
id: dataroomId,
faqId: faq.id,
});
if (!paramValidation.success) {
toast.error("Invalid team, dataroom, or FAQ ID");
return;
}
// Validate form data
const formValidation = editFAQFormSchema.safeParse(formData);
if (!formValidation.success) {
const firstError = formValidation.error.errors[0];
toast.error(firstError.message);
return;
}
setIsUpdating(true);
const validatedData = formValidation.data;
const response = await fetch(
`/api/teams/${paramValidation.data.teamId}/datarooms/${paramValidation.data.id}/faqs/${paramValidation.data.faqId}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(validatedData),
},
);
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || error.details || "Failed to update FAQ");
}
toast.success("FAQ updated successfully!");
onSuccess();
onClose();
} catch (error) {
console.error("Error updating FAQ:", error);
toast.error(
error instanceof Error ? error.message : "Failed to update FAQ",
);
} finally {
setIsUpdating(false);
}
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-h-[90vh] overflow-hidden sm:max-w-5xl md:w-full">
<DialogHeader>
<DialogTitle>Edit FAQ</DialogTitle>
<DialogDescription>
Update the question and answer content. You can see the original
messages below.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleUpdate} className="space-y-6">
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
{/* Original Messages Preview */}
<div className="space-y-4">
<div>
<label className="text-sm font-medium text-gray-700">
Original Question
</label>
<div className="mt-2 rounded-lg border border-blue-200 bg-blue-50/50 p-3">
<div className="flex items-start justify-between">
<p className="flex-1 text-sm">
{faq.questionMessage?.content ||
faq.originalQuestion ||
"No original question available"}
</p>
<Check className="ml-2 h-4 w-4 text-blue-600" />
</div>
<p className="mt-1 text-xs text-gray-500">
Original visitor message
</p>
</div>
</div>
<div>
<label className="text-sm font-medium text-gray-700">
Original Answer
</label>
<div className="mt-2 rounded-lg border border-green-200 bg-green-50/50 p-3">
<div className="flex items-start justify-between">
<p className="flex-1 text-sm">
{faq.answerMessage?.content ||
"No original answer available"}
</p>
<Check className="ml-2 h-4 w-4 text-green-600" />
</div>
<p className="mt-1 text-xs text-gray-500">
Original admin response
</p>
</div>
</div>
</div>
{/* FAQ Details */}
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="editedQuestion">Question *</Label>
<Textarea
id="editedQuestion"
placeholder="Edit the question for clarity..."
className="min-h-[80px]"
value={formData.editedQuestion}
onChange={(e) =>
handleInputChange("editedQuestion", e.target.value)
}
/>
<p className="text-sm text-gray-500">
You can edit the question to make it clearer for visitors.
</p>
</div>
<div className="space-y-2">
<Label htmlFor="answer">Answer *</Label>
<Textarea
id="answer"
placeholder="Edit the answer if needed..."
className="min-h-[80px]"
value={formData.answer}
onChange={(e) => handleInputChange("answer", e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="visibilityMode">Visibility</Label>
<Select
value={formData.visibilityMode}
onValueChange={(value) =>
handleInputChange(
"visibilityMode",
value as EditFAQFormData["visibilityMode"],
)
}
>
<SelectTrigger className="h-12">
<SelectValue placeholder="Select visibility scope" />
</SelectTrigger>
<SelectContent>
{visibilityOptions.map((option) => (
<SelectItem
key={option.value}
value={option.value}
disabled={
option.value === "PUBLIC_DOCUMENT" &&
!faq.dataroomDocument
}
>
<div className="flex items-center space-x-4">
<option.icon className="h-4 w-4" />
<div className="flex flex-col items-start">
<div className="font-medium">{option.label}</div>
<div className="text-xs text-gray-500">
{option.description}
</div>
</div>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={onClose}>
Cancel
</Button>
<Button type="submit" variant="default" disabled={isUpdating}>
{isUpdating ? "Updating..." : "Update FAQ"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}