mirror of
https://github.com/formbricks/formbricks.git
synced 2026-03-04 18:28:54 -06:00
tweaked UX complete
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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...",
|
||||
|
||||
Reference in New Issue
Block a user