mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-27 00:40:29 -06:00
Compare commits
3 Commits
v1.4.2
...
shubham/in
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc72b712d6 | ||
|
|
0bdae282e5 | ||
|
|
28bc87b8e5 |
6
.github/workflows/build-web.yml
vendored
6
.github/workflows/build-web.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/playwright.yml
vendored
2
.github/workflows/playwright.yml
vendored
@@ -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:
|
||||
|
||||
5
.github/workflows/pr.yml
vendored
5
.github/workflows/pr.yml
vendored
@@ -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')
|
||||
|
||||
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -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
|
||||
|
||||
@@ -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" },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -84,7 +84,6 @@ export default function ActionClassesTable({
|
||||
<AddNoCodeActionModal
|
||||
environmentId={environmentId}
|
||||
open={isAddActionModalOpen}
|
||||
actionClasses={actionClasses}
|
||||
setOpen={setAddActionModalOpen}
|
||||
isViewer={isViewer}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -47,7 +47,7 @@ export default function SettingsView({
|
||||
localSurvey={localSurvey}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
environmentId={environment.id}
|
||||
propActionClasses={actionClasses}
|
||||
actionClasses={actionClasses}
|
||||
membershipRole={membershipRole}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
"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": "*",
|
||||
@@ -30,7 +30,7 @@
|
||||
"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"
|
||||
},
|
||||
|
||||
@@ -7,7 +7,6 @@ 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 mime from "mime";
|
||||
import { unstable_cache } from "next/cache";
|
||||
@@ -62,7 +61,6 @@ type TGetSignedUrlResponse =
|
||||
| { signedUrl: string; fileUrl: string; presignedFields: Object }
|
||||
| {
|
||||
signedUrl: string;
|
||||
updatedFileName: string;
|
||||
fileUrl: string;
|
||||
signingData: {
|
||||
signature: string;
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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": {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
821
pnpm-lock.yaml
generated
821
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user