add sendToHq helper to formbricks react, update class merge in react lib, update capture endpoints in hq to support cors

This commit is contained in:
Matthias Nannt
2022-12-01 15:59:33 +01:00
parent 515b23859a
commit 23fd92f85c
16 changed files with 168 additions and 52 deletions

View File

@@ -20,6 +20,7 @@
"jsonwebtoken": "^8.5.1",
"next": "^13.0.5",
"next-auth": "^4.17.0",
"nextjs-cors": "^2.1.2",
"nodemailer": "^6.8.0",
"prismjs": "^1.29.0",
"react": "^18.2.0",

View File

@@ -128,8 +128,8 @@ export default function MeSettingsPage() {
</h2>
<hr className="my-4 w-full text-gray-400" />
<Form
onSubmit={async ({ data }) => {
const apiKey = await createApiKey({ label: data.label });
onSubmit={async ({ submission }) => {
const apiKey = await createApiKey(submission.data);
mutateApiKeys([...JSON.parse(JSON.stringify(apiKeys)), apiKey], false);
setOpenNewApiKeyModal(false);
}}>

View File

@@ -68,9 +68,8 @@ export default function PipelinesPage({ params }) {
<div>
{form.schema.children.map((elem) => (
<>
{["email", "number", "phone", "radio", "search", "text", "textarea", "url"].includes(
elem.type
) ? (
{console.log(elem.type)}
{["email", "number", "phone", "search", "text", "textarea", "url"].includes(elem.type) ? (
<div className="mb-6">
<h2 className="mb-6 text-xl font-bold leading-tight tracking-tight text-gray-900">
{elem.label}
@@ -85,7 +84,7 @@ export default function PipelinesPage({ params }) {
<h2 className="mb-6 text-xl font-bold leading-tight tracking-tight text-gray-900">
{elem.label}
<span className="text-brand-dark ml-4 inline-flex items-center rounded-md border border-teal-100 bg-teal-50 px-2.5 py-0.5 text-sm font-medium">
Checkbox
{elem.type}
</span>
</h2>
<Bar submissions={submissions} schema={form.schema} fieldName={elem.name} />

View File

@@ -0,0 +1,38 @@
import { prisma } from "@formbricks/database";
import type { NextApiRequest, NextApiResponse } from "next";
import NextCors from "nextjs-cors";
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
await NextCors(req, res, {
// Options
methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"],
origin: "*",
optionsSuccessStatus: 200, // some legacy browsers (IE11, various SmartTVs) choke on 204
});
const formId = req.query.formId.toString();
// POST/capture/forms/[formId]/submissions
// Create a new form submission
// Required fields in body: -
// Optional fields in body: customerId, data
if (req.method === "POST") {
const schema = req.body;
// create form in db
await prisma.form.update({
where: {
id: formId,
},
data: {
schema,
},
});
res.status(201).end();
}
// Unknown HTTP Method
else {
throw new Error(`The HTTP ${req.method} method is not supported by this route.`);
}
}

View File

@@ -1,8 +1,16 @@
import { runPipelines } from "@/lib/pipelinesHandler";
import { prisma } from "@formbricks/database";
import type { NextApiRequest, NextApiResponse } from "next";
import NextCors from "nextjs-cors";
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
await NextCors(req, res, {
// Options
methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"],
origin: "*",
optionsSuccessStatus: 200, // some legacy browsers (IE11, various SmartTVs) choke on 204
});
const formId = req.query.formId.toString();
// POST/capture/forms/[formId]/submissions
@@ -12,7 +20,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
if (req.method === "POST") {
const submission = req.body;
// get team
// get form
const form = await prisma.form.findUnique({
where: { id: formId },
});

View File

@@ -7,22 +7,29 @@ export const SchemaContext = createContext({
});
interface OnSubmitProps {
data: any;
submission: any;
schema: any;
event?: React.BaseSyntheticEvent<object, any, any> | undefined;
}
interface FormProps {
incompleteMessage?: string;
onSubmit: ({ data, schema, event }: OnSubmitProps) => void;
onSubmit: ({ submission, schema, event }: OnSubmitProps) => void;
formId?: string;
hqUrl?: string;
customerId?: string;
children: React.ReactNode;
}
export function Form({ onSubmit, children }: FormProps) {
const [schema, setSchema] = useState<any>([]);
export function Form({ onSubmit, children, formId, customerId, hqUrl }: FormProps) {
const [schema, setSchema] = useState<any>({
type: "form",
config: { formId, hqUrl },
children: [],
});
const methods = useForm({ criteriaMode: "all", mode: "onChange" });
const onFormSubmit = (data: any, event: React.BaseSyntheticEvent<object, any, any> | undefined) =>
onSubmit({ data, schema, event });
onSubmit({ submission: { customerId, data }, schema, event });
return (
<SchemaContext.Provider value={{ schema, setSchema }}>
<FormProvider {...methods}>

View File

@@ -3,14 +3,14 @@ import { Text, Textarea } from "..";
import { Form } from "./Form";
interface OnSubmitProps {
data: any;
submission: any;
schema: any;
event?: React.BaseSyntheticEvent<object, any, any> | undefined;
}
interface FormbricksSchemaProps {
schema: any;
onSubmit: ({ data, schema, event }: OnSubmitProps) => void;
onSubmit: ({ submission, schema, event }: OnSubmitProps) => void;
}
export function FormbricksSchema({ schema, onSubmit }: FormbricksSchemaProps) {

View File

@@ -77,7 +77,11 @@ export function Radio(props: FormbricksProps) {
disabled={option?.config?.disabled}
{...register(props.name)}
/>
<Label label={option.label} elemId={`${props.name}-${option.value}`} />
<Label
label={option.label}
elemId={`${props.name}-${option.value}`}
labelClassName={props.labelClassName}
/>
</Inner>
</Wrapper>
</Option>

View File

@@ -22,7 +22,7 @@ export default function ButtonComponent({
elemId,
}: ButtonProps) {
return (
<button className={clsx("formbricks-input", inputClassName)} type={type} id={elemId} onClick={onClick}>
<button className={inputClassName || "formbricks-input"} type={type} id={elemId} onClick={onClick}>
{PrefixIcon && <PrefixIcon className={clsx("formbricks-prefix-icon")} />}
{label}
{SuffixIcon && <SuffixIcon className={clsx("formbricks-suffix-icon")} />}

View File

@@ -7,5 +7,5 @@ interface InnerProps {
}
export function Inner({ innerClassName, children }: InnerProps) {
return <div className={clsx("formbricks-inner", innerClassName)}>{children}</div>;
return <div className={innerClassName || "formbricks-inner"}>{children}</div>;
}

View File

@@ -1,5 +1,3 @@
import clsx from "clsx";
interface LabelProps {
label?: string;
elemId: string;
@@ -10,7 +8,7 @@ export function Label({ label, elemId, labelClassName }: LabelProps) {
return (
<>
{typeof label !== "undefined" && (
<label className={clsx("formbricks-label", labelClassName)} htmlFor={elemId}>
<label className={labelClassName || "formbricks-label"} htmlFor={elemId}>
{label}
</label>
)}

View File

@@ -1,10 +1,8 @@
import clsx from "clsx";
interface LegendProps {
legendClassName?: string;
children: React.ReactNode;
}
export function Legend({ legendClassName, children }: LegendProps) {
return <legend className={clsx("formbricks-legend", legendClassName)}>{children}</legend>;
return <legend className={legendClassName || "formbricks-legend"}>{children}</legend>;
}

View File

@@ -0,0 +1,39 @@
interface sendToHqProps {
submission: { customerId: string; data: any };
schema: any;
event?: React.BaseSyntheticEvent<object, any, any> | undefined;
}
export const sendToHq = async ({ submission, schema }: sendToHqProps) => {
try {
if (!schema.config.formId) {
console.warn(`🧱 FormBricks: formId not set in <Form />. Can't send submission to Formbricks HQ.`);
return;
}
// send answer to snoop platform
await Promise.all([
await fetch(
`${schema.config.hqUrl || "https://hq.formbricks.com"}/api/capture/forms/${
schema.config.formId
}/submissions`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(submission),
}
),
await fetch(
`${schema.config.hqUrl || "https://hq.formbricks.com"}/api/capture/forms/${
schema.config.formId
}/schema`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(schema),
}
),
]);
} catch (e) {
console.error(`🧱 FormBricks: Unable to send submission to Formbricks HQ. Error: ${e}`);
}
};

View File

@@ -13,3 +13,5 @@ export * from "./components/inputs/Submit";
export * from "./components/inputs/Text";
export * from "./components/inputs/Textarea";
export * from "./components/inputs/Url";
// Helpers
export * from "./helpers/sendToHq";

View File

@@ -27,14 +27,14 @@ export const useEffectUpdateSchema = (props: any, type: string) => {
useEffect(() => {
setSchema((schema: any) => {
const newSchema = JSON.parse(JSON.stringify(schema));
let elementIdx = newSchema.findIndex((e: any) => e.name === props.name);
let elementIdx = newSchema.children.findIndex((e: any) => e.name === props.name);
if (elementIdx === -1) {
newSchema.push(props);
elementIdx = newSchema.length - 1;
newSchema.children.push({ ...props, type });
elementIdx = newSchema.children.length - 1; // set elementIdx to newly added elem
}
if ("options" in props) {
newSchema.children[elementIdx].options = getOptionsSchema(props.options);
}
/* if (["checkbox", "radio"].includes(type)) {
newSchema.elements[elementIdx].options = getOptionsSchema(options);
} */
return newSchema;
});
}, [props, setSchema]);

68
pnpm-lock.yaml generated
View File

@@ -22,17 +22,17 @@ importers:
'@formbricks/tsconfig': workspace:*
'@formbricks/ui': workspace:*
'@heroicons/react': ^2.0.13
'@types/node': ^18.11.9
'@types/node': ^18.11.10
'@types/react': ^18.0.25
'@types/react-dom': ^18.0.8
autoprefixer: ^10.4.12
'@types/react-dom': ^18.0.9
autoprefixer: ^10.4.13
eslint-config-formbricks: workspace:*
next: latest
postcss: ^8.4.18
postcss: ^8.4.19
react: ^18.2.0
react-dom: ^18.2.0
tailwindcss: ^3.2.2
typescript: ^4.8.4
tailwindcss: ^3.2.4
typescript: ^4.9.3
dependencies:
'@formbricks/charts': link:../../packages/charts
'@formbricks/react': link:../../packages/react
@@ -44,7 +44,7 @@ importers:
devDependencies:
'@formbricks/tailwind-config': link:../../packages/tailwind-config
'@formbricks/tsconfig': link:../../packages/tsconfig
'@types/node': 18.11.9
'@types/node': 18.11.10
'@types/react': 18.0.25
'@types/react-dom': 18.0.9
autoprefixer: 10.4.13_postcss@8.4.19
@@ -138,6 +138,7 @@ importers:
jsonwebtoken: ^8.5.1
next: ^13.0.5
next-auth: ^4.17.0
nextjs-cors: ^2.1.2
nodemailer: ^6.8.0
postcss: ^8.4.19
prismjs: ^1.29.0
@@ -160,6 +161,7 @@ importers:
jsonwebtoken: 8.5.1
next: 13.0.5_biqbaboplfbrettd7655fr4n2y
next-auth: 4.17.0_2xoejpawkzgot77rbv5mbik6ve
nextjs-cors: 2.1.2_next@13.0.5
nodemailer: 6.8.0
prismjs: 1.29.0
react: 18.2.0
@@ -2404,7 +2406,7 @@ packages:
dependencies:
'@types/istanbul-lib-coverage': 2.0.4
'@types/istanbul-reports': 3.0.1
'@types/node': 18.11.9
'@types/node': 18.11.10
'@types/yargs': 15.0.14
chalk: 4.1.2
dev: true
@@ -4624,7 +4626,7 @@ packages:
/@types/cross-spawn/6.0.2:
resolution: {integrity: sha512-KuwNhp3eza+Rhu8IFI5HUXRP0LIhqH5cAjubUvGXXthh4YYBuP2ntwEX+Cz8GJoZUHlKo247wPWOfA9LYEq4cw==}
dependencies:
'@types/node': 18.11.9
'@types/node': 18.11.10
dev: true
/@types/d3-color/2.0.3:
@@ -4690,20 +4692,20 @@ packages:
resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==}
dependencies:
'@types/minimatch': 5.1.2
'@types/node': 18.11.9
'@types/node': 18.11.10
dev: true
/@types/glob/8.0.0:
resolution: {integrity: sha512-l6NQsDDyQUVeoTynNpC9uRvCUint/gSUXQA2euwmTuWGvPY5LSDUu6tkCtJB2SvGQlJQzLaKqcGZP4//7EDveA==}
dependencies:
'@types/minimatch': 5.1.2
'@types/node': 18.11.9
'@types/node': 18.11.10
dev: true
/@types/graceful-fs/4.1.5:
resolution: {integrity: sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==}
dependencies:
'@types/node': 18.11.9
'@types/node': 18.11.10
dev: true
/@types/hast/2.3.4:
@@ -4751,7 +4753,7 @@ packages:
/@types/keyv/3.1.4:
resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
dependencies:
'@types/node': 18.11.9
'@types/node': 18.11.10
dev: false
/@types/lodash/4.14.189:
@@ -4781,7 +4783,7 @@ packages:
/@types/node-fetch/2.6.2:
resolution: {integrity: sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==}
dependencies:
'@types/node': 18.11.9
'@types/node': 18.11.10
form-data: 3.0.1
dev: true
@@ -4793,12 +4795,16 @@ packages:
resolution: {integrity: sha512-jh6m0QUhIRcZpNv7Z/rpN+ZWXOicUUQbSoWks7Htkbb9IjFQj4kzcX/xFCkjstCj5flMsN8FiSvt+q+Tcs4Llg==}
dev: true
/@types/node/18.11.10:
resolution: {integrity: sha512-juG3RWMBOqcOuXC643OAdSA525V44cVgGV6dUDuiFtss+8Fk5x1hI93Rsld43VeJVIeqlP9I7Fn9/qaVqoEAuQ==}
/@types/node/18.11.6:
resolution: {integrity: sha512-j3CEDa2vd96K0AXF8Wur7UucACvnjkk8hYyQAHhUNciabZLDl9nfAEVUSwmh245OOZV15bRA3Y590Gi5jUcDJg==}
dev: true
/@types/node/18.11.9:
resolution: {integrity: sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==}
dev: true
/@types/normalize-package-data/2.4.1:
resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==}
@@ -4856,7 +4862,7 @@ packages:
/@types/responselike/1.0.0:
resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==}
dependencies:
'@types/node': 18.11.9
'@types/node': 18.11.10
dev: false
/@types/retry/0.12.0:
@@ -4894,7 +4900,7 @@ packages:
/@types/webpack-sources/3.2.0:
resolution: {integrity: sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==}
dependencies:
'@types/node': 18.11.9
'@types/node': 18.11.10
'@types/source-list-map': 0.1.2
source-map: 0.7.4
dev: true
@@ -4902,7 +4908,7 @@ packages:
/@types/webpack/4.41.33:
resolution: {integrity: sha512-PPajH64Ft2vWevkerISMtnZ8rTs4YmRbs+23c402J0INmxDKCrhZNvwZYtzx96gY2wAtXdrK1BS2fiC8MlLr3g==}
dependencies:
'@types/node': 18.11.9
'@types/node': 18.11.10
'@types/tapable': 1.0.8
'@types/uglify-js': 3.17.1
'@types/webpack-sources': 3.2.0
@@ -7060,6 +7066,14 @@ packages:
/core-util-is/1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
/cors/2.8.5:
resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
engines: {node: '>= 0.10'}
dependencies:
object-assign: 4.1.1
vary: 1.1.2
dev: false
/cosmiconfig/6.0.0:
resolution: {integrity: sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==}
engines: {node: '>=8'}
@@ -10728,7 +10742,7 @@ packages:
dependencies:
'@jest/types': 26.6.2
'@types/graceful-fs': 4.1.5
'@types/node': 18.11.9
'@types/node': 18.11.10
anymatch: 3.1.2
fb-watchman: 2.0.2
graceful-fs: 4.2.10
@@ -10754,7 +10768,7 @@ packages:
resolution: {integrity: sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g==}
engines: {node: '>= 10.14.2'}
dependencies:
'@types/node': 18.11.9
'@types/node': 18.11.10
graceful-fs: 4.2.10
dev: true
@@ -10763,7 +10777,7 @@ packages:
engines: {node: '>= 10.14.2'}
dependencies:
'@jest/types': 26.6.2
'@types/node': 18.11.9
'@types/node': 18.11.10
chalk: 4.1.2
graceful-fs: 4.2.10
is-ci: 2.0.0
@@ -10774,7 +10788,7 @@ packages:
resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==}
engines: {node: '>= 10.13.0'}
dependencies:
'@types/node': 18.11.9
'@types/node': 18.11.10
merge-stream: 2.0.0
supports-color: 7.2.0
dev: true
@@ -10783,7 +10797,7 @@ packages:
resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==}
engines: {node: '>= 10.13.0'}
dependencies:
'@types/node': 18.11.9
'@types/node': 18.11.10
merge-stream: 2.0.0
supports-color: 8.1.1
@@ -12506,6 +12520,15 @@ packages:
- babel-plugin-macros
dev: false
/nextjs-cors/2.1.2_next@13.0.5:
resolution: {integrity: sha512-2yOVivaaf2ILe4f/qY32hnj3oC77VCOsUQJQfhVMGsXE/YMEWUY2zy78sH9FKUCM7eG42/l3pDofIzMD781XGA==}
peerDependencies:
next: ^8.1.1-canary.54 || ^9.0.0 || ^10.0.0-0 || ^11.0.0 || ^12.0.0 || ^13.0.0
dependencies:
cors: 2.8.5
next: 13.0.5_biqbaboplfbrettd7655fr4n2y
dev: false
/nice-try/1.0.5:
resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==}
dev: true
@@ -16755,7 +16778,6 @@ packages:
/vary/1.1.2:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
dev: true
/vfile-location/3.2.0:
resolution: {integrity: sha512-aLEIZKv/oxuCDZ8lkJGhuhztf/BW4M+iHdCwglA/eWc+vtuRFJj8EtgceYFX4LRjOhCAAiNHsKGssC6onJ+jbA==}