mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-01 11:50:43 -05:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dc72b712d6 | |||
| 0bdae282e5 | |||
| 28bc87b8e5 |
@@ -22,7 +22,6 @@
|
||||
|
||||
// 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"
|
||||
|
||||
@@ -4,17 +4,17 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
name: Build Formbricks-web
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node.js 18.x
|
||||
- name: Setup Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
|
||||
@@ -4,7 +4,7 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
name: Run E2E Tests
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
name: PR Update
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- shubham/integrate-buildjet
|
||||
pull_request_target:
|
||||
branches:
|
||||
- main
|
||||
@@ -35,7 +38,7 @@ jobs:
|
||||
required:
|
||||
needs: [lint, test, build, e2e-test]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: fail if conditional jobs failed
|
||||
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')
|
||||
|
||||
@@ -4,7 +4,7 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
name: Tests
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
timeout-minutes: 15
|
||||
|
||||
env:
|
||||
@@ -15,10 +15,10 @@ jobs:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node.js 18.x
|
||||
- name: Setup Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
@@ -6,9 +6,12 @@ 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",
|
||||
@@ -35,7 +38,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, 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.
|
||||
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.
|
||||
|
||||
[](https://Github.com/codespaces/new?machine=standardLinux32gb&repo=500289888&ref=main&devcontainer_path=.devcontainer%2Fdevcontainer.json&location=EastUs2)
|
||||
|
||||
@@ -222,7 +225,44 @@ 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. Monitor the logs in the terminal and once you see the following, you are good to go!
|
||||
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!
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="The WebApp is running">
|
||||
@@ -240,7 +280,7 @@ These URLs and port numbers represent various services and endpoints within your
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
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!
|
||||
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!
|
||||
|
||||
<Image
|
||||
src={GithubCodespacePorts}
|
||||
|
||||
@@ -123,7 +123,6 @@ We will first create a Google Cloud Project and then enable the Google Sheets AP
|
||||
- `GOOGLE_SHEETS_CLIENT_ID` - Client ID
|
||||
- `GOOGLE_SHEETS_CLIENT_SECRET` - Client Secret
|
||||
16. Also use the **same Authorized redirect URI** in the `GOOGLE_SHEETS_REDIRECT_URL` environment variable.
|
||||
17. One last that we need to do is to **enable the Google Drive API** for the project. For that, go to the "**APIs & Services**" section and click on the "**Enable APIs and Services**" button and search for "**Google Drive API**" and enable it.
|
||||
|
||||
### By now, your environment variables should include the below ones as well:
|
||||
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import Image from "next/image";
|
||||
|
||||
import ShareLink from "./share-link.webp";
|
||||
import ViewResponse from "./view-response.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Source Tracking",
|
||||
description: "Track the source of your users in an easy & compliant way!",
|
||||
};
|
||||
|
||||
#### Link Surveys
|
||||
|
||||
# Source Tracking
|
||||
|
||||
Understand the source a survey respondent comes from when responding to your survey - all while keeping data privacy standards high!
|
||||
|
||||
Check out this video to learn more about source tracking in link surveys:
|
||||
|
||||
{/* Replace link below with our new link on Source Tracking */}
|
||||
<iframe width="700" height="450" src="https://www.youtube.com/embed/CytWhuyEMVI?si=t-SFB2A1l1RZDdAC" title="YouTube video player: Formbricks" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"></iframe>
|
||||
|
||||
## Purpose
|
||||
|
||||
Source tracking for link surveys is essential when you:
|
||||
|
||||
- Want to analyze the origin of your survey respondents.
|
||||
- Aim to ensure compliance with tracking and data collection regulations.
|
||||
|
||||
## Code Example
|
||||
<Col>
|
||||
<CodeGroup title="Example Source as Google">
|
||||
|
||||
```sh
|
||||
https://formbricks.com/clin3dxja02k8l80hpwmx4bjy?source=Google
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
## How it Works
|
||||
|
||||
To track the source of users in your link surveys effectively, follow these steps:
|
||||
|
||||
1. **Generate Survey URL**: Create a Link Survey and get the sharable link. Append `?source=YourSouce` to the link to reference it with your campaigns and sources.
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Example Source as Google">
|
||||
|
||||
```sh
|
||||
https://formbricks.com/clin3dxja02k8l80hpwmx4bjy?source=Google
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
2. **Collect Data**: When users access the survey through these links, the URL parameters will capture the source information from which they were shared.
|
||||
|
||||
3. **View Responses**: Use the collected source data to analyze where your survey respondents are coming from. You can hover over the user icon in the responses tab to see the source of the user.
|
||||
|
||||
<Image
|
||||
src={ViewResponse}
|
||||
alt="View Source in Response"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
4. **Analyse Data**: Download all the responses as a CSV/Excel and get access to the source information. This can provide valuable insights into your audience.
|
||||
|
||||
Source tracking allows you to make informed decisions based on the origin of your survey participants, helping you tailor your surveys and marketing strategies accordingly.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.5 KiB |
@@ -218,7 +218,6 @@ export const navigation: Array<NavGroup> = [
|
||||
{ title: "Data Prefilling", href: "/docs/link-surveys/data-prefilling" },
|
||||
{ title: "Identify Users", href: "/docs/link-surveys/user-identification" },
|
||||
{ title: "Single Use Links", href: "/docs/link-surveys/single-use-links" },
|
||||
{ title: "Source Tracking", href: "/docs/link-surveys/source-tracking" },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
export const handleFeedbackSubmit = async (YesNo: string, pageUrl: string | null) => {
|
||||
const response_data = {
|
||||
isHelpful: YesNo,
|
||||
pageUrl: pageUrl,
|
||||
data: {
|
||||
isHelpful: YesNo,
|
||||
pageUrl: pageUrl,
|
||||
},
|
||||
};
|
||||
|
||||
const payload = {
|
||||
response: response_data,
|
||||
surveyId: process.env.NEXT_PUBLIC_FORMBRICKS_COM_DOCS_FEEDBACK_SURVEY_ID,
|
||||
finished: true,
|
||||
data: response_data,
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_FORMBRICKS_COM_API_HOST}/api/v1/client/${process.env.NEXT_PUBLIC_FORMBRICKS_COM_ENVIRONMENT_ID}/responses`,
|
||||
`${process.env.NEXT_PUBLIC_FORMBRICKS_COM_API_HOST}/api/v1/client/environments/${process.env.NEXT_PUBLIC_FORMBRICKS_COM_ENVIRONMENT_ID}/responses`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
|
||||
@@ -116,7 +116,8 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
|
||||
},
|
||||
{
|
||||
name: "Lost Pixel",
|
||||
description: "Open source visual regression testing alternative to Percy & Chromatic",
|
||||
description:
|
||||
"Open source visual regression testing alternative to Percy & Chromatic",
|
||||
href: "https://lost-pixel.com",
|
||||
},
|
||||
{
|
||||
|
||||
-1
@@ -84,7 +84,6 @@ export default function ActionClassesTable({
|
||||
<AddNoCodeActionModal
|
||||
environmentId={environmentId}
|
||||
open={isAddActionModalOpen}
|
||||
actionClasses={actionClasses}
|
||||
setOpen={setAddActionModalOpen}
|
||||
isViewer={isViewer}
|
||||
/>
|
||||
|
||||
+9
-14
@@ -24,8 +24,7 @@ interface AddNoCodeActionModalProps {
|
||||
environmentId: string;
|
||||
open: boolean;
|
||||
setOpen: (v: boolean) => void;
|
||||
actionClasses: TActionClass[];
|
||||
setActionClasses?;
|
||||
setActionClassArray?;
|
||||
isViewer: boolean;
|
||||
}
|
||||
|
||||
@@ -46,8 +45,7 @@ export default function AddNoCodeActionModal({
|
||||
environmentId,
|
||||
open,
|
||||
setOpen,
|
||||
actionClasses,
|
||||
setActionClasses,
|
||||
setActionClassArray,
|
||||
isViewer,
|
||||
}: AddNoCodeActionModalProps) {
|
||||
const { register, control, handleSubmit, watch, reset } = useForm();
|
||||
@@ -58,7 +56,6 @@ export default function AddNoCodeActionModal({
|
||||
const [testUrl, setTestUrl] = useState("");
|
||||
const [isMatch, setIsMatch] = useState("");
|
||||
const [type, setType] = useState("noCode");
|
||||
const actionClassNames = actionClasses.map((actionClass) => actionClass.name);
|
||||
|
||||
const filterNoCodeConfig = (noCodeConfig: TActionClassNoCodeConfig): TActionClassNoCodeConfig => {
|
||||
const { pageUrl, innerHtml, cssSelector } = noCodeConfig;
|
||||
@@ -95,12 +92,7 @@ export default function AddNoCodeActionModal({
|
||||
throw new Error("You are not authorised to perform this action.");
|
||||
}
|
||||
setIsCreatingAction(true);
|
||||
if (!data.name || data.name?.trim() === "") {
|
||||
throw new Error("Please give your action a name");
|
||||
}
|
||||
if (data.name && actionClassNames.includes(data.name)) {
|
||||
throw new Error(`Action with name ${data.name} already exist`);
|
||||
}
|
||||
if (data.name === "") throw new Error("Please give your action a name");
|
||||
if (type === "noCode") {
|
||||
if (!isPageUrl && !isCssSelector && !isInnerHtml)
|
||||
throw new Error("Please select at least one selector");
|
||||
@@ -127,8 +119,11 @@ export default function AddNoCodeActionModal({
|
||||
}
|
||||
|
||||
const newActionClass: TActionClass = await createActionClassAction(updatedAction);
|
||||
if (setActionClasses) {
|
||||
setActionClasses((prevActionClasses: TActionClass[]) => [...prevActionClasses, newActionClass]);
|
||||
if (setActionClassArray) {
|
||||
setActionClassArray((prevActionClassArray: TActionClass[]) => [
|
||||
...prevActionClassArray,
|
||||
newActionClass,
|
||||
]);
|
||||
}
|
||||
reset();
|
||||
resetAllStates(false);
|
||||
@@ -183,7 +178,7 @@ export default function AddNoCodeActionModal({
|
||||
<div className="grid w-full grid-cols-2 gap-x-4">
|
||||
<div className="col-span-1">
|
||||
<Label>What did your user do?</Label>
|
||||
<Input placeholder="E.g. Clicked Download" {...register("name")} />
|
||||
<Input placeholder="E.g. Clicked Download" {...register("name", { required: true })} />
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Label>Description</Label>
|
||||
|
||||
+20
-23
@@ -5,7 +5,6 @@ import { DownloadIcon, FileIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { getPersonIdentifier } from "@formbricks/lib/person/util";
|
||||
import { getOriginalFileNameFromUrl } from "@formbricks/lib/storage/utils";
|
||||
import { timeSince } from "@formbricks/lib/time";
|
||||
import type { TSurveyQuestionSummary } from "@formbricks/types/surveys";
|
||||
import { TSurveyFileUploadQuestion } from "@formbricks/types/surveys";
|
||||
@@ -82,31 +81,29 @@ export default function FileUploadSummary({ questionSummary, environmentId }: Fi
|
||||
|
||||
{Array.isArray(response.value) &&
|
||||
(response.value.length > 0 ? (
|
||||
response.value.map((fileUrl, index) => {
|
||||
const fileName = getOriginalFileNameFromUrl(fileUrl);
|
||||
|
||||
return (
|
||||
<div className="relative m-2 rounded-lg bg-slate-200" key={fileUrl}>
|
||||
<a
|
||||
href={fileUrl as string}
|
||||
key={index}
|
||||
download={fileName}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
<div className="absolute right-0 top-0 m-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-50 hover:bg-white">
|
||||
<DownloadIcon className="h-6 text-slate-500" />
|
||||
</div>
|
||||
response.value.map((fileUrl, index) => (
|
||||
<div className="relative m-2 rounded-lg bg-slate-200" key={fileUrl}>
|
||||
<a
|
||||
href={fileUrl as string}
|
||||
key={index}
|
||||
download
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
<div className="absolute right-0 top-0 m-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-50 hover:bg-white">
|
||||
<DownloadIcon className="h-6 text-slate-500" />
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div className="flex flex-col items-center justify-center p-2">
|
||||
<FileIcon className="h-6 text-slate-500" />
|
||||
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">{fileName}</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div className="flex flex-col items-center justify-center p-2">
|
||||
<FileIcon className="h-6 text-slate-500" />
|
||||
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">
|
||||
{fileUrl.split("/").pop()}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="flex w-full flex-col items-center justify-center p-2">
|
||||
<p className="mt-2 text-sm font-semibold text-slate-500 dark:text-slate-400">skipped</p>
|
||||
|
||||
-3
@@ -101,7 +101,6 @@ export default function QuestionCard({
|
||||
|
||||
const updateEmptyNextButtonLabels = (labelValue: string) => {
|
||||
localSurvey.questions.forEach((q, index) => {
|
||||
if (index === localSurvey.questions.length - 1) return;
|
||||
if (!q.buttonLabel || q.buttonLabel?.trim() === "") {
|
||||
updateQuestion(index, { buttonLabel: labelValue });
|
||||
}
|
||||
@@ -320,8 +319,6 @@ export default function QuestionCard({
|
||||
updateQuestion(questionIdx, { buttonLabel: e.target.value });
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
//If it is the last question then do not update labels
|
||||
if (questionIdx === localSurvey.questions.length - 1) return;
|
||||
updateEmptyNextButtonLabels(e.target.value);
|
||||
}}
|
||||
/>
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ export default function SettingsView({
|
||||
localSurvey={localSurvey}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
environmentId={environment.id}
|
||||
propActionClasses={actionClasses}
|
||||
actionClasses={actionClasses}
|
||||
membershipRole={membershipRole}
|
||||
/>
|
||||
|
||||
|
||||
+9
-10
@@ -27,7 +27,7 @@ interface WhenToSendCardProps {
|
||||
localSurvey: TSurvey;
|
||||
setLocalSurvey: (survey: TSurvey) => void;
|
||||
environmentId: string;
|
||||
propActionClasses: TActionClass[];
|
||||
actionClasses: TActionClass[];
|
||||
membershipRole?: TMembershipRole;
|
||||
}
|
||||
|
||||
@@ -35,13 +35,13 @@ export default function WhenToSendCard({
|
||||
environmentId,
|
||||
localSurvey,
|
||||
setLocalSurvey,
|
||||
propActionClasses,
|
||||
actionClasses,
|
||||
membershipRole,
|
||||
}: WhenToSendCardProps) {
|
||||
const [open, setOpen] = useState(localSurvey.type === "web" ? true : false);
|
||||
const [isAddEventModalOpen, setAddEventModalOpen] = useState(false);
|
||||
const [activeIndex, setActiveIndex] = useState<number | null>(null);
|
||||
const [actionClasses, setActionClasses] = useState<TActionClass[]>(propActionClasses);
|
||||
const [actionClassArray, setActionClassArray] = useState<TActionClass[]>(actionClasses);
|
||||
const { isViewer } = getAccessFlags(membershipRole);
|
||||
|
||||
const autoClose = localSurvey.autoClose !== null;
|
||||
@@ -55,7 +55,7 @@ export default function WhenToSendCard({
|
||||
const setTriggerEvent = useCallback(
|
||||
(idx: number, actionClassName: string) => {
|
||||
const updatedSurvey = { ...localSurvey };
|
||||
const newActionClass = actionClasses!.find((actionClass) => {
|
||||
const newActionClass = actionClassArray!.find((actionClass) => {
|
||||
return actionClass.name === actionClassName;
|
||||
});
|
||||
if (!newActionClass) {
|
||||
@@ -64,7 +64,7 @@ export default function WhenToSendCard({
|
||||
updatedSurvey.triggers[idx] = newActionClass.name;
|
||||
setLocalSurvey(updatedSurvey);
|
||||
},
|
||||
[actionClasses, localSurvey, setLocalSurvey]
|
||||
[actionClassArray, localSurvey, setLocalSurvey]
|
||||
);
|
||||
|
||||
const removeTriggerEvent = (idx: number) => {
|
||||
@@ -101,7 +101,7 @@ export default function WhenToSendCard({
|
||||
useEffect(() => {
|
||||
if (isAddEventModalOpen) return;
|
||||
if (activeIndex !== null) {
|
||||
const newActionClass = actionClasses[actionClasses.length - 1].name;
|
||||
const newActionClass = actionClassArray[actionClassArray.length - 1].name;
|
||||
const currentActionClass = localSurvey.triggers[activeIndex];
|
||||
|
||||
if (newActionClass !== currentActionClass) {
|
||||
@@ -110,7 +110,7 @@ export default function WhenToSendCard({
|
||||
|
||||
setActiveIndex(null);
|
||||
}
|
||||
}, [actionClasses, activeIndex, setTriggerEvent, isAddEventModalOpen, localSurvey.triggers]);
|
||||
}, [actionClassArray, activeIndex, setTriggerEvent, isAddEventModalOpen, localSurvey.triggers]);
|
||||
|
||||
useEffect(() => {
|
||||
if (localSurvey.type === "link") {
|
||||
@@ -200,7 +200,7 @@ export default function WhenToSendCard({
|
||||
Add Action
|
||||
</button>
|
||||
<SelectSeparator />
|
||||
{actionClasses.map((actionClass) => (
|
||||
{actionClassArray.map((actionClass) => (
|
||||
<SelectItem
|
||||
value={actionClass.name}
|
||||
key={actionClass.name}
|
||||
@@ -279,8 +279,7 @@ export default function WhenToSendCard({
|
||||
environmentId={environmentId}
|
||||
open={isAddEventModalOpen}
|
||||
setOpen={setAddEventModalOpen}
|
||||
actionClasses={actionClasses}
|
||||
setActionClasses={setActionClasses}
|
||||
setActionClassArray={setActionClassArray}
|
||||
isViewer={isViewer}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { sendToPipeline } from "@/app/lib/pipelines";
|
||||
import { headers } from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
import { UAParser } from "ua-parser-js";
|
||||
|
||||
@@ -23,7 +22,6 @@ export async function POST(request: Request): Promise<NextResponse> {
|
||||
responseInput.personId = null;
|
||||
}
|
||||
const agent = UAParser(request.headers.get("user-agent"));
|
||||
const country = headers().get("CF-IPCountry") || headers().get("X-Vercel-IP-Country") || undefined;
|
||||
const inputValidation = ZResponseLegacyInput.safeParse(responseInput);
|
||||
|
||||
if (!inputValidation.success) {
|
||||
@@ -62,7 +60,6 @@ export async function POST(request: Request): Promise<NextResponse> {
|
||||
device: agent?.device.type,
|
||||
os: agent?.os.name,
|
||||
},
|
||||
country: country,
|
||||
};
|
||||
|
||||
// check if personId is anonymous
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { sendToPipeline } from "@/app/lib/pipelines";
|
||||
import { headers } from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
import { UAParser } from "ua-parser-js";
|
||||
|
||||
@@ -46,7 +45,6 @@ export async function POST(request: Request, context: Context): Promise<NextResp
|
||||
}
|
||||
|
||||
const agent = UAParser(request.headers.get("user-agent"));
|
||||
const country = headers().get("CF-IPCountry") || headers().get("X-Vercel-IP-Country") || undefined;
|
||||
const inputValidation = ZResponseInput.safeParse({ ...responseInput, environmentId });
|
||||
|
||||
if (!inputValidation.success) {
|
||||
@@ -85,7 +83,6 @@ export async function POST(request: Request, context: Context): Promise<NextResp
|
||||
device: agent?.device.type,
|
||||
os: agent?.os.name,
|
||||
},
|
||||
country: country,
|
||||
};
|
||||
|
||||
response = await createResponse({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@formbricks/web",
|
||||
"version": "1.4.2",
|
||||
"version": "1.4.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"clean": "rimraf .turbo node_modules .next",
|
||||
@@ -26,23 +26,23 @@
|
||||
"@radix-ui/react-collapsible": "^1.0.3",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@react-email/components": "^0.0.12",
|
||||
"@sentry/nextjs": "^7.93.0",
|
||||
"@sentry/nextjs": "^7.92.0",
|
||||
"@vercel/og": "^0.6.2",
|
||||
"@vercel/speed-insights": "^1.0.3",
|
||||
"@vercel/speed-insights": "^1.0.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"dotenv": "^16.3.1",
|
||||
"encoding": "^0.1.13",
|
||||
"framer-motion": "10.18.0",
|
||||
"framer-motion": "10.17.12",
|
||||
"googleapis": "^130.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^10.1.0",
|
||||
"lucide-react": "^0.309.0",
|
||||
"lucide-react": "^0.308.0",
|
||||
"mime": "^4.0.1",
|
||||
"next": "14.0.4",
|
||||
"nodemailer": "^6.9.8",
|
||||
"otplib": "^12.0.1",
|
||||
"posthog-js": "^1.98.2",
|
||||
"posthog-js": "^1.97.0",
|
||||
"prismjs": "^1.29.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "18.2.0",
|
||||
@@ -51,7 +51,7 @@
|
||||
"react-email": "^1.10.0",
|
||||
"react-hook-form": "^7.49.3",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-icons": "^5.0.1",
|
||||
"react-icons": "^4.12.0",
|
||||
"ua-parser-js": "^1.0.37",
|
||||
"webpack": "^5.89.0",
|
||||
"xlsx": "^0.18.5"
|
||||
|
||||
@@ -42,7 +42,7 @@ export class StorageAPI {
|
||||
const json = await response.json();
|
||||
|
||||
const { data } = json;
|
||||
const { signedUrl, fileUrl, signingData, presignedFields, updatedFileName } = data;
|
||||
const { signedUrl, fileUrl, signingData, presignedFields } = data;
|
||||
|
||||
let requestHeaders: Record<string, string> = {};
|
||||
|
||||
@@ -51,7 +51,7 @@ export class StorageAPI {
|
||||
|
||||
requestHeaders = {
|
||||
"X-File-Type": file.type,
|
||||
"X-File-Name": encodeURIComponent(updatedFileName),
|
||||
"X-File-Name": encodeURIComponent(file.name),
|
||||
"X-Survey-ID": surveyId ?? "",
|
||||
"X-Signature": signature,
|
||||
"X-Timestamp": String(timestamp),
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"predev": "pnpm generate"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.8.0",
|
||||
"@prisma/client": "^5.7.1",
|
||||
"@prisma/extension-accelerate": "^0.6.2",
|
||||
"dotenv-cli": "^7.3.0"
|
||||
},
|
||||
@@ -33,7 +33,7 @@
|
||||
"@formbricks/tsconfig": "workspace:*",
|
||||
"@formbricks/types": "workspace:*",
|
||||
"eslint-config-formbricks": "workspace:*",
|
||||
"prisma": "^5.8.0",
|
||||
"prisma": "^5.7.1",
|
||||
"prisma-dbml-generator": "^0.10.0",
|
||||
"prisma-json-types-generator": "^3.0.3",
|
||||
"zod": "^3.22.4",
|
||||
|
||||
@@ -18,6 +18,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@formbricks/lib": "workspace:*",
|
||||
"stripe": "^14.12.0"
|
||||
"stripe": "^14.11.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@formbricks/js",
|
||||
"license": "MIT",
|
||||
"version": "1.4.2",
|
||||
"version": "1.4.1",
|
||||
"description": "Formbricks-js allows you to connect your app to Formbricks, display surveys and trigger events.",
|
||||
"homepage": "https://formbricks.com",
|
||||
"repository": {
|
||||
|
||||
@@ -34,7 +34,6 @@ const selectDisplay = {
|
||||
surveyId: true,
|
||||
responseId: true,
|
||||
personId: true,
|
||||
status: true,
|
||||
};
|
||||
|
||||
export const getDisplay = async (displayId: string): Promise<TDisplay | null> => {
|
||||
@@ -43,7 +42,7 @@ export const getDisplay = async (displayId: string): Promise<TDisplay | null> =>
|
||||
validateInputs([displayId, ZId]);
|
||||
|
||||
try {
|
||||
const display = await prisma.display.findUnique({
|
||||
const display = await prisma.response.findUnique({
|
||||
where: {
|
||||
id: displayId,
|
||||
},
|
||||
@@ -144,6 +143,7 @@ export const updateDisplayLegacy = async (
|
||||
data,
|
||||
select: selectDisplay,
|
||||
});
|
||||
|
||||
displayCache.revalidate({
|
||||
id: display.id,
|
||||
surveyId: display.surveyId,
|
||||
@@ -164,6 +164,7 @@ export const createDisplay = async (displayInput: TDisplayCreateInput): Promise<
|
||||
validateInputs([displayInput, ZDisplayCreateInput]);
|
||||
|
||||
const { environmentId, userId, surveyId } = displayInput;
|
||||
|
||||
try {
|
||||
let person;
|
||||
if (userId) {
|
||||
@@ -190,11 +191,13 @@ 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) {
|
||||
@@ -334,6 +337,7 @@ export const deleteDisplayByResponseId = async (
|
||||
personId: display.personId,
|
||||
surveyId,
|
||||
});
|
||||
|
||||
return display;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
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",
|
||||
};
|
||||
@@ -1,287 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -14,10 +14,11 @@
|
||||
"test": "jest -ci --coverage --no-cache"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/s3-presigned-post": "3.490.0",
|
||||
"@aws-sdk/client-s3": "3.490.0",
|
||||
"@aws-sdk/s3-request-presigner": "3.490.0",
|
||||
"@t3-oss/env-nextjs": "^0.7.3",
|
||||
"@aws-sdk/s3-presigned-post": "3.485.0",
|
||||
"@aws-sdk/client-s3": "3.485.0",
|
||||
"@aws-sdk/s3-request-presigner": "3.485.0",
|
||||
"@t3-oss/env-nextjs": "^0.7.1",
|
||||
"mime": "4.0.1",
|
||||
"@formbricks/api": "*",
|
||||
"@formbricks/database": "*",
|
||||
"@formbricks/types": "*",
|
||||
@@ -26,22 +27,21 @@
|
||||
"date-fns": "^3.2.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"markdown-it": "^14.0.0",
|
||||
"mime-types": "^2.1.35",
|
||||
"nanoid": "^5.0.4",
|
||||
"next-auth": "^4.24.5",
|
||||
"nodemailer": "^6.9.8",
|
||||
"posthog-node": "^3.5.0",
|
||||
"posthog-node": "^3.4.0",
|
||||
"server-only": "^0.0.1",
|
||||
"tailwind-merge": "^2.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formbricks/tsconfig": "*",
|
||||
"@types/jsonwebtoken": "^9.0.5",
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"babel-jest": "^29.7.0",
|
||||
"@types/mime": "3.0.4",
|
||||
"eslint-config-formbricks": "workspace:*",
|
||||
"jest": "^29.7.0",
|
||||
"babel-jest": "^29.7.0",
|
||||
"jest-mock-extended": "^3.0.5",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
|
||||
@@ -89,7 +89,6 @@ export const mockDisplay: TDisplay = {
|
||||
surveyId: mockSurveyId,
|
||||
personId: mockPersonId,
|
||||
responseId: mockResponseId,
|
||||
status: null,
|
||||
};
|
||||
|
||||
export const mockResponse: ResponseMock = {
|
||||
|
||||
@@ -7,9 +7,8 @@ import {
|
||||
} from "@aws-sdk/client-s3";
|
||||
import { PresignedPostOptions, createPresignedPost } from "@aws-sdk/s3-presigned-post";
|
||||
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||
import { randomUUID } from "crypto";
|
||||
import { access, mkdir, readFile, rmdir, unlink, writeFile } from "fs/promises";
|
||||
import { lookup } from "mime-types";
|
||||
import mime from "mime";
|
||||
import { unstable_cache } from "next/cache";
|
||||
import { join } from "path";
|
||||
import path from "path";
|
||||
@@ -62,7 +61,6 @@ type TGetSignedUrlResponse =
|
||||
| { signedUrl: string; fileUrl: string; presignedFields: Object }
|
||||
| {
|
||||
signedUrl: string;
|
||||
updatedFileName: string;
|
||||
fileUrl: string;
|
||||
signingData: {
|
||||
signature: string;
|
||||
@@ -149,7 +147,7 @@ export const getLocalFile = async (filePath: string): Promise<TGetFileResponse>
|
||||
let contentType = "";
|
||||
|
||||
try {
|
||||
contentType = lookup(filePath) || "";
|
||||
contentType = mime.getType(filePath) ?? "";
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
@@ -173,21 +171,10 @@ export const getUploadSignedUrl = async (
|
||||
accessType: TAccessType,
|
||||
plan: "free" | "pro" = "free"
|
||||
): Promise<TGetSignedUrlResponse> => {
|
||||
// add a unique id to the file name
|
||||
|
||||
const fileExtension = fileName.split(".").pop();
|
||||
const fileNameWithoutExtension = fileName.split(".").slice(0, -1).join(".");
|
||||
|
||||
if (!fileExtension) {
|
||||
throw new Error("File extension not found");
|
||||
}
|
||||
|
||||
const updatedFileName = `${fileNameWithoutExtension}--fid--${randomUUID()}.${fileExtension}`;
|
||||
|
||||
// handle the local storage case first
|
||||
if (!IS_S3_CONFIGURED) {
|
||||
try {
|
||||
const { signature, timestamp, uuid } = generateLocalSignedUrl(updatedFileName, environmentId, fileType);
|
||||
const { signature, timestamp, uuid } = generateLocalSignedUrl(fileName, environmentId, fileType);
|
||||
|
||||
return {
|
||||
signedUrl:
|
||||
@@ -199,8 +186,7 @@ export const getUploadSignedUrl = async (
|
||||
timestamp,
|
||||
uuid,
|
||||
},
|
||||
updatedFileName,
|
||||
fileUrl: new URL(`${WEBAPP_URL}/storage/${environmentId}/${accessType}/${updatedFileName}`).href,
|
||||
fileUrl: new URL(`${WEBAPP_URL}/storage/${environmentId}/${accessType}/${fileName}`).href,
|
||||
};
|
||||
} catch (err) {
|
||||
throw err;
|
||||
@@ -209,7 +195,7 @@ export const getUploadSignedUrl = async (
|
||||
|
||||
try {
|
||||
const { presignedFields, signedUrl } = await getS3UploadSignedUrl(
|
||||
updatedFileName,
|
||||
fileName,
|
||||
fileType,
|
||||
accessType,
|
||||
environmentId,
|
||||
@@ -220,7 +206,7 @@ export const getUploadSignedUrl = async (
|
||||
return {
|
||||
signedUrl,
|
||||
presignedFields,
|
||||
fileUrl: new URL(`${WEBAPP_URL}/storage/${environmentId}/${accessType}/${updatedFileName}`).href,
|
||||
fileUrl: new URL(`${WEBAPP_URL}/storage/${environmentId}/${accessType}/${fileName}`).href,
|
||||
};
|
||||
} catch (err) {
|
||||
throw err;
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
export const getOriginalFileNameFromUrl = (fileURL: string) => {
|
||||
const fileNameFromURL = new URL(fileURL).pathname.split("/").pop();
|
||||
const fileExt = fileNameFromURL?.split(".").pop();
|
||||
const originalFileName = fileNameFromURL?.split("--fid--")[0];
|
||||
const fileId = fileNameFromURL?.split("--fid--")[1];
|
||||
|
||||
if (!fileId) {
|
||||
const fileName = originalFileName ? decodeURIComponent(originalFileName || "") : "";
|
||||
return fileName;
|
||||
}
|
||||
|
||||
const fileName = originalFileName ? decodeURIComponent(`${originalFileName}.${fileExt}` || "") : "";
|
||||
return fileName;
|
||||
};
|
||||
@@ -1,195 +0,0 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
import { TActionClass } from "@formbricks/types/actionClasses";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TPerson } from "@formbricks/types/people";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyAttributeFilter,
|
||||
TSurveyInput,
|
||||
TSurveyQuestion,
|
||||
TSurveyQuestionType,
|
||||
TSurveyWelcomeCard,
|
||||
} from "@formbricks/types/surveys";
|
||||
|
||||
import { selectSurvey } from "../service";
|
||||
|
||||
const currentDate = new Date();
|
||||
const fourDaysAgo = new Date();
|
||||
fourDaysAgo.setDate(currentDate.getDate() - 4);
|
||||
|
||||
export const mockId = "ars2tjk8hsi8oqk1uac00mo8";
|
||||
const commonMockProperties = {
|
||||
createdAt: currentDate,
|
||||
updatedAt: currentDate,
|
||||
environmentId: mockId,
|
||||
};
|
||||
|
||||
type SurveyMock = Prisma.SurveyGetPayload<{
|
||||
include: typeof selectSurvey;
|
||||
}>;
|
||||
|
||||
export const mockProduct: TProduct = {
|
||||
id: mockId,
|
||||
createdAt: currentDate,
|
||||
updatedAt: currentDate,
|
||||
name: "mock Product",
|
||||
teamId: mockId,
|
||||
brandColor: "#000000",
|
||||
highlightBorderColor: "#000000",
|
||||
recontactDays: 0,
|
||||
linkSurveyBranding: false,
|
||||
inAppSurveyBranding: false,
|
||||
placement: "bottomRight",
|
||||
clickOutsideClose: false,
|
||||
darkOverlay: false,
|
||||
environments: [],
|
||||
};
|
||||
|
||||
export const mockDisplay = {
|
||||
id: mockId,
|
||||
createdAt: fourDaysAgo,
|
||||
updatedAt: fourDaysAgo,
|
||||
surveyId: mockId,
|
||||
personId: null,
|
||||
responseId: null,
|
||||
status: null,
|
||||
};
|
||||
|
||||
export const mockPerson: TPerson = {
|
||||
id: mockId,
|
||||
userId: mockId,
|
||||
attributes: { test: "value" },
|
||||
...commonMockProperties,
|
||||
};
|
||||
|
||||
export const mockActionClass: TActionClass = {
|
||||
id: mockId,
|
||||
name: "mock action class",
|
||||
type: "code",
|
||||
description: "mock desc",
|
||||
noCodeConfig: null,
|
||||
...commonMockProperties,
|
||||
};
|
||||
|
||||
export const mockAttributeClass: TAttributeClass = {
|
||||
id: mockId,
|
||||
name: "mock attribute class",
|
||||
type: "code",
|
||||
description: "mock action class",
|
||||
archived: false,
|
||||
...commonMockProperties,
|
||||
};
|
||||
|
||||
export const mockAttributeFilter: TSurveyAttributeFilter = {
|
||||
attributeClassId: mockId,
|
||||
value: "test",
|
||||
condition: "equals",
|
||||
};
|
||||
|
||||
const mockQuestion: TSurveyQuestion = {
|
||||
id: mockId,
|
||||
type: TSurveyQuestionType.OpenText,
|
||||
headline: "Question Text",
|
||||
required: false,
|
||||
inputType: "text",
|
||||
};
|
||||
|
||||
const mockWelcomeCard: TSurveyWelcomeCard = {
|
||||
enabled: false,
|
||||
headline: "My welcome card",
|
||||
timeToFinish: false,
|
||||
showResponseCount: false,
|
||||
};
|
||||
|
||||
const baseSurveyProperties = {
|
||||
id: mockId,
|
||||
name: "Mock Survey",
|
||||
autoClose: 10,
|
||||
delay: 0,
|
||||
autoComplete: 7,
|
||||
closeOnDate: currentDate,
|
||||
redirectUrl: "http://github.com/formbricks/formbricks",
|
||||
recontactDays: 3,
|
||||
welcomeCard: mockWelcomeCard,
|
||||
questions: [mockQuestion],
|
||||
thankYouCard: { enabled: false },
|
||||
hiddenFields: { enabled: false },
|
||||
surveyClosedMessage: {
|
||||
enabled: false,
|
||||
},
|
||||
verifyEmail: {
|
||||
name: "verifyEmail",
|
||||
subheading: "please verify your email",
|
||||
},
|
||||
attributeFilters: [],
|
||||
...commonMockProperties,
|
||||
};
|
||||
|
||||
export const mockSurveyOutput: SurveyMock = {
|
||||
type: "web",
|
||||
status: "inProgress",
|
||||
displayOption: "respondMultiple",
|
||||
triggers: [{ actionClass: mockActionClass }],
|
||||
productOverwrites: null,
|
||||
singleUse: null,
|
||||
styling: null,
|
||||
pin: null,
|
||||
resultShareKey: null,
|
||||
...baseSurveyProperties,
|
||||
};
|
||||
|
||||
export const createSurveyInput: TSurveyInput = {
|
||||
type: "web",
|
||||
status: "inProgress",
|
||||
displayOption: "respondMultiple",
|
||||
triggers: [mockActionClass.name],
|
||||
...baseSurveyProperties,
|
||||
attributeFilters: [mockAttributeFilter],
|
||||
};
|
||||
|
||||
export const updateSurveyInput: TSurvey = {
|
||||
type: "web",
|
||||
status: "inProgress",
|
||||
displayOption: "respondMultiple",
|
||||
triggers: [mockActionClass.name],
|
||||
productOverwrites: null,
|
||||
styling: null,
|
||||
singleUse: null,
|
||||
pin: null,
|
||||
resultShareKey: null,
|
||||
...commonMockProperties,
|
||||
...baseSurveyProperties,
|
||||
attributeFilters: [mockAttributeFilter],
|
||||
};
|
||||
|
||||
export const mockSurveyWithAttributesOutput: SurveyMock = {
|
||||
...mockSurveyOutput,
|
||||
attributeFilters: [
|
||||
{
|
||||
id: mockId,
|
||||
...mockAttributeFilter,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const mockTransformedSurveyOutput = {
|
||||
...mockSurveyOutput,
|
||||
triggers: mockSurveyOutput.triggers.map((trigger) => trigger.actionClass.name),
|
||||
};
|
||||
|
||||
export const mockTransformedSurveyWithAttributesOutput = {
|
||||
...mockTransformedSurveyOutput,
|
||||
attributeFilters: [mockAttributeFilter],
|
||||
};
|
||||
|
||||
export const mockTransformedSurveyWithAttributesIdOutput = {
|
||||
...mockTransformedSurveyOutput,
|
||||
attributeFilters: [
|
||||
{
|
||||
id: mockId,
|
||||
...mockAttributeFilter,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,321 +0,0 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
import { prismaMock } from "@formbricks/database/src/jestClient";
|
||||
import { DatabaseError, ResourceNotFoundError, ValidationError } from "@formbricks/types/errors";
|
||||
|
||||
import {
|
||||
createSurvey,
|
||||
deleteSurvey,
|
||||
duplicateSurvey,
|
||||
getSurvey,
|
||||
getSurveys,
|
||||
getSurveysByActionClassId,
|
||||
getSurveysByAttributeClassId,
|
||||
getSyncSurveys,
|
||||
updateSurvey,
|
||||
} from "../service";
|
||||
import {
|
||||
createSurveyInput,
|
||||
mockActionClass,
|
||||
mockAttributeClass,
|
||||
mockDisplay,
|
||||
mockId,
|
||||
mockPerson,
|
||||
mockProduct,
|
||||
mockSurveyOutput,
|
||||
mockSurveyWithAttributesOutput,
|
||||
mockTransformedSurveyOutput,
|
||||
mockTransformedSurveyWithAttributesIdOutput,
|
||||
mockTransformedSurveyWithAttributesOutput,
|
||||
updateSurveyInput,
|
||||
} from "./survey.mock";
|
||||
|
||||
// utility function to test input validation for all services
|
||||
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);
|
||||
});
|
||||
};
|
||||
|
||||
describe("Tests for getSurvey", () => {
|
||||
describe("Happy Path", () => {
|
||||
it("Returns a survey", async () => {
|
||||
prismaMock.survey.findUnique.mockResolvedValueOnce(mockSurveyOutput);
|
||||
const survey = await getSurvey(mockId);
|
||||
expect(survey).toEqual(mockTransformedSurveyOutput);
|
||||
});
|
||||
|
||||
it("Returns null if survey is not found", async () => {
|
||||
prismaMock.survey.findUnique.mockResolvedValueOnce(null);
|
||||
const survey = await getSurvey(mockId);
|
||||
expect(survey).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sad Path", () => {
|
||||
testInputValidation(getSurvey, "123");
|
||||
|
||||
it("should throw 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.survey.findUnique.mockRejectedValue(errToThrow);
|
||||
await expect(getSurvey(mockId)).rejects.toThrow(DatabaseError);
|
||||
});
|
||||
|
||||
it("should throw an error if there is an unknown error", async () => {
|
||||
const mockErrorMessage = "Mock error message";
|
||||
prismaMock.survey.findUnique.mockRejectedValue(new Error(mockErrorMessage));
|
||||
await expect(getSurvey(mockId)).rejects.toThrow(Error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tests for getSurveysByAttributeClassId", () => {
|
||||
describe("Happy Path", () => {
|
||||
it("Returns an array of surveys for a given attributeClassId", async () => {
|
||||
prismaMock.survey.findMany.mockResolvedValueOnce([mockSurveyOutput]);
|
||||
const surveys = await getSurveysByAttributeClassId(mockId);
|
||||
expect(surveys).toEqual([mockTransformedSurveyOutput]);
|
||||
});
|
||||
|
||||
it("Returns an empty array if no surveys are found", async () => {
|
||||
prismaMock.survey.findMany.mockResolvedValueOnce([]);
|
||||
const surveys = await getSurveysByAttributeClassId(mockId);
|
||||
expect(surveys).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sad Path", () => {
|
||||
testInputValidation(getSurveysByAttributeClassId, "123");
|
||||
|
||||
it("should throw an error if there is an unknown error", async () => {
|
||||
const mockErrorMessage = "Unknown error occurred";
|
||||
prismaMock.survey.findMany.mockRejectedValue(new Error(mockErrorMessage));
|
||||
await expect(getSurveysByAttributeClassId(mockId)).rejects.toThrow(Error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tests for getSurveysByActionClassId", () => {
|
||||
describe("Happy Path", () => {
|
||||
it("Returns an array of surveys for a given actionClassId", async () => {
|
||||
prismaMock.survey.findMany.mockResolvedValueOnce([mockSurveyOutput]);
|
||||
const surveys = await getSurveysByActionClassId(mockId);
|
||||
expect(surveys).toEqual([mockTransformedSurveyOutput]);
|
||||
});
|
||||
|
||||
it("Returns an empty array if no surveys are found", async () => {
|
||||
prismaMock.survey.findMany.mockResolvedValueOnce([]);
|
||||
const surveys = await getSurveysByActionClassId(mockId);
|
||||
expect(surveys).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sad Path", () => {
|
||||
testInputValidation(getSurveysByActionClassId, "123");
|
||||
|
||||
it("should throw an error if there is an unknown error", async () => {
|
||||
const mockErrorMessage = "Unknown error occurred";
|
||||
prismaMock.survey.findMany.mockRejectedValue(new Error(mockErrorMessage));
|
||||
await expect(getSurveysByActionClassId(mockId)).rejects.toThrow(Error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tests for getSurveys", () => {
|
||||
describe("Happy Path", () => {
|
||||
it("Returns an array of surveys for a given environmentId and page", async () => {
|
||||
prismaMock.survey.findMany.mockResolvedValueOnce([mockSurveyOutput]);
|
||||
const surveys = await getSurveys(mockId);
|
||||
expect(surveys).toEqual([mockTransformedSurveyOutput]);
|
||||
});
|
||||
|
||||
it("Returns an empty array if no surveys are found", async () => {
|
||||
prismaMock.survey.findMany.mockResolvedValueOnce([]);
|
||||
|
||||
const surveys = await getSurveys(mockId);
|
||||
expect(surveys).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sad Path", () => {
|
||||
testInputValidation(getSurveysByActionClassId, "123");
|
||||
|
||||
it("should throw 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.survey.findMany.mockRejectedValue(errToThrow);
|
||||
await expect(getSurveys(mockId)).rejects.toThrow(DatabaseError);
|
||||
});
|
||||
|
||||
it("should throw an error if there is an unknown error", async () => {
|
||||
const mockErrorMessage = "Unknown error occurred";
|
||||
prismaMock.survey.findMany.mockRejectedValue(new Error(mockErrorMessage));
|
||||
await expect(getSurveys(mockId)).rejects.toThrow(Error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tests for updateSurvey", () => {
|
||||
beforeEach(() => {
|
||||
prismaMock.actionClass.findMany.mockResolvedValueOnce([mockActionClass]);
|
||||
});
|
||||
describe("Happy Path", () => {
|
||||
it("Updates a survey successfully", async () => {
|
||||
prismaMock.survey.findUnique.mockResolvedValueOnce(mockSurveyOutput);
|
||||
prismaMock.survey.update.mockResolvedValueOnce(mockSurveyOutput);
|
||||
const updatedSurvey = await updateSurvey(updateSurveyInput);
|
||||
expect(updatedSurvey).toEqual(mockTransformedSurveyWithAttributesOutput);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sad Path", () => {
|
||||
testInputValidation(updateSurvey, "123");
|
||||
|
||||
it("Throws ResourceNotFoundError if the survey does not exist", async () => {
|
||||
prismaMock.survey.findUnique.mockRejectedValueOnce(
|
||||
new ResourceNotFoundError("Survey", updateSurveyInput.id)
|
||||
);
|
||||
await expect(updateSurvey(updateSurveyInput)).rejects.toThrow(ResourceNotFoundError);
|
||||
});
|
||||
|
||||
it("should throw 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.survey.findUnique.mockResolvedValueOnce(mockSurveyOutput);
|
||||
prismaMock.survey.update.mockRejectedValue(errToThrow);
|
||||
await expect(updateSurvey(updateSurveyInput)).rejects.toThrow(DatabaseError);
|
||||
});
|
||||
|
||||
it("should throw an error if there is an unknown error", async () => {
|
||||
const mockErrorMessage = "Unknown error occurred";
|
||||
prismaMock.survey.findUnique.mockResolvedValueOnce(mockSurveyOutput);
|
||||
prismaMock.survey.update.mockRejectedValue(new Error(mockErrorMessage));
|
||||
await expect(updateSurvey(updateSurveyInput)).rejects.toThrow(Error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tests for deleteSurvey", () => {
|
||||
describe("Happy Path", () => {
|
||||
it("Deletes a survey successfully", async () => {
|
||||
prismaMock.survey.delete.mockResolvedValueOnce(mockSurveyWithAttributesOutput);
|
||||
const deletedSurvey = await deleteSurvey(mockId);
|
||||
expect(deletedSurvey).toEqual(mockSurveyWithAttributesOutput);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sad Path", () => {
|
||||
testInputValidation(deleteSurvey, "123");
|
||||
|
||||
it("should throw an error if there is an unknown error", async () => {
|
||||
const mockErrorMessage = "Unknown error occurred";
|
||||
prismaMock.survey.findUnique.mockResolvedValueOnce(mockSurveyOutput);
|
||||
prismaMock.survey.delete.mockRejectedValue(new Error(mockErrorMessage));
|
||||
await expect(deleteSurvey(mockId)).rejects.toThrow(Error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tests for createSurvey", () => {
|
||||
beforeEach(() => {
|
||||
prismaMock.actionClass.findMany.mockResolvedValueOnce([mockActionClass]);
|
||||
});
|
||||
|
||||
describe("Happy Path", () => {
|
||||
it("Creates a survey successfully", async () => {
|
||||
prismaMock.survey.create.mockResolvedValueOnce(mockSurveyWithAttributesOutput);
|
||||
const createdSurvey = await createSurvey(mockId, createSurveyInput);
|
||||
expect(createdSurvey).toEqual(mockTransformedSurveyWithAttributesIdOutput);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sad Path", () => {
|
||||
testInputValidation(createSurvey, "123", createSurveyInput);
|
||||
|
||||
it("should throw an error if there is an unknown error", async () => {
|
||||
const mockErrorMessage = "Unknown error occurred";
|
||||
prismaMock.survey.delete.mockRejectedValue(new Error(mockErrorMessage));
|
||||
await expect(createSurvey(mockId, createSurveyInput)).rejects.toThrow(Error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tests for duplicateSurvey", () => {
|
||||
beforeEach(() => {
|
||||
prismaMock.actionClass.findMany.mockResolvedValueOnce([mockActionClass]);
|
||||
});
|
||||
|
||||
describe("Happy Path", () => {
|
||||
it("Duplicates a survey successfully", async () => {
|
||||
prismaMock.survey.findUnique.mockResolvedValueOnce(mockSurveyWithAttributesOutput);
|
||||
prismaMock.survey.create.mockResolvedValueOnce(mockSurveyWithAttributesOutput);
|
||||
const createdSurvey = await duplicateSurvey(mockId, mockId);
|
||||
expect(createdSurvey).toEqual(mockSurveyWithAttributesOutput);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sad Path", () => {
|
||||
testInputValidation(duplicateSurvey, "123", "123");
|
||||
|
||||
it("Throws ResourceNotFoundError if the survey does not exist", async () => {
|
||||
prismaMock.survey.findUnique.mockRejectedValueOnce(new ResourceNotFoundError("Survey", mockId));
|
||||
await expect(duplicateSurvey(mockId, mockId)).rejects.toThrow(ResourceNotFoundError);
|
||||
});
|
||||
|
||||
it("should throw an error if there is an unknown error", async () => {
|
||||
const mockErrorMessage = "Unknown error occurred";
|
||||
prismaMock.survey.create.mockRejectedValue(new Error(mockErrorMessage));
|
||||
await expect(duplicateSurvey(mockId, mockId)).rejects.toThrow(Error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tests for getSyncedSurveys", () => {
|
||||
describe("Happy Path", () => {
|
||||
beforeEach(() => {
|
||||
prismaMock.product.findFirst.mockResolvedValueOnce(mockProduct);
|
||||
prismaMock.display.findMany.mockResolvedValueOnce([mockDisplay]);
|
||||
prismaMock.attributeClass.findMany.mockResolvedValueOnce([mockAttributeClass]);
|
||||
});
|
||||
|
||||
it("Returns synced surveys", async () => {
|
||||
prismaMock.survey.findMany.mockResolvedValueOnce([mockSurveyOutput]);
|
||||
const surveys = await getSyncSurveys(mockId, mockPerson);
|
||||
expect(surveys).toEqual([mockTransformedSurveyOutput]);
|
||||
});
|
||||
|
||||
it("Returns an empty array if no surveys are found", async () => {
|
||||
prismaMock.survey.findMany.mockResolvedValueOnce([]);
|
||||
const surveys = await getSyncSurveys(mockId, mockPerson);
|
||||
expect(surveys).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sad Path", () => {
|
||||
testInputValidation(getSyncSurveys, "123", {});
|
||||
|
||||
it("does not find a Product", async () => {
|
||||
prismaMock.product.findFirst.mockResolvedValueOnce(null);
|
||||
|
||||
await expect(getSyncSurveys(mockId, mockPerson)).rejects.toThrow(Error);
|
||||
});
|
||||
|
||||
it("should throw an error if there is an unknown error", async () => {
|
||||
const mockErrorMessage = "Unknown error occurred";
|
||||
prismaMock.actionClass.findMany.mockResolvedValueOnce([mockActionClass]);
|
||||
prismaMock.survey.create.mockRejectedValue(new Error(mockErrorMessage));
|
||||
await expect(getSyncSurveys(mockId, mockPerson)).rejects.toThrow(Error);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -8,7 +8,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"prettier": "^3.2.1",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier-plugin-tailwindcss": "^0.5.11"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@formbricks/surveys",
|
||||
"license": "MIT",
|
||||
"version": "1.4.2",
|
||||
"version": "1.4.1",
|
||||
"description": "Formbricks-surveys is a helper library to embed surveys into your application",
|
||||
"homepage": "https://formbricks.com",
|
||||
"repository": {
|
||||
@@ -23,7 +23,7 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite build --watch --mode dev",
|
||||
"dev": "SURVEYS_PACKAGE_MODE=development vite build --watch",
|
||||
"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",
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useMemo } from "preact/hooks";
|
||||
import { JSXInternal } from "preact/src/jsx";
|
||||
import { useState } from "react";
|
||||
|
||||
import { getOriginalFileNameFromUrl } from "@formbricks/lib/storage/utils";
|
||||
import { TAllowedFileExtension } from "@formbricks/types/common";
|
||||
import { TUploadFileConfig } from "@formbricks/types/storage";
|
||||
|
||||
@@ -205,47 +204,45 @@ export default function FileInput({
|
||||
<div className="items-left relative mt-3 flex w-full cursor-pointer flex-col justify-center rounded-lg border-2 border-dashed border-slate-300 bg-slate-50 hover:bg-slate-100 dark:border-slate-600 dark:bg-slate-700 dark:hover:border-slate-500 dark:hover:bg-slate-800">
|
||||
<div>
|
||||
{fileUrls &&
|
||||
fileUrls?.map((file, index) => {
|
||||
const fileName = getOriginalFileNameFromUrl(file);
|
||||
|
||||
return (
|
||||
<div key={index} className="relative m-2 rounded-md bg-slate-200">
|
||||
<div className="absolute right-0 top-0 m-2">
|
||||
<div className="flex h-5 w-5 items-center justify-center rounded-md bg-slate-100 hover:bg-slate-50">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 26 26"
|
||||
strokeWidth={1}
|
||||
stroke="currentColor"
|
||||
className="h-5 text-slate-700 hover:text-slate-900"
|
||||
onClick={(e) => handleDeleteFile(index, e)}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 9l10 10m0-10L9 19" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center justify-center p-2">
|
||||
fileUrls?.map((file, index) => (
|
||||
<div key={index} className="relative m-2 rounded-md bg-slate-200">
|
||||
<div className="absolute right-0 top-0 m-2">
|
||||
<div className="flex h-5 w-5 items-center justify-center rounded-md bg-slate-100 hover:bg-slate-50">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
viewBox="0 0 26 26"
|
||||
strokeWidth={1}
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-file"
|
||||
className="h-6 text-slate-500">
|
||||
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" />
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
className="h-5 text-slate-700 hover:text-slate-900"
|
||||
onClick={(e) => handleDeleteFile(index, e)}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 9l10 10m0-10L9 19" />
|
||||
</svg>
|
||||
<p className="mt-1 text-sm text-slate-600 dark:text-slate-400">{fileName}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="flex flex-col items-center justify-center p-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-file"
|
||||
className="h-6 text-slate-500">
|
||||
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" />
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
</svg>
|
||||
<p className="mt-1 text-sm text-slate-600 dark:text-slate-400">
|
||||
{decodeURIComponent(file).split("/").pop()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -41,7 +41,6 @@ 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
|
||||
@@ -49,7 +48,10 @@ export default function DateQuestion({
|
||||
if (!window.initDatePicker) {
|
||||
const script = document.createElement("script");
|
||||
|
||||
script.src = datePickerScriptSrc;
|
||||
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.async = true;
|
||||
|
||||
|
||||
@@ -13,15 +13,9 @@ 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,
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"clean": "rimraf node_modules dist turbo"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "20.11.0",
|
||||
"@types/node": "20.10.7",
|
||||
"@types/react": "18.2.47",
|
||||
"@types/react-dom": "18.2.18",
|
||||
"typescript": "^5.3.3"
|
||||
|
||||
@@ -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"]).nullable(),
|
||||
status: z.enum(["seen", "responded"]).optional(),
|
||||
});
|
||||
|
||||
export type TDisplay = z.infer<typeof ZDisplay>;
|
||||
|
||||
@@ -45,7 +45,6 @@ export const ZResponseMeta = z.object({
|
||||
device: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
country: z.string().optional(),
|
||||
});
|
||||
|
||||
export type TResponseMeta = z.infer<typeof ZResponseMeta>;
|
||||
@@ -87,7 +86,6 @@ export const ZResponseInput = z.object({
|
||||
os: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
country: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
@@ -46,7 +46,7 @@ const uploadFile = async (
|
||||
const json = await response.json();
|
||||
|
||||
const { data } = json;
|
||||
const { signedUrl, fileUrl, signingData, presignedFields, updatedFileName } = data;
|
||||
const { signedUrl, fileUrl, signingData, presignedFields } = data;
|
||||
|
||||
let requestHeaders: Record<string, string> = {};
|
||||
|
||||
@@ -55,7 +55,7 @@ const uploadFile = async (
|
||||
|
||||
requestHeaders = {
|
||||
"X-File-Type": file.type,
|
||||
"X-File-Name": encodeURIComponent(updatedFileName),
|
||||
"X-File-Name": encodeURIComponent(file.name),
|
||||
"X-Environment-ID": environmentId ?? "",
|
||||
"X-Signature": signature,
|
||||
"X-Timestamp": String(timestamp),
|
||||
|
||||
@@ -2,49 +2,11 @@
|
||||
|
||||
import { FileIcon } from "lucide-react";
|
||||
|
||||
import { getOriginalFileNameFromUrl } from "@formbricks/lib/storage/utils";
|
||||
|
||||
interface FileUploadResponseProps {
|
||||
selected: string | number | string[];
|
||||
}
|
||||
|
||||
export const FileUploadResponse = ({ selected }: FileUploadResponseProps) => {
|
||||
const SingleFileResponse = () => {
|
||||
const selectedFile = selected as string;
|
||||
const fileName = getOriginalFileNameFromUrl(selectedFile);
|
||||
|
||||
return (
|
||||
<div className="relative m-2 rounded-lg bg-slate-300">
|
||||
<a href={selected as string} download={fileName}>
|
||||
<div className="absolute right-0 top-0 m-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-100 bg-opacity-50 hover:bg-slate-200/50">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
className="h-6 w-6">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div className="flex flex-col items-center justify-center p-2">
|
||||
<FileIcon className="h-6 text-slate-500" />
|
||||
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">
|
||||
{selected && typeof selected === "string" && decodeURIComponent(selected).split("/").pop()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{selected === "selected" ? (
|
||||
@@ -52,40 +14,65 @@ export const FileUploadResponse = ({ selected }: FileUploadResponseProps) => {
|
||||
) : (
|
||||
<div className="col-span-2 grid md:grid-cols-2 lg:grid-cols-4">
|
||||
{Array.isArray(selected) ? (
|
||||
selected.map((fileUrl, index) => {
|
||||
const fileName = getOriginalFileNameFromUrl(fileUrl);
|
||||
|
||||
return (
|
||||
<div className="relative m-2 ml-0 rounded-lg bg-slate-200">
|
||||
<a href={fileUrl as string} key={index} download={fileName}>
|
||||
<div className="absolute right-0 top-0 m-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-50 hover:bg-white">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
className="h-6 w-6">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
selected.map((fileUrl, index) => (
|
||||
<div className="relative m-2 ml-0 rounded-lg bg-slate-200">
|
||||
<a href={fileUrl as string} key={index} download>
|
||||
<div className="absolute right-0 top-0 m-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-50 hover:bg-white">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
className="h-6 w-6">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div className="flex flex-col items-center justify-center p-2">
|
||||
<FileIcon className="h-6 text-slate-500" />
|
||||
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">{fileName}</p>
|
||||
<div className="flex flex-col items-center justify-center p-2">
|
||||
<FileIcon className="h-6 text-slate-500" />
|
||||
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">
|
||||
{decodeURIComponent(fileUrl).split("/").pop()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="relative m-2 rounded-lg bg-slate-300">
|
||||
<a href={selected as string} download>
|
||||
<div className="absolute right-0 top-0 m-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-100 bg-opacity-50 hover:bg-slate-200/50">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
className="h-6 w-6">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<SingleFileResponse />
|
||||
</a>
|
||||
|
||||
<div className="flex flex-col items-center justify-center p-2">
|
||||
<FileIcon className="h-6 text-slate-500" />
|
||||
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">
|
||||
{selected && typeof selected === "string" && decodeURIComponent(selected).split("/").pop()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -209,7 +209,6 @@ export default function SingleResponseCard({
|
||||
</p>
|
||||
)}
|
||||
{response.meta?.source && <p>Source: {response.meta.source}</p>}
|
||||
{response.meta?.country && <p>Country: {response.meta.country}</p>}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"clsx": "^2.1.0",
|
||||
"cmdk": "^0.2.0",
|
||||
"lexical": "^0.12.6",
|
||||
"lucide-react": "^0.309.0",
|
||||
"lucide-react": "^0.308.0",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-confetti": "^6.1.0",
|
||||
"react-day-picker": "^8.10.0",
|
||||
|
||||
Generated
+433
-404
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user