fix: applying sonarqube suggestions and PR feedback and doing self review

This commit is contained in:
TheodorTomas
2026-02-20 14:39:05 +07:00
parent bb257ed3d2
commit b02dcbb25f
11 changed files with 403 additions and 252 deletions

View File

@@ -22,6 +22,9 @@ export type AuditLoggingCtx = {
quotaId?: string;
teamId?: string;
integrationId?: string;
chartId?: string;
dashboardId?: string;
dashboardWidgetId?: string;
};
export type ActionClientCtx = {

View File

@@ -13,7 +13,6 @@ type HasFindMany =
| Prisma.ProjectTeamFindManyArgs
| Prisma.UserFindManyArgs
| Prisma.ContactAttributeKeyFindManyArgs
| Prisma.ContactAttributeKeyFindManyArgs
| Prisma.ChartFindManyArgs
| Prisma.DashboardFindManyArgs;

View File

@@ -1,5 +1,4 @@
import { beforeEach, describe, expect, test, vi } from "vitest";
import { executeQuery } from "./cube-client";
const mockLoad = vi.fn();
const mockTablePivot = vi.fn();
@@ -13,12 +12,14 @@ vi.mock("@cubejs-client/core", () => ({
describe("executeQuery", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.resetModules();
const resultSet = { tablePivot: mockTablePivot };
mockLoad.mockResolvedValue(resultSet);
mockTablePivot.mockReturnValue([{ id: "1", count: 42 }]);
});
test("loads query and returns tablePivot result", async () => {
const { executeQuery } = await import("./cube-client");
const query = { measures: ["FeedbackRecords.count"] };
const result = await executeQuery(query);

View File

@@ -12,6 +12,7 @@ let cubeClient: CubeApi | null = null;
function getCubeClient(): CubeApi {
if (!cubeClient) {
// TODO: This will fail silently if the token is not set. We need to fix this before going to production.
const token = process.env.CUBEJS_API_TOKEN ?? "";
cubeClient = cubejs(token, { apiUrl: getApiUrl() });
}

View File

@@ -1,36 +1,22 @@
"use server";
import { z } from "zod";
import { prisma } from "@formbricks/database";
import { ZId } from "@formbricks/types/common";
import { ZChartConfig, ZChartQuery } from "@formbricks/types/dashboard";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/context";
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
import {
createChart,
deleteChart,
duplicateChart,
getChart,
getCharts,
updateChart,
} from "@/modules/ee/analysis/charts/lib/charts";
import { checkProjectAccess } from "@/modules/ee/analysis/lib/access";
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
import { ZChartType } from "../types/analysis";
const checkProjectAccess = async (
userId: string,
environmentId: string,
minPermission: "read" | "readWrite" | "manage"
) => {
const organizationId = await getOrganizationIdFromEnvironmentId(environmentId);
const projectId = await getProjectIdFromEnvironmentId(environmentId);
await checkAuthorizationUpdated({
userId,
organizationId,
access: [
{ type: "organization", roles: ["owner", "manager"] },
{ type: "projectTeam", minPermission, projectId },
],
});
return { organizationId, projectId };
};
const ZCreateChartAction = z.object({
environmentId: ZId,
name: z.string().min(1),
@@ -56,19 +42,18 @@ export const createChartAction = authenticatedActionClient.schema(ZCreateChartAc
"readWrite"
);
const chart = await prisma.chart.create({
data: {
name: parsedInput.name,
type: parsedInput.type,
projectId,
query: parsedInput.query,
config: parsedInput.config || {},
createdBy: ctx.user.id,
},
});
const chart = await createChart(
projectId,
parsedInput.name,
parsedInput.type,
parsedInput.query,
parsedInput.config || {},
ctx.user.id
);
ctx.auditLoggingCtx.organizationId = organizationId;
ctx.auditLoggingCtx.projectId = projectId;
ctx.auditLoggingCtx.chartId = chart.id;
ctx.auditLoggingCtx.newObject = chart;
return chart;
}
@@ -101,26 +86,16 @@ export const updateChartAction = authenticatedActionClient.schema(ZUpdateChartAc
"readWrite"
);
const chart = await prisma.chart.findFirst({
where: { id: parsedInput.chartId, projectId },
});
if (!chart) {
throw new Error("Chart not found");
}
const updatedChart = await prisma.chart.update({
where: { id: parsedInput.chartId },
data: {
...(parsedInput.name !== undefined && { name: parsedInput.name }),
...(parsedInput.type !== undefined && { type: parsedInput.type }),
...(parsedInput.query !== undefined && { query: parsedInput.query }),
...(parsedInput.config !== undefined && { config: parsedInput.config }),
},
const { chart, updatedChart } = await updateChart(parsedInput.chartId, projectId, {
name: parsedInput.name,
type: parsedInput.type,
query: parsedInput.query,
config: parsedInput.config,
});
ctx.auditLoggingCtx.organizationId = organizationId;
ctx.auditLoggingCtx.projectId = projectId;
ctx.auditLoggingCtx.chartId = parsedInput.chartId;
ctx.auditLoggingCtx.oldObject = chart;
ctx.auditLoggingCtx.newObject = updatedChart;
return updatedChart;
@@ -150,27 +125,11 @@ export const duplicateChartAction = authenticatedActionClient.schema(ZDuplicateC
"readWrite"
);
const sourceChart = await prisma.chart.findFirst({
where: { id: parsedInput.chartId, projectId },
});
if (!sourceChart) {
throw new Error("Chart not found");
}
const duplicatedChart = await prisma.chart.create({
data: {
name: `${sourceChart.name} (copy)`,
type: sourceChart.type,
projectId,
query: sourceChart.query as object,
config: (sourceChart.config as object) || {},
createdBy: ctx.user.id,
},
});
const duplicatedChart = await duplicateChart(parsedInput.chartId, projectId, ctx.user.id);
ctx.auditLoggingCtx.organizationId = organizationId;
ctx.auditLoggingCtx.projectId = projectId;
ctx.auditLoggingCtx.chartId = duplicatedChart.id;
ctx.auditLoggingCtx.newObject = duplicatedChart;
return duplicatedChart;
}
@@ -199,20 +158,11 @@ export const deleteChartAction = authenticatedActionClient.schema(ZDeleteChartAc
"readWrite"
);
const chart = await prisma.chart.findFirst({
where: { id: parsedInput.chartId, projectId },
});
if (!chart) {
throw new Error("Chart not found");
}
await prisma.chart.delete({
where: { id: parsedInput.chartId },
});
const chart = await deleteChart(parsedInput.chartId, projectId);
ctx.auditLoggingCtx.organizationId = organizationId;
ctx.auditLoggingCtx.projectId = projectId;
ctx.auditLoggingCtx.chartId = parsedInput.chartId;
ctx.auditLoggingCtx.oldObject = chart;
return { success: true };
}
@@ -236,24 +186,7 @@ export const getChartAction = authenticatedActionClient
}) => {
const { projectId } = await checkProjectAccess(ctx.user.id, parsedInput.environmentId, "read");
const chart = await prisma.chart.findFirst({
where: { id: parsedInput.chartId, projectId },
select: {
id: true,
name: true,
type: true,
query: true,
config: true,
createdAt: true,
updatedAt: true,
},
});
if (!chart) {
throw new Error("Chart not found");
}
return chart;
return getChart(parsedInput.chartId, projectId);
}
);
@@ -273,21 +206,6 @@ export const getChartsAction = authenticatedActionClient
}) => {
const { projectId } = await checkProjectAccess(ctx.user.id, parsedInput.environmentId, "read");
return prisma.chart.findMany({
where: { projectId },
orderBy: { createdAt: "desc" },
select: {
id: true,
name: true,
type: true,
createdAt: true,
updatedAt: true,
query: true,
config: true,
widgets: {
select: { dashboardId: true },
},
},
});
return getCharts(projectId);
}
);

View File

@@ -0,0 +1,174 @@
import "server-only";
import { prisma } from "@formbricks/database";
import { ZId } from "@formbricks/types/common";
import { TChartConfig, TChartQuery } from "@formbricks/types/dashboard";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { validateInputs } from "@/lib/utils/validate";
import { TChartType } from "../../types/analysis";
const selectChart = {
id: true,
name: true,
type: true,
query: true,
config: true,
createdAt: true,
updatedAt: true,
} as const;
export const createChart = async (
projectId: string,
name: string,
type: TChartType,
query: TChartQuery,
config: TChartConfig,
createdBy: string
) => {
validateInputs([projectId, ZId], [createdBy, ZId]);
return prisma.chart.create({
data: {
name,
type,
projectId,
query,
config: config || {},
createdBy,
},
});
};
export const updateChart = async (
chartId: string,
projectId: string,
data: {
name?: string;
type?: TChartType;
query?: TChartQuery;
config?: TChartConfig;
}
) => {
validateInputs([chartId, ZId], [projectId, ZId]);
return prisma.$transaction(async (tx) => {
const chart = await tx.chart.findFirst({
where: { id: chartId, projectId },
});
if (!chart) {
throw new ResourceNotFoundError("Chart", chartId);
}
const updatedChart = await tx.chart.update({
where: { id: chartId },
data: {
...(data.name !== undefined && { name: data.name }),
...(data.type !== undefined && { type: data.type }),
...(data.query !== undefined && { query: data.query }),
...(data.config !== undefined && { config: data.config }),
},
});
return { chart, updatedChart };
});
};
const getUniqueCopyName = async (baseName: string, projectId: string): Promise<string> => {
const stripped = baseName.replace(/\s+\(copy(?:\s+\d+)?\)$/, "");
const existing = await prisma.chart.findMany({
where: {
projectId,
name: { startsWith: `${stripped} (copy` },
},
select: { name: true },
});
const existingNames = new Set(existing.map((c) => c.name));
const firstCandidate = `${stripped} (copy)`;
if (!existingNames.has(firstCandidate)) {
return firstCandidate;
}
let n = 2;
while (existingNames.has(`${stripped} (copy ${n})`)) {
n++;
}
return `${stripped} (copy ${n})`;
};
export const duplicateChart = async (chartId: string, projectId: string, createdBy: string) => {
validateInputs([chartId, ZId], [projectId, ZId], [createdBy, ZId]);
const sourceChart = await prisma.chart.findFirst({
where: { id: chartId, projectId },
});
if (!sourceChart) {
throw new ResourceNotFoundError("Chart", chartId);
}
const uniqueName = await getUniqueCopyName(sourceChart.name, projectId);
return prisma.chart.create({
data: {
name: uniqueName,
type: sourceChart.type,
projectId,
query: sourceChart.query as object,
config: (sourceChart.config as object) || {},
createdBy,
},
});
};
export const deleteChart = async (chartId: string, projectId: string) => {
validateInputs([chartId, ZId], [projectId, ZId]);
return prisma.$transaction(async (tx) => {
const chart = await tx.chart.findFirst({
where: { id: chartId, projectId },
});
if (!chart) {
throw new ResourceNotFoundError("Chart", chartId);
}
await tx.chart.delete({
where: { id: chartId },
});
return chart;
});
};
export const getChart = async (chartId: string, projectId: string) => {
validateInputs([chartId, ZId], [projectId, ZId]);
const chart = await prisma.chart.findFirst({
where: { id: chartId, projectId },
select: selectChart,
});
if (!chart) {
throw new ResourceNotFoundError("Chart", chartId);
}
return chart;
};
export const getCharts = async (projectId: string) => {
validateInputs([projectId, ZId]);
return prisma.chart.findMany({
where: { projectId },
orderBy: { createdAt: "desc" },
select: {
...selectChart,
widgets: {
select: { dashboardId: true },
},
},
});
};

View File

@@ -4,32 +4,12 @@ import { z } from "zod";
import { prisma } from "@formbricks/database";
import { ZId } from "@formbricks/types/common";
import { ZWidgetLayout } from "@formbricks/types/dashboard";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/context";
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
import { checkProjectAccess } from "@/modules/ee/analysis/lib/access";
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
const checkProjectAccess = async (
userId: string,
environmentId: string,
minPermission: "read" | "readWrite" | "manage"
) => {
const organizationId = await getOrganizationIdFromEnvironmentId(environmentId);
const projectId = await getProjectIdFromEnvironmentId(environmentId);
await checkAuthorizationUpdated({
userId,
organizationId,
access: [
{ type: "organization", roles: ["owner", "manager"] },
{ type: "projectTeam", minPermission, projectId },
],
});
return { organizationId, projectId };
};
const ZCreateDashboardAction = z.object({
environmentId: ZId,
name: z.string().min(1),
@@ -64,6 +44,7 @@ export const createDashboardAction = authenticatedActionClient.schema(ZCreateDas
ctx.auditLoggingCtx.organizationId = organizationId;
ctx.auditLoggingCtx.projectId = projectId;
ctx.auditLoggingCtx.dashboardId = dashboard.id;
ctx.auditLoggingCtx.newObject = dashboard;
return dashboard;
}
@@ -99,7 +80,7 @@ export const updateDashboardAction = authenticatedActionClient.schema(ZUpdateDas
});
if (!dashboard) {
throw new Error("Dashboard not found");
throw new ResourceNotFoundError("Dashboard", parsedInput.dashboardId);
}
const updatedDashboard = await prisma.dashboard.update({
@@ -112,6 +93,7 @@ export const updateDashboardAction = authenticatedActionClient.schema(ZUpdateDas
ctx.auditLoggingCtx.organizationId = organizationId;
ctx.auditLoggingCtx.projectId = projectId;
ctx.auditLoggingCtx.dashboardId = parsedInput.dashboardId;
ctx.auditLoggingCtx.oldObject = dashboard;
ctx.auditLoggingCtx.newObject = updatedDashboard;
return updatedDashboard;
@@ -146,7 +128,7 @@ export const deleteDashboardAction = authenticatedActionClient.schema(ZDeleteDas
});
if (!dashboard) {
throw new Error("Dashboard not found");
throw new ResourceNotFoundError("Dashboard", parsedInput.dashboardId);
}
await prisma.dashboard.delete({
@@ -155,6 +137,7 @@ export const deleteDashboardAction = authenticatedActionClient.schema(ZDeleteDas
ctx.auditLoggingCtx.organizationId = organizationId;
ctx.auditLoggingCtx.projectId = projectId;
ctx.auditLoggingCtx.dashboardId = parsedInput.dashboardId;
ctx.auditLoggingCtx.oldObject = dashboard;
return { success: true };
}
@@ -230,7 +213,7 @@ export const getDashboardAction = authenticatedActionClient
});
if (!dashboard) {
throw new Error("Dashboard not found");
throw new ResourceNotFoundError("Dashboard", parsedInput.dashboardId);
}
return dashboard;
@@ -245,56 +228,60 @@ const ZAddChartToDashboardAction = z.object({
layout: ZWidgetLayout.optional().default({ x: 0, y: 0, w: 4, h: 3 }),
});
export const addChartToDashboardAction = authenticatedActionClient
.schema(ZAddChartToDashboardAction)
.action(
withAuditLogging(
"created",
"dashboardWidget",
async ({
ctx,
parsedInput,
}: {
ctx: AuthenticatedActionClientCtx;
parsedInput: z.infer<typeof ZAddChartToDashboardAction>;
}) => {
const { organizationId, projectId } = await checkProjectAccess(
ctx.user.id,
parsedInput.environmentId,
"readWrite"
);
export const addChartToDashboardAction = authenticatedActionClient.schema(ZAddChartToDashboardAction).action(
withAuditLogging(
"created",
"dashboardWidget",
async ({
ctx,
parsedInput,
}: {
ctx: AuthenticatedActionClientCtx;
parsedInput: z.infer<typeof ZAddChartToDashboardAction>;
}) => {
const { organizationId, projectId } = await checkProjectAccess(
ctx.user.id,
parsedInput.environmentId,
"readWrite"
);
const [chart, dashboard] = await Promise.all([
prisma.chart.findFirst({ where: { id: parsedInput.chartId, projectId } }),
prisma.dashboard.findFirst({ where: { id: parsedInput.dashboardId, projectId } }),
]);
const [chart, dashboard] = await Promise.all([
prisma.chart.findFirst({ where: { id: parsedInput.chartId, projectId } }),
prisma.dashboard.findFirst({ where: { id: parsedInput.dashboardId, projectId } }),
]);
if (!chart) {
throw new Error("Chart not found");
}
if (!dashboard) {
throw new Error("Dashboard not found");
}
const maxOrder = await prisma.dashboardWidget.aggregate({
where: { dashboardId: parsedInput.dashboardId },
_max: { order: true },
});
const widget = await prisma.dashboardWidget.create({
data: {
dashboardId: parsedInput.dashboardId,
chartId: parsedInput.chartId,
title: parsedInput.title,
layout: parsedInput.layout,
order: (maxOrder._max.order ?? -1) + 1,
},
});
ctx.auditLoggingCtx.organizationId = organizationId;
ctx.auditLoggingCtx.projectId = projectId;
ctx.auditLoggingCtx.newObject = widget;
return widget;
if (!chart) {
throw new ResourceNotFoundError("Chart", parsedInput.chartId);
}
)
);
if (!dashboard) {
throw new ResourceNotFoundError("Dashboard", parsedInput.dashboardId);
}
const widget = await prisma.$transaction(
async (tx) => {
const maxOrder = await tx.dashboardWidget.aggregate({
where: { dashboardId: parsedInput.dashboardId },
_max: { order: true },
});
return tx.dashboardWidget.create({
data: {
dashboardId: parsedInput.dashboardId,
chartId: parsedInput.chartId,
title: parsedInput.title,
layout: parsedInput.layout,
order: (maxOrder._max.order ?? -1) + 1,
},
});
},
{ isolationLevel: "Serializable" }
);
ctx.auditLoggingCtx.organizationId = organizationId;
ctx.auditLoggingCtx.projectId = projectId;
ctx.auditLoggingCtx.dashboardWidgetId = widget.id;
ctx.auditLoggingCtx.newObject = widget;
return widget;
}
)
);

View File

@@ -0,0 +1,31 @@
import "server-only";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { getEnvironment } from "@/lib/environment/service";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
import { getOrganizationIdFromProjectId } from "@/lib/utils/helper";
import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/team";
export const checkProjectAccess = async (
userId: string,
environmentId: string,
minPermission: TTeamPermission
) => {
const environment = await getEnvironment(environmentId);
if (!environment) {
throw new ResourceNotFoundError("environment", environmentId);
}
const projectId = environment.projectId;
const organizationId = await getOrganizationIdFromProjectId(projectId);
await checkAuthorizationUpdated({
userId,
organizationId,
access: [
{ type: "organization", roles: ["owner", "manager"] },
{ type: "projectTeam", minPermission, projectId },
],
});
return { organizationId, projectId };
};

View File

@@ -292,6 +292,15 @@ export const withAuditLogging = <TParsedInput = Record<string, unknown>, TResult
case "quota":
targetId = auditLoggingCtx.quotaId;
break;
case "chart":
targetId = auditLoggingCtx.chartId;
break;
case "dashboard":
targetId = auditLoggingCtx.dashboardId;
break;
case "dashboardWidget":
targetId = auditLoggingCtx.dashboardWidgetId;
break;
default:
targetId = UNKNOWN_DATA;
break;

View File

@@ -587,9 +587,7 @@ async function main(): Promise<void> {
createdBy: SEED_IDS.USER_ADMIN,
query: {
measures: ["FeedbackRecords.count"],
timeDimensions: [
{ dimension: "FeedbackRecords.createdAt", granularity: "week" },
],
timeDimensions: [{ dimension: "FeedbackRecords.createdAt", granularity: "week" }],
},
config: {
xAxisLabel: "Week",
@@ -676,9 +674,7 @@ async function main(): Promise<void> {
query: {
measures: ["FeedbackRecords.count"],
dimensions: ["FeedbackRecords.channel"],
timeDimensions: [
{ dimension: "FeedbackRecords.createdAt", granularity: "month" },
],
timeDimensions: [{ dimension: "FeedbackRecords.createdAt", granularity: "month" }],
},
config: {
stacked: true,
@@ -716,66 +712,91 @@ async function main(): Promise<void> {
});
// Widgets for Overview dashboard
await prisma.dashboardWidget.createMany({
skipDuplicates: true,
data: [
{
dashboardId: dashboardOverview.id,
chartId: chartNpsScore.id,
title: "Current NPS",
layout: { x: 0, y: 0, w: 2, h: 2 },
order: 0,
},
{
dashboardId: dashboardOverview.id,
chartId: chartResponsesOverTime.id,
title: "Weekly Responses",
layout: { x: 2, y: 0, w: 4, h: 3 },
order: 1,
},
{
dashboardId: dashboardOverview.id,
chartId: chartSatisfactionDist.id,
title: "Satisfaction Breakdown",
layout: { x: 0, y: 3, w: 3, h: 3 },
order: 2,
},
{
dashboardId: dashboardOverview.id,
chartId: chartTopChannels.id,
title: "Channel Trends",
layout: { x: 3, y: 3, w: 3, h: 3 },
order: 3,
},
],
await prisma.dashboardWidget.upsert({
where: { id: SEED_IDS.WIDGET_OVERVIEW_NPS },
update: {},
create: {
id: SEED_IDS.WIDGET_OVERVIEW_NPS,
dashboardId: dashboardOverview.id,
chartId: chartNpsScore.id,
title: "Current NPS",
layout: { x: 0, y: 0, w: 2, h: 2 },
order: 0,
},
});
await prisma.dashboardWidget.upsert({
where: { id: SEED_IDS.WIDGET_OVERVIEW_RESPONSES },
update: {},
create: {
id: SEED_IDS.WIDGET_OVERVIEW_RESPONSES,
dashboardId: dashboardOverview.id,
chartId: chartResponsesOverTime.id,
title: "Weekly Responses",
layout: { x: 2, y: 0, w: 4, h: 3 },
order: 1,
},
});
await prisma.dashboardWidget.upsert({
where: { id: SEED_IDS.WIDGET_OVERVIEW_SATISFACTION },
update: {},
create: {
id: SEED_IDS.WIDGET_OVERVIEW_SATISFACTION,
dashboardId: dashboardOverview.id,
chartId: chartSatisfactionDist.id,
title: "Satisfaction Breakdown",
layout: { x: 0, y: 3, w: 3, h: 3 },
order: 2,
},
});
await prisma.dashboardWidget.upsert({
where: { id: SEED_IDS.WIDGET_OVERVIEW_CHANNELS },
update: {},
create: {
id: SEED_IDS.WIDGET_OVERVIEW_CHANNELS,
dashboardId: dashboardOverview.id,
chartId: chartTopChannels.id,
title: "Channel Trends",
layout: { x: 3, y: 3, w: 3, h: 3 },
order: 3,
},
});
// Widgets for Survey Performance dashboard
await prisma.dashboardWidget.createMany({
skipDuplicates: true,
data: [
{
dashboardId: dashboardSurveyPerf.id,
chartId: chartCompletionRate.id,
title: "Completion by Survey",
layout: { x: 0, y: 0, w: 6, h: 3 },
order: 0,
},
{
dashboardId: dashboardSurveyPerf.id,
chartId: chartResponsesOverTime.id,
title: "Response Volume",
layout: { x: 0, y: 3, w: 4, h: 3 },
order: 1,
},
{
dashboardId: dashboardSurveyPerf.id,
chartId: chartNpsScore.id,
title: "NPS Tracker",
layout: { x: 4, y: 3, w: 2, h: 2 },
order: 2,
},
],
await prisma.dashboardWidget.upsert({
where: { id: SEED_IDS.WIDGET_SURVPERF_COMPLETION },
update: {},
create: {
id: SEED_IDS.WIDGET_SURVPERF_COMPLETION,
dashboardId: dashboardSurveyPerf.id,
chartId: chartCompletionRate.id,
title: "Completion by Survey",
layout: { x: 0, y: 0, w: 6, h: 3 },
order: 0,
},
});
await prisma.dashboardWidget.upsert({
where: { id: SEED_IDS.WIDGET_SURVPERF_RESPONSES },
update: {},
create: {
id: SEED_IDS.WIDGET_SURVPERF_RESPONSES,
dashboardId: dashboardSurveyPerf.id,
chartId: chartResponsesOverTime.id,
title: "Response Volume",
layout: { x: 0, y: 3, w: 4, h: 3 },
order: 1,
},
});
await prisma.dashboardWidget.upsert({
where: { id: SEED_IDS.WIDGET_SURVPERF_NPS },
update: {},
create: {
id: SEED_IDS.WIDGET_SURVPERF_NPS,
dashboardId: dashboardSurveyPerf.id,
chartId: chartNpsScore.id,
title: "NPS Tracker",
layout: { x: 4, y: 3, w: 2, h: 2 },
order: 2,
},
});
logger.info(`\n${"=".repeat(50)}`);

View File

@@ -17,6 +17,13 @@ export const SEED_IDS = {
CHART_TOP_CHANNELS: "clseedcharttopchann00",
DASHBOARD_OVERVIEW: "clseeddashovervieww00",
DASHBOARD_SURVEY_PERF: "clseeddashsurvperf000",
WIDGET_OVERVIEW_NPS: "clseedwidgetovwnps000",
WIDGET_OVERVIEW_RESPONSES: "clseedwidgetovwresp00",
WIDGET_OVERVIEW_SATISFACTION: "clseedwidgetovwsat000",
WIDGET_OVERVIEW_CHANNELS: "clseedwidgetovwchan00",
WIDGET_SURVPERF_COMPLETION: "clseedwidgetspcomp000",
WIDGET_SURVPERF_RESPONSES: "clseedwidgetspresp000",
WIDGET_SURVPERF_NPS: "clseedwidgetspnps0000",
} as const;
export const SEED_CREDENTIALS = {