Compare commits

..

1 Commits

Author SHA1 Message Date
Matthias Nannt 3952431de2 chore: rename github actions for simplification 2024-03-14 14:34:59 +01:00
28 changed files with 92 additions and 1657 deletions
-3
View File
@@ -165,6 +165,3 @@ ENTERPRISE_LICENSE_KEY=
# Ignore Rate Limiting across the Formbricks app
# RATE_LIMITING_DISABLED=1
# OpenTelemetry URL for tracing
# OPENTELEMETRY_LISTENER_URL=http://localhost:4318/v1/traces
-1
View File
@@ -68,7 +68,6 @@ jobs:
S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }}
S3_REGION: ${{ vars.S3_REGION }}
S3_BUCKET_NAME: ${{ vars.S3_BUCKET_NAME }}
OPENTELEMETRY_LISTENER_URL: ${{ vars.OPENTELEMETRY_LISTENER_URL }}
KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
steps:
@@ -1,14 +1,13 @@
import { Fence } from "@/components/shared/Fence";
import { generateManagementApiMetadata } from "@/lib/utils";
import {generateManagementApiMetadata} from "@/lib/utils"
export const metadata = generateManagementApiMetadata("Surveys", ["Fetch", "Create", "Update", "Delete"]);
export const metadata = generateManagementApiMetadata("Surveys",["Fetch","Create","Update","Delete"])
#### Management API
# Surveys API
This set of API can be used to
- [List All Surveys](#list-all-surveys)
- [Get Survey](#get-survey-by-id)
- [Create Survey](#create-survey)
@@ -23,7 +22,8 @@ This set of API can be used to
<Row>
<Col>
Retrieve all the surveys you have for the environment with pagination.
Retrieve all the surveys you have for the environment.
### Mandatory Headers
@@ -33,26 +33,14 @@ This set of API can be used to
</Property>
</Properties>
### Query Parameters
<Properties>
<Property name="offset" type="number">
The number of surveys to skip before returning the results.
</Property>
<Property name="limit" type="number">
The number of surveys to return.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/management/surveys">
```bash {{ title: 'cURL' }}
curl --location \
'https://app.formbricks.com/api/v1/management/surveys?offset=20&limit=10' \
'https://app.formbricks.com/api/v1/management/surveys' \
--header \
'x-api-key: <your-api-key>'
```
@@ -415,6 +403,7 @@ This set of API can be used to
```
</CodeGroup>
</Col>
<Col sticky>
@@ -464,7 +453,7 @@ This set of API can be used to
}
}
```
```json {{ title: '401 Not Authenticated' }}
{
"code": "not_authenticated",
@@ -508,6 +497,7 @@ This set of API can be used to
```
</CodeGroup>
</Col>
<Col sticky>
@@ -578,7 +568,7 @@ This set of API can be used to
}
}
```
```json {{ title: '401 Not Authenticated' }}
{
"code": "not_authenticated",
@@ -595,6 +585,7 @@ This set of API can be used to
---
## Delete Survey by ID {{ tag: 'DELETE', label: '/api/v1/management/surveys/<survey-id>' }}
<Row>
@@ -183,7 +183,6 @@ These variables can be provided at the runtime i.e. in your docker-compose file.
| OIDC_CLIENT_SECRET | Secret for Custom OpenID Connect Provider | optional (required if OIDC auth is enabled) | |
| OIDC_ISSUER | Issuer URL for Custom OpenID Connect Provider (should have `.well-known` configured at this) | optional (required if OIDC auth is enabled) | |
| OIDC_SIGNING_ALGORITHM | Signing Algorithm for Custom OpenID Connect Provider | optional | `RS256` |
| OPENTELEMETRY_LISTENER_URL | URL for OpenTelemetry listener inside Formbricks. | optional | | |
## Build-time Variables
@@ -121,11 +121,6 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
"Open source, end-to-end encrypted platform that lets you securely manage secrets and configs across your team, devices, and infrastructure.",
href: "https://infisical.com",
},
{
name: "Keep",
description: "Open source alert management and AIOps platform.",
href: "https://keephq.dev",
},
{
name: "Langfuse",
description: "Open source LLM engineering platform. Debug, analyze and iterate together.",
@@ -174,7 +169,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
name: "Requestly",
description:
"Makes frontend development cycle 10x faster with API Client, Mock Server, Intercept & Modify HTTP Requests and Session Replays.",
href: "https://requestly.com",
href: "https://requestly.io",
},
{
name: "Revert",
@@ -80,9 +80,6 @@ const ResponsePage = ({
const deleteResponse = (responseId: string) => {
setResponses(responses.filter((response) => response.id !== responseId));
if (responseCount) {
setResponseCount(responseCount - 1);
}
};
const updateResponse = (responseId: string, updatedResponse: TResponse) => {
@@ -48,10 +48,21 @@ export default function SurveyEditor({
}: SurveyEditorProps): JSX.Element {
const [activeView, setActiveView] = useState<"questions" | "settings">("questions");
const [activeQuestionId, setActiveQuestionId] = useState<string | null>(null);
const [localSurvey, setLocalSurvey] = useState<TSurvey | null>(survey);
const [localSurvey, setLocalSurvey] = useState<TSurvey | null>();
const [invalidQuestions, setInvalidQuestions] = useState<String[] | null>(null);
const [localProduct, setLocalProduct] = useState<TProduct>(product);
useEffect(() => {
if (survey) {
const surveyClone = structuredClone(survey);
setLocalSurvey(surveyClone);
if (survey.questions.length > 0) {
setActiveQuestionId(survey.questions[0].id);
}
}
}, [survey]);
useEffect(() => {
const listener = () => {
if (document.visibilityState === "visible") {
@@ -4,13 +4,13 @@ import { Metadata } from "next";
import { getServerSession } from "next-auth";
import { authOptions } from "@formbricks/lib/authOptions";
import { SURVEYS_PER_PAGE, WEBAPP_URL } from "@formbricks/lib/constants";
import { WEBAPP_URL } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getEnvironments } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getSurveyCount } from "@formbricks/lib/survey/service";
import { getSurveys } from "@formbricks/lib/survey/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import ContentWrapper from "@formbricks/ui/ContentWrapper";
import SurveysList from "@formbricks/ui/SurveysList";
@@ -42,22 +42,21 @@ export default async function SurveysPage({ params }) {
if (!environment) {
throw new Error("Environment not found");
}
const surveyCount = await getSurveyCount(params.environmentId);
const surveys = await getSurveys(params.environmentId, 1); // workaround for now; only get the first page; better approach is in development
const environments = await getEnvironments(product.id);
const otherEnvironment = environments.find((e) => e.type !== environment.type)!;
return (
<ContentWrapper className="flex h-full flex-col justify-between">
{surveyCount > 0 ? (
{surveys.length > 0 ? (
<SurveysList
environment={environment}
surveys={surveys}
otherEnvironment={otherEnvironment}
isViewer={isViewer}
WEBAPP_URL={WEBAPP_URL}
userId={session.user.id}
surveysPerPage={SURVEYS_PER_PAGE}
/>
) : (
<SurveyStarter
@@ -67,7 +66,7 @@ export default async function SurveysPage({ params }) {
user={session.user}
/>
)}
{/* <SurveysList environmentId={params.environmentId} /> */}
<WidgetStatusIndicator environmentId={params.environmentId} type="mini" />
</ContentWrapper>
);
@@ -10,12 +10,7 @@ export async function GET(request: Request) {
try {
const authentication = await authenticateRequest(request);
if (!authentication) return responses.notAuthenticatedResponse();
const searchParams = new URL(request.url).searchParams;
const limit = searchParams.has("limit") ? Number(searchParams.get("limit")) : undefined;
const offset = searchParams.has("offset") ? Number(searchParams.get("offset")) : undefined;
const surveys = await getSurveys(authentication.environmentId!, limit, offset);
const surveys = await getSurveys(authentication.environmentId!);
return responses.successResponse(surveys);
} catch (error) {
if (error instanceof DatabaseError) {
-3
View File
@@ -52,9 +52,6 @@ export async function GET(req: NextRequest) {
{
width: 800,
height: 400,
headers: {
"Cache-Control": "public, s-maxage=600, max-age=1800, stale-while-revalidate=600, stale-if-error=600",
},
}
);
}
-27
View File
@@ -1,27 +0,0 @@
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { Resource } from "@opentelemetry/resources";
import { NodeSDK } from "@opentelemetry/sdk-node";
import { SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { SEMRESATTRS_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
export function startInstrumentationForNode(url: string) {
try {
const exporter = new OTLPTraceExporter({
url,
});
const sdk = new NodeSDK({
resource: new Resource({
[SEMRESATTRS_SERVICE_NAME]: "Formbricks",
}),
traceExporter: exporter,
spanProcessor: new SimpleSpanProcessor(exporter),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
} catch (err) {
console.error("Unable to setup Telemetry:", err);
}
}
-7
View File
@@ -1,7 +0,0 @@
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs" && process.env.OPENTELEMETRY_LISTENER_URL) {
const { startInstrumentationForNode } = await import("./instrumentation.node");
startInstrumentationForNode(process.env.OPENTELEMETRY_LISTENER_URL);
}
}
-1
View File
@@ -19,7 +19,6 @@ const nextConfig = {
output: "standalone",
experimental: {
serverComponentsExternalPackages: ["@aws-sdk"],
instrumentationHook: true,
},
transpilePackages: ["@formbricks/database", "@formbricks/ee", "@formbricks/ui", "@formbricks/lib"],
images: {
-5
View File
@@ -23,11 +23,6 @@
"@formbricks/ui": "workspace:*",
"@headlessui/react": "^1.7.18",
"@json2csv/node": "^7.0.6",
"@opentelemetry/auto-instrumentations-node": "^0.43.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.49.1",
"@opentelemetry/resources": "^1.22.0",
"@opentelemetry/sdk-node": "^0.49.1",
"@opentelemetry/semantic-conventions": "^1.22.0",
"@paralleldrive/cuid2": "^2.2.2",
"@radix-ui/react-collapsible": "^1.0.3",
"@react-email/components": "^0.0.15",
+4 -12
View File
@@ -12,17 +12,15 @@ test.describe("JS Package Test", async () => {
await signUpAndLogin(page, name, email, password);
await finishOnboarding(page);
// await page.waitForURL(/\/environments\/[^/]+\/surveys/);
await page.waitForURL(/\/environments\/[^/]+\/surveys/);
await page.getByRole("link", { name: "New survey", exact: true }).click();
await page
.getByText("Product ExperienceProduct Market Fit (Superhuman)Measure PMF by assessing how")
.isVisible();
await page
.getByText("Product ExperienceProduct Market Fit (Superhuman)Measure PMF by assessing how")
.click();
await page.getByRole("button", { name: "Use this template" }).click();
await page.getByRole("button", { name: "Settings", exact: true }).click();
await expect(page.locator("#howToSendCardTrigger")).toBeVisible();
@@ -33,7 +31,7 @@ test.describe("JS Package Test", async () => {
await page.locator("#howToSendCardOption-web").click();
await expect(page.getByText("Survey Trigger")).toBeVisible();
// await page.getByText("Survey Trigger").click();
await page.getByText("Survey Trigger").click();
await page.getByRole("combobox").click();
await page.getByLabel("New Session").click();
@@ -46,12 +44,6 @@ test.describe("JS Package Test", async () => {
})();
await page.waitForURL(/\/environments\/[^/]+\/surveys\/[^/]+\/summary/);
expect(page.getByRole("link", { name: "Surveys" })).toBeVisible();
await page.getByRole("link", { name: "Surveys" }).click();
await expect(page.getByRole("heading", { name: "Surveys" })).toBeVisible();
await page.screenshot();
});
test("JS Display Survey on Page", async ({ page }) => {
@@ -113,7 +105,7 @@ test.describe("JS Package Test", async () => {
// Formbricks Modal is not visible
await expect(page.getByText("Powered by Formbricks")).not.toBeVisible({ timeout: 10000 });
await page.waitForLoadState("networkidle");
await page.waitForTimeout(3000);
await page.waitForTimeout(1500);
});
test("Admin validates Displays & Response", async ({ page }) => {
+1 -1
View File
@@ -86,7 +86,7 @@ test.describe("Invite, accept and remove team member", async () => {
await page.getByRole("link", { name: "Create account" }).click();
await signupUsingInviteToken(page, name, email, password);
await finishOnboarding(page, false);
await finishOnboarding(page);
});
test("Remove member", async ({ page }) => {
+15 -29
View File
@@ -54,40 +54,24 @@ export const login = async (page: Page, email: string, password: string): Promis
await page.getByRole("button", { name: "Login with Email" }).click();
};
export const finishOnboarding = async (page: Page, deleteExampleSurvey: boolean = true): Promise<void> => {
export const finishOnboarding = async (page: Page): Promise<void> => {
await page.waitForURL("/onboarding");
await expect(page).toHaveURL("/onboarding");
const hiddenSkipButton = page.locator("#FB__INTERNAL__SKIP_ONBOARDING");
hiddenSkipButton.evaluate((el: HTMLElement) => el.click());
await expect(page.getByText("My Product")).toBeVisible();
// await page.getByRole("button", { name: "In-app Surveys Run a survey" }).click();
let currentDir = process.cwd();
let htmlFilePath = currentDir + "/packages/js/index.html";
// await page.getByRole("button", { name: "Skip" }).click();
// await page.getByRole("button", { name: "Skip" }).click();
const environmentId =
/\/environments\/([^/]+)\/surveys/.exec(page.url())?.[1] ??
(() => {
throw new Error("Unable to parse environmentId from URL");
})();
let htmlFile = replaceEnvironmentIdInHtml(htmlFilePath, environmentId);
await page.goto(htmlFile);
// Formbricks In App Sync has happened
const syncApi = await page.waitForResponse((response) => response.url().includes("/in-app/sync"));
expect(syncApi.status()).toBe(200);
await page.goto("/");
// await page.getByRole("button", { name: "I am not sure how to do this" }).click();
// await page.locator("input").click();
// await page.locator("input").fill("test@gmail.com");
// await page.getByRole("button", { name: "Invite" }).click();
await page.waitForURL(/\/environments\/[^/]+\/surveys/);
if (deleteExampleSurvey) {
await page.click("#example-survey-survey-actions");
await page.getByRole("menuitem", { name: "Delete" }).click();
await page.getByRole("button", { name: "Delete" }).click();
await page.reload();
}
await expect(page.getByText("My Product")).toBeVisible();
};
export const replaceEnvironmentIdInHtml = (filePath: string, environmentId: string): string => {
@@ -129,11 +113,9 @@ export const createSurvey = async (
const addQuestion = "Add QuestionAdd a new question to your survey";
await signUpAndLogin(page, name, email, password);
await finishOnboarding(page, false);
await finishOnboarding(page);
await page.getByRole("link", { name: "New survey", exact: true }).click();
await page.getByRole("heading", { name: "Start from Scratch" }).click();
await page.getByRole("button", { name: "Create survey", exact: true }).click();
// Welcome Card
await expect(page.locator("#welcome-toggle")).toBeVisible();
@@ -244,7 +226,11 @@ export const createSurvey = async (
await page.getByLabel("Question").fill(params.fileUploadQuestion.question);
// Thank You Card
page.getByText("Thank You CardShownShow").click();
await page
.locator("div")
.filter({ hasText: /^Thank You CardShown$/ })
.nth(1)
.click();
await page.getByLabel("Question").fill(params.thankYouCard.headline);
await page.getByLabel("Description").fill(params.thankYouCard.description);
};
-1
View File
@@ -76,7 +76,6 @@ env:
- NEXT_PUBLIC_FORMBRICKS_API_HOST
- NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID
- NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID
- OPENTELEMETRY_LISTENER_URL
- NEXT_PUBLIC_SENTRY_DSN
- CLOUDFLARE_EMAIL
- CLOUDFLARE_DNS_API_TOKEN
-1
View File
@@ -75,7 +75,6 @@ export const MAIL_FROM = env.MAIL_FROM;
export const NEXTAUTH_SECRET = env.NEXTAUTH_SECRET;
export const ITEMS_PER_PAGE = 50;
export const SURVEYS_PER_PAGE = 20;
export const RESPONSES_PER_PAGE = 10;
export const TEXT_RESPONSES_PER_PAGE = 5;
-2
View File
@@ -50,7 +50,6 @@ export const env = createEnv({
OIDC_DISPLAY_NAME: z.string().optional(),
OIDC_ISSUER: z.string().optional(),
OIDC_SIGNING_ALGORITHM: z.string().optional(),
OPENTELEMETRY_LISTENER_URL: z.string().optional(),
ONBOARDING_DISABLED: z.string().optional(),
PASSWORD_RESET_DISABLED: z.enum(["1", "0"]).optional(),
PRIVACY_URL: z
@@ -146,7 +145,6 @@ export const env = createEnv({
NEXT_PUBLIC_POSTHOG_API_HOST: process.env.NEXT_PUBLIC_POSTHOG_API_HOST,
NEXT_PUBLIC_POSTHOG_API_KEY: process.env.NEXT_PUBLIC_POSTHOG_API_KEY,
NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN,
OPENTELEMETRY_LISTENER_URL: process.env.OPENTELEMETRY_LISTENER_URL,
NOTION_OAUTH_CLIENT_ID: process.env.NOTION_OAUTH_CLIENT_ID,
NOTION_OAUTH_CLIENT_SECRET: process.env.NOTION_OAUTH_CLIENT_SECRET,
OIDC_CLIENT_ID: process.env.OIDC_CLIENT_ID,
+5 -46
View File
@@ -263,29 +263,19 @@ export const getSurveysByActionClassId = async (actionClassId: string, page?: nu
return surveys.map((survey) => formatDateFields(survey, ZSurvey));
};
export const getSurveys = async (
environmentId: string,
limit?: number,
offset?: number
): Promise<TSurvey[]> => {
export const getSurveys = async (environmentId: string, page?: number): Promise<TSurvey[]> => {
const surveys = await unstable_cache(
async () => {
validateInputs([environmentId, ZId], [limit, ZOptionalNumber], [offset, ZOptionalNumber]);
validateInputs([environmentId, ZId], [page, ZOptionalNumber]);
let surveysPrisma;
try {
surveysPrisma = await prisma.survey.findMany({
where: {
environmentId,
},
select: selectSurvey,
orderBy: [
{
updatedAt: "desc",
},
],
take: limit ? limit : undefined,
skip: offset ? offset : undefined,
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
@@ -318,7 +308,7 @@ export const getSurveys = async (
}
return surveys;
},
[`getSurveys-${environmentId}-${limit}-${offset}`],
[`getSurveys-${environmentId}-${page}`],
{
tags: [surveyCache.tag.byEnvironmentId(environmentId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
@@ -330,37 +320,6 @@ export const getSurveys = async (
return surveys.map((survey) => formatDateFields(survey, ZSurvey));
};
export const getSurveyCount = async (environmentId: string): Promise<number> => {
const count = await unstable_cache(
async () => {
validateInputs([environmentId, ZId]);
try {
const surveyCount = await prisma.survey.count({
where: {
environmentId: environmentId,
},
});
return surveyCount;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getSurveyCount-${environmentId}`],
{
tags: [surveyCache.tag.byEnvironmentId(environmentId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
return count;
};
export const updateSurvey = async (updatedSurvey: TSurvey): Promise<TSurvey> => {
validateInputs([updatedSurvey, ZSurveyWithRefinements]);
+1 -32
View File
@@ -11,7 +11,6 @@ import {
deleteSurvey,
duplicateSurvey,
getSurvey,
getSurveyCount,
getSurveys,
getSurveysByActionClassId,
getSyncSurveys,
@@ -32,10 +31,6 @@ import {
updateSurveyInput,
} from "./__mock__/survey.mock";
beforeEach(() => {
prisma.survey.count.mockResolvedValue(1);
});
describe("Tests for getSurvey", () => {
describe("Happy Path", () => {
it("Returns a survey", async () => {
@@ -100,7 +95,7 @@ describe("Tests for getSurveysByActionClassId", () => {
describe("Tests for getSurveys", () => {
describe("Happy Path", () => {
it("Returns an array of surveys for a given environmentId, limit(optional) and offset(optional)", async () => {
it("Returns an array of surveys for a given environmentId and page", async () => {
prisma.survey.findMany.mockResolvedValueOnce([mockSurveyOutput]);
const surveys = await getSurveys(mockId);
expect(surveys).toEqual([mockTransformedSurveyOutput]);
@@ -316,29 +311,3 @@ describe("Tests for getSyncedSurveys", () => {
});
});
});
describe("Tests for getSurveyCount service", () => {
describe("Happy Path", () => {
it("Counts the total number of surveys for a given environment ID", async () => {
const count = await getSurveyCount(mockId);
expect(count).toEqual(1);
});
it("Returns zero count when there are no surveys for a given environment ID", async () => {
prisma.survey.count.mockResolvedValue(0);
const count = await getSurveyCount(mockId);
expect(count).toEqual(0);
});
});
describe("Sad Path", () => {
testInputValidation(getSurveyCount, "123");
it("Throws a generic Error for other unexpected issues", async () => {
const mockErrorMessage = "Mock error message";
prisma.survey.count.mockRejectedValue(new Error(mockErrorMessage));
await expect(getSurveyCount(mockId)).rejects.toThrow(Error);
});
});
});
+1 -21
View File
@@ -8,20 +8,10 @@ import { authOptions } from "@formbricks/lib/authOptions";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { canUserAccessSurvey, verifyUserRoleAccess } from "@formbricks/lib/survey/auth";
import { surveyCache } from "@formbricks/lib/survey/cache";
import { deleteSurvey, duplicateSurvey, getSurvey, getSurveys } from "@formbricks/lib/survey/service";
import { deleteSurvey, duplicateSurvey, getSurvey } from "@formbricks/lib/survey/service";
import { generateSurveySingleUseId } from "@formbricks/lib/utils/singleUseSurveys";
import { AuthorizationError, ResourceNotFoundError } from "@formbricks/types/errors";
export const getSurveyAction = async (surveyId: string) => {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
const isAuthorized = await canUserAccessSurvey(session.user.id, surveyId);
if (!isAuthorized) throw new AuthorizationError("Not authorized");
return await getSurvey(surveyId);
};
export async function duplicateSurveyAction(environmentId: string, surveyId: string) {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
@@ -222,13 +212,3 @@ export async function generateSingleUseIdAction(surveyId: string, isEncrypted: b
return generateSurveySingleUseId(isEncrypted);
}
export async function getSurveysAction(environmentId: string, limit?: number, offset?: number) {
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 getSurveys(environmentId, limit, offset);
}
@@ -18,8 +18,6 @@ interface SurveyCardProps {
isViewer: boolean;
WEBAPP_URL: string;
orientation: string;
duplicateSurvey: (survey: TSurvey) => void;
deleteSurvey: (surveyId: string) => void;
}
export default function SurveyCard({
survey,
@@ -28,8 +26,6 @@ export default function SurveyCard({
isViewer,
WEBAPP_URL,
orientation,
deleteSurvey,
duplicateSurvey,
}: SurveyCardProps) {
const isSurveyCreationDeletionDisabled = isViewer;
@@ -89,8 +85,6 @@ export default function SurveyCard({
webAppUrl={WEBAPP_URL}
singleUseId={singleUseId}
isSurveyCreationDeletionDisabled={isSurveyCreationDeletionDisabled}
duplicateSurvey={duplicateSurvey}
deleteSurvey={deleteSurvey}
/>
</div>
<div>
@@ -151,8 +145,6 @@ export default function SurveyCard({
webAppUrl={WEBAPP_URL}
singleUseId={singleUseId}
isSurveyCreationDeletionDisabled={isSurveyCreationDeletionDisabled}
duplicateSurvey={duplicateSurvey}
deleteSurvey={deleteSurvey}
/>
</div>
</div>
@@ -19,12 +19,7 @@ import {
DropdownMenuTrigger,
} from "../../DropdownMenu";
import LoadingSpinner from "../../LoadingSpinner";
import {
copyToOtherEnvironmentAction,
deleteSurveyAction,
duplicateSurveyAction,
getSurveyAction,
} from "../actions";
import { copyToOtherEnvironmentAction, deleteSurveyAction, duplicateSurveyAction } from "../actions";
interface SurveyDropDownMenuProps {
environmentId: string;
@@ -34,8 +29,6 @@ interface SurveyDropDownMenuProps {
webAppUrl: string;
singleUseId?: string;
isSurveyCreationDeletionDisabled?: boolean;
duplicateSurvey: (survey: TSurvey) => void;
deleteSurvey: (surveyId: string) => void;
}
export default function SurveyDropDownMenu({
@@ -46,8 +39,6 @@ export default function SurveyDropDownMenu({
webAppUrl,
singleUseId,
isSurveyCreationDeletionDisabled,
deleteSurvey,
duplicateSurvey,
}: SurveyDropDownMenuProps) {
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [loading, setLoading] = useState(false);
@@ -60,7 +51,6 @@ export default function SurveyDropDownMenu({
setLoading(true);
try {
await deleteSurveyAction(survey.id);
deleteSurvey(survey.id);
router.refresh();
setDeleteDialogOpen(false);
toast.success("Survey deleted successfully.");
@@ -73,10 +63,8 @@ export default function SurveyDropDownMenu({
const duplicateSurveyAndRefresh = async (surveyId: string) => {
setLoading(true);
try {
const duplicatedSurvey = await duplicateSurveyAction(environmentId, surveyId);
await duplicateSurveyAction(environmentId, surveyId);
router.refresh();
const transformedDuplicatedSurvey = await getSurveyAction(duplicatedSurvey.id);
if (transformedDuplicatedSurvey) duplicateSurvey(transformedDuplicatedSurvey);
toast.success("Survey duplicated successfully.");
} catch (error) {
toast.error("Failed to duplicate the survey.");
@@ -107,9 +95,7 @@ export default function SurveyDropDownMenu({
);
}
return (
<div
id={`${survey.name.toLowerCase().split(" ").join("-")}-survey-actions`}
onClick={(e) => e.stopPropagation()}>
<>
<DropdownMenu open={isDropDownOpen} onOpenChange={setIsDropDownOpen}>
<DropdownMenuTrigger className="z-10 cursor-pointer" asChild>
<div className="rounded-lg border p-2 hover:bg-slate-50">
@@ -242,6 +228,6 @@ export default function SurveyDropDownMenu({
text="Are you sure you want to delete this survey and all of its responses? This action cannot be undone."
/>
)}
</div>
</>
);
}
+4 -53
View File
@@ -1,39 +1,33 @@
"use client";
import { PlusIcon } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { TEnvironment } from "@formbricks/types/environment";
import { TSurvey } from "@formbricks/types/surveys";
import { Button } from "../v2/Button";
import { getSurveysAction } from "./actions";
import SurveyCard from "./components/SurveyCard";
import SurveyFilters from "./components/SurveyFilters";
interface SurveysListProps {
environment: TEnvironment;
surveys: TSurvey[];
otherEnvironment: TEnvironment;
isViewer: boolean;
WEBAPP_URL: string;
userId: string;
surveysPerPage: number;
}
export default function SurveysList({
environment,
surveys,
otherEnvironment,
isViewer,
WEBAPP_URL,
userId,
surveysPerPage: surveysLimit,
}: SurveysListProps) {
const [surveys, setSurveys] = useState<TSurvey[]>([]);
const [isFetching, setIsFetching] = useState(true);
const [hasMore, setHasMore] = useState<boolean>(true);
const [filteredSurveys, setFilteredSurveys] = useState<TSurvey[]>(surveys);
// Initialize orientation state with a function that checks if window is defined
const [orientation, setOrientation] = useState(() =>
typeof localStorage !== "undefined" ? localStorage.getItem("surveyOrientation") || "grid" : "grid"
@@ -44,37 +38,6 @@ export default function SurveysList({
localStorage.setItem("surveyOrientation", orientation);
}, [orientation]);
useEffect(() => {
async function fetchInitialSurveys() {
setIsFetching(true);
const res = await getSurveysAction(environment.id, surveysLimit);
if (res.length < surveysLimit) setHasMore(false);
setSurveys(res);
setIsFetching(false);
}
fetchInitialSurveys();
}, [environment.id, surveysLimit]);
const fetchNextPage = useCallback(async () => {
setIsFetching(true);
const newSurveys = await getSurveysAction(environment.id, surveysLimit, surveys.length);
if (newSurveys.length === 0 || newSurveys.length < surveysLimit) {
setHasMore(false);
}
setSurveys([...surveys, ...newSurveys]);
setIsFetching(false);
}, [environment.id, surveys, surveysLimit]);
const handleDeleteSurvey = async (surveyId: string) => {
const newSurveys = surveys.filter((survey) => survey.id !== surveyId);
setSurveys(newSurveys);
};
const handleDuplicateSurvey = async (survey: TSurvey) => {
const newSurveys = [survey, ...surveys];
setSurveys(newSurveys);
};
return (
<div className="space-y-4">
<div className="flex justify-between">
@@ -114,8 +77,6 @@ export default function SurveysList({
isViewer={isViewer}
WEBAPP_URL={WEBAPP_URL}
orientation={orientation}
duplicateSurvey={handleDuplicateSurvey}
deleteSurvey={handleDeleteSurvey}
/>
);
})}
@@ -133,27 +94,17 @@ export default function SurveysList({
isViewer={isViewer}
WEBAPP_URL={WEBAPP_URL}
orientation={orientation}
duplicateSurvey={handleDuplicateSurvey}
deleteSurvey={handleDeleteSurvey}
/>
);
})}
</div>
)}
{hasMore && (
<div className="flex justify-center py-5">
<Button onClick={fetchNextPage} variant="secondary" size="sm" loading={isFetching}>
Load more
</Button>
</div>
)}
</div>
) : (
<div className="flex h-full flex-col items-center justify-center">
<span className="mb-4 h-24 w-24 rounded-full bg-slate-100 p-6 text-5xl">🕵</span>
<div className="text-slate-600">{isFetching ? "Fetching Surveys" : "No surveys found"}</div>
<div className="text-slate-600">No surveys found</div>
</div>
)}
</div>
+27 -1341
View File
File diff suppressed because it is too large Load Diff
-2
View File
@@ -104,8 +104,6 @@
"NEXT_PUBLIC_FORMBRICKS_COM_API_HOST",
"NEXT_PUBLIC_FORMBRICKS_COM_ENVIRONMENT_ID",
"NEXT_PUBLIC_FORMBRICKS_COM_DOCS_FEEDBACK_SURVEY_ID",
"OPENTELEMETRY_LISTENER_URL",
"NEXT_RUNTIME",
"NEXTAUTH_SECRET",
"NEXTAUTH_URL",
"NODE_ENV",