mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-12 19:39:00 -05:00
fixes
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
+6
@@ -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,
|
||||
+1
-1
@@ -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} />;
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user