mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-01 18:58:46 -06:00
Compare commits
5 Commits
tweak-publ
...
fix/sonarq
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7540c64fdf | ||
|
|
3b815e22e3 | ||
|
|
4d4a5c0e64 | ||
|
|
0e89293974 | ||
|
|
c306911b3a |
163
.github/workflows/docker-build-validation.yml
vendored
Normal file
163
.github/workflows/docker-build-validation.yml
vendored
Normal file
@@ -0,0 +1,163 @@
|
||||
name: Docker Build Validation
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
merge_group:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
validate-docker-build:
|
||||
name: Validate Docker Build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Add PostgreSQL service container
|
||||
services:
|
||||
postgres:
|
||||
image: pgvector/pgvector:pg17
|
||||
env:
|
||||
POSTGRES_USER: test
|
||||
POSTGRES_PASSWORD: test
|
||||
POSTGRES_DB: formbricks
|
||||
ports:
|
||||
- 5432:5432
|
||||
# Health check to ensure PostgreSQL is ready before using it
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./apps/web/Dockerfile
|
||||
push: false
|
||||
load: true
|
||||
tags: formbricks-test:${{ github.sha }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
secrets: |
|
||||
database_url=${{ secrets.DUMMY_DATABASE_URL }}
|
||||
encryption_key=${{ secrets.DUMMY_ENCRYPTION_KEY }}
|
||||
|
||||
- name: Verify PostgreSQL Connection
|
||||
run: |
|
||||
echo "Verifying PostgreSQL connection..."
|
||||
# Install PostgreSQL client to test connection
|
||||
sudo apt-get update && sudo apt-get install -y postgresql-client
|
||||
|
||||
# Test connection using psql
|
||||
PGPASSWORD=test psql -h localhost -U test -d formbricks -c "\dt" || echo "Failed to connect to PostgreSQL"
|
||||
|
||||
# Show network configuration
|
||||
echo "Network configuration:"
|
||||
ip addr show
|
||||
netstat -tulpn | grep 5432 || echo "No process listening on port 5432"
|
||||
|
||||
- name: Test Docker Image with Health Check
|
||||
shell: bash
|
||||
run: |
|
||||
echo "🧪 Testing if the Docker image starts correctly..."
|
||||
|
||||
# Add extra docker run args to support host.docker.internal on Linux
|
||||
DOCKER_RUN_ARGS="--add-host=host.docker.internal:host-gateway"
|
||||
|
||||
# Start the container with host.docker.internal pointing to the host
|
||||
docker run --name formbricks-test \
|
||||
$DOCKER_RUN_ARGS \
|
||||
-p 3000:3000 \
|
||||
-e DATABASE_URL="postgresql://test:test@host.docker.internal:5432/formbricks" \
|
||||
-e ENCRYPTION_KEY="${{ secrets.DUMMY_ENCRYPTION_KEY }}" \
|
||||
-d formbricks-test:${{ github.sha }}
|
||||
|
||||
# Give it more time to start up
|
||||
echo "Waiting 45 seconds for application to start..."
|
||||
sleep 45
|
||||
|
||||
# Check if the container is running
|
||||
if [ "$(docker inspect -f '{{.State.Running}}' formbricks-test)" != "true" ]; then
|
||||
echo "❌ Container failed to start properly!"
|
||||
docker logs formbricks-test
|
||||
exit 1
|
||||
else
|
||||
echo "✅ Container started successfully!"
|
||||
fi
|
||||
|
||||
# Try connecting to PostgreSQL from inside the container
|
||||
echo "Testing PostgreSQL connection from inside container..."
|
||||
docker exec formbricks-test sh -c 'apt-get update && apt-get install -y postgresql-client && PGPASSWORD=test psql -h host.docker.internal -U test -d formbricks -c "\dt" || echo "Failed to connect to PostgreSQL from container"'
|
||||
|
||||
# Try to access the health endpoint
|
||||
echo "🏥 Testing /health endpoint..."
|
||||
MAX_RETRIES=10
|
||||
RETRY_COUNT=0
|
||||
HEALTH_CHECK_SUCCESS=false
|
||||
|
||||
set +e # Disable exit on error to allow for retries
|
||||
|
||||
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
|
||||
RETRY_COUNT=$((RETRY_COUNT + 1))
|
||||
echo "Attempt $RETRY_COUNT of $MAX_RETRIES..."
|
||||
|
||||
# Show container logs before each attempt to help debugging
|
||||
if [ $RETRY_COUNT -gt 1 ]; then
|
||||
echo "📋 Current container logs:"
|
||||
docker logs --tail 20 formbricks-test
|
||||
fi
|
||||
|
||||
# Get detailed curl output for debugging
|
||||
HTTP_OUTPUT=$(curl -v -s -m 30 http://localhost:3000/health 2>&1)
|
||||
CURL_EXIT_CODE=$?
|
||||
|
||||
echo "Curl exit code: $CURL_EXIT_CODE"
|
||||
echo "Curl output: $HTTP_OUTPUT"
|
||||
|
||||
if [ $CURL_EXIT_CODE -eq 0 ]; then
|
||||
STATUS_CODE=$(echo "$HTTP_OUTPUT" | grep -oP "HTTP/\d(\.\d)? \K\d+")
|
||||
echo "Status code detected: $STATUS_CODE"
|
||||
|
||||
if [ "$STATUS_CODE" = "200" ]; then
|
||||
echo "✅ Health check successful!"
|
||||
HEALTH_CHECK_SUCCESS=true
|
||||
break
|
||||
else
|
||||
echo "❌ Health check returned non-200 status code: $STATUS_CODE"
|
||||
fi
|
||||
else
|
||||
echo "❌ Curl command failed with exit code: $CURL_EXIT_CODE"
|
||||
fi
|
||||
|
||||
echo "Waiting 15 seconds before next attempt..."
|
||||
sleep 15
|
||||
done
|
||||
|
||||
# Show full container logs for debugging
|
||||
echo "📋 Full container logs:"
|
||||
docker logs formbricks-test
|
||||
|
||||
# Clean up the container
|
||||
echo "🧹 Cleaning up..."
|
||||
docker rm -f formbricks-test
|
||||
|
||||
# Exit with failure if health check did not succeed
|
||||
if [ "$HEALTH_CHECK_SUCCESS" != "true" ]; then
|
||||
echo "❌ Health check failed after $MAX_RETRIES attempts"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✨ Docker validation complete - all checks passed!"
|
||||
2
.github/workflows/sonarqube.yml
vendored
2
.github/workflows/sonarqube.yml
vendored
@@ -4,7 +4,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
merge_group:
|
||||
permissions:
|
||||
|
||||
@@ -22,7 +22,7 @@ RUN npm install -g corepack@latest
|
||||
RUN corepack enable
|
||||
|
||||
# Install necessary build tools and compilers
|
||||
RUN apk update && apk add --no-cache g++ cmake make gcc python3 openssl-dev jq
|
||||
RUN apk update && apk add --no-cache cmake g++ gcc jq make openssl-dev python3
|
||||
|
||||
# BuildKit secret handling without hardcoded fallback values
|
||||
# This approach relies entirely on secrets passed from GitHub Actions
|
||||
@@ -40,8 +40,6 @@ RUN echo '#!/bin/sh' > /tmp/read-secrets.sh && \
|
||||
echo 'exec "$@"' >> /tmp/read-secrets.sh && \
|
||||
chmod +x /tmp/read-secrets.sh
|
||||
|
||||
ARG SENTRY_AUTH_TOKEN
|
||||
|
||||
# Increase Node.js memory limit as a regular build argument
|
||||
ARG NODE_OPTIONS="--max_old_space_size=4096"
|
||||
ENV NODE_OPTIONS=${NODE_OPTIONS}
|
||||
@@ -87,31 +85,60 @@ RUN apk add --no-cache curl \
|
||||
|
||||
WORKDIR /home/nextjs
|
||||
|
||||
COPY --from=installer /app/apps/web/next.config.mjs .
|
||||
COPY --from=installer /app/apps/web/package.json .
|
||||
# Leverage output traces to reduce image size
|
||||
|
||||
# Ensure no write permissions are assigned to the copied resources
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/apps/web/.next/standalone ./
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/apps/web/.next/static ./apps/web/.next/static
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/apps/web/public ./apps/web/public
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/schema.prisma ./packages/database/schema.prisma
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/package.json ./packages/database/package.json
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/migration ./packages/database/migration
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/src ./packages/database/src
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/node_modules ./packages/database/node_modules
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/logger/dist ./packages/database/node_modules/@formbricks/logger/dist
|
||||
RUN chmod -R 755 ./
|
||||
|
||||
COPY --from=installer /app/apps/web/next.config.mjs .
|
||||
RUN chmod 644 ./next.config.mjs
|
||||
|
||||
COPY --from=installer /app/apps/web/package.json .
|
||||
RUN chmod 644 ./package.json
|
||||
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/apps/web/.next/static ./apps/web/.next/static
|
||||
RUN chmod -R 755 ./apps/web/.next/static
|
||||
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/apps/web/public ./apps/web/public
|
||||
RUN chmod -R 755 ./apps/web/public
|
||||
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/schema.prisma ./packages/database/schema.prisma
|
||||
RUN chmod 644 ./packages/database/schema.prisma
|
||||
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/package.json ./packages/database/package.json
|
||||
RUN chmod 644 ./packages/database/package.json
|
||||
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/migration ./packages/database/migration
|
||||
RUN chmod -R 755 ./packages/database/migration
|
||||
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/src ./packages/database/src
|
||||
RUN chmod -R 755 ./packages/database/src
|
||||
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/node_modules ./packages/database/node_modules
|
||||
RUN chmod -R 755 ./packages/database/node_modules
|
||||
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/logger/dist ./packages/database/node_modules/@formbricks/logger/dist
|
||||
RUN chmod -R 755 ./packages/database/node_modules/@formbricks/logger/dist
|
||||
|
||||
# Copy Prisma-specific generated files
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/node_modules/@prisma/client ./node_modules/@prisma/client
|
||||
RUN chmod -R 755 ./node_modules/@prisma/client
|
||||
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/node_modules/.prisma ./node_modules/.prisma
|
||||
RUN chmod -R 755 ./node_modules/.prisma
|
||||
|
||||
COPY --from=installer --chown=nextjs:nextjs /prisma_version.txt .
|
||||
COPY /docker/cronjobs /app/docker/cronjobs
|
||||
RUN chmod 644 ./prisma_version.txt
|
||||
|
||||
COPY /docker/cronjobs /app/docker/cronjobs
|
||||
RUN chmod -R 755 /app/docker/cronjobs
|
||||
|
||||
# Copy required dependencies
|
||||
COPY --from=installer /app/node_modules/@paralleldrive/cuid2 ./node_modules/@paralleldrive/cuid2
|
||||
RUN chmod -R 755 ./node_modules/@paralleldrive/cuid2
|
||||
|
||||
COPY --from=installer /app/node_modules/@noble/hashes ./node_modules/@noble/hashes
|
||||
RUN chmod -R 755 ./node_modules/@noble/hashes
|
||||
|
||||
COPY --from=installer /app/node_modules/zod ./node_modules/zod
|
||||
RUN chmod -R 755 ./node_modules/zod
|
||||
|
||||
RUN npm install -g tsx typescript prisma pino-pretty
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { getOrganizationAccessKeyDisplayName } from "@/modules/organization/settings/api-keys/lib/utils";
|
||||
import { TOrganizationProject } from "@/modules/organization/settings/api-keys/types/api-keys";
|
||||
import { Alert, AlertTitle } from "@/modules/ui/components/alert";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -15,7 +16,7 @@ import { Modal } from "@/modules/ui/components/modal";
|
||||
import { Switch } from "@/modules/ui/components/switch";
|
||||
import { ApiKeyPermission } from "@prisma/client";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { AlertTriangleIcon, ChevronDownIcon, Trash2Icon } from "lucide-react";
|
||||
import { ChevronDownIcon, Trash2Icon } from "lucide-react";
|
||||
import { Fragment, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "react-hot-toast";
|
||||
@@ -389,11 +390,9 @@ export const AddApiKeyModal = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center rounded-lg border border-slate-200 bg-slate-100 p-2 text-sm text-slate-700">
|
||||
<AlertTriangleIcon className="mx-3 h-12 w-12 text-amber-500" />
|
||||
<p>{t("environments.project.api_keys.api_key_security_warning")}</p>
|
||||
</div>
|
||||
<Alert variant="warning">
|
||||
<AlertTitle>{t("environments.project.api_keys.api_key_security_warning")}</AlertTitle>
|
||||
</Alert>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end border-t border-slate-200 p-6">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { getDefaultEndingCard } from "@/app/lib/templates";
|
||||
import { Alert, AlertButton, AlertDescription, AlertTitle } from "@/modules/ui/components/alert";
|
||||
import { Badge } from "@/modules/ui/components/badge";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/modules/ui/components/radio-group";
|
||||
@@ -8,8 +9,7 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { Environment } from "@prisma/client";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { AlertCircleIcon, CheckIcon, LinkIcon, MonitorIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { CheckIcon, LinkIcon, MonitorIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TSegment } from "@formbricks/types/segment";
|
||||
@@ -106,7 +106,7 @@ export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment }: HowT
|
||||
className="h-full w-full cursor-pointer"
|
||||
id="howToSendCardTrigger">
|
||||
<div className="inline-flex px-4 py-4">
|
||||
<div className="flex items-center pl-2 pr-5">
|
||||
<div className="flex items-center pr-5 pl-2">
|
||||
<CheckIcon
|
||||
strokeWidth={3}
|
||||
className="h-7 w-7 rounded-full border border-green-300 bg-green-100 p-1.5 text-green-600"
|
||||
@@ -171,23 +171,25 @@ export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment }: HowT
|
||||
</div>
|
||||
<p className="mt-2 text-xs font-normal text-slate-600">{option.description}</p>
|
||||
{localSurvey.type === option.id && option.alert && (
|
||||
<div className="mt-2 flex items-center space-x-3 rounded-lg border border-amber-200 bg-amber-50 px-4 py-2">
|
||||
<AlertCircleIcon className="h-5 w-5 text-amber-500" />
|
||||
<div className="text-amber-800">
|
||||
<p className="text-xs font-semibold">
|
||||
{t("environments.surveys.edit.formbricks_sdk_is_not_connected")}
|
||||
</p>
|
||||
<p className="text-xs font-normal">
|
||||
<Link
|
||||
href={`/environments/${environment.id}/project/${option.id}-connection`}
|
||||
className="underline hover:text-amber-900"
|
||||
target="_blank">
|
||||
{t("common.connect_formbricks")}
|
||||
</Link>{" "}
|
||||
{t("environments.surveys.edit.and_launch_surveys_in_your_website_or_app")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Alert variant="warning" className="mt-2">
|
||||
<AlertTitle>
|
||||
{t("environments.surveys.edit.formbricks_sdk_is_not_connected")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t("common.connect_formbricks") +
|
||||
" " +
|
||||
t("environments.surveys.edit.and_launch_surveys_in_your_website_or_app")}
|
||||
</AlertDescription>
|
||||
<AlertButton
|
||||
onClick={() =>
|
||||
window.open(
|
||||
`/environments/${environment.id}/project/${option.id}-connection`,
|
||||
"_blank"
|
||||
)
|
||||
}>
|
||||
{t("common.connect_formbricks")}
|
||||
</AlertButton>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from "@/modules/survey/editor/types/survey-follow-up";
|
||||
import FollowUpActionMultiEmailInput from "@/modules/survey/follow-ups/components/follow-up-action-multi-email-input";
|
||||
import { getQuestionIconMap } from "@/modules/survey/lib/questions";
|
||||
import { Alert, AlertTitle } from "@/modules/ui/components/alert";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Checkbox } from "@/modules/ui/components/checkbox";
|
||||
import { Editor } from "@/modules/ui/components/editor";
|
||||
@@ -423,17 +424,13 @@ export const FollowUpModal = ({
|
||||
</Select>
|
||||
|
||||
{triggerType === "endings" && !localSurvey.endings.length ? (
|
||||
<div className="mt-4 flex items-start text-yellow-600">
|
||||
<TriangleAlertIcon
|
||||
className="mr-2 h-5 min-h-5 w-5 min-w-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<p className="text-sm">
|
||||
<Alert variant="warning" size="small">
|
||||
<AlertTitle>
|
||||
{t(
|
||||
"environments.surveys.edit.follow_ups_modal_trigger_type_ending_warning"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</AlertTitle>
|
||||
</Alert>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { Alert, AlertButton, AlertDescription, AlertTitle } from "./index";
|
||||
|
||||
describe("Alert", () => {
|
||||
it("renders with default variant", () => {
|
||||
render(
|
||||
<Alert>
|
||||
<AlertTitle>Test Title</AlertTitle>
|
||||
<AlertDescription>Test Description</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
expect(screen.getByRole("alert")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Title")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Description")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders with different variants", () => {
|
||||
const variants = ["default", "error", "warning", "info", "success"] as const;
|
||||
|
||||
variants.forEach((variant) => {
|
||||
const { container } = render(
|
||||
<Alert variant={variant}>
|
||||
<AlertTitle>Test Title</AlertTitle>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
expect(container.firstChild).toHaveClass(
|
||||
variant === "default" ? "text-foreground" : `text-${variant}-foreground`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("renders with different sizes", () => {
|
||||
const sizes = ["default", "small"] as const;
|
||||
|
||||
sizes.forEach((size) => {
|
||||
const { container } = render(
|
||||
<Alert size={size}>
|
||||
<AlertTitle>Test Title</AlertTitle>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
expect(container.firstChild).toHaveClass(size === "default" ? "py-3" : "py-2");
|
||||
});
|
||||
});
|
||||
|
||||
it("renders with button and handles click", () => {
|
||||
const handleClick = vi.fn();
|
||||
|
||||
render(
|
||||
<Alert>
|
||||
<AlertTitle>Test Title</AlertTitle>
|
||||
<AlertButton onClick={handleClick}>Click me</AlertButton>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
const button = screen.getByText("Click me");
|
||||
expect(button).toBeInTheDocument();
|
||||
fireEvent.click(button);
|
||||
expect(handleClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("applies custom className", () => {
|
||||
const { container } = render(
|
||||
<Alert className="custom-class">
|
||||
<AlertTitle>Test Title</AlertTitle>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
expect(container.firstChild).toHaveClass("custom-class");
|
||||
});
|
||||
});
|
||||
@@ -21,23 +21,23 @@ const AlertContext = createContext<AlertContextValue>({
|
||||
const useAlertContext = () => useContext(AlertContext);
|
||||
|
||||
// Define alert styles with variants
|
||||
const alertVariants = cva("relative w-full rounded-lg border [&>svg]:size-4 [&>svg]:text-foreground", {
|
||||
const alertVariants = cva("relative w-full rounded-lg border [&>svg]:size-4", {
|
||||
variants: {
|
||||
variant: {
|
||||
default: "text-foreground border-border",
|
||||
error:
|
||||
"text-error-foreground border-error/50 [&_button]:bg-error-background [&_button]:text-error-foreground [&_button:hover]:bg-error-background-muted",
|
||||
"text-error-foreground [&>svg]:text-error border-error/50 [&_button]:bg-error-background [&_button]:text-error-foreground [&_button:hover]:bg-error-background-muted",
|
||||
warning:
|
||||
"text-warning-foreground border-warning/50 [&_button]:bg-warning-background [&_button]:text-warning-foreground [&_button:hover]:bg-warning-background-muted",
|
||||
info: "text-info-foreground border-info/50 [&_button]:bg-info-background [&_button]:text-info-foreground [&_button:hover]:bg-info-background-muted",
|
||||
"text-warning-foreground [&>svg]:text-warning border-warning/50 [&_button]:bg-warning-background [&_button]:text-warning-foreground [&_button:hover]:bg-warning-background-muted",
|
||||
info: "text-info-foreground [&>svg]:text-info border-info/50 [&_button]:bg-info-background [&_button]:text-info-foreground [&_button:hover]:bg-info-background-muted",
|
||||
success:
|
||||
"text-success-foreground border-success/50 [&_button]:bg-success-background [&_button]:text-success-foreground [&_button:hover]:bg-success-background-muted",
|
||||
"text-success-foreground [&>svg]:text-success border-success/50 [&_button]:bg-success-background [&_button]:text-success-foreground [&_button:hover]:bg-success-background-muted",
|
||||
},
|
||||
size: {
|
||||
default:
|
||||
"py-3 px-4 text-sm grid grid-cols-[1fr_auto] grid-rows-[auto_auto] gap-x-3 [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg~*]:pl-7",
|
||||
"py-3 px-4 text-sm grid grid-cols-[2fr_auto] grid-rows-[auto_auto] gap-y-0.5 gap-x-3 [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg~*]:pl-7",
|
||||
small:
|
||||
"px-3 py-2 text-xs flex items-center justify-between gap-2 [&>svg]:flex-shrink-0 [&_button]:text-xs [&_button]:bg-transparent [&_button:hover]:bg-transparent [&>svg~*]:pl-0",
|
||||
"px-4 py-2 text-xs flex items-center gap-2 [&>svg]:flex-shrink-0 [&_button]:bg-transparent [&_button:hover]:bg-transparent [&>svg~*]:pl-0",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
@@ -58,7 +58,7 @@ const Alert = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||
>(({ className, variant, size, ...props }, ref) => {
|
||||
const variantIcon = variant ? (variant !== "default" ? alertVariantIcons[variant] : null) : null;
|
||||
const variantIcon = variant && variant !== "default" ? alertVariantIcons[variant] : null;
|
||||
|
||||
return (
|
||||
<AlertContext.Provider value={{ variant, size }}>
|
||||
@@ -78,8 +78,8 @@ const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<H
|
||||
<h5
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"col-start-1 row-start-1 font-medium leading-none tracking-tight",
|
||||
size === "small" ? "min-w-0 flex-shrink truncate" : "col-start-1 row-start-1",
|
||||
"col-start-1 row-start-1 font-medium tracking-tight",
|
||||
size === "small" ? "flex-shrink truncate" : "col-start-1 row-start-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -99,9 +99,7 @@ const AlertDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttrib
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"[&_p]:leading-relaxed",
|
||||
size === "small"
|
||||
? "hidden min-w-0 flex-shrink flex-grow truncate opacity-80 sm:block" // Hidden on very small screens, limited width
|
||||
: "col-start-1 row-start-2",
|
||||
size === "small" ? "flex-shrink flex-grow-0 truncate" : "col-start-1 row-start-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -124,7 +122,7 @@ const AlertButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
className={cn(
|
||||
"self-end",
|
||||
alertSize === "small"
|
||||
? "-my-2 -mr-3 ml-auto flex-shrink-0"
|
||||
? "-my-2 -mr-4 ml-auto flex-shrink-0"
|
||||
: "col-start-2 row-span-2 row-start-1 flex items-center justify-center"
|
||||
)}>
|
||||
<Button ref={ref} variant={buttonVariant} size={buttonSize} className={className} {...props}>
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { Default, Error, Info, Small, Success, Warning, withButtonAndIcon } from "./stories";
|
||||
|
||||
describe("Alert Stories", () => {
|
||||
const renderStory = (Story: any) => {
|
||||
return render(Story.render(Story.args));
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("renders Default story", () => {
|
||||
renderStory(Default);
|
||||
expect(screen.getByText("Alert Title")).toBeInTheDocument();
|
||||
expect(screen.getByText("This is an important notification.")).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders Small story", () => {
|
||||
renderStory(Small);
|
||||
expect(screen.getByText("Information Alert")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button")).toBeInTheDocument();
|
||||
expect(screen.getByText("Learn more")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders withButtonAndIcon story", () => {
|
||||
renderStory(withButtonAndIcon);
|
||||
expect(screen.getByText("Alert Title")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button")).toBeInTheDocument();
|
||||
expect(screen.getByText("Learn more")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders Error story", () => {
|
||||
renderStory(Error);
|
||||
expect(screen.getByText("Error Alert")).toBeInTheDocument();
|
||||
expect(screen.getByText("Your session has expired. Please log in again.")).toBeInTheDocument();
|
||||
expect(screen.getByText("Log in")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders Warning story", () => {
|
||||
renderStory(Warning);
|
||||
expect(screen.getByText("Warning Alert")).toBeInTheDocument();
|
||||
expect(screen.getByText("You are editing sensitive data. Be cautious")).toBeInTheDocument();
|
||||
expect(screen.getByText("Proceed")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders Info story", () => {
|
||||
renderStory(Info);
|
||||
expect(screen.getByText("Info Alert")).toBeInTheDocument();
|
||||
expect(screen.getByText("There was an update to your application.")).toBeInTheDocument();
|
||||
expect(screen.getByText("Refresh")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders Success story", () => {
|
||||
renderStory(Success);
|
||||
expect(screen.getByText("Success Alert")).toBeInTheDocument();
|
||||
expect(screen.getByText("This worked! Please proceed.")).toBeInTheDocument();
|
||||
expect(screen.getByText("Close")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Alert, AlertButton, AlertTitle } from "@/modules/ui/components/alert";
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
import { LightbulbIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment, getEnvironments } from "@formbricks/lib/environment/service";
|
||||
|
||||
@@ -19,19 +20,19 @@ export const EnvironmentNotice = async ({ environmentId, subPageUrl }: Environme
|
||||
const otherEnvironmentId = environments.filter((e) => e.id !== environment.id)[0].id;
|
||||
|
||||
return (
|
||||
<div className="mt-4 flex max-w-4xl items-center space-y-4 rounded-lg border border-blue-100 bg-blue-50 p-4 text-sm text-blue-900 shadow-sm md:space-y-0 md:text-base">
|
||||
<LightbulbIcon className="mr-3 h-4 w-4 text-blue-400" />
|
||||
<p className="text-sm">
|
||||
{t("common.environment_notice", { environment: environment.type })}
|
||||
<a
|
||||
href={`${WEBAPP_URL}/environments/${otherEnvironmentId}${subPageUrl}`}
|
||||
className="ml-1 cursor-pointer text-sm underline">
|
||||
{t("common.switch_to", {
|
||||
environment: environment.type === "production" ? "Development" : "Production",
|
||||
})}
|
||||
.
|
||||
</a>
|
||||
</p>
|
||||
<div>
|
||||
<Alert variant="info" size="small" className="max-w-4xl">
|
||||
<AlertTitle>{t("common.environment_notice", { environment: environment.type })}</AlertTitle>
|
||||
<AlertButton>
|
||||
<Link
|
||||
href={`${WEBAPP_URL}/environments/${otherEnvironmentId}${subPageUrl}`}
|
||||
className="ml-1 cursor-pointer underline">
|
||||
{t("common.switch_to", {
|
||||
environment: environment.type === "production" ? "Development" : "Production",
|
||||
})}
|
||||
</Link>
|
||||
</AlertButton>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
|
||||
import { Alert, AlertTitle } from "@/modules/ui/components/alert";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { checkForYoutubeUrl, convertToEmbedUrl, extractYoutubeId } from "@formbricks/lib/utils/videoUpload";
|
||||
@@ -112,10 +112,9 @@ export const VideoSettings = ({
|
||||
</div>
|
||||
|
||||
{showPlatformWarning && (
|
||||
<div className="flex items-center space-x-2 rounded-md border bg-slate-100 p-2 text-xs text-slate-600">
|
||||
<AlertTriangle className="h-6 w-6" />
|
||||
<p>{t("environments.surveys.edit.invalid_video_url_warning")}</p>
|
||||
</div>
|
||||
<Alert variant="warning" size="small">
|
||||
<AlertTitle>{t("environments.surveys.edit.invalid_video_url_warning")}</AlertTitle>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{isYoutubeLink && (
|
||||
|
||||
@@ -128,7 +128,7 @@ class FormbricksViewModel : ViewModel() {
|
||||
val jsonObject = JsonObject()
|
||||
environmentDataHolder.getSurveyJson(surveyId).let { jsonObject.add("survey", it) }
|
||||
jsonObject.addProperty("isBrandingEnabled", true)
|
||||
jsonObject.addProperty("apiHost", Formbricks.appUrl)
|
||||
jsonObject.addProperty("appUrl", Formbricks.appUrl)
|
||||
jsonObject.addProperty("languageCode", Formbricks.language)
|
||||
jsonObject.addProperty("environmentId", Formbricks.environmentId)
|
||||
jsonObject.addProperty("contactId", UserManager.contactId)
|
||||
|
||||
@@ -92,7 +92,7 @@ private class WebViewData {
|
||||
data["survey"] = environmentResponse.getSurveyJson(forSurveyId: surveyId)
|
||||
data["isBrandingEnabled"] = true
|
||||
data["languageCode"] = Formbricks.language
|
||||
data["apiHost"] = Formbricks.appUrl
|
||||
data["appUrl"] = Formbricks.appUrl
|
||||
data["environmentId"] = Formbricks.environmentId
|
||||
data["contactId"] = UserManager.shared.contactId
|
||||
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
"dependsOn": ["@formbricks/api#build"],
|
||||
"persistent": true
|
||||
},
|
||||
"@formbricks/database#build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["dist/**"]
|
||||
},
|
||||
"@formbricks/database#lint": {
|
||||
"dependsOn": ["@formbricks/logger#build"]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user