mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-21 11:49:32 -05:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| af51414b03 | |||
| a9e39dd4ab |
-5
@@ -66,11 +66,6 @@ const getFeatureDefinitions = (t: TFunction): TFeatureDefinition[] => {
|
||||
labelKey: t("workspace.settings.general.ai_smart_tools_enabled"),
|
||||
docsUrl: "https://formbricks.com/docs/self-hosting/configuration/ai",
|
||||
},
|
||||
{
|
||||
key: "aiDataAnalysis",
|
||||
labelKey: t("workspace.settings.general.ai_data_analysis_enabled"),
|
||||
docsUrl: "https://formbricks.com/docs/self-hosting/configuration/ai",
|
||||
},
|
||||
{
|
||||
key: "auditLogs",
|
||||
labelKey: t("workspace.settings.enterprise.license_feature_audit_logs"),
|
||||
|
||||
-6
@@ -57,7 +57,6 @@ describe("organization AI settings actions", () => {
|
||||
mocks.getOrganization.mockResolvedValue({
|
||||
id: organizationId,
|
||||
isAISmartToolsEnabled: false,
|
||||
isAIDataAnalysisEnabled: false,
|
||||
});
|
||||
mocks.isInstanceAIConfigured.mockReturnValue(true);
|
||||
mocks.getTranslate.mockResolvedValue((key: string, values?: Record<string, string>) =>
|
||||
@@ -66,7 +65,6 @@ describe("organization AI settings actions", () => {
|
||||
mocks.updateOrganization.mockResolvedValue({
|
||||
id: organizationId,
|
||||
isAISmartToolsEnabled: true,
|
||||
isAIDataAnalysisEnabled: false,
|
||||
});
|
||||
mocks.getIsMultiOrgEnabled.mockResolvedValue(true);
|
||||
});
|
||||
@@ -114,18 +112,15 @@ describe("organization AI settings actions", () => {
|
||||
oldObject: {
|
||||
id: organizationId,
|
||||
isAISmartToolsEnabled: false,
|
||||
isAIDataAnalysisEnabled: false,
|
||||
},
|
||||
newObject: {
|
||||
id: organizationId,
|
||||
isAISmartToolsEnabled: true,
|
||||
isAIDataAnalysisEnabled: false,
|
||||
},
|
||||
});
|
||||
expect(result).toEqual({
|
||||
id: organizationId,
|
||||
isAISmartToolsEnabled: true,
|
||||
isAIDataAnalysisEnabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -194,7 +189,6 @@ describe("organization AI settings actions", () => {
|
||||
mocks.getOrganization.mockResolvedValueOnce({
|
||||
id: organizationId,
|
||||
isAISmartToolsEnabled: true,
|
||||
isAIDataAnalysisEnabled: false,
|
||||
});
|
||||
mocks.isInstanceAIConfigured.mockReturnValueOnce(false);
|
||||
|
||||
|
||||
+2
-9
@@ -71,12 +71,11 @@ export const updateOrganizationNameAction = authenticatedActionClient
|
||||
|
||||
type TOrganizationAISettings = Pick<
|
||||
NonNullable<Awaited<ReturnType<typeof getOrganization>>>,
|
||||
"isAISmartToolsEnabled" | "isAIDataAnalysisEnabled"
|
||||
"isAISmartToolsEnabled"
|
||||
>;
|
||||
|
||||
type TResolvedOrganizationAISettings = {
|
||||
smartToolsEnabled: boolean;
|
||||
dataAnalysisEnabled: boolean;
|
||||
isEnablingAnyAISetting: boolean;
|
||||
};
|
||||
|
||||
@@ -90,16 +89,10 @@ const resolveOrganizationAISettings = ({
|
||||
const smartToolsEnabled = Object.hasOwn(data, "isAISmartToolsEnabled")
|
||||
? (data.isAISmartToolsEnabled ?? organization.isAISmartToolsEnabled)
|
||||
: organization.isAISmartToolsEnabled;
|
||||
const dataAnalysisEnabled = Object.hasOwn(data, "isAIDataAnalysisEnabled")
|
||||
? (data.isAIDataAnalysisEnabled ?? organization.isAIDataAnalysisEnabled)
|
||||
: organization.isAIDataAnalysisEnabled;
|
||||
|
||||
return {
|
||||
smartToolsEnabled,
|
||||
dataAnalysisEnabled,
|
||||
isEnablingAnyAISetting:
|
||||
(smartToolsEnabled && !organization.isAISmartToolsEnabled) ||
|
||||
(dataAnalysisEnabled && !organization.isAIDataAnalysisEnabled),
|
||||
isEnablingAnyAISetting: smartToolsEnabled && !organization.isAISmartToolsEnabled,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
+4
-25
@@ -50,29 +50,18 @@ export const AISettingsToggle = ({
|
||||
currentValue: organization.isAISmartToolsEnabled,
|
||||
isInstanceConfigured: isInstanceAIConfigured,
|
||||
});
|
||||
const displayedDataAnalysisValue = getDisplayedOrganizationAISettingValue({
|
||||
currentValue: organization.isAIDataAnalysisEnabled,
|
||||
isInstanceConfigured: isInstanceAIConfigured,
|
||||
});
|
||||
|
||||
const handleToggle = async (
|
||||
field: "isAISmartToolsEnabled" | "isAIDataAnalysisEnabled",
|
||||
checked: boolean
|
||||
) => {
|
||||
const handleToggle = async (checked: boolean) => {
|
||||
if (checked && !aiEnablementState.canEnableFeatures) {
|
||||
toast.error(aiEnablementBlockedMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingField(field);
|
||||
setLoadingField("isAISmartToolsEnabled");
|
||||
try {
|
||||
const data =
|
||||
field === "isAISmartToolsEnabled"
|
||||
? { isAISmartToolsEnabled: checked }
|
||||
: { isAIDataAnalysisEnabled: checked };
|
||||
const response = await updateOrganizationAISettingsAction({
|
||||
organizationId: organization.id,
|
||||
data,
|
||||
data: { isAISmartToolsEnabled: checked },
|
||||
});
|
||||
|
||||
if (response?.data) {
|
||||
@@ -122,7 +111,7 @@ export const AISettingsToggle = ({
|
||||
|
||||
<AdvancedOptionToggle
|
||||
isChecked={displayedSmartToolsValue}
|
||||
onToggle={(checked) => handleToggle("isAISmartToolsEnabled", checked)}
|
||||
onToggle={handleToggle}
|
||||
htmlId="ai-smart-tools-toggle"
|
||||
title={t("workspace.settings.general.ai_smart_tools_enabled")}
|
||||
description={t("workspace.settings.general.ai_smart_tools_enabled_description")}
|
||||
@@ -130,16 +119,6 @@ export const AISettingsToggle = ({
|
||||
customContainerClass="px-0"
|
||||
/>
|
||||
|
||||
<AdvancedOptionToggle
|
||||
isChecked={displayedDataAnalysisValue}
|
||||
onToggle={(checked) => handleToggle("isAIDataAnalysisEnabled", checked)}
|
||||
htmlId="ai-data-analysis-toggle"
|
||||
title={t("workspace.settings.general.ai_data_analysis_enabled")}
|
||||
description={t("workspace.settings.general.ai_data_analysis_enabled_description")}
|
||||
disabled={isToggleDisabled}
|
||||
customContainerClass="px-0"
|
||||
/>
|
||||
|
||||
{!canEdit && (
|
||||
<Alert variant="warning">
|
||||
<AlertDescription>
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
import { getUser } from "@/lib/user/service";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import {
|
||||
getIsAIDataAnalysisEnabled,
|
||||
getIsAISmartToolsEnabled,
|
||||
getIsMultiOrgEnabled,
|
||||
getWhiteLabelPermission,
|
||||
@@ -38,14 +37,11 @@ const Page = async (props: Readonly<{ params: Promise<{ workspaceId: string }> }
|
||||
|
||||
const user = session?.user?.id ? await getUser(session.user.id) : null;
|
||||
|
||||
const [isMultiOrgEnabled, hasWhiteLabelPermission, hasAISmartToolsPermission, hasAIDataAnalysisPermission] =
|
||||
await Promise.all([
|
||||
getIsMultiOrgEnabled(),
|
||||
getWhiteLabelPermission(organization.id),
|
||||
getIsAISmartToolsEnabled(organization.id),
|
||||
getIsAIDataAnalysisEnabled(organization.id),
|
||||
]);
|
||||
const hasAIPermission = hasAISmartToolsPermission || hasAIDataAnalysisPermission;
|
||||
const [isMultiOrgEnabled, hasWhiteLabelPermission, hasAIPermission] = await Promise.all([
|
||||
getIsMultiOrgEnabled(),
|
||||
getWhiteLabelPermission(organization.id),
|
||||
getIsAISmartToolsEnabled(organization.id),
|
||||
]);
|
||||
|
||||
const isDeleteDisabled = !isOwner || !isMultiOrgEnabled;
|
||||
const currentUserRole = currentUserMembership?.role;
|
||||
|
||||
@@ -4,7 +4,6 @@ import { ZOrganizationUpdateInput } from "@formbricks/types/organizations";
|
||||
|
||||
export const ZOrganizationAISettingsInput = ZOrganizationUpdateInput.pick({
|
||||
isAISmartToolsEnabled: true,
|
||||
isAIDataAnalysisEnabled: true,
|
||||
});
|
||||
|
||||
export const ZUpdateOrganizationAISettingsAction = z.object({
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { DatabaseError, ResourceNotFoundError, UniqueConstraintError } from "@formbricks/types/errors";
|
||||
import {
|
||||
DatabaseError,
|
||||
InvalidInputError,
|
||||
ResourceNotFoundError,
|
||||
UniqueConstraintError,
|
||||
} from "@formbricks/types/errors";
|
||||
import { TSurveyQuota } from "@formbricks/types/quota";
|
||||
import { TResponseInput } from "@formbricks/types/responses";
|
||||
import { getOrganization } from "@/lib/organization/service";
|
||||
@@ -155,6 +160,16 @@ describe("createResponse", () => {
|
||||
await expect(createResponse(mockResponseInput, prisma)).rejects.toThrow(UniqueConstraintError);
|
||||
});
|
||||
|
||||
test("should throw InvalidInputError on P2002 with displayId target (race condition)", async () => {
|
||||
const prismaError = new Prisma.PrismaClientKnownRequestError("Unique constraint failed", {
|
||||
code: "P2002",
|
||||
clientVersion: "test",
|
||||
meta: { target: ["displayId"] },
|
||||
});
|
||||
vi.mocked(prisma.response.create).mockRejectedValue(prismaError);
|
||||
await expect(createResponse(mockResponseInput, prisma)).rejects.toThrow(InvalidInputError);
|
||||
});
|
||||
|
||||
test("should throw original error on other Prisma errors", async () => {
|
||||
const genericError = new Error("Generic database error");
|
||||
vi.mocked(prisma.response.create).mockRejectedValue(genericError);
|
||||
|
||||
@@ -2,7 +2,12 @@ import "server-only";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { TContactAttributes } from "@formbricks/types/contact-attribute";
|
||||
import { DatabaseError, ResourceNotFoundError, UniqueConstraintError } from "@formbricks/types/errors";
|
||||
import {
|
||||
DatabaseError,
|
||||
InvalidInputError,
|
||||
ResourceNotFoundError,
|
||||
UniqueConstraintError,
|
||||
} from "@formbricks/types/errors";
|
||||
import { TResponseWithQuotaFull } from "@formbricks/types/quota";
|
||||
import { TResponse, TResponseInput, ZResponseInput } from "@formbricks/types/responses";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
@@ -11,6 +16,7 @@ import {
|
||||
isSingleUseIdUniqueConstraintError,
|
||||
} from "@/app/api/client/[workspaceId]/responses/lib/response-error";
|
||||
import { buildPrismaResponseData } from "@/app/api/v1/lib/utils";
|
||||
import { assertDisplayOwnership } from "@/lib/display/service";
|
||||
import { getOrganization } from "@/lib/organization/service";
|
||||
import { calculateTtcTotal } from "@/lib/response/utils";
|
||||
import { getOrganizationIdFromWorkspaceId } from "@/lib/utils/helper";
|
||||
@@ -104,6 +110,16 @@ export const createResponse = async (
|
||||
|
||||
const ttc = initialTtc ? (finished ? calculateTtcTotal(initialTtc) : initialTtc) : {};
|
||||
|
||||
if (responseInput.displayId) {
|
||||
await assertDisplayOwnership(
|
||||
responseInput.displayId,
|
||||
workspaceId,
|
||||
responseInput.surveyId,
|
||||
contact?.id ?? null,
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
const prismaData = buildPrismaResponseData(
|
||||
{ ...responseInput, createdAt: undefined, updatedAt: undefined },
|
||||
contact,
|
||||
@@ -131,6 +147,13 @@ export const createResponse = async (
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (isPrismaKnownRequestError(error)) {
|
||||
if (
|
||||
error.code === "P2002" &&
|
||||
Array.isArray(error.meta?.target) &&
|
||||
error.meta.target.includes("displayId")
|
||||
) {
|
||||
throw new InvalidInputError(`Display ${responseInput.displayId} is already linked to a response`);
|
||||
}
|
||||
if (isSingleUseIdUniqueConstraintError(error)) {
|
||||
throw new UniqueConstraintError("Response already submitted for this single-use link");
|
||||
}
|
||||
|
||||
@@ -51,7 +51,6 @@ const mockOrganization: TOrganization = {
|
||||
usageCycleAnchor: new Date(),
|
||||
},
|
||||
isAISmartToolsEnabled: false,
|
||||
isAIDataAnalysisEnabled: false,
|
||||
};
|
||||
|
||||
const mockFollowUp: TSurveyCreateInputWithWorkspaceId["followUps"][number] = {
|
||||
|
||||
@@ -2,7 +2,12 @@ import { Prisma } from "@prisma/client";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { TContactAttributes } from "@formbricks/types/contact-attribute";
|
||||
import { DatabaseError, ResourceNotFoundError, UniqueConstraintError } from "@formbricks/types/errors";
|
||||
import {
|
||||
DatabaseError,
|
||||
InvalidInputError,
|
||||
ResourceNotFoundError,
|
||||
UniqueConstraintError,
|
||||
} from "@formbricks/types/errors";
|
||||
import { TResponseWithQuotaFull, TSurveyQuota } from "@formbricks/types/quota";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
@@ -190,7 +195,19 @@ describe("createResponse V2", () => {
|
||||
).rejects.toThrow(UniqueConstraintError);
|
||||
});
|
||||
|
||||
test("should throw DatabaseError on P2002 without singleUseId target", async () => {
|
||||
test("should throw DatabaseError on P2002 without singleUseId or displayId target", async () => {
|
||||
const prismaError = new Prisma.PrismaClientKnownRequestError("Unique constraint failed", {
|
||||
code: "P2002",
|
||||
clientVersion: "test",
|
||||
meta: { target: ["someOtherField"] },
|
||||
});
|
||||
vi.mocked(mockTx.response.create).mockRejectedValue(prismaError);
|
||||
await expect(
|
||||
createResponse(mockResponseInput, mockTx as unknown as Prisma.TransactionClient)
|
||||
).rejects.toThrow(DatabaseError);
|
||||
});
|
||||
|
||||
test("should throw InvalidInputError on P2002 with displayId target (race condition)", async () => {
|
||||
const prismaError = new Prisma.PrismaClientKnownRequestError("Unique constraint failed", {
|
||||
code: "P2002",
|
||||
clientVersion: "test",
|
||||
@@ -199,7 +216,7 @@ describe("createResponse V2", () => {
|
||||
vi.mocked(mockTx.response.create).mockRejectedValue(prismaError);
|
||||
await expect(
|
||||
createResponse(mockResponseInput, mockTx as unknown as Prisma.TransactionClient)
|
||||
).rejects.toThrow(DatabaseError);
|
||||
).rejects.toThrow(InvalidInputError);
|
||||
});
|
||||
|
||||
test("should throw DatabaseError on non-P2002 Prisma known request error", async () => {
|
||||
|
||||
@@ -2,7 +2,12 @@ import "server-only";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { TContactAttributes } from "@formbricks/types/contact-attribute";
|
||||
import { DatabaseError, ResourceNotFoundError, UniqueConstraintError } from "@formbricks/types/errors";
|
||||
import {
|
||||
DatabaseError,
|
||||
InvalidInputError,
|
||||
ResourceNotFoundError,
|
||||
UniqueConstraintError,
|
||||
} from "@formbricks/types/errors";
|
||||
import { TResponseWithQuotaFull } from "@formbricks/types/quota";
|
||||
import { TResponse, ZResponseInput } from "@formbricks/types/responses";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
@@ -12,6 +17,7 @@ import {
|
||||
} from "@/app/api/client/[workspaceId]/responses/lib/response-error";
|
||||
import { responseSelection } from "@/app/api/v1/client/[workspaceId]/responses/lib/response";
|
||||
import { TResponseInputV2 } from "@/app/api/v2/client/[workspaceId]/responses/types/response";
|
||||
import { assertDisplayOwnership } from "@/lib/display/service";
|
||||
import { getOrganization } from "@/lib/organization/service";
|
||||
import { calculateTtcTotal } from "@/lib/response/utils";
|
||||
import { getOrganizationIdFromWorkspaceId } from "@/lib/utils/helper";
|
||||
@@ -99,6 +105,16 @@ export const createResponse = async (
|
||||
|
||||
const ttc = initialTtc ? (finished ? calculateTtcTotal(initialTtc) : initialTtc) : {};
|
||||
|
||||
if (responseInput.displayId) {
|
||||
await assertDisplayOwnership(
|
||||
responseInput.displayId,
|
||||
workspaceId,
|
||||
responseInput.surveyId,
|
||||
contactId ?? null,
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
const prismaData = buildPrismaResponseData(responseInput, contact, ttc);
|
||||
|
||||
const prismaClient = tx ?? prisma;
|
||||
@@ -122,6 +138,13 @@ export const createResponse = async (
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (isPrismaKnownRequestError(error)) {
|
||||
if (
|
||||
error.code === "P2002" &&
|
||||
Array.isArray(error.meta?.target) &&
|
||||
error.meta.target.includes("displayId")
|
||||
) {
|
||||
throw new InvalidInputError(`Display ${responseInput.displayId} is already linked to a response`);
|
||||
}
|
||||
if (isSingleUseIdUniqueConstraintError(error)) {
|
||||
throw new UniqueConstraintError("Response already submitted for this single-use link");
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { OperationNotAllowedError, ResourceNotFoundError } from "@formbricks/typ
|
||||
import {
|
||||
assertOrganizationAIConfigured,
|
||||
generateOrganizationAIText,
|
||||
getAIDataAnalysisUnavailableReason,
|
||||
getAISmartToolsUnavailableReason,
|
||||
getOrganizationAIConfig,
|
||||
isInstanceAIConfigured,
|
||||
@@ -13,7 +12,6 @@ const mocks = vi.hoisted(() => ({
|
||||
generateText: vi.fn(),
|
||||
isAiConfigured: vi.fn(),
|
||||
getOrganization: vi.fn(),
|
||||
getIsAIDataAnalysisEnabled: vi.fn(),
|
||||
getIsAISmartToolsEnabled: vi.fn(),
|
||||
loggerError: vi.fn(),
|
||||
}));
|
||||
@@ -63,7 +61,6 @@ vi.mock("@/lib/organization/service", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ee/license-check/lib/utils", () => ({
|
||||
getIsAIDataAnalysisEnabled: mocks.getIsAIDataAnalysisEnabled,
|
||||
getIsAISmartToolsEnabled: mocks.getIsAISmartToolsEnabled,
|
||||
}));
|
||||
|
||||
@@ -75,10 +72,8 @@ describe("AI organization service", () => {
|
||||
mocks.getOrganization.mockResolvedValue({
|
||||
id: "org_1",
|
||||
isAISmartToolsEnabled: true,
|
||||
isAIDataAnalysisEnabled: false,
|
||||
});
|
||||
mocks.getIsAISmartToolsEnabled.mockResolvedValue(true);
|
||||
mocks.getIsAIDataAnalysisEnabled.mockResolvedValue(true);
|
||||
});
|
||||
|
||||
test("returns the instance AI status and organization settings", async () => {
|
||||
@@ -89,9 +84,7 @@ describe("AI organization service", () => {
|
||||
expect(result).toMatchObject({
|
||||
organizationId: "org_1",
|
||||
isAISmartToolsEnabled: true,
|
||||
isAIDataAnalysisEnabled: false,
|
||||
isAISmartToolsEntitled: true,
|
||||
isAIDataAnalysisEntitled: true,
|
||||
isInstanceConfigured: true,
|
||||
});
|
||||
});
|
||||
@@ -105,29 +98,22 @@ describe("AI organization service", () => {
|
||||
test("fails closed when the organization is not entitled to AI", async () => {
|
||||
mocks.getIsAISmartToolsEnabled.mockResolvedValueOnce(false);
|
||||
|
||||
await expect(assertOrganizationAIConfigured("org_1", "smartTools")).rejects.toThrow(
|
||||
OperationNotAllowedError
|
||||
);
|
||||
await expect(assertOrganizationAIConfigured("org_1")).rejects.toThrow(OperationNotAllowedError);
|
||||
});
|
||||
|
||||
test("fails closed when the requested AI capability is disabled", async () => {
|
||||
mocks.getOrganization.mockResolvedValueOnce({
|
||||
id: "org_1",
|
||||
isAISmartToolsEnabled: false,
|
||||
isAIDataAnalysisEnabled: true,
|
||||
});
|
||||
|
||||
await expect(assertOrganizationAIConfigured("org_1", "smartTools")).rejects.toThrow(
|
||||
OperationNotAllowedError
|
||||
);
|
||||
await expect(assertOrganizationAIConfigured("org_1")).rejects.toThrow(OperationNotAllowedError);
|
||||
});
|
||||
|
||||
test("fails closed when the instance AI configuration is incomplete", async () => {
|
||||
mocks.isAiConfigured.mockReturnValueOnce(false);
|
||||
|
||||
await expect(assertOrganizationAIConfigured("org_1", "smartTools")).rejects.toThrow(
|
||||
OperationNotAllowedError
|
||||
);
|
||||
await expect(assertOrganizationAIConfigured("org_1")).rejects.toThrow(OperationNotAllowedError);
|
||||
});
|
||||
|
||||
test("generates organization AI text with the configured package abstraction", async () => {
|
||||
@@ -136,7 +122,6 @@ describe("AI organization service", () => {
|
||||
|
||||
const result = await generateOrganizationAIText({
|
||||
organizationId: "org_1",
|
||||
capability: "smartTools",
|
||||
prompt: "Translate this survey",
|
||||
});
|
||||
|
||||
@@ -160,14 +145,12 @@ describe("AI organization service", () => {
|
||||
await expect(
|
||||
generateOrganizationAIText({
|
||||
organizationId: "org_1",
|
||||
capability: "smartTools",
|
||||
prompt: "Translate this survey",
|
||||
})
|
||||
).rejects.toThrow(modelError);
|
||||
expect(mocks.loggerError).toHaveBeenCalledWith(
|
||||
{
|
||||
organizationId: "org_1",
|
||||
capability: "smartTools",
|
||||
isInstanceConfigured: true,
|
||||
errorCode: undefined,
|
||||
err: modelError,
|
||||
@@ -176,46 +159,11 @@ describe("AI organization service", () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe("getAIDataAnalysisUnavailableReason", () => {
|
||||
const baseConfig = {
|
||||
organizationId: "org_1",
|
||||
isAISmartToolsEntitled: true,
|
||||
isAISmartToolsEnabled: true,
|
||||
isAIDataAnalysisEntitled: true,
|
||||
isAIDataAnalysisEnabled: true,
|
||||
isInstanceConfigured: true,
|
||||
};
|
||||
|
||||
test("returns undefined when all checks pass", () => {
|
||||
expect(getAIDataAnalysisUnavailableReason(baseConfig)).toBeUndefined();
|
||||
});
|
||||
|
||||
test("returns not_in_plan when not entitled", () => {
|
||||
expect(getAIDataAnalysisUnavailableReason({ ...baseConfig, isAIDataAnalysisEntitled: false })).toBe(
|
||||
"not_in_plan"
|
||||
);
|
||||
});
|
||||
|
||||
test("returns not_enabled when disabled at org level", () => {
|
||||
expect(getAIDataAnalysisUnavailableReason({ ...baseConfig, isAIDataAnalysisEnabled: false })).toBe(
|
||||
"not_enabled"
|
||||
);
|
||||
});
|
||||
|
||||
test("returns instance_not_configured when instance AI is missing", () => {
|
||||
expect(getAIDataAnalysisUnavailableReason({ ...baseConfig, isInstanceConfigured: false })).toBe(
|
||||
"instance_not_configured"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAISmartToolsUnavailableReason", () => {
|
||||
const baseConfig = {
|
||||
organizationId: "org_1",
|
||||
isAISmartToolsEntitled: true,
|
||||
isAISmartToolsEnabled: true,
|
||||
isAIDataAnalysisEntitled: true,
|
||||
isAIDataAnalysisEnabled: true,
|
||||
isInstanceConfigured: true,
|
||||
};
|
||||
|
||||
@@ -240,15 +188,5 @@ describe("AI organization service", () => {
|
||||
"instance_not_configured"
|
||||
);
|
||||
});
|
||||
|
||||
test("ignores data-analysis flags (smart tools is independent of data analysis state)", () => {
|
||||
expect(
|
||||
getAISmartToolsUnavailableReason({
|
||||
...baseConfig,
|
||||
isAIDataAnalysisEntitled: false,
|
||||
isAIDataAnalysisEnabled: false,
|
||||
})
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,12 +4,11 @@ import { logger } from "@formbricks/logger";
|
||||
import { OperationNotAllowedError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { env } from "@/lib/env";
|
||||
import { getOrganization } from "@/lib/organization/service";
|
||||
import { getIsAIDataAnalysisEnabled, getIsAISmartToolsEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getIsAISmartToolsEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
|
||||
export const AI_ERROR_CODES = {
|
||||
FEATURES_NOT_ENABLED: "ai_features_not_enabled",
|
||||
SMART_TOOLS_DISABLED: "ai_smart_tools_disabled",
|
||||
DATA_ANALYSIS_DISABLED: "ai_data_analysis_disabled",
|
||||
INSTANCE_NOT_CONFIGURED: "ai_instance_not_configured",
|
||||
} as const;
|
||||
|
||||
@@ -18,9 +17,7 @@ export type TAIErrorCode = (typeof AI_ERROR_CODES)[keyof typeof AI_ERROR_CODES];
|
||||
export interface TOrganizationAIConfig {
|
||||
organizationId: string;
|
||||
isAISmartToolsEnabled: boolean;
|
||||
isAIDataAnalysisEnabled: boolean;
|
||||
isAISmartToolsEntitled: boolean;
|
||||
isAIDataAnalysisEntitled: boolean;
|
||||
isInstanceConfigured: boolean;
|
||||
}
|
||||
|
||||
@@ -33,32 +30,18 @@ export const getOrganizationAIConfig = async (organizationId: string): Promise<T
|
||||
throw new ResourceNotFoundError("Organization", organizationId);
|
||||
}
|
||||
|
||||
const [isAISmartToolsEntitled, isAIDataAnalysisEntitled] = await Promise.all([
|
||||
getIsAISmartToolsEnabled(organizationId),
|
||||
getIsAIDataAnalysisEnabled(organizationId),
|
||||
]);
|
||||
const isAISmartToolsEntitled = await getIsAISmartToolsEnabled(organizationId);
|
||||
|
||||
return {
|
||||
organizationId,
|
||||
isAISmartToolsEnabled: organization.isAISmartToolsEnabled,
|
||||
isAIDataAnalysisEnabled: organization.isAIDataAnalysisEnabled,
|
||||
isAISmartToolsEntitled,
|
||||
isAIDataAnalysisEntitled,
|
||||
isInstanceConfigured: isInstanceAIConfigured(),
|
||||
};
|
||||
};
|
||||
|
||||
export type TAIUnavailableReason = "not_in_plan" | "not_enabled" | "instance_not_configured";
|
||||
|
||||
export const getAIDataAnalysisUnavailableReason = (
|
||||
aiConfig: TOrganizationAIConfig
|
||||
): TAIUnavailableReason | undefined => {
|
||||
if (!aiConfig.isAIDataAnalysisEntitled) return "not_in_plan";
|
||||
if (!aiConfig.isAIDataAnalysisEnabled) return "not_enabled";
|
||||
if (!aiConfig.isInstanceConfigured) return "instance_not_configured";
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getAISmartToolsUnavailableReason = (
|
||||
aiConfig: TOrganizationAIConfig
|
||||
): TAIUnavailableReason | undefined => {
|
||||
@@ -69,25 +52,18 @@ export const getAISmartToolsUnavailableReason = (
|
||||
};
|
||||
|
||||
export const assertOrganizationAIConfigured = async (
|
||||
organizationId: string,
|
||||
capability: "smartTools" | "dataAnalysis"
|
||||
organizationId: string
|
||||
): Promise<TOrganizationAIConfig> => {
|
||||
const aiConfig = await getOrganizationAIConfig(organizationId);
|
||||
const isCapabilityEntitled =
|
||||
capability === "smartTools" ? aiConfig.isAISmartToolsEntitled : aiConfig.isAIDataAnalysisEntitled;
|
||||
|
||||
if (!isCapabilityEntitled) {
|
||||
if (!aiConfig.isAISmartToolsEntitled) {
|
||||
throw new OperationNotAllowedError(AI_ERROR_CODES.FEATURES_NOT_ENABLED);
|
||||
}
|
||||
|
||||
if (capability === "smartTools" && !aiConfig.isAISmartToolsEnabled) {
|
||||
if (!aiConfig.isAISmartToolsEnabled) {
|
||||
throw new OperationNotAllowedError(AI_ERROR_CODES.SMART_TOOLS_DISABLED);
|
||||
}
|
||||
|
||||
if (capability === "dataAnalysis" && !aiConfig.isAIDataAnalysisEnabled) {
|
||||
throw new OperationNotAllowedError(AI_ERROR_CODES.DATA_ANALYSIS_DISABLED);
|
||||
}
|
||||
|
||||
if (!aiConfig.isInstanceConfigured) {
|
||||
throw new OperationNotAllowedError(AI_ERROR_CODES.INSTANCE_NOT_CONFIGURED);
|
||||
}
|
||||
@@ -97,15 +73,13 @@ export const assertOrganizationAIConfigured = async (
|
||||
|
||||
type TGenerateOrganizationAITextInput = {
|
||||
organizationId: string;
|
||||
capability: "smartTools" | "dataAnalysis";
|
||||
} & Parameters<typeof generateText>[0];
|
||||
|
||||
export const generateOrganizationAIText = async ({
|
||||
organizationId,
|
||||
capability,
|
||||
...options
|
||||
}: TGenerateOrganizationAITextInput): Promise<Awaited<ReturnType<typeof generateText>>> => {
|
||||
const aiConfig = await assertOrganizationAIConfigured(organizationId, capability);
|
||||
const aiConfig = await assertOrganizationAIConfigured(organizationId);
|
||||
|
||||
try {
|
||||
return await generateText(options, env);
|
||||
@@ -113,7 +87,6 @@ export const generateOrganizationAIText = async ({
|
||||
logger.error(
|
||||
{
|
||||
organizationId,
|
||||
capability,
|
||||
isInstanceConfigured: aiConfig.isInstanceConfigured,
|
||||
errorCode: error instanceof AIConfigurationError ? error.code : undefined,
|
||||
err: error,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { z } from "zod";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { TDisplay, TDisplayFilters, TDisplayWithContact, ZDisplayFilters } from "@formbricks/types/displays";
|
||||
import { DatabaseError } from "@formbricks/types/errors";
|
||||
import { DatabaseError, InvalidInputError } from "@formbricks/types/errors";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
|
||||
export const selectDisplay = {
|
||||
@@ -146,6 +146,58 @@ export const getDisplaysBySurveyIdWithContact = reactCache(
|
||||
}
|
||||
);
|
||||
|
||||
export const getDisplayForResponseValidation = async (
|
||||
displayId: string,
|
||||
tx?: Prisma.TransactionClient
|
||||
): Promise<{
|
||||
surveyId: string;
|
||||
workspaceId: string;
|
||||
responseId: string | null;
|
||||
contactId: string | null;
|
||||
} | null> => {
|
||||
validateInputs([displayId, ZId]);
|
||||
const client = tx ?? prisma;
|
||||
try {
|
||||
const display = await client.display.findUnique({
|
||||
where: { id: displayId },
|
||||
select: {
|
||||
surveyId: true,
|
||||
contactId: true,
|
||||
response: { select: { id: true } },
|
||||
survey: { select: { workspaceId: true } },
|
||||
},
|
||||
});
|
||||
if (!display) return null;
|
||||
return {
|
||||
surveyId: display.surveyId,
|
||||
workspaceId: display.survey.workspaceId,
|
||||
responseId: display.response?.id ?? null,
|
||||
contactId: display.contactId,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) throw new DatabaseError(error.message);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const assertDisplayOwnership = async (
|
||||
displayId: string,
|
||||
workspaceId: string,
|
||||
surveyId: string,
|
||||
contactId: string | null,
|
||||
tx?: Prisma.TransactionClient
|
||||
): Promise<void> => {
|
||||
const display = await getDisplayForResponseValidation(displayId, tx);
|
||||
if (!display) throw new InvalidInputError(`Display ${displayId} not found`);
|
||||
if (display.workspaceId !== workspaceId)
|
||||
throw new InvalidInputError(`Display ${displayId} belongs to a different workspace`);
|
||||
if (display.surveyId !== surveyId)
|
||||
throw new InvalidInputError(`Display ${displayId} is associated with a different survey`);
|
||||
if (display.responseId) throw new InvalidInputError(`Display ${displayId} is already linked to a response`);
|
||||
if (display.contactId !== null && display.contactId !== contactId)
|
||||
throw new InvalidInputError(`Display ${displayId} belongs to a different contact`);
|
||||
};
|
||||
|
||||
export const deleteDisplay = async (displayId: string, tx?: Prisma.TransactionClient): Promise<TDisplay> => {
|
||||
validateInputs([displayId, ZId]);
|
||||
try {
|
||||
|
||||
@@ -3,14 +3,18 @@ import { prisma } from "@/lib/__mocks__/database";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { PrismaErrorType } from "@formbricks/database/types/error";
|
||||
import { DatabaseError, ValidationError } from "@formbricks/types/errors";
|
||||
import { DatabaseError, InvalidInputError, ValidationError } from "@formbricks/types/errors";
|
||||
import {
|
||||
assertDisplayOwnership,
|
||||
getDisplayCountBySurveyId,
|
||||
getDisplayForResponseValidation,
|
||||
getDisplaysByContactId,
|
||||
getDisplaysBySurveyIdWithContact,
|
||||
} from "../service";
|
||||
|
||||
const mockContactId = "clqnj99r9000008lebgf8734j";
|
||||
const mockWorkspaceId = "clqkr8dlv000308jybb08evgz";
|
||||
const mockResponseId = "clqnfg59i000208i426pb4wcv";
|
||||
const mockResponseIds = ["clqnfg59i000208i426pb4wcv", "clqnfg59i000208i426pb4wcw"];
|
||||
|
||||
const mockDisplaysForContact = [
|
||||
@@ -290,3 +294,96 @@ describe("getDisplaysBySurveyIdWithContact", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const mockDisplayRecord = {
|
||||
surveyId: mockSurveyId,
|
||||
contactId: null as string | null,
|
||||
response: null as { id: string } | null,
|
||||
survey: { workspaceId: mockWorkspaceId },
|
||||
};
|
||||
|
||||
describe("getDisplayForResponseValidation", () => {
|
||||
test("returns null when display is not found", async () => {
|
||||
vi.mocked(prisma.display.findUnique).mockResolvedValue(null);
|
||||
const result = await getDisplayForResponseValidation(mockDisplayId);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test("returns mapped shape when display is found", async () => {
|
||||
vi.mocked(prisma.display.findUnique).mockResolvedValue({
|
||||
...mockDisplayRecord,
|
||||
contactId: mockContactId,
|
||||
response: { id: mockResponseId },
|
||||
} as any);
|
||||
const result = await getDisplayForResponseValidation(mockDisplayId);
|
||||
expect(result).toEqual({
|
||||
surveyId: mockSurveyId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
responseId: mockResponseId,
|
||||
contactId: mockContactId,
|
||||
});
|
||||
});
|
||||
|
||||
test("throws DatabaseError on PrismaClientKnownRequestError", async () => {
|
||||
vi.mocked(prisma.display.findUnique).mockRejectedValue(
|
||||
new Prisma.PrismaClientKnownRequestError("Mock error", {
|
||||
code: PrismaErrorType.UniqueConstraintViolation,
|
||||
clientVersion: "0.0.1",
|
||||
})
|
||||
);
|
||||
await expect(getDisplayForResponseValidation(mockDisplayId)).rejects.toThrow(DatabaseError);
|
||||
});
|
||||
});
|
||||
|
||||
describe("assertDisplayOwnership", () => {
|
||||
test("throws InvalidInputError when display is not found", async () => {
|
||||
vi.mocked(prisma.display.findUnique).mockResolvedValue(null);
|
||||
await expect(assertDisplayOwnership(mockDisplayId, mockWorkspaceId, mockSurveyId, null)).rejects.toThrow(
|
||||
InvalidInputError
|
||||
);
|
||||
});
|
||||
|
||||
test("throws InvalidInputError when workspaceId does not match", async () => {
|
||||
vi.mocked(prisma.display.findUnique).mockResolvedValue(mockDisplayRecord as any);
|
||||
await expect(
|
||||
assertDisplayOwnership(mockDisplayId, "wrong-workspace", mockSurveyId, null)
|
||||
).rejects.toThrow(InvalidInputError);
|
||||
});
|
||||
|
||||
test("throws InvalidInputError when surveyId does not match", async () => {
|
||||
vi.mocked(prisma.display.findUnique).mockResolvedValue(mockDisplayRecord as any);
|
||||
await expect(
|
||||
assertDisplayOwnership(mockDisplayId, mockWorkspaceId, "wrong-survey", null)
|
||||
).rejects.toThrow(InvalidInputError);
|
||||
});
|
||||
|
||||
test("throws InvalidInputError when display is already linked to a response", async () => {
|
||||
vi.mocked(prisma.display.findUnique).mockResolvedValue({
|
||||
...mockDisplayRecord,
|
||||
response: { id: mockResponseId },
|
||||
} as any);
|
||||
await expect(assertDisplayOwnership(mockDisplayId, mockWorkspaceId, mockSurveyId, null)).rejects.toThrow(
|
||||
InvalidInputError
|
||||
);
|
||||
});
|
||||
|
||||
test("throws InvalidInputError when contactId does not match", async () => {
|
||||
vi.mocked(prisma.display.findUnique).mockResolvedValue({
|
||||
...mockDisplayRecord,
|
||||
contactId: "contact-a",
|
||||
} as any);
|
||||
await expect(
|
||||
assertDisplayOwnership(mockDisplayId, mockWorkspaceId, mockSurveyId, "contact-b")
|
||||
).rejects.toThrow(InvalidInputError);
|
||||
});
|
||||
|
||||
test("resolves without error when all ownership checks pass", async () => {
|
||||
vi.mocked(prisma.display.findUnique).mockResolvedValue({
|
||||
...mockDisplayRecord,
|
||||
contactId: mockContactId,
|
||||
} as any);
|
||||
await expect(
|
||||
assertDisplayOwnership(mockDisplayId, mockWorkspaceId, mockSurveyId, mockContactId)
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,7 +38,6 @@ describe("auth", () => {
|
||||
usageCycleAnchor: new Date(),
|
||||
},
|
||||
isAISmartToolsEnabled: false,
|
||||
isAIDataAnalysisEnabled: false,
|
||||
},
|
||||
];
|
||||
vi.mocked(getOrganizationsByUserId).mockResolvedValue(mockOrganizations);
|
||||
|
||||
@@ -73,7 +73,6 @@ describe("Organization Service", () => {
|
||||
usageCycleAnchor: new Date(),
|
||||
},
|
||||
isAISmartToolsEnabled: false,
|
||||
isAIDataAnalysisEnabled: false,
|
||||
whitelabel: false,
|
||||
};
|
||||
|
||||
@@ -126,7 +125,6 @@ describe("Organization Service", () => {
|
||||
usageCycleAnchor: new Date(),
|
||||
},
|
||||
isAISmartToolsEnabled: false,
|
||||
isAIDataAnalysisEnabled: false,
|
||||
whitelabel: false,
|
||||
},
|
||||
];
|
||||
@@ -179,7 +177,6 @@ describe("Organization Service", () => {
|
||||
updatedAt: new Date(),
|
||||
billing: expectedBilling,
|
||||
isAISmartToolsEnabled: false,
|
||||
isAIDataAnalysisEnabled: false,
|
||||
whitelabel: false,
|
||||
};
|
||||
|
||||
@@ -239,7 +236,6 @@ describe("Organization Service", () => {
|
||||
usageCycleAnchor: new Date(),
|
||||
},
|
||||
isAISmartToolsEnabled: false,
|
||||
isAIDataAnalysisEnabled: false,
|
||||
whitelabel: false,
|
||||
memberships: [{ userId: "user1" }, { userId: "user2" }],
|
||||
workspaces: [
|
||||
@@ -281,7 +277,6 @@ describe("Organization Service", () => {
|
||||
usageCycleAnchor: expect.any(Date),
|
||||
},
|
||||
isAISmartToolsEnabled: false,
|
||||
isAIDataAnalysisEnabled: false,
|
||||
whitelabel: false,
|
||||
});
|
||||
expect(prisma.organization.update).toHaveBeenCalledWith({
|
||||
|
||||
@@ -35,7 +35,6 @@ export const select = {
|
||||
},
|
||||
},
|
||||
isAISmartToolsEnabled: true,
|
||||
isAIDataAnalysisEnabled: true,
|
||||
whitelabel: true,
|
||||
} satisfies Prisma.OrganizationSelect;
|
||||
|
||||
@@ -74,7 +73,6 @@ const mapOrganization = (organization: TOrganizationWithBilling): TOrganization
|
||||
name: organization.name,
|
||||
billing: mapOrganizationBilling(organization.billing),
|
||||
isAISmartToolsEnabled: organization.isAISmartToolsEnabled,
|
||||
isAIDataAnalysisEnabled: organization.isAIDataAnalysisEnabled,
|
||||
whitelabel: organization.whitelabel as TOrganization["whitelabel"],
|
||||
});
|
||||
|
||||
|
||||
@@ -228,7 +228,6 @@ export const mockOrganizationOutput: TOrganization = {
|
||||
createdAt: currentDate,
|
||||
updatedAt: currentDate,
|
||||
isAISmartToolsEnabled: false,
|
||||
isAIDataAnalysisEnabled: false,
|
||||
billing: {
|
||||
stripeCustomerId: null,
|
||||
limits: {
|
||||
|
||||
@@ -67,7 +67,6 @@ describe("User Service", () => {
|
||||
usageCycleAnchor: new Date(),
|
||||
},
|
||||
isAISmartToolsEnabled: false,
|
||||
isAIDataAnalysisEnabled: false,
|
||||
},
|
||||
{
|
||||
id: "org2",
|
||||
@@ -85,7 +84,6 @@ describe("User Service", () => {
|
||||
usageCycleAnchor: new Date(),
|
||||
},
|
||||
isAISmartToolsEnabled: false,
|
||||
isAIDataAnalysisEnabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -2612,8 +2612,6 @@
|
||||
"workspaces_being_added": "Workspaces, denen Zugriff gewährt wird"
|
||||
},
|
||||
"general": {
|
||||
"ai_data_analysis_enabled": "Datenanreicherung & -analyse (KI)",
|
||||
"ai_data_analysis_enabled_description": "KI nutzen, um mehr aus deinen Daten herauszuholen – richte Dashboards, Diagramme, Berichte und mehr ein. Greift auf deine Erfahrungsdaten zu.",
|
||||
"ai_enabled": "Formbricks KI",
|
||||
"ai_enabled_description": "Verwalte KI-gestützte Funktionen für diese Organisation.",
|
||||
"ai_instance_not_configured": "KI wird auf Instanzebene über Umgebungsvariablen konfiguriert. Bitte deine:n Administrator:in, AI_PROVIDER, AI_MODEL und die passenden Provider-Zugangsdaten zu setzen, bevor du KI-Funktionen aktivierst.",
|
||||
@@ -2820,7 +2818,6 @@
|
||||
"adjust_survey_closed_message": "„Umfrage geschlossen“-Nachricht anpassen",
|
||||
"adjust_survey_closed_message_description": "Ändere die Nachricht, die Besucher sehen, wenn die Umfrage geschlossen ist.",
|
||||
"adjust_theme_in_look_and_feel_settings": "Passe das Theme in den <lookFeelLink>Look & Feel</lookFeelLink> Einstellungen an.",
|
||||
"ai_data_analysis_disabled": "KI-Datenanalyse ist für diese Organisation deaktiviert.",
|
||||
"ai_features_not_enabled": "KI-Funktionen sind für diese Organisation nicht aktiviert.",
|
||||
"ai_instance_not_configured": "KI ist nicht konfiguriert. Kontaktiere deinen Administrator.",
|
||||
"ai_smart_tools_disabled": "KI-Smart-Tools sind für diese Organisation deaktiviert.",
|
||||
|
||||
@@ -2612,8 +2612,6 @@
|
||||
"workspaces_being_added": "Workspaces being granted access"
|
||||
},
|
||||
"general": {
|
||||
"ai_data_analysis_enabled": "Data enrichment & analysis (AI)",
|
||||
"ai_data_analysis_enabled_description": "AI to get more out of your data, setup dashboards, charts, reports and more. Touches your experience data.",
|
||||
"ai_enabled": "Formbricks AI",
|
||||
"ai_enabled_description": "Manage AI-powered features for this organization.",
|
||||
"ai_instance_not_configured": "AI is configured at the instance level via environment variables. Ask your administrator to set AI_PROVIDER, AI_MODEL, and the matching provider credentials before enabling AI features.",
|
||||
@@ -2820,7 +2818,6 @@
|
||||
"adjust_survey_closed_message": "Adjust “Survey Closed” message",
|
||||
"adjust_survey_closed_message_description": "Change the message visitors see when the survey is closed.",
|
||||
"adjust_theme_in_look_and_feel_settings": "Adjust the theme in the <lookFeelLink>Look & Feel</lookFeelLink> Settings.",
|
||||
"ai_data_analysis_disabled": "AI data analysis is disabled for this organization.",
|
||||
"ai_features_not_enabled": "AI features are not enabled for this organization.",
|
||||
"ai_instance_not_configured": "AI is not configured. Contact your administrator.",
|
||||
"ai_smart_tools_disabled": "AI smart tools are disabled for this organization.",
|
||||
|
||||
@@ -2612,8 +2612,6 @@
|
||||
"workspaces_being_added": "Espacios de trabajo a los que se concede acceso"
|
||||
},
|
||||
"general": {
|
||||
"ai_data_analysis_enabled": "Enriquecimiento y análisis de datos (IA)",
|
||||
"ai_data_analysis_enabled_description": "IA para sacar más partido a tus datos, configurar paneles, gráficos, informes y más. Accede a los datos de experiencia.",
|
||||
"ai_enabled": "IA de Formbricks",
|
||||
"ai_enabled_description": "Gestiona las funciones impulsadas por IA para esta organización.",
|
||||
"ai_instance_not_configured": "La IA se configura a nivel de instancia mediante variables de entorno. Pide a tu administrador que configure AI_PROVIDER, las credenciales de ese proveedor y la lista de modelos correspondiente antes de habilitar las funciones de IA.",
|
||||
@@ -2820,7 +2818,6 @@
|
||||
"adjust_survey_closed_message": "Ajustar mensaje 'Encuesta cerrada'",
|
||||
"adjust_survey_closed_message_description": "Cambiar el mensaje que ven los visitantes cuando la encuesta está cerrada.",
|
||||
"adjust_theme_in_look_and_feel_settings": "Ajusta el tema en la configuración de <lookFeelLink>Aspecto</lookFeelLink>.",
|
||||
"ai_data_analysis_disabled": "El análisis de datos con IA está deshabilitado para esta organización.",
|
||||
"ai_features_not_enabled": "Las funciones de IA no están habilitadas para esta organización.",
|
||||
"ai_instance_not_configured": "La IA no está configurada. Contacta con tu administrador.",
|
||||
"ai_smart_tools_disabled": "Las herramientas inteligentes de IA están deshabilitadas para esta organización.",
|
||||
|
||||
@@ -2612,8 +2612,6 @@
|
||||
"workspaces_being_added": "Espaces de travail en cours d'ajout"
|
||||
},
|
||||
"general": {
|
||||
"ai_data_analysis_enabled": "Enrichissement et analyse des données (IA)",
|
||||
"ai_data_analysis_enabled_description": "L'IA pour tirer le meilleur parti de vos données, configurer des tableaux de bord, des graphiques, des rapports et plus encore. Accède à vos données d'expérience.",
|
||||
"ai_enabled": "IA Formbricks",
|
||||
"ai_enabled_description": "Gérer les fonctionnalités alimentées par l'IA pour cette organisation.",
|
||||
"ai_instance_not_configured": "L'IA est configurée au niveau de l'instance via des variables d'environnement. Demandez à votre administrateur de définir AI_PROVIDER, les identifiants du fournisseur et la liste de modèles correspondante avant d'activer les fonctionnalités d'IA.",
|
||||
@@ -2820,7 +2818,6 @@
|
||||
"adjust_survey_closed_message": "Ajuster le message \"Sondage fermé\"",
|
||||
"adjust_survey_closed_message_description": "Modifiez le message que les visiteurs voient lorsque l'enquête est fermée.",
|
||||
"adjust_theme_in_look_and_feel_settings": "Ajuste le thème dans les paramètres <lookFeelLink>Apparence et ressenti</lookFeelLink>.",
|
||||
"ai_data_analysis_disabled": "L'analyse de données par IA est désactivée pour cette organisation.",
|
||||
"ai_features_not_enabled": "Les fonctionnalités IA ne sont pas activées pour cette organisation.",
|
||||
"ai_instance_not_configured": "L'IA n'est pas configurée. Contacte ton administrateur.",
|
||||
"ai_smart_tools_disabled": "Les outils intelligents IA sont désactivés pour cette organisation.",
|
||||
|
||||
@@ -2612,8 +2612,6 @@
|
||||
"workspaces_being_added": "Hozzáférést kapó munkaterületek"
|
||||
},
|
||||
"general": {
|
||||
"ai_data_analysis_enabled": "Adatgazdagítás és elemzés (AI)",
|
||||
"ai_data_analysis_enabled_description": "AI segítségével többet hozhat ki az adataiból, irányítópultokat, diagramokat, jelentéseket és egyebeket állíthat be. Hozzáfér az élményekhez kapcsolódó adatokhoz.",
|
||||
"ai_enabled": "Formbricks AI",
|
||||
"ai_enabled_description": "AI-alapú funkciók kezelése ehhez a szervezethez.",
|
||||
"ai_instance_not_configured": "Az MI példányszinten, környezeti változókkal van konfigurálva. Kérd meg a rendszergazdát, hogy állítsa be az AI_PROVIDER értékét, a szolgáltató hitelesítő adatait és a megfelelő modelllistát, mielőtt engedélyezné az MI-funkciókat.",
|
||||
@@ -2820,7 +2818,6 @@
|
||||
"adjust_survey_closed_message": "A „Kérdőív lezárva” üzenet módosítása",
|
||||
"adjust_survey_closed_message_description": "Annak az üzenetnek a megváltoztatása, amelyet a látogatók akkor látnak, amikor a kérdőív lezárul.",
|
||||
"adjust_theme_in_look_and_feel_settings": "A témát a <lookFeelLink>Megjelenés és Élmény</lookFeelLink> beállításokban módosíthatja.",
|
||||
"ai_data_analysis_disabled": "Az AI adatelemzés le van tiltva ezen szervezet számára.",
|
||||
"ai_features_not_enabled": "Az AI funkciók nincsenek engedélyezve ezen szervezet számára.",
|
||||
"ai_instance_not_configured": "Az AI nincs konfigurálva. Kérjük, forduljon a rendszergazdájához.",
|
||||
"ai_smart_tools_disabled": "Az AI intelligens eszközök le vannak tiltva ezen szervezet számára.",
|
||||
|
||||
@@ -2612,8 +2612,6 @@
|
||||
"workspaces_being_added": "アクセス権が付与されるワークスペース"
|
||||
},
|
||||
"general": {
|
||||
"ai_data_analysis_enabled": "データエンリッチメントと分析(AI)",
|
||||
"ai_data_analysis_enabled_description": "AIを活用してデータから最大限の価値を引き出し、ダッシュボード、チャート、レポートなどを設定できます。エクスペリエンスデータに触れます。",
|
||||
"ai_enabled": "Formbricks AI",
|
||||
"ai_enabled_description": "この組織のAI機能を管理します。",
|
||||
"ai_instance_not_configured": "AI は環境変数を使ってインスタンスレベルで設定されます。AI 機能を有効にする前に、管理者に AI_PROVIDER、このプロバイダーの認証情報、および対応するモデル一覧を設定してもらってください。",
|
||||
@@ -2820,7 +2818,6 @@
|
||||
"adjust_survey_closed_message": "「フォームはクローズしました」メッセージを調整",
|
||||
"adjust_survey_closed_message_description": "フォームがクローズしたときに訪問者が見るメッセージを変更します。",
|
||||
"adjust_theme_in_look_and_feel_settings": "テーマは<lookFeelLink>外観</lookFeelLink>設定で調整できます。",
|
||||
"ai_data_analysis_disabled": "この組織ではAIデータ分析が無効になっています。",
|
||||
"ai_features_not_enabled": "この組織ではAI機能が有効になっていません。",
|
||||
"ai_instance_not_configured": "AIが設定されていません。管理者にお問い合わせください。",
|
||||
"ai_smart_tools_disabled": "この組織ではAIスマートツールが無効になっています。",
|
||||
|
||||
@@ -2612,8 +2612,6 @@
|
||||
"workspaces_being_added": "Werkruimtes die toegang krijgen"
|
||||
},
|
||||
"general": {
|
||||
"ai_data_analysis_enabled": "Dataverrijking & analyse (AI)",
|
||||
"ai_data_analysis_enabled_description": "AI om meer uit je data te halen, dashboards op te zetten, grafieken, rapporten en meer. Raakt je ervaringsdata aan.",
|
||||
"ai_enabled": "Formbricks AI",
|
||||
"ai_enabled_description": "Beheer AI-functies voor deze organisatie.",
|
||||
"ai_instance_not_configured": "AI wordt op instantieniveau geconfigureerd via omgevingsvariabelen. Vraag je beheerder om AI_PROVIDER, de inloggegevens voor die provider en de bijbehorende modellenlijst in te stellen voordat AI-functies worden ingeschakeld.",
|
||||
@@ -2820,7 +2818,6 @@
|
||||
"adjust_survey_closed_message": "Pas het bericht 'Enquête gesloten' aan",
|
||||
"adjust_survey_closed_message_description": "Wijzig het bericht dat bezoekers zien wanneer de enquête wordt gesloten.",
|
||||
"adjust_theme_in_look_and_feel_settings": "Pas het thema aan in de <lookFeelLink>Look & Feel</lookFeelLink> instellingen.",
|
||||
"ai_data_analysis_disabled": "AI-gegevensanalyse is uitgeschakeld voor deze organisatie.",
|
||||
"ai_features_not_enabled": "AI-functies zijn niet ingeschakeld voor deze organisatie.",
|
||||
"ai_instance_not_configured": "AI is niet geconfigureerd. Neem contact op met je beheerder.",
|
||||
"ai_smart_tools_disabled": "AI slimme tools zijn uitgeschakeld voor deze organisatie.",
|
||||
|
||||
@@ -2612,8 +2612,6 @@
|
||||
"workspaces_being_added": "Workspaces recebendo acesso"
|
||||
},
|
||||
"general": {
|
||||
"ai_data_analysis_enabled": "Enriquecimento e análise de dados (IA)",
|
||||
"ai_data_analysis_enabled_description": "IA para extrair mais dos seus dados, configurar dashboards, gráficos, relatórios e muito mais. Acessa os dados da sua experiência.",
|
||||
"ai_enabled": "Formbricks AI",
|
||||
"ai_enabled_description": "Gerencie recursos com IA para esta organização.",
|
||||
"ai_instance_not_configured": "A IA é configurada no nível da instância por meio de variáveis de ambiente. Peça ao seu administrador para definir AI_PROVIDER, as credenciais desse provedor e a lista de modelos correspondente antes de habilitar os recursos de IA.",
|
||||
@@ -2820,7 +2818,6 @@
|
||||
"adjust_survey_closed_message": "Ajustar mensagem 'Pesquisa Encerrada''",
|
||||
"adjust_survey_closed_message_description": "Mude a mensagem que os visitantes veem quando a pesquisa está fechada.",
|
||||
"adjust_theme_in_look_and_feel_settings": "Ajuste o tema nas configurações de <lookFeelLink>Aparência</lookFeelLink>.",
|
||||
"ai_data_analysis_disabled": "A análise de dados por IA está desabilitada para esta organização.",
|
||||
"ai_features_not_enabled": "Os recursos de IA não estão habilitados para esta organização.",
|
||||
"ai_instance_not_configured": "A IA não está configurada. Entre em contato com seu administrador.",
|
||||
"ai_smart_tools_disabled": "As ferramentas inteligentes de IA estão desabilitadas para esta organização.",
|
||||
|
||||
@@ -2612,8 +2612,6 @@
|
||||
"workspaces_being_added": "Workspaces a receber acesso"
|
||||
},
|
||||
"general": {
|
||||
"ai_data_analysis_enabled": "Enriquecimento e análise de dados (IA)",
|
||||
"ai_data_analysis_enabled_description": "IA para tirar mais partido dos teus dados, configurar dashboards, gráficos, relatórios e muito mais. Acede aos dados da tua experiência.",
|
||||
"ai_enabled": "IA da Formbricks",
|
||||
"ai_enabled_description": "Gerir funcionalidades com IA para esta organização.",
|
||||
"ai_instance_not_configured": "A IA é configurada ao nível da instância através de variáveis de ambiente. Peça ao seu administrador para definir AI_PROVIDER, as credenciais desse fornecedor e a lista de modelos correspondente antes de ativar as funcionalidades de IA.",
|
||||
@@ -2820,7 +2818,6 @@
|
||||
"adjust_survey_closed_message": "Ajustar mensagem de 'Inquérito Fechado'",
|
||||
"adjust_survey_closed_message_description": "Alterar a mensagem que os visitantes veem quando o inquérito está fechado.",
|
||||
"adjust_theme_in_look_and_feel_settings": "Ajusta o tema nas definições de <lookFeelLink>Aparência</lookFeelLink>.",
|
||||
"ai_data_analysis_disabled": "A análise de dados por IA está desativada para esta organização.",
|
||||
"ai_features_not_enabled": "As funcionalidades de IA não estão ativadas para esta organização.",
|
||||
"ai_instance_not_configured": "A IA não está configurada. Contacta o teu administrador.",
|
||||
"ai_smart_tools_disabled": "As ferramentas inteligentes de IA estão desativadas para esta organização.",
|
||||
|
||||
@@ -2612,8 +2612,6 @@
|
||||
"workspaces_being_added": "Spații de lucru cărora li se acordă acces"
|
||||
},
|
||||
"general": {
|
||||
"ai_data_analysis_enabled": "Îmbogățire și analiză de date (AI)",
|
||||
"ai_data_analysis_enabled_description": "AI pentru a obține mai mult din datele tale, configurare dashboard-uri, grafice, rapoarte și multe altele. Accesează datele tale de experiență.",
|
||||
"ai_enabled": "Formbricks AI",
|
||||
"ai_enabled_description": "Gestionează funcționalitățile bazate pe AI pentru această organizație.",
|
||||
"ai_instance_not_configured": "AI este configurată la nivel de instanță prin variabile de mediu. Cere administratorului să configureze AI_PROVIDER, credențialele acelui furnizor și lista de modele corespunzătoare înainte de a activa funcționalitățile AI.",
|
||||
@@ -2820,7 +2818,6 @@
|
||||
"adjust_survey_closed_message": "Ajustați mesajul 'Sondaj Închis'",
|
||||
"adjust_survey_closed_message_description": "Schimbați mesajul pe care îl văd vizitatorii atunci când sondajul este închis.",
|
||||
"adjust_theme_in_look_and_feel_settings": "Ajustează tema în setările <lookFeelLink>Aspect și Experiență</lookFeelLink>.",
|
||||
"ai_data_analysis_disabled": "Analiza de date AI este dezactivată pentru această organizație.",
|
||||
"ai_features_not_enabled": "Funcțiile AI nu sunt activate pentru această organizație.",
|
||||
"ai_instance_not_configured": "AI nu este configurat. Contactează administratorul.",
|
||||
"ai_smart_tools_disabled": "Instrumentele inteligente AI sunt dezactivate pentru această organizație.",
|
||||
|
||||
@@ -2612,8 +2612,6 @@
|
||||
"workspaces_being_added": "Рабочие пространства, которым предоставляется доступ"
|
||||
},
|
||||
"general": {
|
||||
"ai_data_analysis_enabled": "Обогащение и анализ данных (ИИ)",
|
||||
"ai_data_analysis_enabled_description": "ИИ для получения большего от твоих данных: настройка дашбордов, графиков, отчетов и не только. Работает с твоими данными об опыте.",
|
||||
"ai_enabled": "Formbricks AI",
|
||||
"ai_enabled_description": "Управляй функциями на базе ИИ для этой организации.",
|
||||
"ai_instance_not_configured": "ИИ настраивается на уровне инстанса через переменные окружения. Попросите администратора настроить AI_PROVIDER, учетные данные этого провайдера и соответствующий список моделей перед включением функций ИИ.",
|
||||
@@ -2820,7 +2818,6 @@
|
||||
"adjust_survey_closed_message": "Изменить сообщение «Опрос закрыт»",
|
||||
"adjust_survey_closed_message_description": "Измените сообщение, которое видят посетители, когда опрос закрыт.",
|
||||
"adjust_theme_in_look_and_feel_settings": "Настройте тему в разделе <lookFeelLink>Внешний вид</lookFeelLink>.",
|
||||
"ai_data_analysis_disabled": "Анализ данных с помощью ИИ отключён для этой организации.",
|
||||
"ai_features_not_enabled": "Функции ИИ не включены для этой организации.",
|
||||
"ai_instance_not_configured": "ИИ не настроен. Свяжись с администратором.",
|
||||
"ai_smart_tools_disabled": "Умные инструменты ИИ отключены для этой организации.",
|
||||
|
||||
@@ -2612,8 +2612,6 @@
|
||||
"workspaces_being_added": "Arbetsytor som beviljas åtkomst"
|
||||
},
|
||||
"general": {
|
||||
"ai_data_analysis_enabled": "Dataförbättring & analys (AI)",
|
||||
"ai_data_analysis_enabled_description": "AI för att få ut mer av din data, skapa dashboards, diagram, rapporter och mer. Använder din upplevelsedata.",
|
||||
"ai_enabled": "Formbricks AI",
|
||||
"ai_enabled_description": "Hantera AI-drivna funktioner för den här organisationen.",
|
||||
"ai_instance_not_configured": "AI konfigureras på instansnivå via miljövariabler. Be din administratör att ange AI_PROVIDER, autentiseringsuppgifterna för den leverantören och den tillhörande modellistan innan AI-funktioner aktiveras.",
|
||||
@@ -2820,7 +2818,6 @@
|
||||
"adjust_survey_closed_message": "Justera meddelande för 'Enkät stängd'",
|
||||
"adjust_survey_closed_message_description": "Ändra meddelandet besökare ser när enkäten är stängd.",
|
||||
"adjust_theme_in_look_and_feel_settings": "Justera temat i inställningarna för <lookFeelLink>Utseende & Känsla</lookFeelLink>.",
|
||||
"ai_data_analysis_disabled": "AI-dataanalys är inaktiverad för den här organisationen.",
|
||||
"ai_features_not_enabled": "AI-funktioner är inte aktiverade för den här organisationen.",
|
||||
"ai_instance_not_configured": "AI är inte konfigurerad. Kontakta din administratör.",
|
||||
"ai_smart_tools_disabled": "AI smarta verktyg är inaktiverade för den här organisationen.",
|
||||
|
||||
@@ -2612,8 +2612,6 @@
|
||||
"workspaces_being_added": "Erişim verilen çalışma alanları"
|
||||
},
|
||||
"general": {
|
||||
"ai_data_analysis_enabled": "Veri zenginleştirme ve analiz (Yapay Zeka)",
|
||||
"ai_data_analysis_enabled_description": "Verilerinden daha fazlasını elde etmek, kontrol panelleri, grafikler, raporlar ve daha fazlasını kurmak için yapay zeka. Deneyim verilerine dokunur.",
|
||||
"ai_enabled": "Formbricks Yapay Zeka",
|
||||
"ai_enabled_description": "Bu organizasyon için yapay zeka destekli özellikleri yönet.",
|
||||
"ai_instance_not_configured": "Yapay zeka, ortam değişkenleri aracılığıyla instance seviyesinde yapılandırılır. Yapay zeka özelliklerini etkinleştirmeden önce yöneticinden AI_PROVIDER, AI_MODEL ve eşleşen sağlayıcı kimlik bilgilerini ayarlamasını iste.",
|
||||
@@ -2820,7 +2818,6 @@
|
||||
"adjust_survey_closed_message": "\"Anket Kapatıldı\" mesajını düzenle",
|
||||
"adjust_survey_closed_message_description": "Anket kapalıyken ziyaretçilerin gördüğü mesajı değiştir.",
|
||||
"adjust_theme_in_look_and_feel_settings": "Temayı <lookFeelLink>Görünüm ve His</lookFeelLink> Ayarlarından düzenleyin.",
|
||||
"ai_data_analysis_disabled": "Bu organizasyon için yapay zeka veri analizi devre dışı.",
|
||||
"ai_features_not_enabled": "Bu organizasyon için yapay zeka özellikleri etkinleştirilmemiş.",
|
||||
"ai_instance_not_configured": "Yapay zeka yapılandırılmamış. Yöneticinle iletişime geç.",
|
||||
"ai_smart_tools_disabled": "Bu organizasyon için yapay zeka akıllı araçları devre dışı.",
|
||||
|
||||
@@ -2612,8 +2612,6 @@
|
||||
"workspaces_being_added": "将被授权访问的工作区"
|
||||
},
|
||||
"general": {
|
||||
"ai_data_analysis_enabled": "数据增强与分析(AI)",
|
||||
"ai_data_analysis_enabled_description": "使用 AI 深度挖掘你的数据,设置仪表盘、图表、报告等。会处理你的体验数据。",
|
||||
"ai_enabled": "Formbricks AI",
|
||||
"ai_enabled_description": "管理该组织的 AI 驱动功能。",
|
||||
"ai_instance_not_configured": "AI 通过环境变量在实例级别进行配置。启用 AI 功能前,请让管理员设置 AI_PROVIDER、该提供商的凭据以及对应的模型列表。",
|
||||
@@ -2820,7 +2818,6 @@
|
||||
"adjust_survey_closed_message": "调整 \"调查 关闭\" 消息",
|
||||
"adjust_survey_closed_message_description": "更改 访客 看到 调查 关闭 时 的 消息。",
|
||||
"adjust_theme_in_look_and_feel_settings": "在<lookFeelLink>外观与感觉</lookFeelLink>设置中调整主题。",
|
||||
"ai_data_analysis_disabled": "此组织已禁用 AI 数据分析。",
|
||||
"ai_features_not_enabled": "此组织未启用 AI 功能。",
|
||||
"ai_instance_not_configured": "AI 未配置。请联系您的管理员。",
|
||||
"ai_smart_tools_disabled": "此组织已禁用 AI 智能工具。",
|
||||
|
||||
@@ -2612,8 +2612,6 @@
|
||||
"workspaces_being_added": "正在授予存取權限的工作區"
|
||||
},
|
||||
"general": {
|
||||
"ai_data_analysis_enabled": "資料增強與分析(AI)",
|
||||
"ai_data_analysis_enabled_description": "利用 AI 深入分析你的資料,建立儀表板、圖表、報告等。會處理你的體驗資料。",
|
||||
"ai_enabled": "Formbricks AI",
|
||||
"ai_enabled_description": "管理此組織的 AI 功能。",
|
||||
"ai_instance_not_configured": "AI 會透過環境變數在實例層級進行設定。啟用 AI 功能前,請管理員設定 AI_PROVIDER、該供應商的憑證,以及對應的模型清單。",
|
||||
@@ -2820,7 +2818,6 @@
|
||||
"adjust_survey_closed_message": "調整「問卷已關閉」訊息",
|
||||
"adjust_survey_closed_message_description": "變更訪客在問卷關閉時看到的訊息。",
|
||||
"adjust_theme_in_look_and_feel_settings": "在<lookFeelLink>外觀與感覺</lookFeelLink>設定中調整主題。",
|
||||
"ai_data_analysis_disabled": "此組織已停用 AI 資料分析。",
|
||||
"ai_features_not_enabled": "此組織未啟用 AI 功能。",
|
||||
"ai_instance_not_configured": "AI 未設定。請聯絡您的管理員。",
|
||||
"ai_smart_tools_disabled": "此組織已停用 AI 智慧工具。",
|
||||
|
||||
@@ -10,7 +10,6 @@ export const CLOUD_STRIPE_FEATURE_LOOKUP_KEYS = {
|
||||
SPAM_PROTECTION: "spam-protection",
|
||||
CONTACTS: "contacts",
|
||||
AI_SMART_TOOLS: "ai-smart-tools",
|
||||
AI_DATA_ANALYSIS: "ai-data-analysis",
|
||||
FEEDBACK_DIRECTORIES: "feedback-directories",
|
||||
DASHBOARDS: "dashboards",
|
||||
} as const;
|
||||
|
||||
@@ -81,7 +81,7 @@ export const translateSurveyFieldsAction = authenticatedActionClient
|
||||
],
|
||||
});
|
||||
|
||||
await assertOrganizationAIConfigured(organizationId, "smartTools");
|
||||
await assertOrganizationAIConfigured(organizationId);
|
||||
|
||||
const translations = await translateFields({
|
||||
organizationId,
|
||||
|
||||
@@ -40,7 +40,6 @@ Rules:
|
||||
|
||||
const result = await generateOrganizationAIText({
|
||||
organizationId,
|
||||
capability: "smartTools",
|
||||
system: systemPrompt,
|
||||
prompt: JSON.stringify(items),
|
||||
});
|
||||
|
||||
@@ -363,10 +363,7 @@ export const generateAIChartAction = authenticatedActionClient
|
||||
|
||||
await checkDashboardsEnabled(organizationId);
|
||||
|
||||
// Verify AI is entitled, enabled at org level, and configured at instance level.
|
||||
// Uses "smartTools" (not "dataAnalysis") because chart generation only sends the
|
||||
// Cube schema context and the user's prompt to the LLM — no response PII.
|
||||
await assertOrganizationAIConfigured(organizationId, "smartTools");
|
||||
await assertOrganizationAIConfigured(organizationId);
|
||||
|
||||
const { feedbackDirectoryId } = await checkFeedbackDirectoryAccess({
|
||||
feedbackDirectoryId: parsedInput.feedbackDirectoryId,
|
||||
|
||||
@@ -148,7 +148,6 @@ describe("License Core Logic", () => {
|
||||
saml: true,
|
||||
spamProtection: true,
|
||||
aiSmartTools: false,
|
||||
aiDataAnalysis: false,
|
||||
auditLogs: true,
|
||||
accessControl: true,
|
||||
quotas: true,
|
||||
@@ -286,7 +285,6 @@ describe("License Core Logic", () => {
|
||||
removeBranding: false,
|
||||
contacts: false,
|
||||
aiSmartTools: false,
|
||||
aiDataAnalysis: false,
|
||||
saml: false,
|
||||
spamProtection: false,
|
||||
auditLogs: false,
|
||||
@@ -310,7 +308,6 @@ describe("License Core Logic", () => {
|
||||
removeBranding: false,
|
||||
contacts: false,
|
||||
aiSmartTools: false,
|
||||
aiDataAnalysis: false,
|
||||
saml: false,
|
||||
spamProtection: false,
|
||||
auditLogs: false,
|
||||
@@ -343,7 +340,6 @@ describe("License Core Logic", () => {
|
||||
removeBranding: false,
|
||||
contacts: false,
|
||||
aiSmartTools: false,
|
||||
aiDataAnalysis: false,
|
||||
saml: false,
|
||||
spamProtection: false,
|
||||
auditLogs: false,
|
||||
@@ -537,7 +533,6 @@ describe("License Core Logic", () => {
|
||||
saml: true,
|
||||
spamProtection: true,
|
||||
aiSmartTools: false,
|
||||
aiDataAnalysis: false,
|
||||
auditLogs: true,
|
||||
accessControl: true,
|
||||
quotas: true,
|
||||
@@ -604,7 +599,6 @@ describe("License Core Logic", () => {
|
||||
saml: true,
|
||||
spamProtection: true,
|
||||
aiSmartTools: false,
|
||||
aiDataAnalysis: false,
|
||||
auditLogs: true,
|
||||
accessControl: true,
|
||||
quotas: true,
|
||||
@@ -662,7 +656,6 @@ describe("License Core Logic", () => {
|
||||
saml: true,
|
||||
spamProtection: true,
|
||||
aiSmartTools: false,
|
||||
aiDataAnalysis: false,
|
||||
auditLogs: true,
|
||||
accessControl: true,
|
||||
quotas: true,
|
||||
@@ -807,7 +800,6 @@ describe("License Core Logic", () => {
|
||||
saml: true,
|
||||
spamProtection: true,
|
||||
aiSmartTools: true,
|
||||
aiDataAnalysis: true,
|
||||
auditLogs: true,
|
||||
accessControl: true,
|
||||
quotas: true,
|
||||
@@ -836,7 +828,6 @@ describe("License Core Logic", () => {
|
||||
saml: true,
|
||||
spamProtection: true,
|
||||
aiSmartTools: true,
|
||||
aiDataAnalysis: true,
|
||||
auditLogs: true,
|
||||
accessControl: true,
|
||||
quotas: true,
|
||||
@@ -866,7 +857,6 @@ describe("License Core Logic", () => {
|
||||
removeBranding: false,
|
||||
contacts: false,
|
||||
aiSmartTools: false,
|
||||
aiDataAnalysis: false,
|
||||
saml: false,
|
||||
spamProtection: false,
|
||||
auditLogs: false,
|
||||
@@ -940,7 +930,6 @@ describe("License Core Logic", () => {
|
||||
removeBranding: true,
|
||||
contacts: true,
|
||||
aiSmartTools: true,
|
||||
aiDataAnalysis: true,
|
||||
saml: true,
|
||||
spamProtection: true,
|
||||
auditLogs: true,
|
||||
@@ -1009,7 +998,6 @@ describe("License Core Logic", () => {
|
||||
removeBranding: true,
|
||||
contacts: true,
|
||||
aiSmartTools: true,
|
||||
aiDataAnalysis: true,
|
||||
saml: true,
|
||||
spamProtection: true,
|
||||
auditLogs: true,
|
||||
@@ -1051,7 +1039,6 @@ describe("License Core Logic", () => {
|
||||
saml: true,
|
||||
spamProtection: true,
|
||||
aiSmartTools: false,
|
||||
aiDataAnalysis: false,
|
||||
auditLogs: true,
|
||||
accessControl: true,
|
||||
quotas: true,
|
||||
@@ -1180,7 +1167,6 @@ describe("License Core Logic", () => {
|
||||
saml: true,
|
||||
spamProtection: true,
|
||||
aiSmartTools: false,
|
||||
aiDataAnalysis: false,
|
||||
auditLogs: true,
|
||||
accessControl: true,
|
||||
quotas: true,
|
||||
@@ -1304,7 +1290,6 @@ describe("License Core Logic", () => {
|
||||
removeBranding: true,
|
||||
contacts: true,
|
||||
aiSmartTools: true,
|
||||
aiDataAnalysis: true,
|
||||
saml: true,
|
||||
spamProtection: true,
|
||||
auditLogs: true,
|
||||
@@ -1360,7 +1345,6 @@ describe("License Core Logic", () => {
|
||||
removeBranding: true,
|
||||
contacts: true,
|
||||
aiSmartTools: true,
|
||||
aiDataAnalysis: true,
|
||||
saml: true,
|
||||
spamProtection: true,
|
||||
auditLogs: true,
|
||||
@@ -1416,7 +1400,6 @@ describe("License Core Logic", () => {
|
||||
removeBranding: true,
|
||||
contacts: true,
|
||||
aiSmartTools: true,
|
||||
aiDataAnalysis: true,
|
||||
saml: true,
|
||||
spamProtection: true,
|
||||
auditLogs: true,
|
||||
|
||||
@@ -83,7 +83,6 @@ const LicenseFeaturesSchema = z.object({
|
||||
removeBranding: z.boolean(),
|
||||
contacts: z.boolean(),
|
||||
aiSmartTools: z.boolean(),
|
||||
aiDataAnalysis: z.boolean(),
|
||||
saml: z.boolean(),
|
||||
spamProtection: z.boolean(),
|
||||
auditLogs: z.boolean(),
|
||||
@@ -153,7 +152,6 @@ const DEFAULT_FEATURES: TEnterpriseLicenseFeatures = {
|
||||
removeBranding: false,
|
||||
contacts: false,
|
||||
aiSmartTools: false,
|
||||
aiDataAnalysis: false,
|
||||
saml: false,
|
||||
spamProtection: false,
|
||||
auditLogs: false,
|
||||
|
||||
@@ -9,7 +9,6 @@ import { getEnterpriseLicense, getLicenseFeatures } from "./license";
|
||||
import {
|
||||
getAccessControlPermission,
|
||||
getBiggerUploadFileSizePermission,
|
||||
getIsAIDataAnalysisEnabled,
|
||||
getIsAISmartToolsEnabled,
|
||||
getIsAuditLogsEnabled,
|
||||
getIsContactsEnabled,
|
||||
@@ -60,7 +59,6 @@ const defaultFeatures: TEnterpriseLicenseFeatures = {
|
||||
saml: false,
|
||||
spamProtection: false,
|
||||
aiSmartTools: false,
|
||||
aiDataAnalysis: false,
|
||||
auditLogs: false,
|
||||
accessControl: false,
|
||||
quotas: false,
|
||||
@@ -216,57 +214,26 @@ describe("License Utils", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("uses cloud AI data analysis entitlement", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = true;
|
||||
vi.mocked(hasOrganizationEntitlementWithLicenseGuard).mockResolvedValueOnce(true);
|
||||
|
||||
const result = await getIsAIDataAnalysisEnabled("org_1");
|
||||
test("returns self-hosted AI smart tools from license", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = false;
|
||||
vi.mocked(getEnterpriseLicense).mockResolvedValue({
|
||||
...defaultLicense,
|
||||
features: { ...defaultFeatures, aiSmartTools: true },
|
||||
});
|
||||
|
||||
const result = await getIsAISmartToolsEnabled("org_1");
|
||||
expect(result).toBe(true);
|
||||
expect(hasOrganizationEntitlementWithLicenseGuard).toHaveBeenCalledWith(
|
||||
"org_1",
|
||||
CLOUD_STRIPE_FEATURE_LOOKUP_KEYS.AI_DATA_ANALYSIS
|
||||
);
|
||||
});
|
||||
|
||||
test("returns self-hosted AI features from license", async () => {
|
||||
test("returns false for self-hosted AI smart tools when not enabled", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = false;
|
||||
vi.mocked(getEnterpriseLicense).mockResolvedValue({
|
||||
...defaultLicense,
|
||||
features: {
|
||||
...defaultFeatures,
|
||||
aiSmartTools: true,
|
||||
aiDataAnalysis: true,
|
||||
},
|
||||
features: { ...defaultFeatures, aiSmartTools: false },
|
||||
});
|
||||
|
||||
const [smartTools, dataAnalysis] = await Promise.all([
|
||||
getIsAISmartToolsEnabled("org_1"),
|
||||
getIsAIDataAnalysisEnabled("org_1"),
|
||||
]);
|
||||
|
||||
expect(smartTools).toBe(true);
|
||||
expect(dataAnalysis).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false for self-hosted AI features when not enabled", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = false;
|
||||
vi.mocked(getEnterpriseLicense).mockResolvedValue({
|
||||
...defaultLicense,
|
||||
features: {
|
||||
...defaultFeatures,
|
||||
aiSmartTools: false,
|
||||
aiDataAnalysis: false,
|
||||
},
|
||||
});
|
||||
|
||||
const [smartTools, dataAnalysis] = await Promise.all([
|
||||
getIsAISmartToolsEnabled("org_1"),
|
||||
getIsAIDataAnalysisEnabled("org_1"),
|
||||
]);
|
||||
|
||||
expect(smartTools).toBe(false);
|
||||
expect(dataAnalysis).toBe(false);
|
||||
const result = await getIsAISmartToolsEnabled("org_1");
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("uses cloud feedback record directories entitlement", async () => {
|
||||
|
||||
@@ -31,13 +31,7 @@ const getCustomPlanFeaturePermission = async (
|
||||
organizationId: string,
|
||||
featureKey: keyof Pick<
|
||||
TEnterpriseLicenseFeatures,
|
||||
| "accessControl"
|
||||
| "quotas"
|
||||
| "contacts"
|
||||
| "aiSmartTools"
|
||||
| "aiDataAnalysis"
|
||||
| "feedbackDirectories"
|
||||
| "dashboards"
|
||||
"accessControl" | "quotas" | "contacts" | "aiSmartTools" | "feedbackDirectories" | "dashboards"
|
||||
>
|
||||
): Promise<boolean> => {
|
||||
if (IS_FORMBRICKS_CLOUD) {
|
||||
@@ -46,7 +40,6 @@ const getCustomPlanFeaturePermission = async (
|
||||
quotas: CLOUD_STRIPE_FEATURE_LOOKUP_KEYS.QUOTA_MANAGEMENT,
|
||||
contacts: CLOUD_STRIPE_FEATURE_LOOKUP_KEYS.CONTACTS,
|
||||
aiSmartTools: CLOUD_STRIPE_FEATURE_LOOKUP_KEYS.AI_SMART_TOOLS,
|
||||
aiDataAnalysis: CLOUD_STRIPE_FEATURE_LOOKUP_KEYS.AI_DATA_ANALYSIS,
|
||||
feedbackDirectories: CLOUD_STRIPE_FEATURE_LOOKUP_KEYS.FEEDBACK_DIRECTORIES,
|
||||
dashboards: CLOUD_STRIPE_FEATURE_LOOKUP_KEYS.DASHBOARDS,
|
||||
};
|
||||
@@ -126,10 +119,6 @@ export const getIsAISmartToolsEnabled = async (organizationId: string): Promise<
|
||||
return getCustomPlanFeaturePermission(organizationId, "aiSmartTools");
|
||||
};
|
||||
|
||||
export const getIsAIDataAnalysisEnabled = async (organizationId: string): Promise<boolean> => {
|
||||
return getCustomPlanFeaturePermission(organizationId, "aiDataAnalysis");
|
||||
};
|
||||
|
||||
export const getIsAuditLogsEnabled = async (): Promise<boolean> => {
|
||||
if (!AUDIT_LOG_ENABLED) return false;
|
||||
return getSpecificFeatureFlag("auditLogs");
|
||||
|
||||
@@ -15,7 +15,6 @@ const ZEnterpriseLicenseFeatures = z.object({
|
||||
saml: z.boolean(),
|
||||
spamProtection: z.boolean(),
|
||||
aiSmartTools: z.boolean(),
|
||||
aiDataAnalysis: z.boolean(),
|
||||
auditLogs: z.boolean(),
|
||||
accessControl: z.boolean(),
|
||||
quotas: z.boolean(),
|
||||
|
||||
@@ -28,7 +28,6 @@ describe("getFirstOrganization", () => {
|
||||
whitelabel: null,
|
||||
updatedAt: new Date(),
|
||||
isAISmartToolsEnabled: false,
|
||||
isAIDataAnalysisEnabled: false,
|
||||
};
|
||||
vi.mocked(prisma.organization.findFirst).mockResolvedValue(org);
|
||||
const result = await getFirstOrganization();
|
||||
|
||||
@@ -46,7 +46,6 @@ export const mockOrganization: TOrganization = {
|
||||
id: "org-123",
|
||||
name: "Test Organization",
|
||||
isAISmartToolsEnabled: false,
|
||||
isAIDataAnalysisEnabled: false,
|
||||
whitelabel: {
|
||||
logoUrl: null,
|
||||
faviconUrl: null,
|
||||
|
||||
@@ -145,26 +145,6 @@ describe("hasOrganizationEntitlementWithLicenseGuard", () => {
|
||||
expect(await hasOrganizationEntitlementWithLicenseGuard("org1", "ai-smart-tools")).toBe(false);
|
||||
});
|
||||
|
||||
test("returns true when license active and ai-data-analysis mapped feature enabled", async () => {
|
||||
mockGetContext.mockResolvedValue({
|
||||
...baseContext,
|
||||
features: ["ai-data-analysis"],
|
||||
licenseStatus: "active",
|
||||
licenseFeatures: { aiDataAnalysis: true } as TOrganizationEntitlementsContext["licenseFeatures"],
|
||||
});
|
||||
expect(await hasOrganizationEntitlementWithLicenseGuard("org1", "ai-data-analysis")).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false when license active but ai-data-analysis mapped feature disabled", async () => {
|
||||
mockGetContext.mockResolvedValue({
|
||||
...baseContext,
|
||||
features: ["ai-data-analysis"],
|
||||
licenseStatus: "active",
|
||||
licenseFeatures: { aiDataAnalysis: false } as TOrganizationEntitlementsContext["licenseFeatures"],
|
||||
});
|
||||
expect(await hasOrganizationEntitlementWithLicenseGuard("org1", "ai-data-analysis")).toBe(false);
|
||||
});
|
||||
|
||||
test("returns true when license active and feature has no license mapping", async () => {
|
||||
mockGetContext.mockResolvedValue({
|
||||
...baseContext,
|
||||
|
||||
@@ -11,7 +11,6 @@ const LICENSE_GUARDED_ENTITLEMENTS: Partial<Record<string, keyof TEnterpriseLice
|
||||
"spam-protection": "spamProtection",
|
||||
contacts: "contacts",
|
||||
"ai-smart-tools": "aiSmartTools",
|
||||
"ai-data-analysis": "aiDataAnalysis",
|
||||
"feedback-directories": "feedbackDirectories",
|
||||
dashboards: "dashboards",
|
||||
};
|
||||
|
||||
@@ -111,35 +111,6 @@ describe("getSelfHostedOrganizationEntitlementsContext", () => {
|
||||
const result = await getSelfHostedOrganizationEntitlementsContext("org1");
|
||||
|
||||
expect(result.features).toContain("ai-smart-tools");
|
||||
expect(result.features).not.toContain("ai-data-analysis");
|
||||
});
|
||||
|
||||
test("maps aiDataAnalysis feature to ai-data-analysis entitlement", async () => {
|
||||
mockGetOrg.mockResolvedValue({ id: "org1" } as any);
|
||||
mockGetLicense.mockResolvedValue({
|
||||
status: "active",
|
||||
active: true,
|
||||
features: { aiDataAnalysis: true },
|
||||
} as any);
|
||||
|
||||
const result = await getSelfHostedOrganizationEntitlementsContext("org1");
|
||||
|
||||
expect(result.features).toContain("ai-data-analysis");
|
||||
expect(result.features).not.toContain("ai-smart-tools");
|
||||
});
|
||||
|
||||
test("maps both AI features when both are enabled", async () => {
|
||||
mockGetOrg.mockResolvedValue({ id: "org1" } as any);
|
||||
mockGetLicense.mockResolvedValue({
|
||||
status: "active",
|
||||
active: true,
|
||||
features: { aiSmartTools: true, aiDataAnalysis: true },
|
||||
} as any);
|
||||
|
||||
const result = await getSelfHostedOrganizationEntitlementsContext("org1");
|
||||
|
||||
expect(result.features).toContain("ai-smart-tools");
|
||||
expect(result.features).toContain("ai-data-analysis");
|
||||
});
|
||||
|
||||
test("maps feedbackDirectories feature to feedback-directories entitlement", async () => {
|
||||
|
||||
@@ -30,9 +30,6 @@ const mapLicenseFeaturesToEntitlements = (
|
||||
if (features.aiSmartTools) {
|
||||
entitlementKeys.push(CLOUD_STRIPE_FEATURE_LOOKUP_KEYS.AI_SMART_TOOLS);
|
||||
}
|
||||
if (features.aiDataAnalysis) {
|
||||
entitlementKeys.push(CLOUD_STRIPE_FEATURE_LOOKUP_KEYS.AI_DATA_ANALYSIS);
|
||||
}
|
||||
if (features.feedbackDirectories) {
|
||||
entitlementKeys.push(CLOUD_STRIPE_FEATURE_LOOKUP_KEYS.FEEDBACK_DIRECTORIES);
|
||||
}
|
||||
|
||||
@@ -77,11 +77,9 @@ describe("getOrganizationAIKeys", () => {
|
||||
const mockOrgId = "org_test789";
|
||||
const mockOrganizationData: {
|
||||
isAISmartToolsEnabled: boolean;
|
||||
isAIDataAnalysisEnabled: boolean;
|
||||
billing: TOrganizationBilling;
|
||||
} = {
|
||||
isAISmartToolsEnabled: true,
|
||||
isAIDataAnalysisEnabled: true,
|
||||
billing: {
|
||||
stripeCustomerId: null,
|
||||
usageCycleAnchor: new Date(),
|
||||
@@ -104,7 +102,6 @@ describe("getOrganizationAIKeys", () => {
|
||||
},
|
||||
select: {
|
||||
isAISmartToolsEnabled: true,
|
||||
isAIDataAnalysisEnabled: true,
|
||||
billing: {
|
||||
select: {
|
||||
stripeCustomerId: true,
|
||||
|
||||
@@ -22,7 +22,6 @@ export const getOrganizationAIKeys = reactCache(
|
||||
organizationId: string
|
||||
): Promise<{
|
||||
isAISmartToolsEnabled: boolean;
|
||||
isAIDataAnalysisEnabled: boolean;
|
||||
billing: TOrganizationBilling;
|
||||
} | null> => {
|
||||
try {
|
||||
@@ -32,7 +31,6 @@ export const getOrganizationAIKeys = reactCache(
|
||||
},
|
||||
select: {
|
||||
isAISmartToolsEnabled: true,
|
||||
isAIDataAnalysisEnabled: true,
|
||||
billing: {
|
||||
select: {
|
||||
stripeCustomerId: true,
|
||||
@@ -50,7 +48,6 @@ export const getOrganizationAIKeys = reactCache(
|
||||
|
||||
return {
|
||||
isAISmartToolsEnabled: organization.isAISmartToolsEnabled,
|
||||
isAIDataAnalysisEnabled: organization.isAIDataAnalysisEnabled,
|
||||
billing: {
|
||||
stripeCustomerId: organization.billing.stripeCustomerId,
|
||||
limits: organization.billing.limits as TOrganizationBilling["limits"],
|
||||
|
||||
-1
@@ -145,7 +145,6 @@ export const ManageTranslationsModal = ({
|
||||
const errorMessages: Record<string, string> = {
|
||||
ai_features_not_enabled: t("workspace.surveys.edit.ai_features_not_enabled"),
|
||||
ai_smart_tools_disabled: t("workspace.surveys.edit.ai_smart_tools_disabled"),
|
||||
ai_data_analysis_disabled: t("workspace.surveys.edit.ai_data_analysis_disabled"),
|
||||
ai_instance_not_configured: t("workspace.surveys.edit.ai_instance_not_configured"),
|
||||
};
|
||||
return errorMessages[errorCode] ?? errorCode;
|
||||
|
||||
@@ -119,7 +119,6 @@ export const workspaceIdLayoutChecks = async (workspaceId: string) => {
|
||||
},
|
||||
},
|
||||
isAISmartToolsEnabled: true,
|
||||
isAIDataAnalysisEnabled: true,
|
||||
whitelabel: true,
|
||||
},
|
||||
},
|
||||
@@ -173,7 +172,6 @@ export const getWorkspaceWithRelations = reactCache(async (workspaceId: string,
|
||||
},
|
||||
},
|
||||
isAISmartToolsEnabled: true,
|
||||
isAIDataAnalysisEnabled: true,
|
||||
whitelabel: true,
|
||||
memberships: {
|
||||
where: { userId },
|
||||
@@ -223,7 +221,6 @@ export const getWorkspaceWithRelations = reactCache(async (workspaceId: string,
|
||||
name: data.organization.name,
|
||||
billing: data.organization.billing,
|
||||
isAISmartToolsEnabled: data.organization.isAISmartToolsEnabled,
|
||||
isAIDataAnalysisEnabled: data.organization.isAIDataAnalysisEnabled,
|
||||
whitelabel: data.organization.whitelabel,
|
||||
},
|
||||
membership: data.organization.memberships[0] || null,
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- Remove isAIDataAnalysisEnabled — feature flag added prematurely before any AI data analysis
|
||||
-- feature existed. Will be reintroduced at the directory level when the feature is built.
|
||||
ALTER TABLE "Organization" DROP COLUMN IF EXISTS "isAIDataAnalysisEnabled";
|
||||
@@ -648,23 +648,21 @@ model Workspace {
|
||||
/// @property billing - JSON field containing billing information
|
||||
/// @property whitelabel - Whitelabel configuration for the organization
|
||||
/// @property isAISmartToolsEnabled - Controls access to AI smart tools (e.g. translations) that never touch collected data
|
||||
/// @property isAIDataAnalysisEnabled - Controls access to AI data analysis features that touch experience data
|
||||
model Organization {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @updatedAt @map(name: "updated_at")
|
||||
name String
|
||||
memberships Membership[]
|
||||
workspaces Workspace[]
|
||||
billing OrganizationBilling?
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @updatedAt @map(name: "updated_at")
|
||||
name String
|
||||
memberships Membership[]
|
||||
workspaces Workspace[]
|
||||
billing OrganizationBilling?
|
||||
/// [OrganizationWhitelabel]
|
||||
whitelabel Json @default("{}")
|
||||
invites Invite[]
|
||||
isAISmartToolsEnabled Boolean @default(false)
|
||||
isAIDataAnalysisEnabled Boolean @default(false)
|
||||
teams Team[]
|
||||
apiKeys ApiKey[]
|
||||
feedbackDirectories FeedbackDirectory[]
|
||||
whitelabel Json @default("{}")
|
||||
invites Invite[]
|
||||
isAISmartToolsEnabled Boolean @default(false)
|
||||
teams Team[]
|
||||
apiKeys ApiKey[]
|
||||
feedbackDirectories FeedbackDirectory[]
|
||||
}
|
||||
|
||||
/// Stores billing and Stripe synchronization data for an organization.
|
||||
|
||||
@@ -69,5 +69,4 @@ export const ZOrganization = z.object({
|
||||
name: z.string(),
|
||||
whitelabel: ZOrganizationWhiteLabel,
|
||||
isAISmartToolsEnabled: z.boolean().default(false) as z.ZodType<Organization["isAISmartToolsEnabled"]>,
|
||||
isAIDataAnalysisEnabled: z.boolean().default(false) as z.ZodType<Organization["isAIDataAnalysisEnabled"]>,
|
||||
}) satisfies z.ZodType<Organization>;
|
||||
|
||||
@@ -135,7 +135,6 @@ export const exampleData = {
|
||||
},
|
||||
},
|
||||
isAISmartToolsEnabled: false,
|
||||
isAIDataAnalysisEnabled: false,
|
||||
} as unknown as TOrganization,
|
||||
},
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "./button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface BackButtonProps {
|
||||
onClick: () => void;
|
||||
@@ -7,11 +7,18 @@ interface BackButtonProps {
|
||||
tabIndex?: number;
|
||||
}
|
||||
|
||||
export function BackButton({ onClick, backButtonLabel, tabIndex = 2 }: Readonly<BackButtonProps>) {
|
||||
export function BackButton({ onClick, backButtonLabel, tabIndex = 2 }: BackButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Button dir="auto" type="button" variant="secondary" tabIndex={tabIndex} onClick={onClick}>
|
||||
<button
|
||||
dir="auto"
|
||||
tabIndex={tabIndex}
|
||||
type="button"
|
||||
className={cn(
|
||||
"hover:bg-input-bg text-heading focus:ring-focus rounded-custom mb-1 flex items-center px-3 py-3 text-base leading-4 font-medium focus:ring-2 focus:ring-offset-2 focus:outline-hidden"
|
||||
)}
|
||||
onClick={onClick}>
|
||||
{backButtonLabel || t("common.back")}
|
||||
</Button>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { ButtonHTMLAttributes } from "preact";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type ButtonProps = {
|
||||
variant: "primary" | "secondary";
|
||||
} & ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
|
||||
export function Button({ variant, className, children, ...rest }: Readonly<ButtonProps>) {
|
||||
return (
|
||||
<button
|
||||
dir="auto"
|
||||
type="button"
|
||||
className={cn([
|
||||
"border-border mb-1 flex items-center justify-center border leading-4",
|
||||
"focus:ring-focus shadow-xs focus:ring-2 focus:ring-offset-2 focus:outline-hidden",
|
||||
"enabled:hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-60",
|
||||
[
|
||||
// secondary uses primary's bg color as text color and vice-versa
|
||||
'data-[variant="secondary"]:bg-(--fb-button-text-color)!',
|
||||
'data-[variant="secondary"]:text-(--fb-button-bg-color)!',
|
||||
],
|
||||
String(className || ""),
|
||||
])}
|
||||
data-variant={variant}
|
||||
style={{
|
||||
backgroundColor: "var(--fb-button-bg-color)",
|
||||
color: "var(--fb-button-text-color)",
|
||||
borderRadius: "var(--fb-button-border-radius)",
|
||||
height: "var(--fb-button-height)",
|
||||
fontSize: "var(--fb-button-font-size)",
|
||||
fontWeight: "var(--fb-button-font-weight)",
|
||||
paddingLeft: "var(--fb-button-padding-x)",
|
||||
paddingRight: "var(--fb-button-padding-x)",
|
||||
paddingTop: "var(--fb-button-padding-y)",
|
||||
paddingBottom: "var(--fb-button-padding-y)",
|
||||
}}
|
||||
{...rest}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import { ButtonHTMLAttributes } from "preact";
|
||||
import { useRef } from "preact/compat";
|
||||
import { ButtonHTMLAttributes, useRef } from "preact/compat";
|
||||
import { useCallback, useEffect, useState } from "preact/hooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "./button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface SubmitButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
buttonLabel?: string;
|
||||
@@ -67,18 +66,32 @@ export function SubmitButton({
|
||||
}, [focus]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
<button
|
||||
{...props}
|
||||
dir="auto"
|
||||
variant="primary"
|
||||
className="button-custom border-submit-button-border"
|
||||
ref={buttonRef}
|
||||
type={type}
|
||||
tabIndex={tabIndex}
|
||||
autoFocus={focus}
|
||||
className={cn(
|
||||
"border-submit-button-border focus:ring-focus mb-1 flex items-center justify-center border leading-4 shadow-xs focus:ring-2 focus:ring-offset-2 focus:outline-hidden enabled:hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-60",
|
||||
"button-custom"
|
||||
)}
|
||||
style={{
|
||||
borderRadius: "var(--fb-button-border-radius)",
|
||||
backgroundColor: "var(--fb-button-bg-color)",
|
||||
color: "var(--fb-button-text-color)",
|
||||
height: "var(--fb-button-height)",
|
||||
fontSize: "var(--fb-button-font-size)",
|
||||
fontWeight: "var(--fb-button-font-weight)",
|
||||
paddingLeft: "var(--fb-button-padding-x)",
|
||||
paddingRight: "var(--fb-button-padding-x)",
|
||||
paddingTop: "var(--fb-button-padding-y)",
|
||||
paddingBottom: "var(--fb-button-padding-y)",
|
||||
}}
|
||||
onClick={onClick}
|
||||
disabled={disabled}>
|
||||
{buttonLabel || (isLastQuestion ? t("common.finish") : t("common.next"))}
|
||||
</Button>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -552,12 +552,4 @@ describe("cn", () => {
|
||||
test("handles all undefined", () => {
|
||||
expect(cn(undefined, undefined)).toBe("");
|
||||
});
|
||||
|
||||
test("handles nested arrays of classes", () => {
|
||||
expect(cn(["foo", ["foo", ["foo", "bar"]]])).toBe("foo foo foo bar");
|
||||
});
|
||||
|
||||
test("handles nulls, booleans and undefined values", () => {
|
||||
expect(cn(null, true, false, undefined, [null, true, false, undefined])).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,14 +12,8 @@ import { type TSurveyElement, type TSurveyElementChoice } from "@formbricks/type
|
||||
import { type TShuffleOption } from "@formbricks/types/surveys/types";
|
||||
import { ApiResponse, ApiSuccessResponse } from "@/types/api";
|
||||
|
||||
type ClassValue = string | boolean | null | undefined | ClassValue[];
|
||||
export const cn = (...classes: ClassValue[]): string => {
|
||||
return twMerge(
|
||||
classes
|
||||
.map((c) => (Array.isArray(c) ? cn(...c) : c))
|
||||
.filter((c): c is string => typeof c === "string" && c.length > 0)
|
||||
.join(" ")
|
||||
);
|
||||
export const cn = (...classes: (string | undefined)[]) => {
|
||||
return twMerge(classes.filter(Boolean).join(" "));
|
||||
};
|
||||
|
||||
export const getSecureRandom = (): number => {
|
||||
|
||||
@@ -86,7 +86,6 @@ export const ZOrganization = z.object({
|
||||
whitelabel: ZOrganizationWhitelabel.optional(),
|
||||
billing: ZOrganizationBilling,
|
||||
isAISmartToolsEnabled: z.boolean().prefault(false),
|
||||
isAIDataAnalysisEnabled: z.boolean().prefault(false),
|
||||
});
|
||||
|
||||
export const ZOrganizationCreateInput = z.object({
|
||||
@@ -101,7 +100,6 @@ export const ZOrganizationUpdateInput = z.object({
|
||||
whitelabel: ZOrganizationWhitelabel.optional(),
|
||||
billing: ZOrganizationBilling.optional(),
|
||||
isAISmartToolsEnabled: z.boolean().optional(),
|
||||
isAIDataAnalysisEnabled: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type TOrganizationUpdateInput = z.infer<typeof ZOrganizationUpdateInput>;
|
||||
|
||||
Reference in New Issue
Block a user