mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-06 05:40:02 -06:00
Merge branch 'main' of https://github.com/formbricks/formbricks into fix/multi-select-optional
This commit is contained in:
@@ -266,7 +266,7 @@ const FAQ = [
|
||||
const Leaderboard = [
|
||||
{
|
||||
name: "Piyush",
|
||||
points: "450",
|
||||
points: "550",
|
||||
link: "https://github.com/gupta-piyush19",
|
||||
},
|
||||
{
|
||||
@@ -279,11 +279,7 @@ const Leaderboard = [
|
||||
},
|
||||
{
|
||||
name: "Pratik",
|
||||
points: "100",
|
||||
},
|
||||
{
|
||||
name: "/home/babubaap",
|
||||
points: "200",
|
||||
points: "250",
|
||||
},
|
||||
{
|
||||
name: "Karuppiah",
|
||||
@@ -327,7 +323,7 @@ const Leaderboard = [
|
||||
},
|
||||
{
|
||||
name: "Vishrut",
|
||||
points: "100",
|
||||
points: "250",
|
||||
},
|
||||
{
|
||||
name: "cataxcab",
|
||||
@@ -363,9 +359,17 @@ const Leaderboard = [
|
||||
},
|
||||
{
|
||||
name: "Aditya Deshlahre",
|
||||
points: "250",
|
||||
points: "450",
|
||||
link: "https://github.com/adityadeshlahre",
|
||||
},
|
||||
{
|
||||
name: "Rutam",
|
||||
points: "250",
|
||||
},
|
||||
{
|
||||
name: "Sagnik Sahoo",
|
||||
points: "100",
|
||||
},
|
||||
];
|
||||
|
||||
export default function FormTribeHackathon() {
|
||||
|
||||
@@ -9,11 +9,14 @@ import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { testURLmatch } from "./testURLmatch";
|
||||
import { deleteActionClass, updateActionClass } from "@formbricks/lib/services/actionClass";
|
||||
import { TActionClassInput, TActionClassNoCodeConfig } from "@formbricks/types/v1/actionClasses";
|
||||
import { CssSelector } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/(selectors)/CssSelector";
|
||||
import { PageUrlSelector } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/(selectors)/PageUrlSelector";
|
||||
import { InnerHtmlSelector } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/(selectors)/InnerHtmlSelector";
|
||||
import {
|
||||
deleteActionClassAction,
|
||||
updateActionClassAction,
|
||||
} from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/actions";
|
||||
|
||||
interface ActionSettingsTabProps {
|
||||
environmentId: string;
|
||||
@@ -77,10 +80,11 @@ export default function ActionSettingsTab({ environmentId, actionClass, setOpen
|
||||
const filteredNoCodeConfig = filterNoCodeConfig(data.noCodeConfig as NoCodeConfig);
|
||||
const updatedData: TActionClassInput = {
|
||||
...data,
|
||||
environmentId,
|
||||
noCodeConfig: filteredNoCodeConfig,
|
||||
type: "noCode",
|
||||
} as TActionClassInput;
|
||||
await updateActionClass(environmentId, actionClass.id, updatedData);
|
||||
await updateActionClassAction(environmentId, actionClass.id, updatedData);
|
||||
setOpen(false);
|
||||
router.refresh();
|
||||
toast.success("Action updated successfully");
|
||||
@@ -94,7 +98,7 @@ export default function ActionSettingsTab({ environmentId, actionClass, setOpen
|
||||
const handleDeleteAction = async () => {
|
||||
try {
|
||||
setIsDeletingAction(true);
|
||||
await deleteActionClass(environmentId, actionClass.id);
|
||||
await deleteActionClassAction(environmentId, actionClass.id);
|
||||
router.refresh();
|
||||
toast.success("Action deleted successfully");
|
||||
setOpen(false);
|
||||
|
||||
@@ -7,7 +7,6 @@ import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
import { testURLmatch } from "./testURLmatch";
|
||||
import { createActionClass } from "@formbricks/lib/services/actionClass";
|
||||
import {
|
||||
TActionClassInput,
|
||||
TActionClassNoCodeConfig,
|
||||
@@ -16,6 +15,7 @@ import {
|
||||
import { CssSelector } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/(selectors)/CssSelector";
|
||||
import { PageUrlSelector } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/(selectors)/PageUrlSelector";
|
||||
import { InnerHtmlSelector } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/(selectors)/InnerHtmlSelector";
|
||||
import { createActionClassAction } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/actions";
|
||||
|
||||
interface AddNoCodeActionModalProps {
|
||||
environmentId: string;
|
||||
@@ -84,7 +84,7 @@ export default function AddNoCodeActionModal({
|
||||
type: "noCode",
|
||||
} as TActionClassInput;
|
||||
|
||||
const newActionClass: TActionClass = await createActionClass(environmentId, updatedData);
|
||||
const newActionClass: TActionClass = await createActionClassAction(updatedData);
|
||||
if (setActionClassArray) {
|
||||
setActionClassArray((prevActionClassArray: TActionClass[]) => [
|
||||
...prevActionClassArray,
|
||||
|
||||
@@ -1,27 +1,93 @@
|
||||
"use server";
|
||||
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { createActionClass, deleteActionClass, updateActionClass } from "@formbricks/lib/actionClass/service";
|
||||
import { canUserAccessActionClass } from "@formbricks/lib/actionClass/auth";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { TActionClassInput } from "@formbricks/types/v1/actionClasses";
|
||||
|
||||
import {
|
||||
getActionCountInLast24Hours,
|
||||
getActionCountInLast7Days,
|
||||
getActionCountInLastHour,
|
||||
} from "@formbricks/lib/services/actions";
|
||||
import { getSurveysByActionClassId } from "@formbricks/lib/services/survey";
|
||||
import { AuthorizationError } from "@formbricks/types/v1/errors";
|
||||
|
||||
export async function deleteActionClassAction(environmentId, actionClassId: string) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await canUserAccessActionClass(session.user.id, actionClassId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
await deleteActionClass(environmentId, actionClassId);
|
||||
}
|
||||
|
||||
export async function updateActionClassAction(
|
||||
environmentId: string,
|
||||
actionClassId: string,
|
||||
updatedAction: Partial<TActionClassInput>
|
||||
) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await canUserAccessActionClass(session.user.id, actionClassId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await updateActionClass(environmentId, actionClassId, updatedAction);
|
||||
}
|
||||
|
||||
export async function createActionClassAction(action: TActionClassInput) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, action.environmentId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await createActionClass(action.environmentId, action);
|
||||
}
|
||||
|
||||
export const getActionCountInLastHourAction = async (actionClassId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await canUserAccessActionClass(session.user.id, actionClassId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await getActionCountInLastHour(actionClassId);
|
||||
};
|
||||
|
||||
export const getActionCountInLast24HoursAction = async (actionClassId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await canUserAccessActionClass(session.user.id, actionClassId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await getActionCountInLast24Hours(actionClassId);
|
||||
};
|
||||
|
||||
export const getActionCountInLast7DaysAction = async (actionClassId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await canUserAccessActionClass(session.user.id, actionClassId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await getActionCountInLast7Days(actionClassId);
|
||||
};
|
||||
|
||||
export const GetActiveInactiveSurveysAction = async (
|
||||
actionClassId: string
|
||||
): Promise<{ activeSurveys: string[]; inactiveSurveys: string[] }> => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await canUserAccessActionClass(session.user.id, actionClassId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const surveys = await getSurveysByActionClassId(actionClassId);
|
||||
const response = {
|
||||
activeSurveys: surveys.filter((s) => s.status === "inProgress").map((survey) => survey.name),
|
||||
|
||||
@@ -4,7 +4,7 @@ import ActionClassesTable from "@/app/(app)/environments/[environmentId]/(action
|
||||
import ActionClassDataRow from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionRowData";
|
||||
import ActionTableHeading from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionTableHeading";
|
||||
import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import { getActionClasses } from "@formbricks/lib/services/actionClass";
|
||||
import { getActionClasses } from "@formbricks/lib/actionClass/service";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
|
||||
import { capitalizeFirstLetter } from "@/lib/utils";
|
||||
import { getPerson } from "@formbricks/lib/services/person";
|
||||
import { getResponsesByPersonId } from "@formbricks/lib/services/response";
|
||||
import { getResponsesByPersonId } from "@formbricks/lib/response/service";
|
||||
import { getSessionCount } from "@formbricks/lib/services/session";
|
||||
|
||||
export default async function AttributesSection({ personId }: { personId: string }) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import ResponseTimeline from "@/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponseTimeline";
|
||||
import { getResponsesByPersonId } from "@formbricks/lib/services/response";
|
||||
import { getResponsesByPersonId } from "@formbricks/lib/response/service";
|
||||
import { getSurveys } from "@formbricks/lib/services/survey";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { TResponseWithSurvey } from "@formbricks/types/v1/responses";
|
||||
|
||||
@@ -11,22 +11,18 @@ import { AuthorizationError } from "@formbricks/types/v1/errors";
|
||||
export async function deleteApiKeyAction(id: string) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const isAuthorized = await canUserAccessApiKey(session.user.id, id);
|
||||
|
||||
if (isAuthorized) {
|
||||
return await deleteApiKey(id);
|
||||
} else {
|
||||
throw new AuthorizationError("Not authorized");
|
||||
}
|
||||
const isAuthorized = await canUserAccessApiKey(session.user.id, id);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await deleteApiKey(id);
|
||||
}
|
||||
export async function createApiKeyAction(environmentId: string, apiKeyData: TApiKeyCreateInput) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
|
||||
if (isAuthorized) {
|
||||
return await createApiKey(environmentId, apiKeyData);
|
||||
} else {
|
||||
throw new AuthorizationError("Not authorized");
|
||||
}
|
||||
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await createApiKey(environmentId, apiKeyData);
|
||||
}
|
||||
|
||||
@@ -3,14 +3,13 @@
|
||||
import DeleteDialog from "@/components/shared/DeleteDialog";
|
||||
import AvatarPlaceholder from "@/images/avatar-placeholder.png";
|
||||
import { formbricksLogout } from "@/lib/formbricks";
|
||||
import { TProfile } from "@formbricks/types/v1/profile";
|
||||
import { Button, Input, ProfileAvatar } from "@formbricks/ui";
|
||||
import { Session } from "next-auth";
|
||||
import { signOut } from "next-auth/react";
|
||||
import Image from "next/image";
|
||||
import { Dispatch, SetStateAction, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { profileDeleteAction } from "./actions";
|
||||
import { deleteProfileAction } from "./actions";
|
||||
|
||||
export function EditAvatar({ session }) {
|
||||
return (
|
||||
@@ -38,10 +37,9 @@ interface DeleteAccountModalProps {
|
||||
open: boolean;
|
||||
setOpen: Dispatch<SetStateAction<boolean>>;
|
||||
session: Session;
|
||||
profile: TProfile;
|
||||
}
|
||||
|
||||
function DeleteAccountModal({ setOpen, open, session, profile }: DeleteAccountModalProps) {
|
||||
function DeleteAccountModal({ setOpen, open, session }: DeleteAccountModalProps) {
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
|
||||
@@ -52,7 +50,7 @@ function DeleteAccountModal({ setOpen, open, session, profile }: DeleteAccountMo
|
||||
const deleteAccount = async () => {
|
||||
try {
|
||||
setDeleting(true);
|
||||
await profileDeleteAction(profile.id);
|
||||
await deleteProfileAction();
|
||||
await signOut();
|
||||
await formbricksLogout();
|
||||
} catch (error) {
|
||||
@@ -105,7 +103,7 @@ function DeleteAccountModal({ setOpen, open, session, profile }: DeleteAccountMo
|
||||
);
|
||||
}
|
||||
|
||||
export function DeleteAccount({ session, profile }: { session: Session | null; profile: TProfile }) {
|
||||
export function DeleteAccount({ session }: { session: Session | null }) {
|
||||
const [isModalOpen, setModalOpen] = useState(false);
|
||||
|
||||
if (!session) {
|
||||
@@ -114,7 +112,7 @@ export function DeleteAccount({ session, profile }: { session: Session | null; p
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DeleteAccountModal open={isModalOpen} setOpen={setModalOpen} session={session} profile={profile} />
|
||||
<DeleteAccountModal open={isModalOpen} setOpen={setModalOpen} session={session} />
|
||||
<p className="text-sm text-slate-700">
|
||||
Delete your account with all personal data. <strong>This cannot be undone!</strong>
|
||||
</p>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { Button, Input, Label } from "@formbricks/ui";
|
||||
import { useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
import { profileEditAction } from "./actions";
|
||||
import { updateProfileAction } from "./actions";
|
||||
import { TProfile } from "@formbricks/types/v1/profile";
|
||||
|
||||
export function EditName({ profile }: { profile: TProfile }) {
|
||||
@@ -19,7 +19,7 @@ export function EditName({ profile }: { profile: TProfile }) {
|
||||
className="w-full max-w-sm items-center"
|
||||
onSubmit={handleSubmit(async (data) => {
|
||||
try {
|
||||
await profileEditAction(profile.id, data);
|
||||
await updateProfileAction(data);
|
||||
toast.success("Your name was updated successfully.");
|
||||
} catch (error) {
|
||||
toast.error(`Error: ${error.message}`);
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
"use server";
|
||||
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { updateProfile, deleteProfile } from "@formbricks/lib/services/profile";
|
||||
import { TProfileUpdateInput } from "@formbricks/types/v1/profile";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { AuthorizationError } from "@formbricks/types/v1/errors";
|
||||
|
||||
export async function profileEditAction(userId: string, data: Partial<TProfileUpdateInput>) {
|
||||
return await updateProfile(userId, data);
|
||||
export async function updateProfileAction(data: Partial<TProfileUpdateInput>) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await updateProfile(session.user.id, data);
|
||||
}
|
||||
|
||||
export async function profileDeleteAction(userId: string) {
|
||||
return await deleteProfile(userId);
|
||||
export async function deleteProfileAction() {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await deleteProfile(session.user.id);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export default async function ProfileSettingsPage() {
|
||||
<SettingsCard
|
||||
title="Delete account"
|
||||
description="Delete your account with all of your personal information and data.">
|
||||
<DeleteAccount session={session} profile={profile} />
|
||||
<DeleteAccount session={session} />
|
||||
</SettingsCard>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,15 +1,38 @@
|
||||
"use server";
|
||||
|
||||
import { deleteTag, mergeTags, updateTagName } from "@formbricks/lib/services/tag";
|
||||
import { deleteTag, mergeTags, updateTagName } from "@formbricks/lib/tag/service";
|
||||
import { canUserAccessTag } from "@formbricks/lib/tag/auth";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { AuthorizationError } from "@formbricks/types/v1/errors";
|
||||
|
||||
export const deleteTagAction = async (tagId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await canUserAccessTag(session.user.id, tagId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await deleteTag(tagId);
|
||||
};
|
||||
|
||||
export const updateTagNameAction = async (tagId: string, name: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await canUserAccessTag(session.user.id, tagId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await updateTagName(tagId, name);
|
||||
};
|
||||
|
||||
export const mergeTagsAction = async (originalTagId: string, newTagId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorizedForOld = await canUserAccessTag(session.user.id, originalTagId);
|
||||
const isAuthorizedForNew = await canUserAccessTag(session.user.id, newTagId);
|
||||
if (!isAuthorizedForOld || !isAuthorizedForNew) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await mergeTags(originalTagId, newTagId);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import EditTagsWrapper from "./EditTagsWrapper";
|
||||
import SettingsTitle from "../SettingsTitle";
|
||||
import { getEnvironment } from "@formbricks/lib/services/environment";
|
||||
import { getTagsByEnvironmentId } from "@formbricks/lib/services/tag";
|
||||
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
|
||||
import { getTagsOnResponsesCount } from "@formbricks/lib/services/tagOnResponse";
|
||||
|
||||
export default async function MembersSettingsPage({ params }) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { RESPONSES_LIMIT_FREE } from "@formbricks/lib/constants";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
import { getSurveyResponses } from "@formbricks/lib/services/response";
|
||||
import { getSurveyResponses } from "@formbricks/lib/response/service";
|
||||
import { getSurveyWithAnalytics } from "@formbricks/lib/services/survey";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/services/team";
|
||||
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
"use server";
|
||||
|
||||
import { deleteResponse } from "@formbricks/lib/services/response";
|
||||
import { deleteResponse } from "@formbricks/lib/response/service";
|
||||
import { updateResponseNote, resolveResponseNote } from "@formbricks/lib/services/responseNote";
|
||||
import { createTag } from "@formbricks/lib/services/tag";
|
||||
import { addTagToRespone, deleteTagFromResponse } from "@formbricks/lib/services/tagOnResponse";
|
||||
import { createTag } from "@formbricks/lib/tag/service";
|
||||
import { addTagToRespone, deleteTagOnResponse } from "@formbricks/lib/services/tagOnResponse";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { AuthorizationError } from "@formbricks/types/v1/errors";
|
||||
import { canUserAccessResponse } from "@formbricks/lib/response/auth";
|
||||
import { canUserAccessTagOnResponse } from "@formbricks/lib/tagOnResponse/auth";
|
||||
|
||||
export const updateResponseNoteAction = async (responseNoteId: string, text: string) => {
|
||||
await updateResponseNote(responseNoteId, text);
|
||||
@@ -14,17 +20,41 @@ export const resolveResponseNoteAction = async (responseNoteId: string) => {
|
||||
};
|
||||
|
||||
export const deleteResponseAction = async (responseId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await canUserAccessResponse(session.user.id, responseId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await deleteResponse(responseId);
|
||||
};
|
||||
|
||||
export const createTagAction = async (environmentId: string, tagName: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await createTag(environmentId, tagName);
|
||||
};
|
||||
|
||||
export const addTagToResponeAction = async (responseId: string, tagId: string) => {
|
||||
export const createTagToResponeAction = async (responseId: string, tagId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await canUserAccessTagOnResponse(session.user.id, tagId, responseId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await addTagToRespone(responseId, tagId);
|
||||
};
|
||||
|
||||
export const removeTagFromResponseAction = async (responseId: string, tagId: string) => {
|
||||
return await deleteTagFromResponse(responseId, tagId);
|
||||
export const deleteTagOnResponseAction = async (responseId: string, tagId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await canUserAccessTagOnResponse(session.user.id, tagId, responseId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await deleteTagOnResponse(responseId, tagId);
|
||||
};
|
||||
|
||||
@@ -9,9 +9,9 @@ import { useRouter } from "next/navigation";
|
||||
import { Button } from "@formbricks/ui";
|
||||
import { TTag } from "@formbricks/types/v1/tags";
|
||||
import {
|
||||
addTagToResponeAction,
|
||||
createTagToResponeAction,
|
||||
createTagAction,
|
||||
removeTagFromResponseAction,
|
||||
deleteTagOnResponseAction,
|
||||
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/actions";
|
||||
|
||||
interface ResponseTagsWrapperProps {
|
||||
@@ -38,7 +38,7 @@ const ResponseTagsWrapper: React.FC<ResponseTagsWrapperProps> = ({
|
||||
|
||||
const onDelete = async (tagId: string) => {
|
||||
try {
|
||||
await removeTagFromResponseAction(responseId, tagId);
|
||||
await deleteTagOnResponseAction(responseId, tagId);
|
||||
|
||||
router.refresh();
|
||||
} catch (e) {
|
||||
@@ -89,7 +89,7 @@ const ResponseTagsWrapper: React.FC<ResponseTagsWrapperProps> = ({
|
||||
tagName: tag.name,
|
||||
},
|
||||
]);
|
||||
addTagToResponeAction(responseId, tag.id).then(() => {
|
||||
createTagToResponeAction(responseId, tag.id).then(() => {
|
||||
setSearchValue("");
|
||||
setOpen(false);
|
||||
router.refresh();
|
||||
@@ -121,7 +121,7 @@ const ResponseTagsWrapper: React.FC<ResponseTagsWrapperProps> = ({
|
||||
},
|
||||
]);
|
||||
|
||||
addTagToResponeAction(responseId, tagId).then(() => {
|
||||
createTagToResponeAction(responseId, tagId).then(() => {
|
||||
setSearchValue("");
|
||||
setOpen(false);
|
||||
router.refresh();
|
||||
|
||||
@@ -8,7 +8,7 @@ import { REVALIDATION_INTERVAL, SURVEY_BASE_URL } from "@formbricks/lib/constant
|
||||
import ResponsesLimitReachedBanner from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/ResponsesLimitReachedBanner";
|
||||
import { getEnvironment } from "@formbricks/lib/services/environment";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/services/product";
|
||||
import { getTagsByEnvironmentId } from "@formbricks/lib/services/tag";
|
||||
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
|
||||
|
||||
export default async function Page({ params }) {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { TSurveyCTAQuestion } from "@formbricks/types/v1/surveys";
|
||||
import { ProgressBar } from "@formbricks/ui";
|
||||
import { InboxStackIcon } from "@heroicons/react/24/solid";
|
||||
import { useMemo } from "react";
|
||||
import { questionTypes } from "@/lib/questions";
|
||||
|
||||
interface CTASummaryProps {
|
||||
questionSummary: QuestionSummary<TSurveyCTAQuestion>;
|
||||
@@ -15,6 +16,8 @@ interface ChoiceResult {
|
||||
}
|
||||
|
||||
export default function CTASummary({ questionSummary }: CTASummaryProps) {
|
||||
const questionTypeInfo = questionTypes.find((type) => type.id === questionSummary.question.type);
|
||||
|
||||
const ctr: ChoiceResult = useMemo(() => {
|
||||
const clickedAbs = questionSummary.responses.filter((response) => response.value === "clicked").length;
|
||||
const count = questionSummary.responses.length;
|
||||
@@ -31,7 +34,10 @@ export default function CTASummary({ questionSummary }: CTASummaryProps) {
|
||||
<Headline headline={questionSummary.question.headline} required={questionSummary.question.required} />
|
||||
|
||||
<div className="flex space-x-2 text-xs font-semibold text-slate-600 md:text-sm">
|
||||
<div className="rounded-lg bg-slate-100 p-2 ">Call-to-Action</div>
|
||||
<div className=" flex items-center rounded-lg bg-slate-100 p-2 ">
|
||||
{questionTypeInfo && <questionTypeInfo.icon className="mr-2 h-4 w-4 " />}
|
||||
{questionTypeInfo ? questionTypeInfo.label : "Unknown Question Type"}
|
||||
</div>
|
||||
<div className=" flex items-center rounded-lg bg-slate-100 p-2">
|
||||
<InboxStackIcon className="mr-2 h-4 w-4 " />
|
||||
{ctr.count} responses
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ProgressBar } from "@formbricks/ui";
|
||||
import { InboxStackIcon } from "@heroicons/react/24/solid";
|
||||
import { useMemo } from "react";
|
||||
import { TSurveyConsentQuestion } from "@formbricks/types/v1/surveys";
|
||||
import { questionTypes } from "@/lib/questions";
|
||||
import Headline from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/Headline";
|
||||
|
||||
interface ConsentSummaryProps {
|
||||
@@ -18,6 +19,8 @@ interface ChoiceResult {
|
||||
}
|
||||
|
||||
export default function ConsentSummary({ questionSummary }: ConsentSummaryProps) {
|
||||
const questionTypeInfo = questionTypes.find((type) => type.id === questionSummary.question.type);
|
||||
|
||||
const ctr: ChoiceResult = useMemo(() => {
|
||||
const total = questionSummary.responses.length;
|
||||
const clickedAbs = questionSummary.responses.filter((response) => response.value !== "dismissed").length;
|
||||
@@ -38,7 +41,10 @@ export default function ConsentSummary({ questionSummary }: ConsentSummaryProps)
|
||||
<div className="space-y-2 px-4 pb-5 pt-6 md:px-6">
|
||||
<Headline headline={questionSummary.question.headline} required={questionSummary.question.required} />
|
||||
<div className="flex space-x-2 text-xs font-semibold text-slate-600 md:text-sm">
|
||||
<div className="rounded-lg bg-slate-100 p-2">Consent</div>
|
||||
<div className=" flex items-center rounded-lg bg-slate-100 p-2">
|
||||
{questionTypeInfo && <questionTypeInfo.icon className="mr-2 h-4 w-4 " />}
|
||||
{questionTypeInfo ? questionTypeInfo.label : "Unknown Question Type"}
|
||||
</div>
|
||||
<div className=" flex items-center rounded-lg bg-slate-100 p-2">
|
||||
<InboxStackIcon className="mr-2 h-4 w-4 " />
|
||||
{ctr.count} responses
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
TSurveyMultipleChoiceMultiQuestion,
|
||||
TSurveyMultipleChoiceSingleQuestion,
|
||||
} from "@formbricks/types/v1/surveys";
|
||||
import { questionTypes } from "@/lib/questions";
|
||||
import Headline from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/Headline";
|
||||
|
||||
interface MultipleChoiceSummaryProps {
|
||||
@@ -39,6 +40,8 @@ export default function MultipleChoiceSummary({
|
||||
}: MultipleChoiceSummaryProps) {
|
||||
const isSingleChoice = questionSummary.question.type === QuestionType.MultipleChoiceSingle;
|
||||
|
||||
const questionTypeInfo = questionTypes.find((type) => type.id === questionSummary.question.type);
|
||||
|
||||
const results: ChoiceResult[] = useMemo(() => {
|
||||
if (!("choices" in questionSummary.question)) return [];
|
||||
|
||||
@@ -129,10 +132,9 @@ export default function MultipleChoiceSummary({
|
||||
<Headline headline={questionSummary.question.headline} required={questionSummary.question.required} />
|
||||
|
||||
<div className="flex space-x-2 text-xs font-semibold text-slate-600 md:text-sm">
|
||||
<div className="rounded-lg bg-slate-100 p-2">
|
||||
{isSingleChoice
|
||||
? "Multiple-Choice Single Select Question"
|
||||
: "Multiple-Choice Multi Select Question"}
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">
|
||||
{questionTypeInfo && <questionTypeInfo.icon className="mr-2 h-4 w-4 " />}
|
||||
Multiple-Choice {questionTypeInfo ? questionTypeInfo.label : "Unknown Question Type"} Question
|
||||
</div>
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">
|
||||
<InboxStackIcon className="mr-2 h-4 w-4 " />
|
||||
|
||||
@@ -4,6 +4,7 @@ import { TSurveyNPSQuestion } from "@formbricks/types/v1/surveys";
|
||||
import { HalfCircle, ProgressBar } from "@formbricks/ui";
|
||||
import { InboxStackIcon } from "@heroicons/react/24/solid";
|
||||
import { useMemo } from "react";
|
||||
import { questionTypes } from "@/lib/questions";
|
||||
|
||||
interface NPSSummaryProps {
|
||||
questionSummary: QuestionSummary<TSurveyNPSQuestion>;
|
||||
@@ -29,6 +30,8 @@ export default function NPSSummary({ questionSummary }: NPSSummaryProps) {
|
||||
return result || 0;
|
||||
};
|
||||
|
||||
const questionTypeInfo = questionTypes.find((type) => type.id === questionSummary.question.type);
|
||||
|
||||
const result: Result = useMemo(() => {
|
||||
let data = {
|
||||
promoters: 0,
|
||||
@@ -79,7 +82,10 @@ export default function NPSSummary({ questionSummary }: NPSSummaryProps) {
|
||||
<Headline headline={questionSummary.question.headline} required={questionSummary.question.required} />
|
||||
|
||||
<div className="flex space-x-2 text-xs font-semibold text-slate-600 md:text-sm">
|
||||
<div className="rounded-lg bg-slate-100 p-2">Net Promoter Score (NPS)</div>
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">
|
||||
{questionTypeInfo && <questionTypeInfo.icon className="mr-2 h-4 w-4 " />}
|
||||
{questionTypeInfo ? questionTypeInfo.label : "Unknown Question Type"}
|
||||
</div>
|
||||
<div className=" flex items-center rounded-lg bg-slate-100 p-2">
|
||||
<InboxStackIcon className="mr-2 h-4 w-4 " />
|
||||
{result.total} responses
|
||||
|
||||
@@ -6,6 +6,7 @@ import { TSurveyOpenTextQuestion } from "@formbricks/types/v1/surveys";
|
||||
import { PersonAvatar } from "@formbricks/ui";
|
||||
import { InboxStackIcon } from "@heroicons/react/24/solid";
|
||||
import Link from "next/link";
|
||||
import { questionTypes } from "@/lib/questions";
|
||||
|
||||
interface OpenTextSummaryProps {
|
||||
questionSummary: QuestionSummary<TSurveyOpenTextQuestion>;
|
||||
@@ -17,13 +18,18 @@ function findEmail(person) {
|
||||
}
|
||||
|
||||
export default function OpenTextSummary({ questionSummary, environmentId }: OpenTextSummaryProps) {
|
||||
const questionTypeInfo = questionTypes.find((type) => type.id === questionSummary.question.type);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-slate-200 bg-slate-50 shadow-sm">
|
||||
<div className="space-y-2 px-4 pb-5 pt-6 md:px-6">
|
||||
<Headline headline={questionSummary.question.headline} required={questionSummary.question.required} />
|
||||
|
||||
<div className="flex space-x-2 text-xs font-semibold text-slate-600 md:text-sm">
|
||||
<div className="rounded-lg bg-slate-100 p-2 ">Open Text Question</div>
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2 ">
|
||||
{questionTypeInfo && <questionTypeInfo.icon className="mr-2 h-4 w-4 " />}
|
||||
{questionTypeInfo ? questionTypeInfo.label : "Unknown Question Type"} Question
|
||||
</div>
|
||||
<div className=" flex items-center rounded-lg bg-slate-100 p-2">
|
||||
<InboxStackIcon className="mr-2 h-4 w-4" />
|
||||
{questionSummary.responses.length} Responses
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useMemo } from "react";
|
||||
import { QuestionType } from "@formbricks/types/questions";
|
||||
import { TSurveyRatingQuestion } from "@formbricks/types/v1/surveys";
|
||||
import { RatingResponse } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/RatingResponse";
|
||||
import { questionTypes } from "@/lib/questions";
|
||||
import Headline from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/Headline";
|
||||
|
||||
interface RatingSummaryProps {
|
||||
@@ -18,6 +19,8 @@ interface ChoiceResult {
|
||||
}
|
||||
|
||||
export default function RatingSummary({ questionSummary }: RatingSummaryProps) {
|
||||
const questionTypeInfo = questionTypes.find((type) => type.id === questionSummary.question.type);
|
||||
|
||||
const results: ChoiceResult[] = useMemo(() => {
|
||||
if (questionSummary.question.type !== QuestionType.Rating) return [];
|
||||
// build a dictionary of choices
|
||||
@@ -81,7 +84,10 @@ export default function RatingSummary({ questionSummary }: RatingSummaryProps) {
|
||||
<Headline headline={questionSummary.question.headline} required={questionSummary.question.required} />
|
||||
|
||||
<div className="flex space-x-2 text-xs font-semibold text-slate-600 md:text-sm">
|
||||
<div className="rounded-lg bg-slate-100 p-2">Rating Question</div>
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">
|
||||
{questionTypeInfo && <questionTypeInfo.icon className="mr-2 h-4 w-4 " />}
|
||||
{questionTypeInfo ? questionTypeInfo.label : "Unknown Question Type"} Question
|
||||
</div>
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">
|
||||
<InboxStackIcon className="mr-2 h-4 w-4 " />
|
||||
{totalResponses} responses
|
||||
|
||||
@@ -7,7 +7,7 @@ import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { REVALIDATION_INTERVAL, SURVEY_BASE_URL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/services/environment";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/services/product";
|
||||
import { getTagsByEnvironmentId } from "@formbricks/lib/services/tag";
|
||||
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
export default async function Page({ params }) {
|
||||
|
||||
76
apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/CustomFilter.tsx
Normal file → Executable file
76
apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/CustomFilter.tsx
Normal file → Executable file
@@ -17,7 +17,7 @@ import {
|
||||
} from "@/lib/surveys/surveys";
|
||||
import toast from "react-hot-toast";
|
||||
import { getTodaysDateFormatted } from "@formbricks/lib/time";
|
||||
import { convertToCSV } from "@/lib/csvConversion";
|
||||
import { fetchFile } from "@/lib/fetchFile";
|
||||
import useClickOutside from "@formbricks/lib/useClickOutside";
|
||||
import { TResponse } from "@formbricks/types/v1/responses";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
@@ -131,7 +131,7 @@ const CustomFilter = ({ environmentTags, responses, survey, totalResponses }: Cu
|
||||
return [];
|
||||
};
|
||||
|
||||
const csvFileName = useMemo(() => {
|
||||
const downloadFileName = useMemo(() => {
|
||||
if (survey) {
|
||||
const formattedDateString = getTodaysDateFormatted("_");
|
||||
return `${survey.name.split(" ").join("_")}_responses_${formattedDateString}`.toLocaleLowerCase();
|
||||
@@ -141,12 +141,12 @@ const CustomFilter = ({ environmentTags, responses, survey, totalResponses }: Cu
|
||||
}, [survey]);
|
||||
|
||||
const downloadResponses = useCallback(
|
||||
async (filter: FilterDownload) => {
|
||||
async (filter: FilterDownload, filetype: "csv" | "xlsx") => {
|
||||
const downloadResponse = filter === FilterDownload.ALL ? totalResponses : responses;
|
||||
const { attributeMap, questionNames } = generateQuestionsAndAttributes(survey, downloadResponse);
|
||||
const matchQandA = getMatchQandA(downloadResponse, survey);
|
||||
const csvData = matchQandA.map((response) => {
|
||||
const csvResponse = {
|
||||
const jsonData = matchQandA.map((response) => {
|
||||
const fileResponse = {
|
||||
"Response ID": response.id,
|
||||
Timestamp: response.createdAt,
|
||||
Finished: response.finished,
|
||||
@@ -166,26 +166,25 @@ const CustomFilter = ({ environmentTags, responses, survey, totalResponses }: Cu
|
||||
transformedAnswer = answer;
|
||||
}
|
||||
}
|
||||
csvResponse[questionName] = matchingQuestion ? transformedAnswer : "";
|
||||
fileResponse[questionName] = matchingQuestion ? transformedAnswer : "";
|
||||
});
|
||||
|
||||
return csvResponse;
|
||||
return fileResponse;
|
||||
});
|
||||
|
||||
// Add attribute columns to the CSV
|
||||
|
||||
// Add attribute columns to the file
|
||||
Object.keys(attributeMap).forEach((attributeName) => {
|
||||
const attributeValues = attributeMap[attributeName];
|
||||
Object.keys(attributeValues).forEach((personId) => {
|
||||
const value = attributeValues[personId];
|
||||
const matchingResponse = csvData.find((response) => response["Formbricks User ID"] === personId);
|
||||
const matchingResponse = jsonData.find((response) => response["Formbricks User ID"] === personId);
|
||||
if (matchingResponse) {
|
||||
matchingResponse[attributeName] = value;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Fields which will be used as column headers in the CSV
|
||||
// Fields which will be used as column headers in the file
|
||||
const fields = [
|
||||
"Response ID",
|
||||
"Timestamp",
|
||||
@@ -199,23 +198,40 @@ const CustomFilter = ({ environmentTags, responses, survey, totalResponses }: Cu
|
||||
let response;
|
||||
|
||||
try {
|
||||
response = await convertToCSV({
|
||||
json: csvData,
|
||||
fields,
|
||||
fileName: csvFileName,
|
||||
});
|
||||
response = await fetchFile(
|
||||
{
|
||||
json: jsonData,
|
||||
fields,
|
||||
fileName: downloadFileName,
|
||||
},
|
||||
filetype
|
||||
);
|
||||
} catch (err) {
|
||||
toast.error("Error downloading CSV");
|
||||
toast.error(`Error downloading ${filetype === "csv" ? "CSV" : "Excel"}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = new Blob([response.csvResponse], { type: "text/csv;charset=utf-8;" });
|
||||
let blob: Blob;
|
||||
if (filetype === "csv") {
|
||||
blob = new Blob([response.fileResponse], { type: "text/csv;charset=utf-8;" });
|
||||
} else if (filetype === "xlsx") {
|
||||
const binaryString = atob(response["fileResponse"]);
|
||||
const byteArray = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
byteArray[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
blob = new Blob([byteArray], {
|
||||
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
});
|
||||
} else {
|
||||
throw new Error(`Unsupported filetype: ${filetype}`);
|
||||
}
|
||||
|
||||
const downloadUrl = URL.createObjectURL(blob);
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = downloadUrl;
|
||||
|
||||
link.download = `${csvFileName}.csv`;
|
||||
link.download = `${downloadFileName}.${filetype}`;
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
@@ -224,7 +240,7 @@ const CustomFilter = ({ environmentTags, responses, survey, totalResponses }: Cu
|
||||
|
||||
URL.revokeObjectURL(downloadUrl);
|
||||
},
|
||||
[csvFileName, responses, totalResponses, survey]
|
||||
[downloadFileName, responses, totalResponses, survey]
|
||||
);
|
||||
|
||||
const handleDateHoveredChange = (date: Date) => {
|
||||
@@ -375,17 +391,31 @@ const CustomFilter = ({ environmentTags, responses, survey, totalResponses }: Cu
|
||||
<DropdownMenuItem
|
||||
className="hover:ring-0"
|
||||
onClick={() => {
|
||||
downloadResponses(FilterDownload.ALL);
|
||||
downloadResponses(FilterDownload.ALL, "csv");
|
||||
}}>
|
||||
<p className="text-slate-700">All responses (CSV)</p>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="hover:ring-0"
|
||||
onClick={() => {
|
||||
downloadResponses(FilterDownload.FILTER);
|
||||
downloadResponses(FilterDownload.ALL, "xlsx");
|
||||
}}>
|
||||
<p className="text-slate-700">All responses (Excel)</p>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="hover:ring-0"
|
||||
onClick={() => {
|
||||
downloadResponses(FilterDownload.FILTER, "csv");
|
||||
}}>
|
||||
<p className="text-slate-700">Current selection (CSV)</p>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="hover:ring-0"
|
||||
onClick={() => {
|
||||
downloadResponses(FilterDownload.FILTER, "xlsx");
|
||||
}}>
|
||||
<p className="text-slate-700">Current selection (Excel)</p>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ import SurveyEditor from "./SurveyEditor";
|
||||
import { getSurveyWithAnalytics } from "@formbricks/lib/services/survey";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/services/product";
|
||||
import { getEnvironment } from "@formbricks/lib/services/environment";
|
||||
import { getActionClasses } from "@formbricks/lib/services/actionClass";
|
||||
import { getActionClasses } from "@formbricks/lib/actionClass/service";
|
||||
import { getAttributeClasses } from "@formbricks/lib/services/attributeClass";
|
||||
import { ErrorComponent } from "@formbricks/ui";
|
||||
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
"use server";
|
||||
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { updateProduct } from "@formbricks/lib/services/product";
|
||||
import { updateProfile } from "@formbricks/lib/services/profile";
|
||||
import { TProductUpdateInput } from "@formbricks/types/v1/product";
|
||||
import { TProfileUpdateInput } from "@formbricks/types/v1/profile";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { AuthorizationError } from "@formbricks/types/v1/errors";
|
||||
|
||||
export async function updateProfileAction(personId: string, updatedProfile: Partial<TProfileUpdateInput>) {
|
||||
return await updateProfile(personId, updatedProfile);
|
||||
export async function updateProfileAction(updatedProfile: Partial<TProfileUpdateInput>) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await updateProfile(session.user.id, updatedProfile);
|
||||
}
|
||||
|
||||
export async function updateProductAction(productId: string, updatedProduct: Partial<TProductUpdateInput>) {
|
||||
|
||||
@@ -42,7 +42,7 @@ const Objective: React.FC<ObjectiveProps> = ({ next, skip, formbricksResponseId,
|
||||
try {
|
||||
setIsProfileUpdating(true);
|
||||
const updatedProfile = { ...profile, objective: selectedObjective.id };
|
||||
await updateProfileAction(profile.id, updatedProfile);
|
||||
await updateProfileAction(updatedProfile);
|
||||
setIsProfileUpdating(false);
|
||||
} catch (e) {
|
||||
setIsProfileUpdating(false);
|
||||
|
||||
@@ -54,7 +54,7 @@ export default function Onboarding({ session, environmentId, profile, product }:
|
||||
|
||||
try {
|
||||
const updatedProfile = { ...profile, onboardingCompleted: true };
|
||||
await updateProfileAction(profile.id, updatedProfile);
|
||||
await updateProfileAction(updatedProfile);
|
||||
|
||||
if (environmentId) {
|
||||
router.push(`/environments/${environmentId}/surveys`);
|
||||
|
||||
@@ -40,7 +40,7 @@ const Role: React.FC<RoleProps> = ({ next, skip, setFormbricksResponseId, profil
|
||||
try {
|
||||
setIsUpdating(true);
|
||||
const updatedProfile = { ...profile, role: selectedRole.id };
|
||||
await updateProfileAction(profile.id, updatedProfile);
|
||||
await updateProfileAction(updatedProfile);
|
||||
setIsUpdating(false);
|
||||
} catch (e) {
|
||||
setIsUpdating(false);
|
||||
|
||||
11
apps/web/app/api/csv-conversion/route.ts → apps/web/app/api/internal/csv-conversion/route.ts
Normal file → Executable file
11
apps/web/app/api/csv-conversion/route.ts → apps/web/app/api/internal/csv-conversion/route.ts
Normal file → Executable file
@@ -1,7 +1,16 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { AsyncParser } from "@json2csv/node";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { responses } from "@/lib/api/response";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session) {
|
||||
return responses.unauthorizedResponse();
|
||||
}
|
||||
|
||||
const data = await request.json();
|
||||
let csv: string = "";
|
||||
|
||||
@@ -24,7 +33,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
csvResponse: csv,
|
||||
fileResponse: csv,
|
||||
},
|
||||
{
|
||||
headers,
|
||||
39
apps/web/app/api/internal/excel-conversion/route.ts
Executable file
39
apps/web/app/api/internal/excel-conversion/route.ts
Executable file
@@ -0,0 +1,39 @@
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { responses } from "@/lib/api/response";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import * as xlsx from "xlsx";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session) {
|
||||
return responses.unauthorizedResponse();
|
||||
}
|
||||
|
||||
const data = await request.json();
|
||||
|
||||
const { json, fields, fileName } = data;
|
||||
|
||||
const wb = xlsx.utils.book_new();
|
||||
const ws = xlsx.utils.json_to_sheet(json, { header: fields });
|
||||
xlsx.utils.book_append_sheet(wb, ws, "Sheet1");
|
||||
|
||||
const buffer = xlsx.write(wb, { type: "buffer", bookType: "xlsx" });
|
||||
|
||||
const binaryString = String.fromCharCode.apply(null, buffer);
|
||||
const base64String = btoa(binaryString);
|
||||
|
||||
const headers = new Headers();
|
||||
headers.set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
headers.set("Content-Disposition", `attachment; filename=${fileName ?? "converted"}.xlsx`);
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
fileResponse: base64String,
|
||||
},
|
||||
{
|
||||
headers,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { responses } from "@/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/lib/api/validator";
|
||||
import { sendToPipeline } from "@/lib/pipelines";
|
||||
import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/v1/errors";
|
||||
import { updateResponse } from "@formbricks/lib/services/response";
|
||||
import { updateResponse } from "@formbricks/lib/response/service";
|
||||
import { getSurvey } from "@formbricks/lib/services/survey";
|
||||
import { ZResponseUpdateInput } from "@formbricks/types/v1/responses";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
@@ -3,7 +3,7 @@ import { transformErrorToDetails } from "@/lib/api/validator";
|
||||
import { sendToPipeline } from "@/lib/pipelines";
|
||||
import { InvalidInputError } from "@formbricks/types/v1/errors";
|
||||
import { capturePosthogEvent } from "@formbricks/lib/posthogServer";
|
||||
import { createResponse } from "@formbricks/lib/services/response";
|
||||
import { createResponse } from "@formbricks/lib/response/service";
|
||||
import { getSurvey } from "@formbricks/lib/services/survey";
|
||||
import { getTeamDetails } from "@formbricks/lib/services/teamDetails";
|
||||
import { TResponse, TResponseInput, ZResponseInput } from "@formbricks/types/v1/responses";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getSurveysCached } from "@/app/api/v1/js/surveys";
|
||||
import { MAU_LIMIT } from "@formbricks/lib/constants";
|
||||
import { getActionClassesCached } from "@formbricks/lib/services/actionClass";
|
||||
import { getActionClasses } from "@formbricks/lib/actionClass/service";
|
||||
import { getEnvironment } from "@formbricks/lib/services/environment";
|
||||
import { createPerson, getMonthlyActivePeopleCount, getPersonCached } from "@formbricks/lib/services/person";
|
||||
import { getProductByEnvironmentIdCached } from "@formbricks/lib/services/product";
|
||||
@@ -101,7 +101,7 @@ export const getUpdatedState = async (
|
||||
// get/create rest of the state
|
||||
const [surveys, noCodeActionClasses, product] = await Promise.all([
|
||||
getSurveysCached(environmentId, person),
|
||||
getActionClassesCached(environmentId),
|
||||
getActionClasses(environmentId),
|
||||
getProductByEnvironmentIdCached(environmentId),
|
||||
]);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { responses } from "@/lib/api/response";
|
||||
import { NextResponse } from "next/server";
|
||||
import { deleteActionClass, getActionClass, updateActionClass } from "@formbricks/lib/services/actionClass";
|
||||
import { deleteActionClass, getActionClass, updateActionClass } from "@formbricks/lib/actionClass/service";
|
||||
import { TActionClass, ZActionClassInput } from "@formbricks/types/v1/actionClasses";
|
||||
import { authenticateRequest } from "@/app/api/v1/auth";
|
||||
import { transformErrorToDetails } from "@/lib/api/validator";
|
||||
|
||||
@@ -3,7 +3,7 @@ import { DatabaseError } from "@formbricks/types/v1/errors";
|
||||
import { authenticateRequest } from "@/app/api/v1/auth";
|
||||
import { NextResponse } from "next/server";
|
||||
import { TActionClass, ZActionClassInput } from "@formbricks/types/v1/actionClasses";
|
||||
import { createActionClass, getActionClasses } from "@formbricks/lib/services/actionClass";
|
||||
import { createActionClass, getActionClasses } from "@formbricks/lib/actionClass/service";
|
||||
import { transformErrorToDetails } from "@/lib/api/validator";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { responses } from "@/lib/api/response";
|
||||
import { NextResponse } from "next/server";
|
||||
import { transformErrorToDetails } from "@/lib/api/validator";
|
||||
import { deleteResponse, getResponse, updateResponse } from "@formbricks/lib/services/response";
|
||||
import { deleteResponse, getResponse, updateResponse } from "@formbricks/lib/response/service";
|
||||
import { TResponse, ZResponseUpdateInput } from "@formbricks/types/v1/responses";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { getSurvey } from "@formbricks/lib/services/survey";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { responses } from "@/lib/api/response";
|
||||
import { getEnvironmentResponses } from "@formbricks/lib/services/response";
|
||||
import { getEnvironmentResponses } from "@formbricks/lib/response/service";
|
||||
import { authenticateRequest } from "@/app/api/v1/auth";
|
||||
import { DatabaseError } from "@formbricks/types/v1/errors";
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
export const convertToCSV = async (data: { json: any; fields?: string[]; fileName?: string }) => {
|
||||
const response = await fetch("/api/csv-conversion", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("Failed to convert to CSV");
|
||||
|
||||
return response.json();
|
||||
};
|
||||
18
apps/web/lib/fetchFile.ts
Executable file
18
apps/web/lib/fetchFile.ts
Executable file
@@ -0,0 +1,18 @@
|
||||
export const fetchFile = async (
|
||||
data: { json: any; fields?: string[]; fileName?: string },
|
||||
filetype: string
|
||||
) => {
|
||||
const endpoint = filetype === "csv" ? "csv-conversion" : "excel-conversion";
|
||||
|
||||
const response = await fetch(`/api/internal/${endpoint}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("Failed to convert to file");
|
||||
|
||||
return response.json();
|
||||
};
|
||||
@@ -46,7 +46,8 @@
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-icons": "^4.11.0",
|
||||
"swr": "^2.2.4",
|
||||
"ua-parser-js": "^1.0.36"
|
||||
"ua-parser-js": "^1.0.36",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formbricks/tsconfig": "workspace:*",
|
||||
|
||||
24
packages/lib/actionClass/auth.ts
Normal file
24
packages/lib/actionClass/auth.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { ZId } from "@formbricks/types/v1/environment";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
import { hasUserEnvironmentAccess } from "../environment/auth";
|
||||
import { getActionClass } from "./service";
|
||||
import { unstable_cache } from "next/cache";
|
||||
|
||||
export const canUserAccessActionClass = async (userId: string, actionClassId: string): Promise<boolean> =>
|
||||
await unstable_cache(
|
||||
async () => {
|
||||
validateInputs([userId, ZId], [actionClassId, ZId]);
|
||||
if (!userId) return false;
|
||||
|
||||
const actionClass = await getActionClass(actionClassId);
|
||||
if (!actionClass) return false;
|
||||
|
||||
const hasAccessToEnvironment = await hasUserEnvironmentAccess(userId, actionClass.environmentId);
|
||||
if (!hasAccessToEnvironment) return false;
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
[`users-${userId}-actionClasses-${actionClassId}`],
|
||||
{ revalidate: 30 * 60, tags: [`actionClasses-${actionClassId}`] }
|
||||
)(); // 30 minutes
|
||||
@@ -6,22 +6,15 @@ import { TActionClass, TActionClassInput, ZActionClassInput } from "@formbricks/
|
||||
import { ZId } from "@formbricks/types/v1/environment";
|
||||
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/v1/errors";
|
||||
import { revalidateTag, unstable_cache } from "next/cache";
|
||||
import { cache } from "react";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
|
||||
const halfHourInSeconds = 60 * 30;
|
||||
|
||||
export const getActionClassCacheTag = (name: string, environmentId: string): string =>
|
||||
`environments-${environmentId}-actionClass-${name}`;
|
||||
const getActionClassCacheKey = (name: string, environmentId: string): string[] => [
|
||||
getActionClassCacheTag(name, environmentId),
|
||||
];
|
||||
|
||||
const getActionClassesCacheTag = (environmentId: string): string =>
|
||||
`environments-${environmentId}-actionClasses`;
|
||||
const getActionClassesCacheKey = (environmentId: string): string[] => [
|
||||
getActionClassesCacheTag(environmentId),
|
||||
];
|
||||
|
||||
const select = {
|
||||
id: true,
|
||||
@@ -34,33 +27,29 @@ const select = {
|
||||
environmentId: true,
|
||||
};
|
||||
|
||||
export const getActionClasses = cache(async (environmentId: string): Promise<TActionClass[]> => {
|
||||
validateInputs([environmentId, ZId]);
|
||||
try {
|
||||
let actionClasses = await prisma.eventClass.findMany({
|
||||
where: {
|
||||
environmentId: environmentId,
|
||||
},
|
||||
select,
|
||||
orderBy: {
|
||||
createdAt: "asc",
|
||||
},
|
||||
});
|
||||
|
||||
return actionClasses;
|
||||
} catch (error) {
|
||||
throw new DatabaseError(`Database error when fetching actions for environment ${environmentId}`);
|
||||
}
|
||||
});
|
||||
|
||||
export const getActionClassesCached = (environmentId: string) =>
|
||||
export const getActionClasses = (environmentId: string): Promise<TActionClass[]> =>
|
||||
unstable_cache(
|
||||
async () => {
|
||||
return await getActionClasses(environmentId);
|
||||
validateInputs([environmentId, ZId]);
|
||||
try {
|
||||
let actionClasses = await prisma.eventClass.findMany({
|
||||
where: {
|
||||
environmentId: environmentId,
|
||||
},
|
||||
select,
|
||||
orderBy: {
|
||||
createdAt: "asc",
|
||||
},
|
||||
});
|
||||
|
||||
return actionClasses;
|
||||
} catch (error) {
|
||||
throw new DatabaseError(`Database error when fetching actions for environment ${environmentId}`);
|
||||
}
|
||||
},
|
||||
getActionClassesCacheKey(environmentId),
|
||||
[`environments-${environmentId}-actionClasses`],
|
||||
{
|
||||
tags: getActionClassesCacheKey(environmentId),
|
||||
tags: [getActionClassesCacheTag(environmentId)],
|
||||
revalidate: halfHourInSeconds,
|
||||
}
|
||||
)();
|
||||
@@ -96,7 +85,7 @@ export const deleteActionClass = async (
|
||||
if (result === null) throw new ResourceNotFoundError("Action", actionClassId);
|
||||
|
||||
// revalidate cache
|
||||
revalidateTag(getActionClassesCacheTag(environmentId));
|
||||
revalidateTag(getActionClassesCacheTag(result.environmentId));
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
@@ -157,8 +146,8 @@ export const updateActionClass = async (
|
||||
});
|
||||
|
||||
// revalidate cache
|
||||
revalidateTag(getActionClassCacheTag(result.name, environmentId));
|
||||
revalidateTag(getActionClassesCacheTag(environmentId));
|
||||
revalidateTag(getActionClassCacheTag(result.name, result.environmentId));
|
||||
revalidateTag(getActionClassesCacheTag(result.environmentId));
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
@@ -176,9 +165,9 @@ export const getActionClassCached = async (name: string, environmentId: string)
|
||||
},
|
||||
});
|
||||
},
|
||||
getActionClassCacheKey(name, environmentId),
|
||||
[`environments-${environmentId}-actionClasses-${name}`],
|
||||
{
|
||||
tags: getActionClassCacheKey(name, environmentId),
|
||||
tags: [getActionClassesCacheTag(environmentId)],
|
||||
revalidate: halfHourInSeconds,
|
||||
}
|
||||
)();
|
||||
@@ -1,11 +1,13 @@
|
||||
import { ZId } from "@formbricks/types/v1/environment";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
import { hasUserEnvironmentAccess } from "../environment/auth";
|
||||
import { getApiKey } from "./service";
|
||||
import { unstable_cache } from "next/cache";
|
||||
|
||||
export const canUserAccessApiKey = async (userId: string, apiKeyId: string): Promise<boolean> => {
|
||||
return await unstable_cache(
|
||||
export const canUserAccessApiKey = async (userId: string, apiKeyId: string): Promise<boolean> =>
|
||||
await unstable_cache(
|
||||
async () => {
|
||||
if (!userId) return false;
|
||||
validateInputs([userId, ZId], [apiKeyId, ZId]);
|
||||
|
||||
const apiKeyFromServer = await getApiKey(apiKeyId);
|
||||
if (!apiKeyFromServer) return false;
|
||||
@@ -19,4 +21,3 @@ export const canUserAccessApiKey = async (userId: string, apiKeyId: string): Pro
|
||||
[`users-${userId}-apiKeys-${apiKeyId}`],
|
||||
{ revalidate: 30 * 60, tags: [`apiKeys-${apiKeyId}`] }
|
||||
)(); // 30 minutes
|
||||
};
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { ZId } from "@formbricks/types/v1/environment";
|
||||
import { unstable_cache } from "next/cache";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
|
||||
export const hasUserEnvironmentAccess = async (userId: string, environmentId: string) => {
|
||||
return await unstable_cache(
|
||||
async () => {
|
||||
if (!userId) return false;
|
||||
validateInputs([userId, ZId], [environmentId, ZId]);
|
||||
const environment = await prisma.environment.findUnique({
|
||||
where: {
|
||||
id: environmentId,
|
||||
|
||||
28
packages/lib/response/auth.ts
Normal file
28
packages/lib/response/auth.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { ZId } from "@formbricks/types/v1/environment";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
import { hasUserEnvironmentAccess } from "../environment/auth";
|
||||
import { getResponse, getResponseCacheTag } from "./service";
|
||||
import { unstable_cache } from "next/cache";
|
||||
import { getSurvey } from "../services/survey";
|
||||
|
||||
export const canUserAccessResponse = async (userId: string, responseId: string): Promise<boolean> =>
|
||||
await unstable_cache(
|
||||
async () => {
|
||||
validateInputs([userId, ZId], [responseId, ZId]);
|
||||
|
||||
if (!userId) return false;
|
||||
|
||||
const response = await getResponse(responseId);
|
||||
if (!response) return false;
|
||||
|
||||
const survey = await getSurvey(response.surveyId);
|
||||
if (!survey) return false;
|
||||
|
||||
const hasAccessToEnvironment = await hasUserEnvironmentAccess(userId, survey.environmentId);
|
||||
if (!hasAccessToEnvironment) return false;
|
||||
|
||||
return true;
|
||||
},
|
||||
[`users-${userId}-responses-${responseId}`],
|
||||
{ revalidate: 30 * 60, tags: [getResponseCacheTag(responseId)] }
|
||||
)(); // 30 minutes
|
||||
@@ -12,7 +12,7 @@ import { TTag } from "@formbricks/types/v1/tags";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { cache } from "react";
|
||||
import "server-only";
|
||||
import { getPerson, transformPrismaPerson } from "./person";
|
||||
import { getPerson, transformPrismaPerson } from "../services/person";
|
||||
import { captureTelemetry } from "../telemetry";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
import { ZId } from "@formbricks/types/v1/environment";
|
||||
@@ -78,6 +78,8 @@ const responseSelection = {
|
||||
|
||||
export const getResponsesCacheTag = (surveyId: string) => `surveys-${surveyId}-responses`;
|
||||
|
||||
export const getResponseCacheTag = (responseId: string) => `responses-${responseId}`;
|
||||
|
||||
export const getResponsesByPersonId = async (personId: string): Promise<Array<TResponse> | null> => {
|
||||
validateInputs([personId, ZId]);
|
||||
try {
|
||||
@@ -11,7 +11,7 @@ import { validateInputs } from "../utils/validate";
|
||||
import { TJsActionInput } from "@formbricks/types/v1/js";
|
||||
import { revalidateTag } from "next/cache";
|
||||
import { EventType } from "@prisma/client";
|
||||
import { getActionClassCacheTag, getActionClassCached } from "../services/actionClass";
|
||||
import { getActionClassCacheTag, getActionClassCached } from "../actionClass/service";
|
||||
import { getSessionCached } from "../services/session";
|
||||
|
||||
export const getActionsByEnvironmentId = cache(
|
||||
|
||||
@@ -116,6 +116,7 @@ export const updateProfile = async (
|
||||
id: personId,
|
||||
},
|
||||
data: data,
|
||||
select: responseSelection,
|
||||
});
|
||||
|
||||
revalidateTag(getProfileByEmailCacheTag(updatedProfile.email));
|
||||
@@ -137,6 +138,7 @@ const deleteUser = async (userId: string): Promise<TProfile> => {
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: responseSelection,
|
||||
});
|
||||
revalidateTag(getProfileByEmailCacheTag(profile.email));
|
||||
revalidateTag(getProfileCacheTag(userId));
|
||||
|
||||
@@ -15,7 +15,7 @@ import { z } from "zod";
|
||||
import { captureTelemetry } from "../telemetry";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
import { getDisplaysCacheTag } from "./displays";
|
||||
import { getResponsesCacheTag } from "./response";
|
||||
import { getResponsesCacheTag } from "../response/service";
|
||||
|
||||
// surveys cache key and tags
|
||||
const getSurveysCacheKey = (environmentId: string): string => `environments-${environmentId}-surveys`;
|
||||
|
||||
@@ -2,6 +2,9 @@ import { prisma } from "@formbricks/database";
|
||||
import { TTagsCount } from "@formbricks/types/v1/tags";
|
||||
import { cache } from "react";
|
||||
|
||||
export const getTagOnResponseCacheTag = (tagId: string, responseId: string) =>
|
||||
`tagsOnResponse-${tagId}-${responseId}`;
|
||||
|
||||
export const addTagToRespone = async (responseId: string, tagId: string) => {
|
||||
try {
|
||||
const tagOnResponse = await prisma.tagsOnResponses.create({
|
||||
@@ -16,7 +19,7 @@ export const addTagToRespone = async (responseId: string, tagId: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteTagFromResponse = async (responseId: string, tagId: string) => {
|
||||
export const deleteTagOnResponse = async (responseId: string, tagId: string) => {
|
||||
try {
|
||||
const deletedTag = await prisma.tagsOnResponses.delete({
|
||||
where: {
|
||||
|
||||
24
packages/lib/tag/auth.ts
Normal file
24
packages/lib/tag/auth.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { validateInputs } from "../utils/validate";
|
||||
import { hasUserEnvironmentAccess } from "../environment/auth";
|
||||
import { getTag } from "./service";
|
||||
import { unstable_cache } from "next/cache";
|
||||
import { ZId } from "@formbricks/types/v1/environment";
|
||||
|
||||
export const canUserAccessTag = async (userId: string, tagId: string): Promise<boolean> =>
|
||||
await unstable_cache(
|
||||
async () => {
|
||||
validateInputs([userId, ZId], [tagId, ZId]);
|
||||
|
||||
const tag = await getTag(tagId);
|
||||
if (!tag) return false;
|
||||
|
||||
const hasAccessToEnvironment = await hasUserEnvironmentAccess(userId, tag.environmentId);
|
||||
if (!hasAccessToEnvironment) return false;
|
||||
|
||||
return true;
|
||||
},
|
||||
[`${userId}-${tagId}`],
|
||||
{
|
||||
revalidate: 30 * 60, // 30 minutes
|
||||
}
|
||||
)();
|
||||
@@ -16,6 +16,20 @@ export const getTagsByEnvironmentId = cache(async (environmentId: string): Promi
|
||||
}
|
||||
});
|
||||
|
||||
export const getTag = async (tagId: string): Promise<TTag | null> => {
|
||||
try {
|
||||
const tag = await prisma.tag.findUnique({
|
||||
where: {
|
||||
id: tagId,
|
||||
},
|
||||
});
|
||||
|
||||
return tag;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const createTag = async (environmentId: string, name: string): Promise<TTag> => {
|
||||
try {
|
||||
const tag = await prisma.tag.create({
|
||||
24
packages/lib/tagOnResponse/auth.ts
Normal file
24
packages/lib/tagOnResponse/auth.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { validateInputs } from "../utils/validate";
|
||||
import { unstable_cache } from "next/cache";
|
||||
import { ZId } from "@formbricks/types/v1/environment";
|
||||
import { canUserAccessResponse } from "../response/auth";
|
||||
import { canUserAccessTag } from "../tag/auth";
|
||||
import { getTagOnResponseCacheTag } from "../services/tagOnResponse";
|
||||
|
||||
export const canUserAccessTagOnResponse = async (
|
||||
userId: string,
|
||||
tagId: string,
|
||||
responseId: string
|
||||
): Promise<boolean> =>
|
||||
await unstable_cache(
|
||||
async () => {
|
||||
validateInputs([userId, ZId], [tagId, ZId], [responseId, ZId]);
|
||||
|
||||
const isAuthorizedForTag = await canUserAccessTag(userId, tagId);
|
||||
const isAuthorizedForResponse = await canUserAccessResponse(userId, responseId);
|
||||
|
||||
return isAuthorizedForTag && isAuthorizedForResponse;
|
||||
},
|
||||
[`users-${userId}-tagOnResponse-${tagId}-${responseId}`],
|
||||
{ revalidate: 30 * 60, tags: [getTagOnResponseCacheTag(tagId, responseId)] }
|
||||
)();
|
||||
159
pnpm-lock.yaml
generated
159
pnpm-lock.yaml
generated
@@ -1,5 +1,9 @@
|
||||
lockfileVersion: '6.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
@@ -24,7 +28,7 @@ importers:
|
||||
version: 3.12.7
|
||||
turbo:
|
||||
specifier: latest
|
||||
version: 1.10.14
|
||||
version: 1.10.13
|
||||
|
||||
apps/demo:
|
||||
dependencies:
|
||||
@@ -327,6 +331,9 @@ importers:
|
||||
ua-parser-js:
|
||||
specifier: ^1.0.36
|
||||
version: 1.0.36
|
||||
xlsx:
|
||||
specifier: ^0.18.5
|
||||
version: 0.18.5
|
||||
devDependencies:
|
||||
'@formbricks/tsconfig':
|
||||
specifier: workspace:*
|
||||
@@ -434,7 +441,7 @@ importers:
|
||||
version: 9.0.0(eslint@8.50.0)
|
||||
eslint-config-turbo:
|
||||
specifier: latest
|
||||
version: 1.10.14(eslint@8.50.0)
|
||||
version: 1.8.8(eslint@8.50.0)
|
||||
eslint-plugin-react:
|
||||
specifier: 7.33.2
|
||||
version: 7.33.2(eslint@8.50.0)
|
||||
@@ -1147,7 +1154,7 @@ packages:
|
||||
'@babel/helper-plugin-utils': 7.22.5
|
||||
debug: 4.3.4
|
||||
lodash.debounce: 4.0.8
|
||||
resolve: 1.22.2
|
||||
resolve: 1.22.6
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
@@ -6771,6 +6778,11 @@ packages:
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
/adler-32@1.3.1:
|
||||
resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==}
|
||||
engines: {node: '>=0.8'}
|
||||
dev: false
|
||||
|
||||
/agent-base@6.0.2:
|
||||
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
|
||||
engines: {node: '>= 6.0.0'}
|
||||
@@ -8198,6 +8210,14 @@ packages:
|
||||
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
||||
dev: false
|
||||
|
||||
/cfb@1.2.2:
|
||||
resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==}
|
||||
engines: {node: '>=0.8'}
|
||||
dependencies:
|
||||
adler-32: 1.3.1
|
||||
crc-32: 1.2.2
|
||||
dev: false
|
||||
|
||||
/chalk@1.1.3:
|
||||
resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -8579,6 +8599,11 @@ packages:
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/codepage@1.15.0:
|
||||
resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==}
|
||||
engines: {node: '>=0.8'}
|
||||
dev: false
|
||||
|
||||
/collect-v8-coverage@1.0.1:
|
||||
resolution: {integrity: sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==}
|
||||
dev: true
|
||||
@@ -8967,7 +8992,6 @@ packages:
|
||||
resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==}
|
||||
engines: {node: '>=0.8'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/crc32-stream@4.0.2:
|
||||
resolution: {integrity: sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w==}
|
||||
@@ -10673,13 +10697,13 @@ packages:
|
||||
resolution: {integrity: sha512-NB/L/1Y30qyJcG5xZxCJKW/+bqyj+llbcCwo9DEz8bESIP0SLTOQ8T1DWCCFc+wJ61AMEstj4511PSScqMMfCw==}
|
||||
dev: true
|
||||
|
||||
/eslint-config-turbo@1.10.14(eslint@8.50.0):
|
||||
resolution: {integrity: sha512-ZeB+IcuFXy1OICkLuAplVa0euoYbhK+bMEQd0nH9+Lns18lgZRm33mVz/iSoH9VdUzl/1ZmFmoK+RpZc+8R80A==}
|
||||
/eslint-config-turbo@1.8.8(eslint@8.50.0):
|
||||
resolution: {integrity: sha512-+yT22sHOT5iC1sbBXfLIdXfbZuiv9bAyOXsxTxFCWelTeFFnANqmuKB3x274CFvf7WRuZ/vYP/VMjzU9xnFnxA==}
|
||||
peerDependencies:
|
||||
eslint: '>6.6.0'
|
||||
dependencies:
|
||||
eslint: 8.50.0
|
||||
eslint-plugin-turbo: 1.10.14(eslint@8.50.0)
|
||||
eslint-plugin-turbo: 1.8.8(eslint@8.50.0)
|
||||
dev: true
|
||||
|
||||
/eslint-import-resolver-node@0.3.9:
|
||||
@@ -10885,12 +10909,11 @@ packages:
|
||||
semver: 6.3.1
|
||||
string.prototype.matchall: 4.0.8
|
||||
|
||||
/eslint-plugin-turbo@1.10.14(eslint@8.50.0):
|
||||
resolution: {integrity: sha512-sBdBDnYr9AjT1g4lR3PBkZDonTrMnR4TvuGv5W0OiF7z9az1rI68yj2UHJZvjkwwcGu5mazWA1AfB0oaagpmfg==}
|
||||
/eslint-plugin-turbo@1.8.8(eslint@8.50.0):
|
||||
resolution: {integrity: sha512-zqyTIvveOY4YU5jviDWw9GXHd4RiKmfEgwsjBrV/a965w0PpDwJgEUoSMB/C/dU310Sv9mF3DSdEjxjJLaw6rA==}
|
||||
peerDependencies:
|
||||
eslint: '>6.6.0'
|
||||
dependencies:
|
||||
dotenv: 16.0.3
|
||||
eslint: 8.50.0
|
||||
dev: true
|
||||
|
||||
@@ -11690,6 +11713,11 @@ packages:
|
||||
resolution: {integrity: sha512-0eu5ULPS2c/jsa1lGFneEFFEdTbembJv8e4QKXeVJ3lm/5hyve06dlKZrpxmMwJt6rYen7sxmHHK2CLaXvWuWQ==}
|
||||
dev: true
|
||||
|
||||
/frac@1.1.2:
|
||||
resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==}
|
||||
engines: {node: '>=0.8'}
|
||||
dev: false
|
||||
|
||||
/fraction.js@4.3.6:
|
||||
resolution: {integrity: sha512-n2aZ9tNfYDwaHhvFTkhFErqOMIb8uyzSQ+vGJBjZyanAKZVbGUQ1sngfk9FdkBw7G26O7AgNjLcecLffD1c7eg==}
|
||||
|
||||
@@ -13159,11 +13187,6 @@ packages:
|
||||
rgba-regex: 1.0.0
|
||||
dev: true
|
||||
|
||||
/is-core-module@2.11.0:
|
||||
resolution: {integrity: sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==}
|
||||
dependencies:
|
||||
has: 1.0.3
|
||||
|
||||
/is-core-module@2.13.0:
|
||||
resolution: {integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==}
|
||||
dependencies:
|
||||
@@ -14718,7 +14741,7 @@ packages:
|
||||
is-plain-object: 2.0.4
|
||||
object.map: 1.0.1
|
||||
rechoir: 0.6.2
|
||||
resolve: 1.22.2
|
||||
resolve: 1.22.6
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
@@ -15141,7 +15164,7 @@ packages:
|
||||
dependencies:
|
||||
findup-sync: 2.0.0
|
||||
micromatch: 3.1.10
|
||||
resolve: 1.22.2
|
||||
resolve: 1.22.6
|
||||
stack-trace: 0.0.10
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -16557,7 +16580,7 @@ packages:
|
||||
resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
|
||||
dependencies:
|
||||
hosted-git-info: 2.8.9
|
||||
resolve: 1.22.2
|
||||
resolve: 1.22.6
|
||||
semver: 5.7.2
|
||||
validate-npm-package-license: 3.0.4
|
||||
dev: true
|
||||
@@ -17621,7 +17644,7 @@ packages:
|
||||
postcss: 8.4.27
|
||||
postcss-value-parser: 4.2.0
|
||||
read-cache: 1.0.0
|
||||
resolve: 1.22.2
|
||||
resolve: 1.22.6
|
||||
|
||||
/postcss-js@4.0.1(postcss@8.4.27):
|
||||
resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==}
|
||||
@@ -19917,7 +19940,7 @@ packages:
|
||||
resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
is-core-module: 2.11.0
|
||||
is-core-module: 2.13.0
|
||||
path-parse: 1.0.7
|
||||
supports-preserve-symlinks-flag: 1.0.0
|
||||
dev: true
|
||||
@@ -19926,7 +19949,7 @@ packages:
|
||||
resolution: {integrity: sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
is-core-module: 2.11.0
|
||||
is-core-module: 2.13.0
|
||||
path-parse: 1.0.7
|
||||
supports-preserve-symlinks-flag: 1.0.0
|
||||
|
||||
@@ -19942,7 +19965,7 @@ packages:
|
||||
resolution: {integrity: sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
is-core-module: 2.11.0
|
||||
is-core-module: 2.13.0
|
||||
path-parse: 1.0.7
|
||||
supports-preserve-symlinks-flag: 1.0.0
|
||||
|
||||
@@ -20117,16 +20140,8 @@ packages:
|
||||
fsevents: 2.3.2
|
||||
dev: true
|
||||
|
||||
/rollup@3.28.1:
|
||||
resolution: {integrity: sha512-R9OMQmIHJm9znrU3m3cpE8uhN0fGdXiawME7aZIpQqvpS/85+Vt1Hq1/yVIcYfOmaQiHjvXkQAoJukvLpau6Yw==}
|
||||
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
|
||||
hasBin: true
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
dev: true
|
||||
|
||||
/rollup@3.5.1:
|
||||
resolution: {integrity: sha512-hdQWTvPeiAbM6SUkxV70HdGUVxsgsc+CLy5fuh4KdgUBJ0SowXiix8gANgXoG3wEuLwfoJhCT2V+WwxfWq9Ikw==}
|
||||
/rollup@3.29.4:
|
||||
resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==}
|
||||
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
|
||||
hasBin: true
|
||||
optionalDependencies:
|
||||
@@ -20845,6 +20860,13 @@ packages:
|
||||
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
|
||||
dev: true
|
||||
|
||||
/ssf@0.11.2:
|
||||
resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==}
|
||||
engines: {node: '>=0.8'}
|
||||
dependencies:
|
||||
frac: 1.1.2
|
||||
dev: false
|
||||
|
||||
/sshpk@1.17.0:
|
||||
resolution: {integrity: sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -21995,7 +22017,7 @@ packages:
|
||||
joycon: 3.1.1
|
||||
postcss-load-config: 4.0.1(postcss@8.4.27)
|
||||
resolve-from: 5.0.0
|
||||
rollup: 3.5.1
|
||||
rollup: 3.29.4
|
||||
source-map: 0.8.0-beta.0
|
||||
sucrase: 3.32.0
|
||||
tree-kill: 1.2.2
|
||||
@@ -22049,64 +22071,65 @@ packages:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
/turbo-darwin-64@1.10.14:
|
||||
resolution: {integrity: sha512-I8RtFk1b9UILAExPdG/XRgGQz95nmXPE7OiGb6ytjtNIR5/UZBS/xVX/7HYpCdmfriKdVwBKhalCoV4oDvAGEg==}
|
||||
/turbo-darwin-64@1.10.13:
|
||||
resolution: {integrity: sha512-vmngGfa2dlYvX7UFVncsNDMuT4X2KPyPJ2Jj+xvf5nvQnZR/3IeDEGleGVuMi/hRzdinoxwXqgk9flEmAYp0Xw==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/turbo-darwin-arm64@1.10.14:
|
||||
resolution: {integrity: sha512-KAdUWryJi/XX7OD0alOuOa0aJ5TLyd4DNIYkHPHYcM6/d7YAovYvxRNwmx9iv6Vx6IkzTnLeTiUB8zy69QkG9Q==}
|
||||
/turbo-darwin-arm64@1.10.13:
|
||||
resolution: {integrity: sha512-eMoJC+k7gIS4i2qL6rKmrIQGP6Wr9nN4odzzgHFngLTMimok2cGLK3qbJs5O5F/XAtEeRAmuxeRnzQwTl/iuAw==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/turbo-linux-64@1.10.14:
|
||||
resolution: {integrity: sha512-BOBzoREC2u4Vgpap/WDxM6wETVqVMRcM8OZw4hWzqCj2bqbQ6L0wxs1LCLWVrghQf93JBQtIGAdFFLyCSBXjWQ==}
|
||||
/turbo-linux-64@1.10.13:
|
||||
resolution: {integrity: sha512-0CyYmnKTs6kcx7+JRH3nPEqCnzWduM0hj8GP/aodhaIkLNSAGAa+RiYZz6C7IXN+xUVh5rrWTnU2f1SkIy7Gdg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/turbo-linux-arm64@1.10.14:
|
||||
resolution: {integrity: sha512-D8T6XxoTdN5D4V5qE2VZG+/lbZX/89BkAEHzXcsSUTRjrwfMepT3d2z8aT6hxv4yu8EDdooZq/2Bn/vjMI32xw==}
|
||||
/turbo-linux-arm64@1.10.13:
|
||||
resolution: {integrity: sha512-0iBKviSGQQlh2OjZgBsGjkPXoxvRIxrrLLbLObwJo3sOjIH0loGmVIimGS5E323soMfi/o+sidjk2wU1kFfD7Q==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/turbo-windows-64@1.10.14:
|
||||
resolution: {integrity: sha512-zKNS3c1w4i6432N0cexZ20r/aIhV62g69opUn82FLVs/zk3Ie0GVkSB6h0rqIvMalCp7enIR87LkPSDGz9K4UA==}
|
||||
/turbo-windows-64@1.10.13:
|
||||
resolution: {integrity: sha512-S5XySRfW2AmnTeY1IT+Jdr6Goq7mxWganVFfrmqU+qqq3Om/nr0GkcUX+KTIo9mPrN0D3p5QViBRzulwB5iuUQ==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/turbo-windows-arm64@1.10.14:
|
||||
resolution: {integrity: sha512-rkBwrTPTxNSOUF7of8eVvvM+BkfkhA2OvpHM94if8tVsU+khrjglilp8MTVPHlyS9byfemPAmFN90oRIPB05BA==}
|
||||
/turbo-windows-arm64@1.10.13:
|
||||
resolution: {integrity: sha512-nKol6+CyiExJIuoIc3exUQPIBjP9nIq5SkMJgJuxsot2hkgGrafAg/izVDRDrRduQcXj2s8LdtxJHvvnbI8hEQ==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/turbo@1.10.14:
|
||||
resolution: {integrity: sha512-hr9wDNYcsee+vLkCDIm8qTtwhJ6+UAMJc3nIY6+PNgUTtXcQgHxCq8BGoL7gbABvNWv76CNbK5qL4Lp9G3ZYRA==}
|
||||
/turbo@1.10.13:
|
||||
resolution: {integrity: sha512-vOF5IPytgQPIsgGtT0n2uGZizR2N3kKuPIn4b5p5DdeLoI0BV7uNiydT7eSzdkPRpdXNnO8UwS658VaI4+YSzQ==}
|
||||
hasBin: true
|
||||
requiresBuild: true
|
||||
optionalDependencies:
|
||||
turbo-darwin-64: 1.10.14
|
||||
turbo-darwin-arm64: 1.10.14
|
||||
turbo-linux-64: 1.10.14
|
||||
turbo-linux-arm64: 1.10.14
|
||||
turbo-windows-64: 1.10.14
|
||||
turbo-windows-arm64: 1.10.14
|
||||
turbo-darwin-64: 1.10.13
|
||||
turbo-darwin-arm64: 1.10.13
|
||||
turbo-linux-64: 1.10.13
|
||||
turbo-linux-arm64: 1.10.13
|
||||
turbo-windows-64: 1.10.13
|
||||
turbo-windows-arm64: 1.10.13
|
||||
dev: true
|
||||
|
||||
/tween-functions@1.2.0:
|
||||
@@ -22980,7 +23003,7 @@ packages:
|
||||
dependencies:
|
||||
esbuild: 0.18.10
|
||||
postcss: 8.4.30
|
||||
rollup: 3.28.1
|
||||
rollup: 3.29.4
|
||||
terser: 5.20.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
@@ -23452,11 +23475,21 @@ packages:
|
||||
resolution: {integrity: sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==}
|
||||
dev: true
|
||||
|
||||
/wmf@1.0.2:
|
||||
resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==}
|
||||
engines: {node: '>=0.8'}
|
||||
dev: false
|
||||
|
||||
/word-wrap@1.2.3:
|
||||
resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/word@0.3.0:
|
||||
resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==}
|
||||
engines: {node: '>=0.8'}
|
||||
dev: false
|
||||
|
||||
/workbox-background-sync@6.5.4:
|
||||
resolution: {integrity: sha512-0r4INQZMyPky/lj4Ou98qxcThrETucOde+7mRGJl13MPJugQNKeZQOdIJe/1AchOP23cTqHcN/YVpD6r8E6I8g==}
|
||||
dependencies:
|
||||
@@ -23752,6 +23785,20 @@ packages:
|
||||
resolution: {integrity: sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
/xlsx@0.18.5:
|
||||
resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==}
|
||||
engines: {node: '>=0.8'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
adler-32: 1.3.1
|
||||
cfb: 1.2.2
|
||||
codepage: 1.15.0
|
||||
crc-32: 1.2.2
|
||||
ssf: 0.11.2
|
||||
wmf: 1.0.2
|
||||
word: 0.3.0
|
||||
dev: false
|
||||
|
||||
/xml-name-validator@3.0.0:
|
||||
resolution: {integrity: sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==}
|
||||
dev: true
|
||||
@@ -23973,7 +24020,3 @@ packages:
|
||||
/zwitch@2.0.4:
|
||||
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
|
||||
dev: false
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
Reference in New Issue
Block a user