mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-17 19:14:53 -05:00
clean ups
This commit is contained in:
@@ -61,6 +61,70 @@ export const createChartAction = authenticatedActionClient.schema(ZCreateChartAc
|
||||
)
|
||||
);
|
||||
|
||||
const ZUpdateChartAction = z.object({
|
||||
environmentId: ZId,
|
||||
chartId: ZId,
|
||||
name: z.string().min(1).optional(),
|
||||
type: z
|
||||
.enum(["area", "bar", "line", "pie", "big_number", "big_number_total", "table", "funnel", "map"])
|
||||
.optional(),
|
||||
query: z.record(z.any()).optional(),
|
||||
config: z.record(z.any()).optional(),
|
||||
});
|
||||
|
||||
export const updateChartAction = authenticatedActionClient.schema(ZUpdateChartAction).action(
|
||||
withAuditLogging(
|
||||
"updated",
|
||||
"chart",
|
||||
async ({ ctx, parsedInput }: { ctx: any; parsedInput: z.infer<typeof ZUpdateChartAction> }) => {
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(parsedInput.environmentId);
|
||||
const projectId = await getProjectIdFromEnvironmentId(parsedInput.environmentId);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "projectTeam",
|
||||
minPermission: "readWrite",
|
||||
projectId,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Verify chart belongs to the project
|
||||
const chart = await prisma.chart.findFirst({
|
||||
where: { id: parsedInput.chartId, projectId },
|
||||
});
|
||||
|
||||
if (!chart) {
|
||||
throw new Error("Chart not found");
|
||||
}
|
||||
|
||||
const updateData: any = {};
|
||||
if (parsedInput.name !== undefined) updateData.name = parsedInput.name;
|
||||
if (parsedInput.type !== undefined) updateData.type = parsedInput.type as ChartType;
|
||||
if (parsedInput.query !== undefined) updateData.query = parsedInput.query;
|
||||
if (parsedInput.config !== undefined) updateData.config = parsedInput.config;
|
||||
|
||||
const updatedChart = await prisma.chart.update({
|
||||
where: { id: parsedInput.chartId },
|
||||
data: updateData,
|
||||
});
|
||||
|
||||
ctx.auditLoggingCtx.organizationId = organizationId;
|
||||
ctx.auditLoggingCtx.projectId = projectId;
|
||||
ctx.auditLoggingCtx.oldObject = chart;
|
||||
ctx.auditLoggingCtx.newObject = updatedChart;
|
||||
return updatedChart;
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const ZAddChartToDashboardAction = z.object({
|
||||
environmentId: ZId,
|
||||
chartId: ZId,
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
executeQueryAction,
|
||||
getChartAction,
|
||||
getDashboardsAction,
|
||||
updateChartAction,
|
||||
} from "../../actions";
|
||||
import { mapChartType, mapDatabaseChartTypeToApi } from "../lib/chart-utils";
|
||||
import { AIQuerySection } from "./AIQuerySection";
|
||||
@@ -42,6 +43,7 @@ export function ChartBuilderClient({ environmentId, chartId }: ChartBuilderClien
|
||||
const [configuredChartType, setConfiguredChartType] = useState<string | null>(null);
|
||||
const [showAdvancedBuilder, setShowAdvancedBuilder] = useState(false);
|
||||
const [isLoadingChart, setIsLoadingChart] = useState(false);
|
||||
const [currentChartId, setCurrentChartId] = useState<string | undefined>(chartId);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAddToDashboardDialogOpen) {
|
||||
@@ -86,6 +88,7 @@ export function ChartBuilderClient({ environmentId, chartId }: ChartBuilderClien
|
||||
|
||||
setChartData(chartData);
|
||||
setConfiguredChartType(mapDatabaseChartTypeToApi(chart.type));
|
||||
setCurrentChartId(chart.id);
|
||||
} else {
|
||||
toast.error("No data returned for chart");
|
||||
}
|
||||
@@ -114,22 +117,44 @@ export function ChartBuilderClient({ environmentId, chartId }: ChartBuilderClien
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const result = await createChartAction({
|
||||
environmentId,
|
||||
name: chartName,
|
||||
type: mapChartType(chartData.chartType),
|
||||
query: chartData.query,
|
||||
config: {},
|
||||
});
|
||||
// If we have a currentChartId, update the existing chart; otherwise create a new one
|
||||
if (currentChartId) {
|
||||
const result = await updateChartAction({
|
||||
environmentId,
|
||||
chartId: currentChartId,
|
||||
name: chartName.trim(),
|
||||
type: mapChartType(chartData.chartType),
|
||||
query: chartData.query,
|
||||
config: {},
|
||||
});
|
||||
|
||||
if (!result?.data) {
|
||||
toast.error(result?.serverError || "Failed to save chart");
|
||||
return;
|
||||
if (!result?.data) {
|
||||
toast.error(result?.serverError || "Failed to update chart");
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success("Chart updated successfully!");
|
||||
setIsSaveDialogOpen(false);
|
||||
router.push(`/environments/${environmentId}/analysis/charts`);
|
||||
} else {
|
||||
const result = await createChartAction({
|
||||
environmentId,
|
||||
name: chartName.trim(),
|
||||
type: mapChartType(chartData.chartType),
|
||||
query: chartData.query,
|
||||
config: {},
|
||||
});
|
||||
|
||||
if (!result?.data) {
|
||||
toast.error(result?.serverError || "Failed to save chart");
|
||||
return;
|
||||
}
|
||||
|
||||
setCurrentChartId(result.data.id);
|
||||
toast.success("Chart saved successfully!");
|
||||
setIsSaveDialogOpen(false);
|
||||
router.push(`/environments/${environmentId}/analysis/charts`);
|
||||
}
|
||||
|
||||
toast.success("Chart saved successfully!");
|
||||
setIsSaveDialogOpen(false);
|
||||
router.push(`/environments/${environmentId}/analysis/charts`);
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || "Failed to save chart");
|
||||
} finally {
|
||||
@@ -145,22 +170,38 @@ export function ChartBuilderClient({ environmentId, chartId }: ChartBuilderClien
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const chartResult = await createChartAction({
|
||||
environmentId,
|
||||
name: chartName || `Chart ${new Date().toLocaleString()}`,
|
||||
type: mapChartType(chartData.chartType),
|
||||
query: chartData.query,
|
||||
config: {},
|
||||
});
|
||||
let chartIdToUse = currentChartId;
|
||||
|
||||
if (!chartResult?.data) {
|
||||
toast.error(chartResult?.serverError || "Failed to save chart");
|
||||
return;
|
||||
// If we don't have a chartId (creating new chart), create it first
|
||||
if (!chartIdToUse) {
|
||||
if (!chartName.trim()) {
|
||||
toast.error("Please enter a chart name");
|
||||
setIsSaving(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const chartResult = await createChartAction({
|
||||
environmentId,
|
||||
name: chartName.trim(),
|
||||
type: mapChartType(chartData.chartType),
|
||||
query: chartData.query,
|
||||
config: {},
|
||||
});
|
||||
|
||||
if (!chartResult?.data) {
|
||||
toast.error(chartResult?.serverError || "Failed to save chart");
|
||||
setIsSaving(false);
|
||||
return;
|
||||
}
|
||||
|
||||
chartIdToUse = chartResult.data.id;
|
||||
setCurrentChartId(chartIdToUse);
|
||||
}
|
||||
|
||||
// Add the chart (existing or newly created) to the dashboard
|
||||
const widgetResult = await addChartToDashboardAction({
|
||||
environmentId,
|
||||
chartId: chartResult.data.id,
|
||||
chartId: chartIdToUse,
|
||||
dashboardId: selectedDashboardId,
|
||||
});
|
||||
|
||||
@@ -212,6 +253,37 @@ export function ChartBuilderClient({ environmentId, chartId }: ChartBuilderClien
|
||||
onSave={() => setIsSaveDialogOpen(true)}
|
||||
onAddToDashboard={() => setIsAddToDashboardDialogOpen(true)}
|
||||
/>
|
||||
|
||||
{/* Dialogs */}
|
||||
<SaveChartDialog
|
||||
open={isSaveDialogOpen}
|
||||
onOpenChange={setIsSaveDialogOpen}
|
||||
chartName={chartName}
|
||||
onChartNameChange={setChartName}
|
||||
onSave={handleSaveChart}
|
||||
isSaving={isSaving}
|
||||
/>
|
||||
|
||||
<AddToDashboardDialog
|
||||
open={isAddToDashboardDialogOpen}
|
||||
onOpenChange={setIsAddToDashboardDialogOpen}
|
||||
chartName={chartName}
|
||||
onChartNameChange={setChartName}
|
||||
dashboards={dashboards}
|
||||
selectedDashboardId={selectedDashboardId}
|
||||
onDashboardSelect={setSelectedDashboardId}
|
||||
onAdd={handleAddToDashboard}
|
||||
isSaving={isSaving}
|
||||
/>
|
||||
|
||||
<ConfigureChartDialog
|
||||
open={isConfigureDialogOpen}
|
||||
onOpenChange={setIsConfigureDialogOpen}
|
||||
currentChartType={chartData?.chartType || "bar"}
|
||||
configuredChartType={configuredChartType}
|
||||
onChartTypeSelect={setConfiguredChartType}
|
||||
onReset={() => setConfiguredChartType(null)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,9 +25,9 @@ export function ChartsListClient({ charts, dashboards, environmentId }: ChartsLi
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Header / Actions */}
|
||||
<div className="flex flex-col gap-4 border-b border-gray-200 bg-white px-6 py-4">
|
||||
<div className="flex flex-col gap-4 border-gray-200 px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-semibold text-gray-900">Charts</h1>
|
||||
<h1 className="text-xl font-semibold text-gray-900"></h1>
|
||||
<div className="flex items-center gap-2">
|
||||
<Link href="chart-builder">
|
||||
<Button size="sm">
|
||||
@@ -38,22 +38,7 @@ export function ChartsListClient({ charts, dashboards, environmentId }: ChartsLi
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative">
|
||||
<SearchIcon className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
||||
<Input placeholder="Type a value" className="w-[300px] pl-9" />
|
||||
</div>
|
||||
{/* Filter Dropdowns */}
|
||||
<div className="no-scrollbar flex items-center gap-2 overflow-x-auto">
|
||||
{["Type", "Dataset", "Owner", "Dashboard", "Favorite", "Certified", "Modified by"].map(
|
||||
(filter) => (
|
||||
<Button key={filter} variant="outline" className="whitespace-nowrap text-gray-500" size="sm">
|
||||
{filter} <span className="ml-2 text-xs">▼</span>
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4"></div>
|
||||
</div>
|
||||
|
||||
{/* Table Content */}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogBody,
|
||||
@@ -9,7 +10,6 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/modules/ui/components/dialog";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
|
||||
interface CreateDashboardDialogProps {
|
||||
@@ -35,7 +35,7 @@ export function CreateDashboardDialog({
|
||||
}: CreateDashboardDialogProps) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Dashboard</DialogTitle>
|
||||
<DialogDescription>Enter a name for your dashboard to create it.</DialogDescription>
|
||||
|
||||
@@ -15,15 +15,18 @@ import {
|
||||
} from "@/modules/ui/components/dropdown-menu";
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
import { createDashboardAction } from "../../actions";
|
||||
import { CreateDashboardDialog } from "./CreateDashboardDialog";
|
||||
import { TDashboard } from "../../types/analysis";
|
||||
import { CreateDashboardDialog } from "./CreateDashboardDialog";
|
||||
|
||||
interface DashboardsListClientProps {
|
||||
dashboards: TDashboard[];
|
||||
environmentId: string;
|
||||
}
|
||||
|
||||
export function DashboardsListClient({ dashboards: initialDashboards, environmentId }: DashboardsListClientProps) {
|
||||
export function DashboardsListClient({
|
||||
dashboards: initialDashboards,
|
||||
environmentId,
|
||||
}: DashboardsListClientProps) {
|
||||
const router = useRouter();
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [dashboards] = useState(initialDashboards);
|
||||
@@ -75,25 +78,8 @@ export function DashboardsListClient({ dashboards: initialDashboards, environmen
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Header / Actions */}
|
||||
<div className="flex items-center justify-between border-b border-gray-200 bg-white px-6 py-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative">
|
||||
<SearchIcon className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
||||
<Input
|
||||
placeholder="Search dashboards"
|
||||
className="w-[300px] pl-9"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{/* Filter Dropdowns - Visual Only for MVP port */}
|
||||
<Button variant="outline" className="text-gray-500">
|
||||
Owner <span className="ml-2 text-xs">▼</span>
|
||||
</Button>
|
||||
<Button variant="outline" className="text-gray-500">
|
||||
Status <span className="ml-2 text-xs">▼</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-gray-200 px-6 py-4">
|
||||
<div className="flex items-center gap-4"></div>
|
||||
<Button onClick={handleCreateDashboard}>
|
||||
<PlusIcon className="mr-2 h-4 w-4" />
|
||||
Dashboard
|
||||
|
||||
Reference in New Issue
Block a user