tweaked UX complete

This commit is contained in:
Johannes
2026-03-04 20:14:44 -03:00
parent d3e26e29e1
commit fc8d43f980
6 changed files with 176 additions and 38 deletions

View File

@@ -13,13 +13,6 @@ import { useState } from "react";
import { useTranslation } from "react-i18next";
import { TConnectorWithMappings } from "@formbricks/types/connector";
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/modules/ui/components/dialog";
import {
DropdownMenu,
DropdownMenuContent,
@@ -28,7 +21,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/modules/ui/components/dropdown-menu";
import { CsvImportSection } from "./csv-import-section";
import { CsvImportModal } from "./csv-import-modal";
interface ConnectorRowDropdownProps {
connector: TConnectorWithMappings;
@@ -164,19 +157,12 @@ export function ConnectorRowDropdown({
/>
{connector.type === "csv" && (
<Dialog open={isCsvImportDialogOpen} onOpenChange={setIsCsvImportDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t("environments.unify.import_csv_data")}</DialogTitle>
<DialogDescription>{t("environments.unify.upload_csv_data_description")}</DialogDescription>
</DialogHeader>
<CsvImportSection
connectorId={connector.id}
environmentId={connector.environmentId}
onImportComplete={() => setIsCsvImportDialogOpen(false)}
/>
</DialogContent>
</Dialog>
<CsvImportModal
open={isCsvImportDialogOpen}
onOpenChange={setIsCsvImportDialogOpen}
connectorId={connector.id}
environmentId={connector.environmentId}
/>
)}
</div>
);

View File

@@ -18,6 +18,7 @@ import { UnifyConfigNavigation } from "../../components/UnifyConfigNavigation";
import { TFieldMapping, TUnifySurvey } from "../types";
import { ConnectorsTable } from "./connectors-table";
import { CreateConnectorModal } from "./create-connector-modal";
import { CsvImportModal } from "./csv-import-modal";
import { EditConnectorModal } from "./edit-connector-modal";
interface ConnectorsSectionProps {
@@ -35,6 +36,7 @@ export function ConnectorsSection({
const router = useRouter();
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [editingConnector, setEditingConnector] = useState<TConnectorWithMappings | null>(null);
const [csvImportConnector, setCsvImportConnector] = useState<TConnectorWithMappings | null>(null);
const handleCreateConnector = async (data: {
name: string;
@@ -179,7 +181,24 @@ export function ConnectorsSection({
onOpenChange={(open) => !open && setEditingConnector(null)}
onUpdateConnector={handleUpdateConnector}
surveys={initialSurveys}
onOpenCsvImport={() => {
if (editingConnector) {
setCsvImportConnector(editingConnector);
}
}}
/>
{csvImportConnector && (
<CsvImportModal
open={csvImportConnector !== null}
onOpenChange={(open) => !open && setCsvImportConnector(null)}
connectorId={csvImportConnector.id}
environmentId={csvImportConnector.environmentId}
onOpenEditConnector={() => {
setEditingConnector(csvImportConnector);
}}
/>
)}
</PageContentWrapper>
);
}

View File

@@ -0,0 +1,86 @@
"use client";
import { Loader2Icon } from "lucide-react";
import { useCallback, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/modules/ui/components/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/modules/ui/components/dialog";
import { CsvImportHandle, CsvImportSection, CsvImportState } from "./csv-import-section";
interface CsvImportModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
connectorId: string;
environmentId: string;
onOpenEditConnector?: () => void;
}
export function CsvImportModal({
open,
onOpenChange,
connectorId,
environmentId,
onOpenEditConnector,
}: CsvImportModalProps) {
const { t } = useTranslation();
const importHandleRef = useRef<CsvImportHandle | null>(null);
const [importState, setImportState] = useState<CsvImportState>({
rowCount: 0,
isImporting: false,
hasData: false,
});
const handleStateChange = useCallback((state: CsvImportState) => {
setImportState(state);
}, []);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t("environments.unify.import_csv_data")}</DialogTitle>
<DialogDescription>{t("environments.unify.upload_csv_data_description")}</DialogDescription>
</DialogHeader>
<CsvImportSection
connectorId={connectorId}
environmentId={environmentId}
onImportComplete={() => onOpenChange(false)}
onStateChange={handleStateChange}
handleRef={importHandleRef}
renderFooter={false}
/>
<DialogFooter>
{onOpenEditConnector && (
<Button
variant="secondary"
onClick={() => {
onOpenChange(false);
onOpenEditConnector();
}}>
{t("environments.unify.edit_csv_mapping")}
</Button>
)}
<Button
onClick={() => importHandleRef.current?.import()}
disabled={!importState.hasData || importState.isImporting}>
{importState.isImporting ? (
<>
<Loader2Icon className="mr-2 h-4 w-4 animate-spin" />
{t("environments.unify.importing_data")}
</>
) : (
t("environments.unify.import_rows", { count: importState.rowCount })
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -2,7 +2,7 @@
import { parse } from "csv-parse/sync";
import { ArrowUpFromLineIcon, Loader2Icon } from "lucide-react";
import { useState } from "react";
import { useEffect, useRef, useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { validateCsvFile } from "@/app/(app)/environments/[environmentId]/workspace/unify/sources/utils";
@@ -13,13 +13,33 @@ import { Badge } from "@/modules/ui/components/badge";
import { Button } from "@/modules/ui/components/button";
import { createFeedbackCSVDataSchema } from "../types";
export interface CsvImportState {
rowCount: number;
isImporting: boolean;
hasData: boolean;
}
export interface CsvImportHandle {
import: () => Promise<void>;
}
interface CsvImportSectionProps {
connectorId: string;
environmentId: string;
onImportComplete?: () => void;
onStateChange?: (state: CsvImportState) => void;
handleRef?: React.MutableRefObject<CsvImportHandle | null>;
renderFooter?: boolean;
}
export function CsvImportSection({ connectorId, environmentId, onImportComplete }: CsvImportSectionProps) {
export function CsvImportSection({
connectorId,
environmentId,
onImportComplete,
onStateChange,
handleRef,
renderFooter = true,
}: CsvImportSectionProps) {
const { t } = useTranslation();
const [csvFile, setCsvFile] = useState<File | null>(null);
const [rowCount, setRowCount] = useState(0);
@@ -99,6 +119,17 @@ export function CsvImportSection({ connectorId, environmentId, onImportComplete
}
};
const handleImportRef = useRef(handleImport);
handleImportRef.current = handleImport;
if (handleRef) {
handleRef.current = { import: () => handleImportRef.current() };
}
useEffect(() => {
onStateChange?.({ rowCount, isImporting, hasData: parsedData.length > 0 });
}, [rowCount, isImporting, parsedData.length, onStateChange]);
const handleClear = () => {
setCsvFile(null);
setParsedData([]);
@@ -108,9 +139,9 @@ export function CsvImportSection({ connectorId, environmentId, onImportComplete
return (
<div className="space-y-3">
<div className="rounded-lg border border-amber-200 bg-amber-50 p-3">
<p className="text-xs text-amber-800">{t("environments.unify.csv_import_duplicate_warning")}</p>
</div>
<Alert variant="info" size="small">
{t("environments.unify.csv_import_duplicate_warning")}
</Alert>
{csvError && (
<Alert variant="error" size="small">
@@ -130,16 +161,18 @@ export function CsvImportSection({ connectorId, environmentId, onImportComplete
</Button>
</div>
<Button onClick={handleImport} disabled={isImporting} className="w-full">
{isImporting ? (
<>
<Loader2Icon className="mr-2 h-4 w-4 animate-spin" />
{t("environments.unify.importing_data")}
</>
) : (
t("environments.unify.import_rows", { count: rowCount })
)}
</Button>
{renderFooter && (
<Button onClick={handleImport} disabled={isImporting} className="w-full">
{isImporting ? (
<>
<Loader2Icon className="mr-2 h-4 w-4 animate-spin" />
{t("environments.unify.importing_data")}
</>
) : (
t("environments.unify.import_rows", { count: rowCount })
)}
</Button>
)}
</div>
) : (
<div className="rounded-lg border-2 border-dashed border-slate-300 bg-slate-50 p-6">

View File

@@ -38,6 +38,7 @@ interface EditConnectorModalProps {
fieldMappings?: TFieldMapping[];
}) => Promise<void>;
surveys: TUnifySurvey[];
onOpenCsvImport?: () => void;
}
const getConnectorIcon = (type: TConnectorType) => {
@@ -79,6 +80,7 @@ export const EditConnectorModal = ({
onOpenChange,
onUpdateConnector,
surveys,
onOpenCsvImport,
}: EditConnectorModalProps) => {
const { t } = useTranslation();
const [connectorName, setConnectorName] = useState("");
@@ -271,6 +273,16 @@ export const EditConnectorModal = ({
</div>
<DialogFooter>
{connector.type === "csv" && (
<Button
variant="secondary"
onClick={() => {
handleOpenChange(false);
onOpenCsvImport?.();
}}>
{t("environments.unify.import_feedback")}
</Button>
)}
<Button onClick={handleUpdate} disabled={saveChangesDisbaled}>
{t("environments.unify.save_changes")}
</Button>

View File

@@ -2064,7 +2064,7 @@
"csv_files_only": "CSV files only",
"csv_import": "CSV Import",
"csv_import_complete": "CSV import complete: {successes} succeeded, {failures} failed, {skipped} skipped",
"csv_import_duplicate_warning": "Importing data that was already imported may create duplicate records.",
"csv_import_duplicate_warning": "Importing data twice will create duplicate records.",
"csv_inconsistent_columns": "Row {row} has inconsistent columns. All rows must have the same headers.",
"csv_max_records": "Maximum {max} records allowed.",
"default_connector_name_csv": "CSV Import",
@@ -2072,6 +2072,7 @@
"deselect_all": "Deselect all",
"drop_a_field_here": "Drop a field here",
"drop_field_or": "Drop field or",
"edit_csv_mapping": "Edit CSV mapping",
"edit_source_connection": "Edit Source Connection",
"enter_name_for_source": "Enter a name for this source",
"enter_value": "Enter value...",
@@ -2080,7 +2081,8 @@
"feedback_record_fields": "Feedback Record Fields",
"formbricks_surveys": "Formbricks Surveys",
"historical_import_complete": "Import complete: {successes} succeeded, {failures} failed, {skipped} skipped (no data)",
"import_csv_data": "Import CSV Data",
"import_csv_data": "Import feedback",
"import_feedback": "Import feedback",
"import_rows": "Import {count} rows",
"importing_data": "Importing data...",
"importing_historical_data": "Importing historical data...",