remove previous changes and reduce duplicate

This commit is contained in:
harshsbhat
2025-07-16 13:55:16 +05:30
parent 416f142385
commit 44c5bec535
6 changed files with 278 additions and 214 deletions
@@ -0,0 +1,129 @@
"use client";
import { Button } from "@/modules/ui/components/button";
import { EmptySpaceFiller } from "@/modules/ui/components/empty-space-filler";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
import React from "react";
import { TEnvironment } from "@formbricks/types/environment";
interface Column<T> {
/** Header text rendered in the table head */
header: React.ReactNode;
/** Cell renderer for an item */
render: (item: T) => React.ReactNode;
}
interface ActionButtonProps {
label: string;
onClick: () => void;
/** Optional Lucide Icon */
icon?: React.ReactNode;
/** Tooltip content */
tooltip?: string;
/** Variant override */
variant?: "default" | "outline" | "secondary" | "destructive" | "ghost";
}
interface IntegrationListPanelProps<T> {
environment: TEnvironment;
/** Green dot + status text */
statusNode: React.ReactNode;
/** Reconnect button props */
reconnectAction: ActionButtonProps;
/** Add new mapping button props */
addNewAction: ActionButtonProps;
/** Empty state message */
emptyMessage: string;
/** Data rows */
items: T[];
/** Columns definition (max 3 for current UI) */
columns: Column<T>[];
/** Row click handler */
onRowClick: (index: number) => void;
/** Function to derive unique key for row */
getRowKey?: (item: T, index: number) => string | number;
}
/**
* Generic list panel used by integration manage pages to avoid repeating toolbar + table boilerplate.
*/
export function IntegrationListPanel<T>({
environment,
statusNode,
reconnectAction,
addNewAction,
emptyMessage,
items,
columns,
onRowClick,
getRowKey,
}: IntegrationListPanelProps<T>) {
return (
<div className="mt-6 flex w-full flex-col items-center justify-center p-6">
{/* Toolbar */}
<div className="flex w-full justify-end space-x-2">
<div className="mr-6 flex items-center">{statusNode}</div>
{/* Re-connect */}
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant={reconnectAction.variant ?? "outline"} onClick={reconnectAction.onClick}>
{reconnectAction.icon}
{reconnectAction.label}
</Button>
</TooltipTrigger>
{reconnectAction.tooltip && <TooltipContent>{reconnectAction.tooltip}</TooltipContent>}
</Tooltip>
</TooltipProvider>
{/* Add new */}
<Button variant={addNewAction.variant ?? "default"} onClick={addNewAction.onClick}>
{addNewAction.icon}
{addNewAction.label}
</Button>
</div>
{/* Empty table view */}
{!items || items.length === 0 ? (
<div className="mt-4 w-full">
<EmptySpaceFiller
type="table"
environment={environment}
noWidgetRequired={true}
emptyMessage={emptyMessage}
/>
</div>
) : (
<div className="mt-4 flex w-full flex-col items-center justify-center">
<div className="mt-6 w-full rounded-lg border border-slate-200">
{/* Header */}
<div className="grid h-12 grid-cols-6 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
{columns.map((col, idx) => (
<div key={`hdr-${idx}`} className="col-span-2 hidden text-center sm:block">
{col.header}
</div>
))}
</div>
{/* Rows */}
{items.map((item, index) => {
const key = getRowKey ? getRowKey(item, index) : index;
return (
<button
key={key}
className="grid h-16 w-full cursor-pointer grid-cols-6 content-center rounded-lg p-2 hover:bg-slate-100"
onClick={() => onRowClick(index)}>
{columns.map((col, idx) => (
<div key={`cell-${idx}`} className="col-span-2 text-center">
{col.render(item)}
</div>
))}
</button>
);
})}
</div>
</div>
)}
</div>
);
}
@@ -1,45 +0,0 @@
"use client";
import { Button } from "@/modules/ui/components/button";
import { DialogFooter } from "@/modules/ui/components/dialog";
interface IntegrationModalFooterProps {
hasExistingIntegration: boolean;
deleteLabel: string;
cancelLabel: string;
submitLabel: string;
isDeleting: boolean;
onDelete: () => void;
onCancel: () => void;
submitLoading: boolean;
submitDisabled: boolean;
}
export const IntegrationModalFooter = ({
hasExistingIntegration,
deleteLabel,
cancelLabel,
submitLabel,
isDeleting,
onDelete,
onCancel,
submitLoading,
submitDisabled,
}: IntegrationModalFooterProps) => {
return (
<DialogFooter>
{hasExistingIntegration ? (
<Button type="button" variant="destructive" loading={isDeleting} onClick={onDelete}>
{deleteLabel}
</Button>
) : (
<Button type="button" variant="secondary" onClick={onCancel}>
{cancelLabel}
</Button>
)}
<Button type="submit" loading={submitLoading} disabled={submitDisabled}>
{submitLabel}
</Button>
</DialogFooter>
);
};
@@ -16,6 +16,7 @@ import {
DialogBody,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/modules/ui/components/dialog";
@@ -34,7 +35,6 @@ import {
TIntegrationNotionDatabase,
} from "@formbricks/types/integration/notion";
import { TSurvey } from "@formbricks/types/surveys/types";
import { IntegrationModalFooter } from "../../components/IntegrationModalFooter";
interface AddIntegrationModalProps {
environmentId: string;
@@ -491,23 +491,36 @@ export const AddIntegrationModal = ({
</div>
</DialogBody>
<IntegrationModalFooter
hasExistingIntegration={!!selectedIntegration}
deleteLabel={t("common.delete")}
cancelLabel={t("common.cancel")}
submitLabel={
selectedIntegration ? t("common.update") : t("environments.integrations.notion.link_database")
}
isDeleting={isDeleting}
onDelete={deleteLink}
onCancel={() => {
setOpen(false);
resetForm();
setMapping([]);
}}
submitLoading={isLinkingDatabase}
submitDisabled={mapping.filter((m) => m.error).length > 0}
/>
<DialogFooter>
{selectedIntegration ? (
<Button
type="button"
variant="destructive"
loading={isDeleting}
onClick={() => {
deleteLink();
}}>
{t("common.delete")}
</Button>
) : (
<Button
type="button"
variant="secondary"
onClick={() => {
setOpen(false);
resetForm();
setMapping([]);
}}>
{t("common.cancel")}
</Button>
)}
<Button
type="submit"
loading={isLinkingDatabase}
disabled={mapping.filter((m) => m.error).length > 0}>
{selectedIntegration ? t("common.update") : t("environments.integrations.notion.link_database")}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
@@ -5,8 +5,6 @@ import { timeSince } from "@/lib/time";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { Button } from "@/modules/ui/components/button";
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
import { EmptySpaceFiller } from "@/modules/ui/components/empty-space-filler";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
import { useTranslate } from "@tolgee/react";
import { RefreshCcwIcon, Trash2Icon } from "lucide-react";
import React, { useState } from "react";
@@ -14,6 +12,7 @@ import toast from "react-hot-toast";
import { TEnvironment } from "@formbricks/types/environment";
import { TIntegrationNotion, TIntegrationNotionConfigData } from "@formbricks/types/integration/notion";
import { TUserLocale } from "@formbricks/types/user";
import { IntegrationListPanel } from "../../components/IntegrationListPanel";
interface ManageIntegrationProps {
environment: TEnvironment;
@@ -70,74 +69,52 @@ export const ManageIntegration = ({
};
return (
<div className="mt-6 flex w-full flex-col items-center justify-center p-6">
<div className="flex w-full justify-end space-x-2">
<div className="mr-6 flex items-center">
<span className="mr-4 h-4 w-4 rounded-full bg-green-600"></span>
<span className="text-slate-500">
{t("environments.integrations.notion.connected_with_workspace", {
workspace: notionIntegration.config.key.workspace_name,
})}
</span>
</div>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline" onClick={handleNotionAuthorization}>
<RefreshCcwIcon className="mr-2 h-4 w-4" />
{t("environments.integrations.notion.update_connection")}
</Button>
</TooltipTrigger>
<TooltipContent>{t("environments.integrations.notion.update_connection_tooltip")}</TooltipContent>
</Tooltip>
</TooltipProvider>
<Button
onClick={() => {
<>
<IntegrationListPanel
environment={environment}
statusNode={
<>
<span className="mr-4 h-4 w-4 rounded-full bg-green-600"></span>
<span className="text-slate-500">
{t("environments.integrations.notion.connected_with_workspace", {
workspace: notionIntegration.config.key.workspace_name,
})}
</span>
</>
}
reconnectAction={{
label: t("environments.integrations.notion.update_connection"),
onClick: handleNotionAuthorization,
icon: <RefreshCcwIcon className="mr-2 h-4 w-4" />,
tooltip: t("environments.integrations.notion.update_connection_tooltip"),
variant: "outline",
}}
addNewAction={{
label: t("environments.integrations.notion.link_new_database"),
onClick: () => {
setSelectedIntegration(null);
setOpenAddIntegrationModal(true);
}}>
{t("environments.integrations.notion.link_new_database")}
</Button>
</div>
{!integrationArray || integrationArray.length === 0 ? (
<div className="mt-4 w-full">
<EmptySpaceFiller
type="table"
environment={environment}
noWidgetRequired={true}
emptyMessage={t("environments.integrations.notion.no_databases_found")}
/>
</div>
) : (
<div className="mt-4 flex w-full flex-col items-center justify-center">
<div className="mt-6 w-full rounded-lg border border-slate-200">
<div className="grid h-12 grid-cols-6 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
<div className="col-span-2 hidden text-center sm:block">{t("common.survey")}</div>
<div className="col-span-2 hidden text-center sm:block">
{t("environments.integrations.notion.database_name")}
</div>
<div className="col-span-2 hidden text-center sm:block">{t("common.updated_at")}</div>
</div>
{integrationArray &&
integrationArray.map((data, index) => {
return (
<button
key={`${index}-${data.databaseId}`}
className="grid h-16 w-full cursor-pointer grid-cols-6 content-center rounded-lg p-2 hover:bg-slate-100"
onClick={() => {
editIntegration(index);
}}>
<div className="col-span-2 text-center">{data.surveyName}</div>
<div className="col-span-2 text-center">{data.databaseName}</div>
<div className="col-span-2 text-center">
{timeSince(data.createdAt.toString(), locale)}
</div>
</button>
);
})}
</div>
</div>
)}
},
}}
emptyMessage={t("environments.integrations.notion.no_databases_found")}
items={integrationArray}
columns={[
{
header: t("common.survey"),
render: (item: TIntegrationNotionConfigData) => item.surveyName,
},
{
header: t("environments.integrations.notion.database_name"),
render: (item: TIntegrationNotionConfigData) => item.databaseName,
},
{
header: t("common.updated_at"),
render: (item: TIntegrationNotionConfigData) => timeSince(item.createdAt.toString(), locale),
},
]}
onRowClick={editIntegration}
getRowKey={(item: TIntegrationNotionConfigData, idx) => `${idx}-${item.databaseId}`}
/>
<Button variant="ghost" onClick={() => setIsDeleteIntegrationModalOpen(true)} className="mt-4">
<Trash2Icon />
{t("environments.integrations.delete_integration")}
@@ -151,6 +128,6 @@ export const ManageIntegration = ({
text={t("environments.integrations.delete_integration_confirmation")}
isDeleting={isDeleting}
/>
</div>
</>
);
};
@@ -10,6 +10,7 @@ import {
DialogBody,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/modules/ui/components/dialog";
@@ -29,7 +30,6 @@ import {
TPlainMapping,
} from "@formbricks/types/integration/plain";
import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
import { IntegrationModalFooter } from "../../components/IntegrationModalFooter";
import { INITIAL_MAPPING, PLAIN_FIELD_TYPES } from "../constants";
interface AddIntegrationModalProps {
@@ -524,22 +524,35 @@ export const AddIntegrationModal = ({
</div>
</DialogBody>
<IntegrationModalFooter
hasExistingIntegration={!!selectedIntegration}
deleteLabel={t("common.delete")}
cancelLabel={t("common.cancel")}
submitLabel={
selectedIntegration ? t("common.update") : t("environments.integrations.plain.connect")
}
isDeleting={isDeleting}
onDelete={deleteLink}
onCancel={() => {
setOpen(false);
resetForm();
}}
submitLoading={isLinkingIntegration}
submitDisabled={mapping.filter((m) => m.error).length > 0}
/>
<DialogFooter>
{selectedIntegration ? (
<Button
type="button"
variant="destructive"
loading={isDeleting}
onClick={() => {
deleteLink();
}}>
{t("common.delete")}
</Button>
) : (
<Button
type="button"
variant="secondary"
onClick={() => {
setOpen(false);
resetForm();
}}>
{t("common.cancel")}
</Button>
)}
<Button
type="submit"
loading={isLinkingIntegration}
disabled={mapping.filter((m) => m.error).length > 0}>
{selectedIntegration ? t("common.update") : t("environments.integrations.plain.connect")}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
@@ -5,8 +5,6 @@ import { timeSince } from "@/lib/time";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { Button } from "@/modules/ui/components/button";
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
import { EmptySpaceFiller } from "@/modules/ui/components/empty-space-filler";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
import { useTranslate } from "@tolgee/react";
import { RefreshCcwIcon, Trash2Icon } from "lucide-react";
import React, { useState } from "react";
@@ -14,6 +12,7 @@ import toast from "react-hot-toast";
import { TEnvironment } from "@formbricks/types/environment";
import { TIntegrationPlain, TIntegrationPlainConfigData } from "@formbricks/types/integration/plain";
import { TUserLocale } from "@formbricks/types/user";
import { IntegrationListPanel } from "../../components/IntegrationListPanel";
import { AddKeyModal } from "./AddKeyModal";
interface ManageIntegrationProps {
@@ -70,70 +69,48 @@ export const ManageIntegration = ({
};
return (
<div className="mt-6 flex w-full flex-col items-center justify-center p-6">
<div className="flex w-full justify-end space-x-2">
<div className="mr-6 flex items-center">
<span className="mr-4 h-4 w-4 rounded-full bg-green-600"></span>
<span className="text-slate-500">{t("common.connected")}</span>
</div>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline" onClick={() => setIsKeyModalOpen(true)}>
<RefreshCcwIcon className="mr-2 h-4 w-4" />
{t("environments.integrations.plain.update_connection")}
</Button>
</TooltipTrigger>
<TooltipContent>{t("environments.integrations.plain.update_connection_tooltip")}</TooltipContent>
</Tooltip>
</TooltipProvider>
<Button
onClick={() => {
<>
<IntegrationListPanel
environment={environment}
statusNode={
<>
<span className="mr-4 h-4 w-4 rounded-full bg-green-600"></span>
<span className="text-slate-500">{t("common.connected")}</span>
</>
}
reconnectAction={{
label: t("environments.integrations.plain.update_connection"),
onClick: () => setIsKeyModalOpen(true),
icon: <RefreshCcwIcon className="mr-2 h-4 w-4" />,
tooltip: t("environments.integrations.plain.update_connection_tooltip"),
variant: "outline",
}}
addNewAction={{
label: t("environments.integrations.plain.link_new_database"),
onClick: () => {
setSelectedIntegration(null);
setOpenAddIntegrationModal(true);
}}>
{t("environments.integrations.plain.link_new_database")}
</Button>
</div>
{!integrationArray || integrationArray.length === 0 ? (
<div className="mt-4 w-full">
<EmptySpaceFiller
type="table"
environment={environment}
noWidgetRequired={true}
emptyMessage={t("environments.integrations.plain.no_databases_found")}
/>
</div>
) : (
<div className="mt-4 flex w-full flex-col items-center justify-center">
<div className="mt-6 w-full rounded-lg border border-slate-200">
<div className="grid h-12 grid-cols-6 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
<div className="col-span-2 hidden text-center sm:block">{t("common.survey")}</div>
<div className="col-span-2 hidden text-center sm:block">
{t("environments.integrations.plain.survey_id")}
</div>
<div className="col-span-2 hidden text-center sm:block">{t("common.updated_at")}</div>
</div>
{integrationArray &&
integrationArray.map((data, index) => {
return (
<button
key={`${index}-${data.surveyId}`}
className="grid h-16 w-full cursor-pointer grid-cols-6 content-center rounded-lg p-2 hover:bg-slate-100"
onClick={() => {
editIntegration(index);
}}>
<div className="col-span-2 text-center">{data.surveyName}</div>
<div className="col-span-2 text-center">{data.surveyId}</div>
<div className="col-span-2 text-center">
{timeSince(data.createdAt.toString(), locale)}
</div>
</button>
);
})}
</div>
</div>
)}
},
}}
emptyMessage={t("environments.integrations.plain.no_databases_found")}
items={integrationArray}
columns={[
{
header: t("common.survey"),
render: (item: TIntegrationPlainConfigData) => item.surveyName,
},
{
header: t("environments.integrations.plain.survey_id"),
render: (item: TIntegrationPlainConfigData) => item.surveyId,
},
{
header: t("common.updated_at"),
render: (item: TIntegrationPlainConfigData) => timeSince(item.createdAt.toString(), locale),
},
]}
onRowClick={editIntegration}
getRowKey={(item: TIntegrationPlainConfigData, idx) => `${idx}-${item.surveyId}`}
/>
<Button variant="ghost" onClick={() => setIsDeleteIntegrationModalOpen(true)} className="mt-4">
<Trash2Icon />
{t("environments.integrations.delete_integration")}
@@ -149,6 +126,6 @@ export const ManageIntegration = ({
text={t("environments.integrations.delete_integration_confirmation")}
isDeleting={isDeleting}
/>
</div>
</>
);
};