This commit is contained in:
pandeymangg
2026-05-05 16:13:12 +05:30
parent 62d09f6a8f
commit b5e6567194
18 changed files with 343 additions and 173 deletions
@@ -72,6 +72,9 @@ interface NavigationProps {
organizationWorkspacesLimit: number;
isLicenseActive: boolean;
isAccessControlAllowed: boolean;
isUnifyFeedbackAllowed: boolean;
isFeedbackDirectoriesAllowed: boolean;
isDashboardsAllowed: boolean;
}
export const MainNavigation = ({
@@ -86,6 +89,9 @@ export const MainNavigation = ({
organizationWorkspacesLimit,
isLicenseActive,
isAccessControlAllowed,
isUnifyFeedbackAllowed,
isFeedbackDirectoriesAllowed,
isDashboardsAllowed,
}: NavigationProps) => {
const router = useRouter();
const pathname = usePathname();
@@ -158,7 +164,7 @@ export const MainNavigation = ({
href: `/workspaces/${workspace.id}/unify/feedback-records`,
icon: MessageSquareTextIcon,
isActive: pathname?.includes("/unify/feedback-records"),
isHidden: false,
isHidden: !isUnifyFeedbackAllowed,
disabled: isMembershipPending || isBilling,
},
{
@@ -166,13 +172,13 @@ export const MainNavigation = ({
href: `/workspaces/${workspace.id}/dashboards`,
icon: BarChart3Icon,
isActive: pathname?.includes("/dashboards") || pathname?.includes("/charts"),
isHidden: false,
isHidden: !isDashboardsAllowed,
disabled: isMembershipPending || isBilling,
},
],
},
],
[t, workspace.id, pathname, isMembershipPending, isBilling]
[t, workspace.id, pathname, isMembershipPending, isBilling, isUnifyFeedbackAllowed, isDashboardsAllowed]
);
const settingsNavigationItem = useMemo(
@@ -464,6 +470,8 @@ export const MainNavigation = ({
isFormbricksCloud={isFormbricksCloud}
isCollapsed={false}
isTextVisible={false}
isUnifyFeedbackAllowed={isUnifyFeedbackAllowed}
isFeedbackDirectoriesAllowed={isFeedbackDirectoriesAllowed}
workspaces={workspaces}
isLoadingWorkspaces={isLoadingWorkspaces}
onWorkspaceChange={handleSettingsWorkspaceChange}
@@ -506,34 +514,36 @@ export const MainNavigation = ({
{/* Main Nav */}
<ul className="space-y-2">
{mainNavigationSections.map((section) => (
<li key={section.id}>
{!isCollapsed && !isTextVisible && (
<p className="px-4 pb-1 pt-2 text-xs font-semibold uppercase tracking-wide text-slate-400">
{section.name}
</p>
)}
<ul>
{section.items.map(
(item) =>
!item.isHidden && (
<NavigationLink
key={item.name}
href={item.href}
isActive={item.isActive}
isCollapsed={isCollapsed}
isTextVisible={isTextVisible}
disabled={item.disabled}
disabledMessage={item.disabled ? disabledNavigationMessage : undefined}
linkText={item.name}>
<item.icon strokeWidth={1.5} />
</NavigationLink>
)
{mainNavigationSections
.filter((section) => section.items.some((item) => !item.isHidden))
.map((section) => (
<li key={section.id}>
{!isCollapsed && !isTextVisible && (
<p className="px-4 pb-1 pt-2 text-xs font-semibold uppercase tracking-wide text-slate-400">
{section.name}
</p>
)}
</ul>
</li>
))}
<ul>
{section.items.map(
(item) =>
!item.isHidden && (
<NavigationLink
key={item.name}
href={item.href}
isActive={item.isActive}
isCollapsed={isCollapsed}
isTextVisible={isTextVisible}
disabled={item.disabled}
disabledMessage={item.disabled ? disabledNavigationMessage : undefined}
linkText={item.name}>
<item.icon strokeWidth={1.5} />
</NavigationLink>
)
)}
</ul>
</li>
))}
<li className={cn("mt-2 border-t border-slate-100 pt-2", isCollapsed && "border-t-0 pt-0")}>
<ul>
@@ -45,6 +45,8 @@ interface SettingsSidebarContentProps {
isFormbricksCloud: boolean;
isCollapsed: boolean;
isTextVisible: boolean;
isUnifyFeedbackAllowed: boolean;
isFeedbackDirectoriesAllowed: boolean;
// Workspace switcher
workspaces: { id: string; name: string }[];
isLoadingWorkspaces: boolean;
@@ -231,6 +233,8 @@ export const SettingsSidebarContent = ({
isFormbricksCloud,
isCollapsed,
isTextVisible,
isUnifyFeedbackAllowed,
isFeedbackDirectoriesAllowed,
workspaces,
isLoadingWorkspaces,
onWorkspaceChange,
@@ -283,6 +287,7 @@ export const SettingsSidebarContent = ({
href: `${basePath}/workspace/feedback-sources`,
icon: <ShapesIcon className={iconClassName} />,
disabled: isBilling,
hidden: !isUnifyFeedbackAllowed,
},
{
id: "integrations",
@@ -334,7 +339,7 @@ export const SettingsSidebarContent = ({
label: t("workspace.settings.feedback_directories.nav_label"),
href: `${basePath}/organization/feedback-directories`,
icon: <FoldersIcon className={iconClassName} />,
hidden: isMember,
hidden: isMember || !isFeedbackDirectoriesAllowed,
},
{
id: "org-api-keys",
@@ -26,6 +26,9 @@ export const WorkspaceLayout = async ({ layoutData, children }: WorkspaceLayoutP
membership,
workspace, // Current workspace details
isAccessControlAllowed,
isUnifyFeedbackAllowed,
isFeedbackDirectoriesAllowed,
isDashboardsAllowed,
workspacePermission,
license,
responseCount,
@@ -71,6 +74,9 @@ export const WorkspaceLayout = async ({ layoutData, children }: WorkspaceLayoutP
organizationWorkspacesLimit={organizationWorkspacesLimit}
isLicenseActive={active}
isAccessControlAllowed={isAccessControlAllowed}
isUnifyFeedbackAllowed={isUnifyFeedbackAllowed}
isFeedbackDirectoriesAllowed={isFeedbackDirectoriesAllowed}
isDashboardsAllowed={isDashboardsAllowed}
/>
<div id="mainContent" className="flex flex-1 flex-col overflow-hidden bg-slate-50">
<TopControlBar
@@ -1,29 +1 @@
import { notFound } from "next/navigation";
import { getTranslate } from "@/lingodotdev/server";
import { getFeedbackDirectoriesByWorkspaceId } from "@/modules/ee/feedback-directory/lib/feedback-directory";
import { getWorkspaceAuth } from "@/modules/workspaces/lib/utils";
import { TopicsSubtopicsPreview } from "./components/topics-subtopics-preview";
export default async function UnifyTopicsSubtopicsPage(
props: Readonly<{ params: Promise<{ workspaceId: string }> }>
) {
const t = await getTranslate();
const params = await props.params;
const { isOwner, isManager, hasReadAccess, hasReadWriteAccess, hasManageAccess, session } =
await getWorkspaceAuth(params.workspaceId);
if (!session) {
throw new Error(t("common.session_not_found"));
}
const hasAccess = isOwner || isManager || hasReadAccess || hasReadWriteAccess || hasManageAccess;
if (!hasAccess) {
return notFound();
}
const directories = await getFeedbackDirectoriesByWorkspaceId(params.workspaceId);
const directoryMap = Object.fromEntries(directories.map((directory) => [directory.id, directory.name]));
return <TopicsSubtopicsPreview workspaceId={params.workspaceId} directoryMap={directoryMap} />;
}
export { UnifyTopicsSubtopicsPage as default } from "@/modules/ee/unify-feedback/topics-subtopics/page";
+37 -4
View File
@@ -5,6 +5,7 @@ import { Output, generateText } from "ai";
import { z } from "zod";
import { type TChartQuery, ZChartQuery } from "@formbricks/types/analysis";
import { ZId } from "@formbricks/types/common";
import { OperationNotAllowedError } from "@formbricks/types/errors";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/context";
import { executeQuery } from "@/modules/ee/analysis/api/lib/cube-client";
@@ -21,6 +22,14 @@ import { checkWorkspaceAccess, verifyFeedbackDirectoryAccess } from "@/modules/e
import { generateSchemaContext } from "@/modules/ee/analysis/lib/ai-schema-context";
import { ZChartCreateInput, ZChartType, ZChartUpdateInput } from "@/modules/ee/analysis/types/analysis";
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
import { getIsDashboardsEnabled } from "@/modules/ee/license-check/lib/utils";
const checkDashboardsEnabled = async (organizationId: string) => {
const isAllowed = await getIsDashboardsEnabled(organizationId);
if (!isAllowed) {
throw new OperationNotAllowedError("Dashboards are not enabled for this organization");
}
};
/** Client-facing chart input (workspaceId and createdBy are resolved server-side) */
const ZChartCreateInputClient = ZChartCreateInput.omit({ workspaceId: true, createdBy: true });
@@ -46,6 +55,7 @@ export const createChartAction = authenticatedActionClient.inputSchema(ZCreateCh
parsedInput.workspaceId,
"readWrite"
);
await checkDashboardsEnabled(organizationId);
const chart = await createChart({
...parsedInput.chartInput,
@@ -84,6 +94,7 @@ export const updateChartAction = authenticatedActionClient.inputSchema(ZUpdateCh
parsedInput.workspaceId,
"readWrite"
);
await checkDashboardsEnabled(organizationId);
const { chart, updatedChart } = await updateChart(
parsedInput.chartId,
@@ -122,6 +133,7 @@ export const duplicateChartAction = authenticatedActionClient.inputSchema(ZDupli
parsedInput.workspaceId,
"readWrite"
);
await checkDashboardsEnabled(organizationId);
const duplicatedChart = await duplicateChart(parsedInput.chartId, workspaceId, ctx.user.id);
@@ -155,6 +167,7 @@ export const deleteChartAction = authenticatedActionClient.inputSchema(ZDeleteCh
parsedInput.workspaceId,
"readWrite"
);
await checkDashboardsEnabled(organizationId);
const chart = await deleteChart(parsedInput.chartId, workspaceId);
@@ -182,7 +195,12 @@ export const getChartAction = authenticatedActionClient
ctx: AuthenticatedActionClientCtx;
parsedInput: z.infer<typeof ZGetChartAction>;
}) => {
const { workspaceId } = await checkWorkspaceAccess(ctx.user.id, parsedInput.workspaceId, "read");
const { organizationId, workspaceId } = await checkWorkspaceAccess(
ctx.user.id,
parsedInput.workspaceId,
"read"
);
await checkDashboardsEnabled(organizationId);
return getChart(parsedInput.chartId, workspaceId);
}
@@ -202,7 +220,12 @@ export const getChartsAction = authenticatedActionClient
ctx: AuthenticatedActionClientCtx;
parsedInput: z.infer<typeof ZGetChartsAction>;
}) => {
const { workspaceId } = await checkWorkspaceAccess(ctx.user.id, parsedInput.workspaceId, "read");
const { organizationId, workspaceId } = await checkWorkspaceAccess(
ctx.user.id,
parsedInput.workspaceId,
"read"
);
await checkDashboardsEnabled(organizationId);
const charts = await getCharts(workspaceId);
return charts;
}
@@ -226,7 +249,12 @@ export const executeQueryAction = authenticatedActionClient
ctx: AuthenticatedActionClientCtx;
parsedInput: z.infer<typeof ZExecuteQueryAction>;
}) => {
const { workspaceId } = await checkWorkspaceAccess(ctx.user.id, parsedInput.workspaceId, "read");
const { organizationId, workspaceId } = await checkWorkspaceAccess(
ctx.user.id,
parsedInput.workspaceId,
"read"
);
await checkDashboardsEnabled(organizationId);
await verifyFeedbackDirectoryAccess(parsedInput.feedbackDirectoryId, workspaceId);
validateQueryMembers(parsedInput.query);
@@ -294,7 +322,12 @@ export const generateAIChartAction = authenticatedActionClient
ctx: AuthenticatedActionClientCtx;
parsedInput: z.infer<typeof ZGenerateAIChartAction>;
}) => {
const { workspaceId } = await checkWorkspaceAccess(ctx.user.id, parsedInput.workspaceId, "read");
const { organizationId, workspaceId } = await checkWorkspaceAccess(
ctx.user.id,
parsedInput.workspaceId,
"read"
);
await checkDashboardsEnabled(organizationId);
await verifyFeedbackDirectoryAccess(parsedInput.feedbackDirectoryId, workspaceId);
if (!process.env.OPENAI_API_KEY) {
@@ -1,5 +1,6 @@
import { use } from "react";
import { getConnectorsWithMappings } from "@/lib/connector/service";
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { getTranslate } from "@/lingodotdev/server";
import { ChartsList } from "@/modules/ee/analysis/charts/components/charts-list";
import { CreateChartButton } from "@/modules/ee/analysis/charts/components/create-chart-button";
@@ -9,6 +10,8 @@ import { NoFeedbackRecordsState } from "@/modules/ee/analysis/components/no-feed
import { hasFeedbackRecordsInDirectories } from "@/modules/ee/analysis/lib/feedback-records";
import type { TChartWithCreator } from "@/modules/ee/analysis/types/analysis";
import { getFeedbackDirectoriesByWorkspaceId } from "@/modules/ee/feedback-directory/lib/feedback-directory";
import { getIsDashboardsEnabled } from "@/modules/ee/license-check/lib/utils";
import { UpgradePrompt } from "@/modules/ui/components/upgrade-prompt";
import { getWorkspaceAuth } from "@/modules/workspaces/lib/utils";
interface ChartsListContentProps {
@@ -37,7 +40,37 @@ interface ChartsListPageProps {
export async function ChartsListPage({ workspaceId }: Readonly<ChartsListPageProps>) {
const t = await getTranslate();
const { isReadOnly } = await getWorkspaceAuth(workspaceId);
const { isReadOnly, organization } = await getWorkspaceAuth(workspaceId);
const isDashboardsAllowed = await getIsDashboardsEnabled(organization.id);
if (!isDashboardsAllowed) {
return (
<AnalysisPageLayout pageTitle={t("common.analysis")} workspaceId={workspaceId}>
<div className="flex items-center justify-center">
<UpgradePrompt
title={t("workspace.analysis.dashboards.upgrade_prompt_title")}
description={t("workspace.analysis.dashboards.upgrade_prompt_description")}
feature="dashboards"
buttons={[
{
text: IS_FORMBRICKS_CLOUD ? t("common.upgrade_plan") : t("common.request_trial_license"),
href: IS_FORMBRICKS_CLOUD
? `/workspaces/${workspaceId}/settings/organization/billing`
: "https://formbricks.com/upgrade-self-hosting-license",
},
{
text: t("common.learn_more"),
href: IS_FORMBRICKS_CLOUD
? `/workspaces/${workspaceId}/settings/organization/billing`
: "https://formbricks.com/learn-more-self-hosting-license",
},
]}
/>
</div>
</AnalysisPageLayout>
);
}
const [directories, connectors] = await Promise.all([
getFeedbackDirectoriesByWorkspaceId(workspaceId),
getConnectorsWithMappings(workspaceId),
@@ -50,25 +50,27 @@ export async function DashboardDetailPage({
if (!isDashboardsAllowed) {
return (
<AnalysisPageLayout pageTitle={t("common.analysis")} workspaceId={workspaceId}>
<UpgradePrompt
title={t("workspace.analysis.dashboards.upgrade_prompt_title")}
description={t("workspace.analysis.dashboards.upgrade_prompt_description")}
feature="dashboards"
buttons={[
{
text: IS_FORMBRICKS_CLOUD ? t("common.upgrade_plan") : t("common.request_trial_license"),
href: IS_FORMBRICKS_CLOUD
? `/workspaces/${workspaceId}/settings/billing`
: "https://formbricks.com/upgrade-self-hosting-license",
},
{
text: t("common.learn_more"),
href: IS_FORMBRICKS_CLOUD
? `/workspaces/${workspaceId}/settings/billing`
: "https://formbricks.com/learn-more-self-hosting-license",
},
]}
/>
<div className="flex items-center justify-center">
<UpgradePrompt
title={t("workspace.analysis.dashboards.upgrade_prompt_title")}
description={t("workspace.analysis.dashboards.upgrade_prompt_description")}
feature="dashboards"
buttons={[
{
text: IS_FORMBRICKS_CLOUD ? t("common.upgrade_plan") : t("common.request_trial_license"),
href: IS_FORMBRICKS_CLOUD
? `/workspaces/${workspaceId}/settings/organization/billing`
: "https://formbricks.com/upgrade-self-hosting-license",
},
{
text: t("common.learn_more"),
href: IS_FORMBRICKS_CLOUD
? `/workspaces/${workspaceId}/settings/organization/billing`
: "https://formbricks.com/learn-more-self-hosting-license",
},
]}
/>
</div>
</AnalysisPageLayout>
);
}
@@ -41,25 +41,27 @@ export const DashboardsListPage = async ({ workspaceId }: Readonly<DashboardsLis
if (!isDashboardsAllowed) {
return (
<AnalysisPageLayout pageTitle={t("common.analysis")} workspaceId={workspaceId}>
<UpgradePrompt
title={t("workspace.analysis.dashboards.upgrade_prompt_title")}
description={t("workspace.analysis.dashboards.upgrade_prompt_description")}
feature="dashboards"
buttons={[
{
text: IS_FORMBRICKS_CLOUD ? t("common.upgrade_plan") : t("common.request_trial_license"),
href: IS_FORMBRICKS_CLOUD
? `/workspaces/${workspaceId}/settings/billing`
: "https://formbricks.com/upgrade-self-hosting-license",
},
{
text: t("common.learn_more"),
href: IS_FORMBRICKS_CLOUD
? `/workspaces/${workspaceId}/settings/billing`
: "https://formbricks.com/learn-more-self-hosting-license",
},
]}
/>
<div className="flex items-center justify-center">
<UpgradePrompt
title={t("workspace.analysis.dashboards.upgrade_prompt_title")}
description={t("workspace.analysis.dashboards.upgrade_prompt_description")}
feature="dashboards"
buttons={[
{
text: IS_FORMBRICKS_CLOUD ? t("common.upgrade_plan") : t("common.request_trial_license"),
href: IS_FORMBRICKS_CLOUD
? `/workspaces/${workspaceId}/settings/organization/billing`
: "https://formbricks.com/upgrade-self-hosting-license",
},
{
text: t("common.learn_more"),
href: IS_FORMBRICKS_CLOUD
? `/workspaces/${workspaceId}/settings/organization/billing`
: "https://formbricks.com/learn-more-self-hosting-license",
},
]}
/>
</div>
</AnalysisPageLayout>
);
}
+21 -19
View File
@@ -19,25 +19,27 @@ export const FeedbackDirectoriesPage = async (props: { params: Promise<{ workspa
if (!isFeedbackDirectoriesAllowed) {
return (
<PageContentWrapper>
<UpgradePrompt
title={t("workspace.settings.feedback_directories.upgrade_prompt_title")}
description={t("workspace.settings.feedback_directories.upgrade_prompt_description")}
feature="feedback-directories"
buttons={[
{
text: IS_FORMBRICKS_CLOUD ? t("common.upgrade_plan") : t("common.request_trial_license"),
href: IS_FORMBRICKS_CLOUD
? `/workspaces/${params.workspaceId}/settings/billing`
: "https://formbricks.com/upgrade-self-hosting-license",
},
{
text: t("common.learn_more"),
href: IS_FORMBRICKS_CLOUD
? `/workspaces/${params.workspaceId}/settings/billing`
: "https://formbricks.com/learn-more-self-hosting-license",
},
]}
/>
<div className="flex items-center justify-center">
<UpgradePrompt
title={t("workspace.settings.feedback_directories.upgrade_prompt_title")}
description={t("workspace.settings.feedback_directories.upgrade_prompt_description")}
feature="feedback-directories"
buttons={[
{
text: IS_FORMBRICKS_CLOUD ? t("common.upgrade_plan") : t("common.request_trial_license"),
href: IS_FORMBRICKS_CLOUD
? `/workspaces/${params.workspaceId}/settings/organization/billing`
: "https://formbricks.com/upgrade-self-hosting-license",
},
{
text: t("common.learn_more"),
href: IS_FORMBRICKS_CLOUD
? `/workspaces/${params.workspaceId}/settings/organization/billing`
: "https://formbricks.com/learn-more-self-hosting-license",
},
]}
/>
</div>
</PageContentWrapper>
);
}
+26 -19
View File
@@ -6,9 +6,11 @@ import { getFeedbackDirectoriesByWorkspaceId } from "@/modules/ee/feedback-direc
import { getIsUnifyFeedbackEnabled } from "@/modules/ee/license-check/lib/utils";
import { listFeedbackRecords } from "@/modules/hub/service";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { UpgradePrompt } from "@/modules/ui/components/upgrade-prompt";
import { getWorkspaceAuth } from "@/modules/workspaces/lib/utils";
import { FeedbackRecordsPageClient } from "./components/feedback-records-page-client";
import { UnifyConfigNavigation } from "./components/unify-config-navigation";
const INITIAL_PAGE_SIZE = 50;
@@ -35,25 +37,30 @@ export const UnifyFeedbackRecordsPage = async (
if (!isUnifyFeedbackAllowed) {
return (
<PageContentWrapper>
<UpgradePrompt
title={t("workspace.unify.upgrade_prompt_title")}
description={t("workspace.unify.upgrade_prompt_description")}
feature="unify-feedback"
buttons={[
{
text: IS_FORMBRICKS_CLOUD ? t("common.upgrade_plan") : t("common.request_trial_license"),
href: IS_FORMBRICKS_CLOUD
? `/workspaces/${params.workspaceId}/settings/billing`
: "https://formbricks.com/upgrade-self-hosting-license",
},
{
text: t("common.learn_more"),
href: IS_FORMBRICKS_CLOUD
? `/workspaces/${params.workspaceId}/settings/billing`
: "https://formbricks.com/learn-more-self-hosting-license",
},
]}
/>
<PageHeader pageTitle={t("workspace.unify.feedback_records")}>
<UnifyConfigNavigation workspaceId={params.workspaceId} activeId="feedback-records" />
</PageHeader>
<div className="flex items-center justify-center">
<UpgradePrompt
title={t("workspace.unify.upgrade_prompt_title")}
description={t("workspace.unify.upgrade_prompt_description")}
feature="unify-feedback"
buttons={[
{
text: IS_FORMBRICKS_CLOUD ? t("common.upgrade_plan") : t("common.request_trial_license"),
href: IS_FORMBRICKS_CLOUD
? `/workspaces/${params.workspaceId}/settings/organization/billing`
: "https://formbricks.com/upgrade-self-hosting-license",
},
{
text: t("common.learn_more"),
href: IS_FORMBRICKS_CLOUD
? `/workspaces/${params.workspaceId}/settings/organization/billing`
: "https://formbricks.com/learn-more-self-hosting-license",
},
]}
/>
</div>
</PageContentWrapper>
);
}
@@ -142,7 +142,7 @@ export function CsvConnectorUI({
<thead className="bg-slate-50">
<tr>
{csvPreview[0]?.map((header, i) => (
<th key={i} className="px-3 py-2 text-left font-medium text-slate-700">
<th key={`${header}-${i}`} className="px-3 py-2 text-left font-medium text-slate-700">
{header}
</th>
))}
@@ -150,9 +150,11 @@ export function CsvConnectorUI({
</thead>
<tbody>
{csvPreview.slice(1, 4).map((row, rowIndex) => (
<tr key={rowIndex} className="border-t border-slate-100">
<tr key={`${rowIndex}-${row.join("|")}`} className="border-t border-slate-100">
{row.map((cell, cellIndex) => (
<td key={cellIndex} className="px-3 py-2 text-slate-600">
<td
key={`${csvPreview[0]?.[cellIndex] ?? cellIndex}-${cellIndex}`}
className="px-3 py-2 text-slate-600">
{cell || <span className="text-slate-300"></span>}
</td>
))}
@@ -20,6 +20,12 @@ interface DraggableSourceFieldProps {
isMapped: boolean;
}
const getSourceFieldStateClass = (isDragging: boolean, isMapped: boolean): string => {
if (isDragging) return "border-brand-dark bg-slate-100 opacity-50";
if (isMapped) return "border-green-300 bg-green-50 text-green-800";
return "border-slate-200 bg-white hover:border-slate-300";
};
export const DraggableSourceField = ({ field, isMapped }: DraggableSourceFieldProps) => {
const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({
id: field.id,
@@ -38,13 +44,7 @@ export const DraggableSourceField = ({ field, isMapped }: DraggableSourceFieldPr
style={style}
{...listeners}
{...attributes}
className={`flex cursor-grab items-center gap-2 rounded-md border p-2 text-sm transition-colors ${
isDragging
? "border-brand-dark bg-slate-100 opacity-50"
: isMapped
? "border-green-300 bg-green-50 text-green-800"
: "border-slate-200 bg-white hover:border-slate-300"
}`}>
className={`flex cursor-grab items-center gap-2 rounded-md border p-2 text-sm transition-colors ${getSourceFieldStateClass(isDragging, isMapped)}`}>
<GripVerticalIcon className="h-4 w-4 text-slate-400" />
<div className="flex-1 truncate">
<span className="font-medium">{field.name}</span>
@@ -33,25 +33,27 @@ export const WorkspaceFeedbackSourcesPage = async (
if (!isUnifyFeedbackAllowed) {
return (
<PageContentWrapper>
<UpgradePrompt
title={t("workspace.unify.upgrade_prompt_title")}
description={t("workspace.unify.upgrade_prompt_description")}
feature="unify-feedback"
buttons={[
{
text: IS_FORMBRICKS_CLOUD ? t("common.upgrade_plan") : t("common.request_trial_license"),
href: IS_FORMBRICKS_CLOUD
? `/workspaces/${params.workspaceId}/settings/billing`
: "https://formbricks.com/upgrade-self-hosting-license",
},
{
text: t("common.learn_more"),
href: IS_FORMBRICKS_CLOUD
? `/workspaces/${params.workspaceId}/settings/billing`
: "https://formbricks.com/learn-more-self-hosting-license",
},
]}
/>
<div className="flex items-center justify-center">
<UpgradePrompt
title={t("workspace.unify.upgrade_prompt_title")}
description={t("workspace.unify.upgrade_prompt_description")}
feature="unify-feedback"
buttons={[
{
text: IS_FORMBRICKS_CLOUD ? t("common.upgrade_plan") : t("common.request_trial_license"),
href: IS_FORMBRICKS_CLOUD
? `/workspaces/${params.workspaceId}/settings/organization/billing`
: "https://formbricks.com/upgrade-self-hosting-license",
},
{
text: t("common.learn_more"),
href: IS_FORMBRICKS_CLOUD
? `/workspaces/${params.workspaceId}/settings/organization/billing`
: "https://formbricks.com/learn-more-self-hosting-license",
},
]}
/>
</div>
</PageContentWrapper>
);
}
@@ -2,11 +2,13 @@
import { z } from "zod";
import { ZId } from "@formbricks/types/common";
import { OperationNotAllowedError } from "@formbricks/types/errors";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/context";
import { getOrganizationIdFromWorkspaceId } from "@/lib/utils/helper";
import { getFeedbackDirectoriesByWorkspaceId } from "@/modules/ee/feedback-directory/lib/feedback-directory";
import { getIsUnifyFeedbackEnabled } from "@/modules/ee/license-check/lib/utils";
import { semanticSearchFeedbackRecords } from "@/modules/hub/service";
import type { SemanticSearchResultItem } from "@/modules/hub/types";
@@ -33,6 +35,10 @@ export type TTopicsPreviewSearchActionResult = {
const ensureReadAccess = async (userId: string, workspaceId: string): Promise<void> => {
const organizationId = await getOrganizationIdFromWorkspaceId(workspaceId);
const isUnifyFeedbackAllowed = await getIsUnifyFeedbackEnabled(organizationId);
if (!isUnifyFeedbackAllowed) {
throw new OperationNotAllowedError("Unify Feedback is not enabled for this organization");
}
await checkAuthorizationUpdated({
userId,
organizationId,
@@ -10,7 +10,7 @@ import { Button } from "@/modules/ui/components/button";
import { Input } from "@/modules/ui/components/input";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { UnifyConfigNavigation } from "../../components/UnifyConfigNavigation";
import { UnifyConfigNavigation } from "../../components/unify-config-navigation";
import { semanticSearchFeedbackRecordsAction } from "../actions";
import type { TTopicsPreviewSearchResult } from "../actions";
@@ -0,0 +1,67 @@
import { notFound } from "next/navigation";
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { getTranslate } from "@/lingodotdev/server";
import { getFeedbackDirectoriesByWorkspaceId } from "@/modules/ee/feedback-directory/lib/feedback-directory";
import { getIsUnifyFeedbackEnabled } from "@/modules/ee/license-check/lib/utils";
import { UnifyConfigNavigation } from "@/modules/ee/unify-feedback/components/unify-config-navigation";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { UpgradePrompt } from "@/modules/ui/components/upgrade-prompt";
import { getWorkspaceAuth } from "@/modules/workspaces/lib/utils";
import { TopicsSubtopicsPreview } from "./components/topics-subtopics-preview";
export const UnifyTopicsSubtopicsPage = async (
props: Readonly<{ params: Promise<{ workspaceId: string }> }>
) => {
const t = await getTranslate();
const params = await props.params;
const { isOwner, isManager, hasReadAccess, hasReadWriteAccess, hasManageAccess, session, organization } =
await getWorkspaceAuth(params.workspaceId);
if (!session) {
throw new Error(t("common.session_not_found"));
}
const hasAccess = isOwner || isManager || hasReadAccess || hasReadWriteAccess || hasManageAccess;
if (!hasAccess) {
return notFound();
}
const isUnifyFeedbackAllowed = await getIsUnifyFeedbackEnabled(organization.id);
if (!isUnifyFeedbackAllowed) {
return (
<PageContentWrapper>
<PageHeader pageTitle={t("workspace.unify.feedback_records")}>
<UnifyConfigNavigation workspaceId={params.workspaceId} activeId="topics-subtopics" />
</PageHeader>
<div className="flex items-center justify-center">
<UpgradePrompt
title={t("workspace.unify.upgrade_prompt_title")}
description={t("workspace.unify.upgrade_prompt_description")}
feature="unify-feedback"
buttons={[
{
text: IS_FORMBRICKS_CLOUD ? t("common.upgrade_plan") : t("common.request_trial_license"),
href: IS_FORMBRICKS_CLOUD
? `/workspaces/${params.workspaceId}/settings/organization/billing`
: "https://formbricks.com/upgrade-self-hosting-license",
},
{
text: t("common.learn_more"),
href: IS_FORMBRICKS_CLOUD
? `/workspaces/${params.workspaceId}/settings/organization/billing`
: "https://formbricks.com/learn-more-self-hosting-license",
},
]}
/>
</div>
</PageContentWrapper>
);
}
const directories = await getFeedbackDirectoriesByWorkspaceId(params.workspaceId);
const directoryMap = Object.fromEntries(directories.map((directory) => [directory.id, directory.name]));
return <TopicsSubtopicsPreview workspaceId={params.workspaceId} directoryMap={directoryMap} />;
};
+20 -2
View File
@@ -21,7 +21,12 @@ import { getWorkspace } from "@/lib/workspace/service";
import { getTranslate } from "@/lingodotdev/server";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/license";
import { getAccessControlPermission } from "@/modules/ee/license-check/lib/utils";
import {
getAccessControlPermission,
getIsDashboardsEnabled,
getIsFeedbackDirectoriesEnabled,
getIsUnifyFeedbackEnabled,
} from "@/modules/ee/license-check/lib/utils";
import { getWorkspacePermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { TWorkspaceAuth, TWorkspaceLayoutData } from "@/modules/workspaces/types/workspace-auth";
@@ -278,8 +283,18 @@ export const getWorkspaceLayoutData = reactCache(
throw new AuthorizationError(t("common.membership_not_found"));
}
const [isAccessControlAllowed, workspacePermission, license] = await Promise.all([
const [
isAccessControlAllowed,
isUnifyFeedbackAllowed,
isFeedbackDirectoriesAllowed,
isDashboardsAllowed,
workspacePermission,
license,
] = await Promise.all([
getAccessControlPermission(organization.id),
getIsUnifyFeedbackEnabled(organization.id),
getIsFeedbackDirectoriesEnabled(organization.id),
getIsDashboardsEnabled(organization.id),
getWorkspacePermissionByUserId(userId, workspace.id),
getEnterpriseLicense(),
]);
@@ -296,6 +311,9 @@ export const getWorkspaceLayoutData = reactCache(
organization,
membership,
isAccessControlAllowed,
isUnifyFeedbackAllowed,
isFeedbackDirectoriesAllowed,
isDashboardsAllowed,
workspacePermission,
license,
responseCount,
@@ -55,6 +55,9 @@ export type TWorkspaceLayoutData = {
organization: TOrganization;
membership: TMembership;
isAccessControlAllowed: boolean;
isUnifyFeedbackAllowed: boolean;
isFeedbackDirectoriesAllowed: boolean;
isDashboardsAllowed: boolean;
workspacePermission: TTeamPermission | null;
license: TEnterpriseLicense;
responseCount: number;