feat: adds product config (#2760)

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
Piyush Gupta
2024-06-13 18:21:02 +05:30
committed by GitHub
parent 543d85eb28
commit 4e6ab1c2bb
8 changed files with 138 additions and 3 deletions

View File

@@ -101,7 +101,13 @@ export const POST = async (request: Request) => {
if (isMultiOrgEnabled) {
const organization = await createOrganization({ name: user.name + "'s Organization" });
await createMembership(organization.id, user.id, { role: "owner", accepted: true });
const product = await createProduct(organization.id, { name: "My Product" });
const product = await createProduct(organization.id, {
name: "My Product",
config: {
channel: null,
industry: null,
},
});
const updatedNotificationSettings = {
...user.notificationSettings,

View File

@@ -0,0 +1,109 @@
import { PrismaClient, SurveyType } from "@prisma/client";
import { TProductConfigChannel } from "@formbricks/types/product";
const prisma = new PrismaClient();
const main = async () => {
await prisma.$transaction(
async (tx) => {
const startTime = Date.now();
console.log("Starting data migration...");
// Fetch all products
const products = await tx.product.findMany({
include: {
environments: {
select: {
surveys: {
select: {
type: true,
},
},
},
},
},
});
console.log(`Found ${products.length} products to migrate...\n`);
const channelStatusCounts = {
[SurveyType.app]: 0,
[SurveyType.link]: 0,
[SurveyType.website]: 0,
null: 0,
};
const updatePromises = products.map((product) => {
const surveyTypes = new Set<SurveyType>();
// Collect all unique survey types for the product
for (const environment of product.environments) {
for (const survey of environment.surveys) {
surveyTypes.add(survey.type);
}
}
// Determine the channel based on the survey types, default to null
let channel: TProductConfigChannel = null;
if (surveyTypes.size === 0 || surveyTypes.size === 3) {
// if there are no surveys or all 3 types of surveys (website, app, and link) are present, set channel to null
channel = null;
} else if (surveyTypes.size === 1) {
// if there is only one type of survey, set channel to that type
const type = Array.from(surveyTypes)[0];
if (type === SurveyType.web) {
// if the survey type is web, set channel to null, since web is a legacy type and will be removed
channel = null;
} else {
// if the survey type is not web, set channel to that type
channel = type;
}
} else if (surveyTypes.has(SurveyType.link) && surveyTypes.has(SurveyType.app)) {
// if both link and app surveys are present, set channel to app
channel = SurveyType.app;
} else if (surveyTypes.has(SurveyType.link) && surveyTypes.has(SurveyType.website)) {
// if both link and website surveys are present, set channel to website
channel = SurveyType.website;
}
// Increment the count for the determined channel
channelStatusCounts[channel ?? "null"]++;
// Update the product with the determined channel and set industry to null
return tx.product.update({
where: { id: product.id },
data: {
config: {
channel,
industry: null,
},
},
});
});
await Promise.all(updatePromises);
console.log(
`Channel status counts: ${Object.entries(channelStatusCounts).map(
([channel, count]) => `\n${channel}: ${count}`
)}\n`
);
const endTime = Date.now();
console.log(`Data migration completed. Total time: ${(endTime - startTime) / 1000}s`);
},
{
timeout: 180000, // 3 minutes
}
);
};
main()
.catch((e: Error) => {
console.error("Error during migration: ", e.message);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

View File

@@ -1,7 +1,7 @@
import { TActionClassNoCodeConfig } from "@formbricks/types/actionClasses";
import { TIntegrationConfig } from "@formbricks/types/integration";
import { TOrganizationBilling } from "@formbricks/types/organizations";
import { TProductStyling } from "@formbricks/types/product";
import { TProductConfig, TProductStyling } from "@formbricks/types/product";
import { TResponseData, TResponseMeta, TResponsePersonAttributes } from "@formbricks/types/responses";
import { TBaseFilters } from "@formbricks/types/segment";
import {
@@ -22,6 +22,7 @@ declare global {
export type ActionProperties = { [key: string]: string };
export type ActionClassNoCodeConfig = TActionClassNoCodeConfig;
export type IntegrationConfig = TIntegrationConfig;
export type ProductConfig = TProductConfig;
export type ResponseData = TResponseData;
export type ResponseMeta = TResponseMeta;
export type ResponsePersonAttributes = TResponsePersonAttributes;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Product" ADD COLUMN "config" JSONB NOT NULL DEFAULT '{}';

View File

@@ -34,7 +34,8 @@
"data-migration:mls-welcomeCard-fix": "ts-node ./data-migrations/20240318050527_add_languages_and_survey_languages/data-migration-welcomeCard-fix.ts",
"data-migration:v2.0": "pnpm data-migration:mls && pnpm data-migration:styling && pnpm data-migration:styling-fix && pnpm data-migration:website-surveys && pnpm data-migration:userId && pnpm data-migration:mls-welcomeCard-fix && pnpm data-migration:refactor-actions",
"data-migration:extended-noCodeActions": "ts-node ./data-migrations/20240524053239_extends_no_code_action_schema/data-migration.ts",
"data-migration:v2.1": "pnpm data-migration:extended-noCodeActions"
"data-migration:v2.1": "pnpm data-migration:extended-noCodeActions",
"data-migration:product-config": "ts-node ./data-migrations/20240612115151_adds_product_config/data-migration.ts"
},
"dependencies": {
"@prisma/client": "^5.14.0",

View File

@@ -433,6 +433,9 @@ model Product {
/// @zod.custom(imports.ZProductStyling)
/// [Styling]
styling Json @default("{\"allowStyleOverwrite\":true}")
/// @zod.custom(imports.ZProductConfig)
/// [ProductConfig]
config Json @default("{}")
recontactDays Int @default(7)
linkSurveyBranding Boolean @default(true) // Determines if the survey branding should be displayed in link surveys
inAppSurveyBranding Boolean @default(true) // Determines if the survey branding should be displayed in in-app surveys

View File

@@ -25,6 +25,7 @@ const selectProduct = {
recontactDays: true,
linkSurveyBranding: true,
inAppSurveyBranding: true,
config: true,
placement: true,
clickOutsideClose: true,
darkOverlay: true,

View File

@@ -9,6 +9,16 @@ export const ZProductStyling = ZBaseStyling.extend({
export type TProductStyling = z.infer<typeof ZProductStyling>;
export const ZProductConfigChannel = z.enum(["link", "app", "website"]).nullable();
export type TProductConfigChannel = z.infer<typeof ZProductConfigChannel>;
export const ZProductConfig = z.object({
channel: ZProductConfigChannel,
industry: z.enum(["eCommerce", "saas"]).nullable(),
});
export type TProductConfig = z.infer<typeof ZProductConfig>;
export const ZLanguage = z.object({
id: z.string().cuid2(),
createdAt: z.date(),
@@ -50,6 +60,7 @@ export const ZProduct = z.object({
.max(365, { message: "Must be less than 365" }),
inAppSurveyBranding: z.boolean(),
linkSurveyBranding: z.boolean(),
config: ZProductConfig,
placement: ZPlacement,
clickOutsideClose: z.boolean(),
darkOverlay: z.boolean(),
@@ -70,6 +81,7 @@ export const ZProductUpdateInput = z.object({
recontactDays: z.number().int().optional(),
inAppSurveyBranding: z.boolean().optional(),
linkSurveyBranding: z.boolean().optional(),
config: ZProductConfig.optional(),
placement: ZPlacement.optional(),
clickOutsideClose: z.boolean().optional(),
darkOverlay: z.boolean().optional(),