Compare commits

...

4 Commits

Author SHA1 Message Date
Dhruwang
0c866aee66 fix: delete response 2024-01-16 12:24:42 +05:30
Dhruwang Jariwala
82302360fa test: unit test for display services (#1832)
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2024-01-15 16:21:33 +00:00
Shubham Palriwala
f6f45d74d5 feat: (codespaces) run formbricks app on load (#1871) 2024-01-15 16:13:54 +00:00
Anshuman Pandey
04a47b3d0a fix: adds vite dev mode (#1893) 2024-01-15 14:06:06 +00:00
15 changed files with 385 additions and 59 deletions

View File

@@ -22,6 +22,7 @@
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "cp .env.example .env && sed -i '/^ENCRYPTION_KEY=/c\\ENCRYPTION_KEY='$(openssl rand -hex 32) .env && sed -i '/^NEXTAUTH_SECRET=/c\\NEXTAUTH_SECRET='$(openssl rand -hex 32) .env && pnpm install && pnpm db:migrate:dev",
"postAttachCommand": "pnpm dev --filter=web... --filter=demo...",
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "node"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

View File

@@ -6,12 +6,9 @@ import GitpodPorts from "./gitpod/ports.webp";
import GitpodPreparing from "./gitpod/preparing.webp";
import GitpodRunning from "./gitpod/running.webp";
import GithubCodespaceEnvFile from "./github-codespaces/env.webp";
import GithubCodespaceLoading from "./github-codespaces/loading.webp";
import GithubCodespaceNew from "./github-codespaces/new.webp";
import GithubCodespacePorts from "./github-codespaces/ports.webp";
import GithubCodespaceRun from "./github-codespaces/run.webp";
import GithubCodespaceTerminal from "./github-codespaces/terminal.webp";
export const metadata = {
title: "Formbricks Development Setup: Complete Guide to Local Environment Configuration for Dev",
@@ -38,7 +35,7 @@ This will open a fully configured workspace in your browser with all the necessa
### [Github Codespaces](#Github-codespaces)
This will open a Github VSCode Interface on the cloud for you. This setup will have the Formbricks codebase and all the dependencies installed. Click the button below to configure your instance and open the project in Github Codespaces. For a detailed guide, visit the [Github Codespaces Setup Guide](#github-codespaces-guide) section below.
This will open a Github VSCode Interface on the cloud for you. This setup will have the Formbricks codebase, all the dependencies installed & Formbricks running. Click the button below to configure your instance and open the project in Github Codespaces. For a detailed guide, visit the [Github Codespaces Setup Guide](#github-codespaces-guide) section below.
[![Open in Github Codespaces](https://img.shields.io/badge/Open%20in-Github%20Codespaces-blue?logo=Github)](https://Github.com/codespaces/new?machine=standardLinux32gb&repo=500289888&ref=main&devcontainer_path=.devcontainer%2Fdevcontainer.json&location=EastUs2)
@@ -225,44 +222,7 @@ These URLs and port numbers represent various services and endpoints within your
3. Once the Codespace is loaded, you will be redirected to the VSCode editor. You can start working on your project in this environment.
4. Make the changes you want to, and now, to run the app, we first need to configure the .env file. Copy the .env.example and edit the variables as mentioned in the file itself.
<Image
src={GithubCodespaceEnvFile}
alt="Github Codespace Env File"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
5. Once you have configured the .env, it's now time to run the app and see the changes. Lets open the terminal first
<Image
src={GithubCodespaceTerminal}
alt="Github Codespace Open Terminal"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
6. Now, run the following command to run the app
<Col>
<CodeGroup title="Run the entire Formbricks Stack">
```bash
pnpm dev
```
</CodeGroup>
</Col>
<Image
src={GithubCodespaceRun}
alt="Run on Github Codespace"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
7. Monitor the logs in the terminal and once you see the following, you are good to go!
4. Monitor the logs in the terminal and once you see the following, you are good to go!
<Col>
<CodeGroup title="The WebApp is running">
@@ -280,7 +240,7 @@ pnpm dev
</CodeGroup>
</Col>
8. Right next to the Terminal, you will see a **Ports** tab, click on it to see the ports and their respective URLs. Now access the Forwarded Address for port 3000 and you should be able to visit your Formbricks App!
5. Right next to the Terminal, you will see a **Ports** tab, click on it to see the ports and their respective URLs. Now access the Forwarded Address for port 3000 and you should be able to visit your Formbricks App!
<Image
src={GithubCodespacePorts}

View File

@@ -65,7 +65,7 @@ export default function ResponseTimeline({
observer.unobserve(currentLoadingRef);
}
};
}, [responses, responsesPerPage, page, survey.id, fetchedResponses.length, hasMoreResponses]);
}, [responsesPerPage, page, survey.id, fetchedResponses.length, hasMoreResponses]);
return (
<div className="space-y-4">
@@ -89,6 +89,7 @@ export default function ResponseTimeline({
environmentTags={environmentTags}
pageType="response"
environment={environment}
setFetchedResponses={setFetchedResponses}
/>
</div>
);

View File

@@ -34,6 +34,7 @@ const selectDisplay = {
surveyId: true,
responseId: true,
personId: true,
status: true,
};
export const getDisplay = async (displayId: string): Promise<TDisplay | null> => {
@@ -42,7 +43,7 @@ export const getDisplay = async (displayId: string): Promise<TDisplay | null> =>
validateInputs([displayId, ZId]);
try {
const display = await prisma.response.findUnique({
const display = await prisma.display.findUnique({
where: {
id: displayId,
},
@@ -143,7 +144,6 @@ export const updateDisplayLegacy = async (
data,
select: selectDisplay,
});
displayCache.revalidate({
id: display.id,
surveyId: display.surveyId,
@@ -164,7 +164,6 @@ export const createDisplay = async (displayInput: TDisplayCreateInput): Promise<
validateInputs([displayInput, ZDisplayCreateInput]);
const { environmentId, userId, surveyId } = displayInput;
try {
let person;
if (userId) {
@@ -191,13 +190,11 @@ export const createDisplay = async (displayInput: TDisplayCreateInput): Promise<
},
select: selectDisplay,
});
displayCache.revalidate({
id: display.id,
personId: display.personId,
surveyId: display.surveyId,
});
return display;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
@@ -337,7 +334,6 @@ export const deleteDisplayByResponseId = async (
personId: display.personId,
surveyId,
});
return display;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {

View File

@@ -0,0 +1,76 @@
import {
TDisplay,
TDisplayCreateInput,
TDisplayLegacyCreateInput,
TDisplayLegacyUpdateInput,
TDisplayUpdateInput,
} from "@formbricks/types/displays";
export const mockEnvironmentId = "clqkr5961000108jyfnjmbjhi";
export const mockSingleUseId = "qj57j3opsw8b5sxgea20fgcq";
export const mockSurveyId = "clqkr8dlv000308jybb08evgr";
export const mockUserId = "qwywazmugeezyfr3zcg9jk8a";
export const mockDisplayId = "clqkr5smu000208jy50v6g5k4";
export const mockId = "ars2tjk8hsi8oqk1uac00mo8";
export const mockPersonId = "clqnj99r9000008lebgf8734j";
export const mockResponseId = "clqnfg59i000208i426pb4wcv";
function createMockDisplay(overrides = {}) {
return {
id: mockDisplayId,
createdAt: new Date(),
updatedAt: new Date(),
surveyId: mockSurveyId,
responseId: null,
personId: null,
status: null,
...overrides,
};
}
export const mockDisplay: TDisplay = createMockDisplay();
export const mockDisplayWithPersonId: TDisplay = createMockDisplay({ personId: mockPersonId });
export const mockDisplayWithResponseId: TDisplay = createMockDisplay({
personId: mockPersonId,
responseId: mockResponseId,
});
export const mockDisplayInput: TDisplayCreateInput = {
environmentId: mockEnvironmentId,
surveyId: mockSurveyId,
};
export const mockDisplayInputWithUserId: TDisplayCreateInput = {
...mockDisplayInput,
userId: mockUserId,
};
export const mockDisplayInputWithResponseId: TDisplayCreateInput = {
...mockDisplayInputWithUserId,
responseId: mockResponseId,
};
export const mockDisplayLegacyInput: TDisplayLegacyCreateInput = {
responseId: mockResponseId,
surveyId: mockSurveyId,
};
export const mockDisplayLegacyInputWithPersonId: TDisplayLegacyCreateInput = {
...mockDisplayLegacyInput,
personId: mockPersonId,
};
export const mockDisplayUpdate: TDisplayUpdateInput = {
environmentId: mockEnvironmentId,
userId: mockUserId,
responseId: mockResponseId,
};
export const mockDisplayLegacyUpdateInput: TDisplayLegacyUpdateInput = {
personId: mockPersonId,
responseId: mockResponseId,
};
export const mockDisplayLegacyWithRespondedStatus: TDisplay = {
...mockDisplayWithPersonId,
status: "responded",
};

View File

@@ -0,0 +1,287 @@
import { mockPerson } from "../../response/tests/__mocks__/data.mock";
import {
mockDisplay,
mockDisplayInput,
mockDisplayInputWithUserId,
mockDisplayLegacyInput,
mockDisplayLegacyInputWithPersonId,
mockDisplayLegacyUpdateInput,
mockDisplayLegacyWithRespondedStatus,
mockDisplayUpdate,
mockDisplayWithPersonId,
mockDisplayWithResponseId,
mockResponseId,
mockSurveyId,
} from "./__mocks__/data.mock";
import { Prisma } from "@prisma/client";
import { prismaMock } from "@formbricks/database/src/jestClient";
import { DatabaseError, ValidationError } from "@formbricks/types/errors";
import {
createDisplay,
createDisplayLegacy,
deleteDisplayByResponseId,
getDisplay,
getDisplayCountBySurveyId,
getDisplaysByPersonId,
markDisplayRespondedLegacy,
updateDisplay,
updateDisplayLegacy,
} from "../service";
const testInputValidation = async (service: Function, ...args: any[]): Promise<void> => {
it("it should throw a ValidationError if the inputs are invalid", async () => {
await expect(service(...args)).rejects.toThrow(ValidationError);
});
};
beforeEach(() => {
prismaMock.person.findFirst.mockResolvedValue(mockPerson);
});
describe("Tests for getDisplay", () => {
describe("Happy Path", () => {
it("Returns display associated with a given display ID", async () => {
prismaMock.display.findUnique.mockResolvedValue(mockDisplay);
const display = await getDisplay(mockDisplay.id);
expect(display).toEqual(mockDisplay);
});
it("Returns all displays associated with a given person ID", async () => {
prismaMock.display.findMany.mockResolvedValue([mockDisplayWithPersonId]);
const displays = await getDisplaysByPersonId(mockPerson.id);
expect(displays).toEqual([mockDisplayWithPersonId]);
});
it("Returns an empty array when no displays are found for the given person ID", async () => {
prismaMock.display.findMany.mockResolvedValue([]);
const displays = await getDisplaysByPersonId(mockPerson.id);
expect(displays).toEqual([]);
});
it("Returns display count for the given survey ID", async () => {
prismaMock.display.count.mockResolvedValue(1);
const displaCount = await getDisplayCountBySurveyId(mockSurveyId);
expect(displaCount).toEqual(1);
});
});
describe("Sad Path", () => {
testInputValidation(getDisplaysByPersonId, "123", 1);
it("Throws a DatabaseError error if there is a PrismaClientKnownRequestError", async () => {
const mockErrorMessage = "Mock error message";
const errToThrow = new Prisma.PrismaClientKnownRequestError(mockErrorMessage, {
code: "P2002",
clientVersion: "0.0.1",
});
prismaMock.display.findMany.mockRejectedValue(errToThrow);
await expect(getDisplaysByPersonId(mockPerson.id)).rejects.toThrow(DatabaseError);
});
it("Throws a generic Error for unexpected exceptions", async () => {
const mockErrorMessage = "Mock error message";
prismaMock.display.findMany.mockRejectedValue(new Error(mockErrorMessage));
await expect(getDisplaysByPersonId(mockPerson.id)).rejects.toThrow(Error);
});
});
});
describe("Tests for createDisplay service", () => {
describe("Happy Path", () => {
it("Creates a new display when a userId exists", async () => {
prismaMock.display.create.mockResolvedValue(mockDisplayWithPersonId);
const display = await createDisplay(mockDisplayInputWithUserId);
expect(display).toEqual(mockDisplayWithPersonId);
});
it("Creates a new display when a userId does not exists", async () => {
prismaMock.display.create.mockResolvedValue(mockDisplay);
const display = await createDisplay(mockDisplayInput);
expect(display).toEqual(mockDisplay);
});
});
describe("Sad Path", () => {
testInputValidation(createDisplay, "123");
it("Throws DatabaseError on PrismaClientKnownRequestError occurrence", async () => {
const mockErrorMessage = "Mock error message";
const errToThrow = new Prisma.PrismaClientKnownRequestError(mockErrorMessage, {
code: "P2002",
clientVersion: "0.0.1",
});
prismaMock.display.create.mockRejectedValue(errToThrow);
await expect(createDisplay(mockDisplayInputWithUserId)).rejects.toThrow(DatabaseError);
});
it("Throws a generic Error for other exceptions", async () => {
const mockErrorMessage = "Mock error message";
prismaMock.display.create.mockRejectedValue(new Error(mockErrorMessage));
await expect(createDisplay(mockDisplayInput)).rejects.toThrow(Error);
});
});
});
describe("Tests for updateDisplay Service", () => {
describe("Happy Path", () => {
it("Updates a display (responded)", async () => {
prismaMock.display.update.mockResolvedValue(mockDisplayWithResponseId);
const display = await updateDisplay(mockDisplay.id, mockDisplayUpdate);
expect(display).toEqual(mockDisplayWithResponseId);
});
});
describe("Sad Path", () => {
testInputValidation(updateDisplay, "123", "123");
it("Throws DatabaseError on PrismaClientKnownRequestError", async () => {
const mockErrorMessage = "Mock error message";
const errToThrow = new Prisma.PrismaClientKnownRequestError(mockErrorMessage, {
code: "P2002",
clientVersion: "0.0.1",
});
prismaMock.display.update.mockRejectedValue(errToThrow);
await expect(updateDisplay(mockDisplay.id, mockDisplayUpdate)).rejects.toThrow(DatabaseError);
});
it("Throws a generic Error for other unexpected issues", async () => {
const mockErrorMessage = "Mock error message";
prismaMock.display.update.mockRejectedValue(new Error(mockErrorMessage));
await expect(updateDisplay(mockDisplay.id, mockDisplayUpdate)).rejects.toThrow(Error);
});
});
});
describe("Tests for createDisplayLegacy service", () => {
describe("Happy Path", () => {
it("Creates a display when a person ID exist", async () => {
prismaMock.display.create.mockResolvedValue(mockDisplayWithPersonId);
const display = await createDisplayLegacy(mockDisplayLegacyInputWithPersonId);
expect(display).toEqual(mockDisplayWithPersonId);
});
it("Creates a display when a person ID does not exist", async () => {
prismaMock.display.create.mockResolvedValue(mockDisplay);
const display = await createDisplayLegacy(mockDisplayLegacyInput);
expect(display).toEqual(mockDisplay);
});
});
describe("Sad Path", () => {
testInputValidation(createDisplayLegacy, "123");
it("Throws DatabaseError on PrismaClientKnownRequestError occurrence", async () => {
const mockErrorMessage = "Mock error message";
const errToThrow = new Prisma.PrismaClientKnownRequestError(mockErrorMessage, {
code: "P2002",
clientVersion: "0.0.1",
});
prismaMock.display.create.mockRejectedValue(errToThrow);
await expect(createDisplayLegacy(mockDisplayLegacyInputWithPersonId)).rejects.toThrow(DatabaseError);
});
it("Throws a generic Error for other exceptions", async () => {
const mockErrorMessage = "Mock error message";
prismaMock.display.create.mockRejectedValue(new Error(mockErrorMessage));
await expect(createDisplayLegacy(mockDisplayLegacyInputWithPersonId)).rejects.toThrow(Error);
});
});
});
describe("Tests for updateDisplayLegacy Service", () => {
describe("Happy Path", () => {
it("Updates a display", async () => {
prismaMock.display.update.mockResolvedValue(mockDisplayWithPersonId);
const display = await updateDisplayLegacy(mockDisplay.id, mockDisplayLegacyUpdateInput);
expect(display).toEqual(mockDisplayWithPersonId);
});
it("marks display as responded legacy", async () => {
prismaMock.display.update.mockResolvedValue(mockDisplayLegacyWithRespondedStatus);
const display = await markDisplayRespondedLegacy(mockDisplay.id);
expect(display).toEqual(mockDisplayLegacyWithRespondedStatus);
});
});
describe("Sad Path", () => {
testInputValidation(updateDisplayLegacy, "123", "123");
it("Throws DatabaseError on PrismaClientKnownRequestError", async () => {
const mockErrorMessage = "Mock error message";
const errToThrow = new Prisma.PrismaClientKnownRequestError(mockErrorMessage, {
code: "P2002",
clientVersion: "0.0.1",
});
prismaMock.display.update.mockRejectedValue(errToThrow);
await expect(updateDisplayLegacy(mockDisplay.id, mockDisplayLegacyUpdateInput)).rejects.toThrow(
DatabaseError
);
});
it("Throws a generic Error for other unexpected issues", async () => {
const mockErrorMessage = "Mock error message";
prismaMock.display.update.mockRejectedValue(new Error(mockErrorMessage));
await expect(updateDisplayLegacy(mockDisplay.id, mockDisplayLegacyUpdateInput)).rejects.toThrow(Error);
});
});
});
describe("Tests for deleteDisplayByResponseId service", () => {
describe("Happy Path", () => {
it("Deletes a display when a response associated to it is deleted", async () => {
prismaMock.display.delete.mockResolvedValue(mockDisplayWithResponseId);
const display = await deleteDisplayByResponseId(mockResponseId, mockSurveyId);
expect(display).toEqual(mockDisplayWithResponseId);
});
});
describe("Sad Path", () => {
testInputValidation(createDisplayLegacy, "123");
it("Throws DatabaseError on PrismaClientKnownRequestError occurrence", async () => {
const mockErrorMessage = "Mock error message";
const errToThrow = new Prisma.PrismaClientKnownRequestError(mockErrorMessage, {
code: "P2002",
clientVersion: "0.0.1",
});
prismaMock.display.delete.mockRejectedValue(errToThrow);
await expect(deleteDisplayByResponseId(mockResponseId, mockSurveyId)).rejects.toThrow(DatabaseError);
});
it("Throws a generic Error for other exceptions", async () => {
const mockErrorMessage = "Mock error message";
prismaMock.display.delete.mockRejectedValue(new Error(mockErrorMessage));
await expect(deleteDisplayByResponseId(mockResponseId, mockSurveyId)).rejects.toThrow(Error);
});
});
});

View File

@@ -89,6 +89,7 @@ export const mockDisplay: TDisplay = {
surveyId: mockSurveyId,
personId: mockPersonId,
responseId: mockResponseId,
status: null,
};
export const mockResponse: ResponseMock = {

View File

@@ -23,7 +23,7 @@
}
},
"scripts": {
"dev": "SURVEYS_PACKAGE_MODE=development vite build --watch",
"dev": "vite build --watch --mode dev",
"build": "pnpm run build:surveys && pnpm run build:question-date",
"build:surveys": "tsc && SURVEYS_PACKAGE_BUILD=surveys vite build",
"build:question-date": "tsc && SURVEYS_PACKAGE_BUILD=question-date vite build",

View File

@@ -41,6 +41,7 @@ export default function DateQuestion({
useTtc(question.id, ttc, setTtc, startTime, setStartTime);
const defaultDate = value ? new Date(value as string) : undefined;
const datePickerScriptSrc = import.meta.env.DATE_PICKER_SCRIPT_SRC;
useEffect(() => {
// Check if the DatePicker has already been loaded
@@ -48,10 +49,7 @@ export default function DateQuestion({
if (!window.initDatePicker) {
const script = document.createElement("script");
script.src =
process.env.SURVEYS_PACKAGE_MODE === "development"
? "http://localhost:3003/question-date.umd.js"
: "https://unpkg.com/@formbricks/surveys@^1.4.0/dist/question-date.umd.js";
script.src = datePickerScriptSrc;
script.async = true;

View File

@@ -13,9 +13,15 @@ const fileName = buildPackage === "surveys" ? "index" : "question-date";
const config = ({ mode }) => {
const env = loadEnv(mode, process.cwd(), "");
const isDevelopment = mode === "dev";
const datePickerScriptSrc = isDevelopment
? "http://localhost:3003/question-date.umd.js"
: "https://unpkg.com/@formbricks/surveys@^1.4.0/dist/question-date.umd.js";
return defineConfig({
define: {
"process.env": env,
"import.meta.env.DATE_PICKER_SCRIPT_SRC": JSON.stringify(datePickerScriptSrc),
},
build: {
emptyOutDir: false,

View File

@@ -7,7 +7,7 @@ export const ZDisplay = z.object({
personId: z.string().cuid().nullable(),
surveyId: z.string().cuid(),
responseId: z.string().cuid().nullable(),
status: z.enum(["seen", "responded"]).optional(),
status: z.enum(["seen", "responded"]).nullable(),
});
export type TDisplay = z.infer<typeof ZDisplay>;

View File

@@ -4,7 +4,6 @@ import { TrashIcon } from "@heroicons/react/24/outline";
import { CheckCircleIcon } from "@heroicons/react/24/solid";
import clsx from "clsx";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { ReactNode, useState } from "react";
import toast from "react-hot-toast";
@@ -41,6 +40,7 @@ export interface SingleResponseCardProps {
pageType: string;
environmentTags: TTag[];
environment: TEnvironment;
setFetchedResponses: React.Dispatch<React.SetStateAction<TResponse[]>>;
}
interface TooltipRendererProps {
@@ -79,9 +79,9 @@ export default function SingleResponseCard({
pageType,
environmentTags,
environment,
setFetchedResponses,
}: SingleResponseCardProps) {
const environmentId = survey.environmentId;
const router = useRouter();
const displayIdentifier = response.person ? getPersonIdentifier(response.person) : null;
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
@@ -152,7 +152,7 @@ export default function SingleResponseCard({
throw new Error("You are not authorized to perform this action.");
}
await deleteResponseAction(response.id);
router.refresh();
setFetchedResponses((prevResponses) => prevResponses.filter((r) => r.id !== response.id));
toast.success("Submission deleted successfully.");
setDeleteDialogOpen(false);
} catch (error) {