From 2009089e2d2f7e858957f1b087585be734620daf Mon Sep 17 00:00:00 2001 From: AmitChauhan63390 Date: Sun, 26 Jan 2025 14:22:36 +0530 Subject: [PATCH 01/20] airtable Oauth --- .gitignore | 2 + package.json | 8 + server/config/config.json | 26 ++ ...125120216-add-airtable-columns-to-robot.js | 16 + ...0125182726-update-airtable-token-length.js | 22 + server/src/models/Robot.ts | 28 +- server/src/routes/auth.ts | 243 ++++++++++ server/src/server.ts | 11 + .../integration/IntegrationSettings.tsx | 422 +++++++++++------- 9 files changed, 617 insertions(+), 161 deletions(-) create mode 100644 server/config/config.json create mode 100644 server/migrations/20250125120216-add-airtable-columns-to-robot.js create mode 100644 server/migrations/20250125182726-update-airtable-token-length.js diff --git a/.gitignore b/.gitignore index 74279fc9..a2a80766 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ /build package-lock.json + +docker-compose.yml \ No newline at end of file diff --git a/package.json b/package.json index 5dae78a2..974dfc06 100644 --- a/package.json +++ b/package.json @@ -16,16 +16,19 @@ "@types/bcrypt": "^5.0.2", "@types/body-parser": "^1.19.5", "@types/csurf": "^1.11.5", + "@types/express-session": "^1.18.1", "@types/jsonwebtoken": "^9.0.7", "@types/node": "22.7.9", "@types/react": "^18.0.5", "@types/react-dom": "^18.0.1", "@types/uuid": "^8.3.4", + "airtable": "^0.12.2", "axios": "^0.26.0", "bcrypt": "^5.1.1", "body-parser": "^1.20.3", "buffer": "^6.0.3", "bullmq": "^5.12.15", + "connect-redis": "^8.0.1", "cookie-parser": "^1.4.6", "cors": "^2.8.5", "cron-parser": "^4.9.0", @@ -33,6 +36,7 @@ "csurf": "^1.11.0", "dotenv": "^16.0.0", "express": "^4.17.2", + "express-session": "^1.18.1", "fortawesome": "^0.0.1-security", "google-auth-library": "^9.14.1", "googleapis": "^144.0.0", @@ -52,6 +56,7 @@ "moment-timezone": "^0.5.45", "node-cron": "^3.0.3", "pg": "^8.13.0", + "pkce-challenge": "^4.1.0", "playwright": "^1.48.2", "playwright-extra": "^4.3.6", "posthog-node": "^4.2.1", @@ -65,6 +70,7 @@ "react-router-dom": "^6.26.1", "react-simple-code-editor": "^0.11.2", "react-transition-group": "^4.4.2", + "redis": "^4.7.0", "sequelize": "^6.37.3", "sequelize-typescript": "^2.1.6", "sharp": "^0.33.5", @@ -106,6 +112,7 @@ "@types/prismjs": "^1.26.0", "@types/react-highlight": "^0.12.5", "@types/react-transition-group": "^4.4.4", + "@types/redis": "^4.0.11", "@types/styled-components": "^5.1.23", "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.6", @@ -115,6 +122,7 @@ "cross-env": "^7.0.3", "js-cookie": "^3.0.5", "nodemon": "^2.0.15", + "sequelize-cli": "^6.6.2", "ts-node": "^10.4.0", "vite": "^5.4.10" } diff --git a/server/config/config.json b/server/config/config.json new file mode 100644 index 00000000..395914f9 --- /dev/null +++ b/server/config/config.json @@ -0,0 +1,26 @@ +{ + "development": { + "username": "postgres", + "password": "postgres", + "database": "maxun", + "host": "localhost", + "port": 5432, + "dialect": "postgres" + }, + "test": { + "username": "postgres", + "password": "postgres", + "database": "maxun_test", + "host": "localhost", + "port": 5432, + "dialect": "postgres" + }, + "production": { + "username": "postgres", + "password": "postgres", + "database": "maxun_prod", + "host": "localhost", + "port": 5432, + "dialect": "postgres" + } + } \ No newline at end of file diff --git a/server/migrations/20250125120216-add-airtable-columns-to-robot.js b/server/migrations/20250125120216-add-airtable-columns-to-robot.js new file mode 100644 index 00000000..ac2c852b --- /dev/null +++ b/server/migrations/20250125120216-add-airtable-columns-to-robot.js @@ -0,0 +1,16 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + + await queryInterface.addColumn('robot', 'airtable_refresh_token', { + type: Sequelize.STRING, + allowNull: true, + }); + }, + + down: async (queryInterface, Sequelize) => { + + await queryInterface.removeColumn('robot', 'airtable_refresh_token'); + }, +}; \ No newline at end of file diff --git a/server/migrations/20250125182726-update-airtable-token-length.js b/server/migrations/20250125182726-update-airtable-token-length.js new file mode 100644 index 00000000..b75d788c --- /dev/null +++ b/server/migrations/20250125182726-update-airtable-token-length.js @@ -0,0 +1,22 @@ +// migrations/XXXXXXXXXXXXXX-update-airtable-token-length.js +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.changeColumn('robot', 'airtable_access_token', { + type: Sequelize.TEXT + }); + + await queryInterface.changeColumn('robot', 'airtable_refresh_token', { + type: Sequelize.TEXT + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.changeColumn('robot', 'airtable_access_token', { + type: Sequelize.STRING(255) + }); + + await queryInterface.changeColumn('robot', 'airtable_refresh_token', { + type: Sequelize.STRING(255) + }); + } +}; \ No newline at end of file diff --git a/server/src/models/Robot.ts b/server/src/models/Robot.ts index 3b2717d6..ffd4746d 100644 --- a/server/src/models/Robot.ts +++ b/server/src/models/Robot.ts @@ -25,6 +25,10 @@ interface RobotAttributes { google_sheet_id?: string | null; google_access_token?: string | null; google_refresh_token?: string | null; + airtable_base_id?: string | null; // New field for Airtable base ID + airtable_table_name?: string | null; // New field for Airtable table name + airtable_access_token?: string | null; // New field for Airtable access token + airtable_refresh_token?: string | null; // New field for Airtable refresh token schedule?: ScheduleConfig | null; } @@ -49,10 +53,14 @@ class Robot extends Model implements R public recording_meta!: RobotMeta; public recording!: RobotWorkflow; public google_sheet_email!: string | null; - public google_sheet_name?: string | null; - public google_sheet_id?: string | null; + public google_sheet_name!: string | null; + public google_sheet_id!: string | null; public google_access_token!: string | null; public google_refresh_token!: string | null; + public airtable_base_id!: string | null; // New field for Airtable base ID + public airtable_table_name!: string | null; // New field for Airtable table name + public airtable_access_token!: string | null; // New field for Airtable access token + public airtable_refresh_token!: string | null; // New field for Airtable refresh token public schedule!: ScheduleConfig | null; } @@ -95,6 +103,22 @@ Robot.init( type: DataTypes.STRING, allowNull: true, }, + airtable_base_id: { + type: DataTypes.STRING, + allowNull: true, + }, + airtable_table_name: { + type: DataTypes.STRING, + allowNull: true, + }, + airtable_access_token: { + type: DataTypes.TEXT, + allowNull: true, + }, + airtable_refresh_token: { + type: DataTypes.TEXT, + allowNull: true, + }, schedule: { type: DataTypes.JSONB, allowNull: true, diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 837fcd8a..574e2176 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -1,4 +1,6 @@ import { Router, Request, Response } from "express"; +import Airtable from "airtable"; + import User from "../models/User"; import Robot from "../models/Robot"; import jwt from "jsonwebtoken"; @@ -7,6 +9,16 @@ import { requireSignIn } from "../middlewares/auth"; import { genAPIKey } from "../utils/api"; import { google } from "googleapis"; import { capture } from "../utils/analytics"; + + + +declare module "express-session" { + interface SessionData { + code_verifier: string; + robotId: string; + } +} + export const router = Router(); interface AuthenticatedRequest extends Request { @@ -555,3 +567,234 @@ router.post( } } ); + + + + +import crypto from 'crypto'; + +// Add these environment variables to your .env file +// AIRTABLE_CLIENT_ID=your_client_id +// AIRTABLE_CLIENT_SECRET=your_client_secret +// AIRTABLE_REDIRECT_URI=http://localhost:8080/auth/airtable/callback + +// Airtable OAuth Routes +router.get("/airtable", (req, res) => { + const { robotId } = req.query; + if (!robotId) { + return res.status(400).json({ message: "Robot ID is required" }); + } + + // Generate PKCE codes + const code_verifier = crypto.randomBytes(64).toString('base64url'); + const code_challenge = crypto.createHash('sha256') + .update(code_verifier) + .digest('base64url'); + + // Store in session + req.session.code_verifier = code_verifier; + req.session.robotId = robotId.toString(); + + const params = new URLSearchParams({ + client_id: process.env.AIRTABLE_CLIENT_ID!, + redirect_uri: process.env.AIRTABLE_REDIRECT_URI!, + + response_type: 'code', + state: robotId.toString(), + scope: 'data.records:read data.records:write schema.bases:read', + code_challenge: code_challenge, + code_challenge_method: 'S256' + }); + + res.redirect(`https://airtable.com/oauth2/v1/authorize?${params}`); +}); + +router.get("/airtable/callback", async (req, res) => { + try { + const { code, state, error } = req.query; + + if (error) { + return res.redirect( + `${process.env.PUBLIC_URL}/robots/${state}/integrate?error=${encodeURIComponent(error.toString())}` + ); + } + + if (!code || !state) { + return res.status(400).json({ message: "Missing authorization code or state" }); + } + + // Verify session data + if (!req.session?.code_verifier || req.session.robotId !== state.toString()) { + return res.status(400).json({ + message: "Session expired - please restart the OAuth flow" + }); + } + + // Exchange code for tokens + const tokenResponse = await fetch("https://airtable.com/oauth2/v1/token", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + grant_type: "authorization_code", + code: code.toString(), + client_id: process.env.AIRTABLE_CLIENT_ID!, + + redirect_uri: process.env.AIRTABLE_REDIRECT_URI!, + code_verifier: req.session.code_verifier + }), + }); + + if (!tokenResponse.ok) { + const errorData = await tokenResponse.json(); + console.error('Token exchange failed:', errorData); + return res.redirect( + `${process.env.PUBLIC_URL}/robots/${state}/integrate?error=${encodeURIComponent(errorData.error_description || 'Authentication failed')}` + ); + } + + const tokens = await tokenResponse.json(); + + // Update robot with credentials + const robot = await Robot.findOne({ + where: { "recording_meta.id": req.session.robotId } + }); + + if (!robot) { + return res.status(404).json({ message: "Robot not found" }); + } + + await robot.update({ + airtable_access_token: tokens.access_token, + airtable_refresh_token: tokens.refresh_token, + + }); + + // Clear session data + req.session.destroy((err) => { + if (err) console.error('Session cleanup error:', err); + }); + + res.redirect( + `${process.env.PUBLIC_URL}/robots/${state}/integrate?success=true` + ); + + } catch (error: any) { + console.error('Airtable callback error:', error); + res.redirect( + `${process.env.PUBLIC_URL}/robots/${req.session.robotId}/integrate?error=${encodeURIComponent(error.message)}` + ); + } +}); + +// Get Airtable bases +router.get("/airtable/bases", requireSignIn, async (req: AuthenticatedRequest, res) => { + try { + const { robotId } = req.query; + if (!robotId) { + return res.status(400).json({ message: "Robot ID is required" }); + } + + const robot = await Robot.findOne({ + where: { "recording_meta.id": robotId }, + raw: true, + }); + + if (!robot?.airtable_access_token) { + return res.status(400).json({ message: "Robot not authenticated with Airtable" }); + } + + const response = await fetch('https://api.airtable.com/v0/meta/bases', { + headers: { + 'Authorization': `Bearer ${robot.airtable_access_token}` + } + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error.message || 'Failed to fetch bases'); + } + + const data = await response.json(); + res.json(data.bases.map((base: any) => ({ + id: base.id, + name: base.name + }))); + + } catch (error: any) { + res.status(500).json({ message: error.message }); + } +}); + +// Update robot with selected base +router.post("/airtable/update", requireSignIn, async (req: AuthenticatedRequest, res) => { + const { baseId, baseName, robotId } = req.body; + + if (!baseId || !robotId) { + return res.status(400).json({ message: "Base ID and Robot ID are required" }); + } + + try { + const robot = await Robot.findOne({ + where: { "recording_meta.id": robotId } + }); + + if (!robot) { + return res.status(404).json({ message: "Robot not found" }); + } + + await robot.update({ + airtable_base_id: baseId, + airtable_table_name: baseName, + }); + + capture("maxun-oss-airtable-integration-created", { + user_id: req.user?.id, + robot_id: robotId, + created_at: new Date().toISOString(), + }); + + res.json({ message: "Airtable base updated successfully" }); + + } catch (error: any) { + res.status(500).json({ message: error.message }); + } +}); + +// Remove Airtable integration +router.post("/airtable/remove", requireSignIn, async (req: AuthenticatedRequest, res) => { + const { robotId } = req.body; + if (!robotId) { + return res.status(400).json({ message: "Robot ID is required" }); + } + + try { + const robot = await Robot.findOne({ + where: { "recording_meta.id": robotId } + }); + + if (!robot) { + return res.status(404).json({ message: "Robot not found" }); + } + + await robot.update({ + airtable_access_token: null, + airtable_refresh_token: null, + airtable_base_id: null, + + }); + + capture("maxun-oss-airtable-integration-removed", { + user_id: req.user?.id, + robot_id: robotId, + deleted_at: new Date().toISOString(), + }); + + res.json({ message: "Airtable integration removed successfully" }); + + } catch (error: any) { + res.status(500).json({ message: error.message }); + } +}); + diff --git a/server/src/server.ts b/server/src/server.ts index 8c28c2d2..94197cfb 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -18,6 +18,7 @@ import { fork } from 'child_process'; import { capture } from "./utils/analytics"; import swaggerUi from 'swagger-ui-express'; import swaggerSpec from './swagger/config'; +import session from 'express-session'; const app = express(); app.use(cors({ @@ -26,6 +27,16 @@ app.use(cors({ })); app.use(express.json()); + +app.use( + session({ + secret: 'your_secret_key', // Replace with a secure secret key + resave: false, // Do not resave the session if it hasn't changed + saveUninitialized: true, // Save new sessions + cookie: { secure: false }, // Set to true if using HTTPS + }) +); + const server = http.createServer(app); /** diff --git a/src/components/integration/IntegrationSettings.tsx b/src/components/integration/IntegrationSettings.tsx index f9c397ae..9101b9a7 100644 --- a/src/components/integration/IntegrationSettings.tsx +++ b/src/components/integration/IntegrationSettings.tsx @@ -6,7 +6,6 @@ import { CircularProgress, Alert, AlertTitle, - Chip, } from "@mui/material"; import Button from "@mui/material/Button"; import TextField from "@mui/material/TextField"; @@ -14,10 +13,9 @@ import axios from "axios"; import { useGlobalInfoStore } from "../../context/globalInfo"; import { getStoredRecording } from "../../api/storage"; import { apiUrl } from "../../apiConfig.js"; -import Cookies from 'js-cookie'; +import Cookies from "js-cookie"; import { useTranslation } from "react-i18next"; - interface IntegrationProps { isOpen: boolean; handleStart: (data: IntegrationSettings) => void; @@ -27,6 +25,8 @@ interface IntegrationProps { export interface IntegrationSettings { spreadsheetId: string; spreadsheetName: string; + airtableBaseId: string; + airtableBaseName: string; data: string; } @@ -35,7 +35,7 @@ const getCookie = (name: string): string | null => { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) { - return parts.pop()?.split(';').shift() || null; + return parts.pop()?.split(";").shift() || null; } return null; }; @@ -53,58 +53,114 @@ export const IntegrationSettingsModal = ({ const [settings, setSettings] = useState({ spreadsheetId: "", spreadsheetName: "", + airtableBaseId: "", + airtableBaseName: "", data: "", }); - const [spreadsheets, setSpreadsheets] = useState< - { id: string; name: string }[] - >([]); + const [spreadsheets, setSpreadsheets] = useState<{ id: string; name: string }[]>([]); + const [airtableBases, setAirtableBases] = useState<{ id: string; name: string }[]>([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const { recordingId, notify } = useGlobalInfoStore(); const [recording, setRecording] = useState(null); + // Authenticate with Google const authenticateWithGoogle = () => { - window.location.href = `${apiUrl}/auth/google?robotId=${recordingId}`; + const redirectUri = `${window.location.origin}/google/callback`; + window.location.href = `${apiUrl}/auth/google?robotId=${recordingId}&redirect_uri=${encodeURIComponent(redirectUri)}`; + }; + + const authenticateWithAirtable = () => { + const redirectUri = `${window.location.origin}/airtable/callback`; + window.location.href = `${apiUrl}/auth/airtable?robotId=${recordingId}&redirect_uri=${encodeURIComponent(redirectUri)}`; }; - const handleOAuthCallback = async () => { + // Handle OAuth callback + const handleGoogleCallback = async () => { try { - const response = await axios.get(`${apiUrl}/auth/google/callback`); - const { google_sheet_email, files } = response.data; + const urlParams = new URLSearchParams(window.location.search); + const code = urlParams.get('code'); + + if (!code) { + setError(t("integration_settings.errors.no_auth_code")); + return; + } + + const response = await axios.get( + `${apiUrl}/auth/google/callback?code=${code}&robotId=${recordingId}` + ); + + if (response.data.accessToken) { + notify("success", t("integration_settings.notifications.google_auth_success")); + await fetchSpreadsheetFiles(); + } + + // Clear URL parameters + window.history.replaceState({}, document.title, window.location.pathname); + } catch (error) { - setError("Error authenticating with Google"); + setError(t("integration_settings.errors.google_auth_error")); } }; + const handleAirtableCallback = async () => { + try { + const urlParams = new URLSearchParams(window.location.search); + const code = urlParams.get('code'); + + if (!code) { + setError(t("integration_settings.errors.no_auth_code")); + return; + } + + const response = await axios.get( + `${apiUrl}/auth/airtable/callback?code=${code}&robotId=${recordingId}` + ); + + if (response.data.accessToken) { + notify("success", t("integration_settings.notifications.airtable_auth_success")); + await fetchAirtableBases(); + } + + // Clear URL parameters + window.history.replaceState({}, document.title, window.location.pathname); + + } catch (error) { + setError(t("integration_settings.errors.airtable_auth_error")); + } + }; + + // Fetch Google Sheets const fetchSpreadsheetFiles = async () => { try { - const response = await axios.get( - `${apiUrl}/auth/gsheets/files?robotId=${recordingId}`, - { - withCredentials: true, - } - ); + const response = await axios.get(`${apiUrl}/auth/gsheets/files?robotId=${recordingId}`, { + withCredentials: true, + }); setSpreadsheets(response.data); } catch (error: any) { - console.error( - "Error fetching spreadsheet files:", - error.response?.data?.message || error.message - ); - notify( - "error", - t('integration_settings.errors.fetch_error', { - message: error.response?.data?.message || error.message - }) - ); + console.error("Error fetching spreadsheet files:", error.response?.data?.message || error.message); + notify("error", t("integration_settings.errors.fetch_error", { message: error.response?.data?.message || error.message })); } }; + // Fetch Airtable Bases + const fetchAirtableBases = async () => { + try { + const response = await axios.get(`${apiUrl}/auth/airtable/bases?robotId=${recordingId}`, { + withCredentials: true, + }); + setAirtableBases(response.data); + } catch (error: any) { + console.error("Error fetching Airtable bases:", error.response?.data?.message || error.message); + notify("error", t("integration_settings.errors.fetch_error", { message: error.response?.data?.message || error.message })); + } + }; + + // Handle Google Sheet selection const handleSpreadsheetSelect = (e: React.ChangeEvent) => { - const selectedSheet = spreadsheets.find( - (sheet) => sheet.id === e.target.value - ); + const selectedSheet = spreadsheets.find((sheet) => sheet.id === e.target.value); if (selectedSheet) { setSettings({ ...settings, @@ -114,6 +170,19 @@ export const IntegrationSettingsModal = ({ } }; + // Handle Airtable Base selection + const handleAirtableBaseSelect = (e: React.ChangeEvent) => { + const selectedBase = airtableBases.find((base) => base.id === e.target.value); + if (selectedBase) { + setSettings({ + ...settings, + airtableBaseId: selectedBase.id, + airtableBaseName: selectedBase.name, + }); + } + }; + + // Update Google Sheet ID const updateGoogleSheetId = async () => { try { const response = await axios.post( @@ -125,16 +194,33 @@ export const IntegrationSettingsModal = ({ }, { withCredentials: true } ); - notify(`success`, t('integration_settings.notifications.sheet_selected')); + notify("success", t("integration_settings.notifications.sheet_selected")); console.log("Google Sheet ID updated:", response.data); } catch (error: any) { - console.error( - "Error updating Google Sheet ID:", - error.response?.data?.message || error.message - ); + console.error("Error updating Google Sheet ID:", error.response?.data?.message || error.message); } }; + // Update Airtable Base ID + const updateAirtableBaseId = async () => { + try { + const response = await axios.post( + `${apiUrl}/auth/airtable/update`, + { + baseId: settings.airtableBaseId, + baseName: settings.airtableBaseName, + robotId: recordingId, + }, + { withCredentials: true } + ); + notify("success", t("integration_settings.notifications.base_selected")); + console.log("Airtable Base ID updated:", response.data); + } catch (error: any) { + console.error("Error updating Airtable Base ID:", error.response?.data?.message || error.message); + } + }; + + // Remove Integration const removeIntegration = async () => { try { await axios.post( @@ -145,98 +231,89 @@ export const IntegrationSettingsModal = ({ setRecording(null); setSpreadsheets([]); - setSettings({ spreadsheetId: "", spreadsheetName: "", data: "" }); + setAirtableBases([]); + setSettings({ spreadsheetId: "", spreadsheetName: "", airtableBaseId: "", airtableBaseName: "", data: "" }); } catch (error: any) { - console.error( - "Error removing Google Sheets integration:", - error.response?.data?.message || error.message - ); + console.error("Error removing integration:", error.response?.data?.message || error.message); } }; useEffect(() => { - // Check if there is a success message in cookies - const status = getCookie("robot_auth_status"); - const message = getCookie("robot_auth_message"); - - if (status === "success" && message) { - notify("success", message); - // Clear the cookies after reading - removeCookie("robot_auth_status"); - removeCookie("robot_auth_message"); - } - - // Check if we're on the callback URL - const urlParams = new URLSearchParams(window.location.search); - const code = urlParams.get("code"); - if (code) { - handleOAuthCallback(); - } - - const fetchRecordingInfo = async () => { - if (!recordingId) return; - const recording = await getStoredRecording(recordingId); - if (recording) { - setRecording(recording); + const checkAuthCallback = () => { + const path = window.location.pathname; + const urlParams = new URLSearchParams(window.location.search); + const code = urlParams.get('code'); + + if (code) { + if (path.includes('/google/callback')) { + handleGoogleCallback(); + } else if (path.includes('/airtable/callback')) { + handleAirtableCallback(); + } } }; - - fetchRecordingInfo(); - }, [recordingId]); + + checkAuthCallback(); + + // Cleanup function + return () => { + window.history.replaceState({}, document.title, window.location.pathname); + }; + }, []); return ( -
- - {t('integration_settings.title')} - +
+ {t("integration_settings.title")} - {recording && recording.google_sheet_id ? ( + {recording && (recording.google_sheet_id || recording.airtable_base_id) ? ( <> - - {t('integration_settings.alerts.success.title')} - {t('integration_settings.alerts.success.content', { sheetName: recording.google_sheet_name })} - - {t('integration_settings.alerts.success.here')} - . -
- {t('integration_settings.alerts.success.note')} {t('integration_settings.alerts.success.sync_limitation')} -
- ) : ( <> - {!recording?.google_sheet_email ? ( + {!recording?.google_sheet_email && !recording?.airtable_email ? ( <> -

{t('integration_settings.descriptions.sync_info')}

- + ) : ( <> - {recording.google_sheet_email && ( + {(recording.google_sheet_email || recording.airtable_email) && ( - {t('integration_settings.descriptions.authenticated_as', { - email: recording.google_sheet_email + {t("integration_settings.descriptions.authenticated_as", { + email: recording.google_sheet_email || recording.airtable_email, })} )} @@ -245,64 +322,91 @@ export const IntegrationSettingsModal = ({ ) : error ? ( {error} - ) : spreadsheets.length === 0 ? ( - <> -
- - -
- ) : ( <> - - {spreadsheets.map((sheet) => ( - - {sheet.name} - - ))} - + {recording.google_sheet_email && ( + <> + + {spreadsheets.map((sheet) => ( + + {sheet.name} + + ))} + - {settings.spreadsheetId && ( - - {t('integration_settings.fields.selected_sheet', { - name: spreadsheets.find((s) => s.id === settings.spreadsheetId)?.name, - id: settings.spreadsheetId - })} - + {settings.spreadsheetId && ( + + {t("integration_settings.fields.selected_sheet", { + name: spreadsheets.find((s) => s.id === settings.spreadsheetId)?.name, + id: settings.spreadsheetId, + })} + + )} + + + )} - + {recording.airtable_email && ( + <> + + {airtableBases.map((base) => ( + + {base.name} + + ))} + + + {settings.airtableBaseId && ( + + {t("integration_settings.fields.selected_base", { + name: airtableBases.find((b) => b.id === settings.airtableBaseId)?.name, + id: settings.airtableBaseId, + })} + + )} + + + + )} )} From 6dfdb496db09ab8f5e0d79057e92867298ad86d8 Mon Sep 17 00:00:00 2001 From: AmitChauhan63390 Date: Sun, 26 Jan 2025 14:33:34 +0530 Subject: [PATCH 02/20] integrationsettings --- .../integration/IntegrationSettings.tsx | 76 ++++++++++++++----- 1 file changed, 55 insertions(+), 21 deletions(-) diff --git a/src/components/integration/IntegrationSettings.tsx b/src/components/integration/IntegrationSettings.tsx index 9101b9a7..874b248d 100644 --- a/src/components/integration/IntegrationSettings.tsx +++ b/src/components/integration/IntegrationSettings.tsx @@ -71,62 +71,62 @@ export const IntegrationSettingsModal = ({ const redirectUri = `${window.location.origin}/google/callback`; window.location.href = `${apiUrl}/auth/google?robotId=${recordingId}&redirect_uri=${encodeURIComponent(redirectUri)}`; }; - + + // Authenticate with Airtable const authenticateWithAirtable = () => { const redirectUri = `${window.location.origin}/airtable/callback`; window.location.href = `${apiUrl}/auth/airtable?robotId=${recordingId}&redirect_uri=${encodeURIComponent(redirectUri)}`; }; - // Handle OAuth callback + // Handle Google OAuth callback const handleGoogleCallback = async () => { try { const urlParams = new URLSearchParams(window.location.search); - const code = urlParams.get('code'); - + const code = urlParams.get("code"); + if (!code) { setError(t("integration_settings.errors.no_auth_code")); return; } - + const response = await axios.get( `${apiUrl}/auth/google/callback?code=${code}&robotId=${recordingId}` ); - + if (response.data.accessToken) { notify("success", t("integration_settings.notifications.google_auth_success")); await fetchSpreadsheetFiles(); } - + // Clear URL parameters window.history.replaceState({}, document.title, window.location.pathname); - } catch (error) { setError(t("integration_settings.errors.google_auth_error")); } }; + // Handle Airtable OAuth callback const handleAirtableCallback = async () => { try { const urlParams = new URLSearchParams(window.location.search); - const code = urlParams.get('code'); - + const code = urlParams.get("code"); + if (!code) { setError(t("integration_settings.errors.no_auth_code")); return; } - + const response = await axios.get( `${apiUrl}/auth/airtable/callback?code=${code}&robotId=${recordingId}` ); - + if (response.data.accessToken) { notify("success", t("integration_settings.notifications.airtable_auth_success")); await fetchAirtableBases(); } - + // Clear URL parameters window.history.replaceState({}, document.title, window.location.pathname); - } catch (error) { setError(t("integration_settings.errors.airtable_auth_error")); } @@ -135,6 +135,7 @@ export const IntegrationSettingsModal = ({ // Fetch Google Sheets const fetchSpreadsheetFiles = async () => { try { + setLoading(true); const response = await axios.get(`${apiUrl}/auth/gsheets/files?robotId=${recordingId}`, { withCredentials: true, }); @@ -142,12 +143,15 @@ export const IntegrationSettingsModal = ({ } catch (error: any) { console.error("Error fetching spreadsheet files:", error.response?.data?.message || error.message); notify("error", t("integration_settings.errors.fetch_error", { message: error.response?.data?.message || error.message })); + } finally { + setLoading(false); } }; // Fetch Airtable Bases const fetchAirtableBases = async () => { try { + setLoading(true); const response = await axios.get(`${apiUrl}/auth/airtable/bases?robotId=${recordingId}`, { withCredentials: true, }); @@ -155,6 +159,8 @@ export const IntegrationSettingsModal = ({ } catch (error: any) { console.error("Error fetching Airtable bases:", error.response?.data?.message || error.message); notify("error", t("integration_settings.errors.fetch_error", { message: error.response?.data?.message || error.message })); + } finally { + setLoading(false); } }; @@ -185,6 +191,7 @@ export const IntegrationSettingsModal = ({ // Update Google Sheet ID const updateGoogleSheetId = async () => { try { + setLoading(true); const response = await axios.post( `${apiUrl}/auth/gsheets/update`, { @@ -198,12 +205,15 @@ export const IntegrationSettingsModal = ({ console.log("Google Sheet ID updated:", response.data); } catch (error: any) { console.error("Error updating Google Sheet ID:", error.response?.data?.message || error.message); + } finally { + setLoading(false); } }; // Update Airtable Base ID const updateAirtableBaseId = async () => { try { + setLoading(true); const response = await axios.post( `${apiUrl}/auth/airtable/update`, { @@ -217,12 +227,15 @@ export const IntegrationSettingsModal = ({ console.log("Airtable Base ID updated:", response.data); } catch (error: any) { console.error("Error updating Airtable Base ID:", error.response?.data?.message || error.message); + } finally { + setLoading(false); } }; // Remove Integration const removeIntegration = async () => { try { + setLoading(true); await axios.post( `${apiUrl}/auth/gsheets/remove`, { robotId: recordingId }, @@ -232,9 +245,18 @@ export const IntegrationSettingsModal = ({ setRecording(null); setSpreadsheets([]); setAirtableBases([]); - setSettings({ spreadsheetId: "", spreadsheetName: "", airtableBaseId: "", airtableBaseName: "", data: "" }); + setSettings({ + spreadsheetId: "", + spreadsheetName: "", + airtableBaseId: "", + airtableBaseName: "", + data: "", + }); + notify("success", t("integration_settings.notifications.integration_removed")); } catch (error: any) { console.error("Error removing integration:", error.response?.data?.message || error.message); + } finally { + setLoading(false); } }; @@ -242,25 +264,37 @@ export const IntegrationSettingsModal = ({ const checkAuthCallback = () => { const path = window.location.pathname; const urlParams = new URLSearchParams(window.location.search); - const code = urlParams.get('code'); - + const code = urlParams.get("code"); + if (code) { - if (path.includes('/google/callback')) { + if (path.includes("/google/callback")) { handleGoogleCallback(); - } else if (path.includes('/airtable/callback')) { + } else if (path.includes("/airtable/callback")) { handleAirtableCallback(); } } }; - + checkAuthCallback(); - + // Cleanup function return () => { window.history.replaceState({}, document.title, window.location.pathname); }; }, []); + useEffect(() => { + const fetchRecordingInfo = async () => { + if (!recordingId) return; + const recording = await getStoredRecording(recordingId); + if (recording) { + setRecording(recording); + } + }; + + fetchRecordingInfo(); + }, [recordingId]); + return (
From e89d64d04feaf33511dc5f245ffcdc8ee0db4c0d Mon Sep 17 00:00:00 2001 From: AmitChauhan63390 Date: Sun, 26 Jan 2025 21:05:08 +0530 Subject: [PATCH 03/20] minor changes --- server/src/routes/auth.ts | 10 +- .../integration/IntegrationSettings.tsx | 616 ++++++++++-------- 2 files changed, 365 insertions(+), 261 deletions(-) diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 574e2176..c2994942 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -573,11 +573,6 @@ router.post( import crypto from 'crypto'; -// Add these environment variables to your .env file -// AIRTABLE_CLIENT_ID=your_client_id -// AIRTABLE_CLIENT_SECRET=your_client_secret -// AIRTABLE_REDIRECT_URI=http://localhost:8080/auth/airtable/callback - // Airtable OAuth Routes router.get("/airtable", (req, res) => { const { robotId } = req.query; @@ -668,6 +663,7 @@ router.get("/airtable/callback", async (req, res) => { await robot.update({ airtable_access_token: tokens.access_token, airtable_refresh_token: tokens.refresh_token, + }); @@ -689,7 +685,7 @@ router.get("/airtable/callback", async (req, res) => { }); // Get Airtable bases -router.get("/airtable/bases", requireSignIn, async (req: AuthenticatedRequest, res) => { +router.get("/airtable/bases", async (req: AuthenticatedRequest, res) => { try { const { robotId } = req.query; if (!robotId) { @@ -697,7 +693,7 @@ router.get("/airtable/bases", requireSignIn, async (req: AuthenticatedRequest, r } const robot = await Robot.findOne({ - where: { "recording_meta.id": robotId }, + where: { "recording_meta.id": robotId.toString() }, raw: true, }); diff --git a/src/components/integration/IntegrationSettings.tsx b/src/components/integration/IntegrationSettings.tsx index 874b248d..1d0534d1 100644 --- a/src/components/integration/IntegrationSettings.tsx +++ b/src/components/integration/IntegrationSettings.tsx @@ -6,6 +6,7 @@ import { CircularProgress, Alert, AlertTitle, + Chip, } from "@mui/material"; import Button from "@mui/material/Button"; import TextField from "@mui/material/TextField"; @@ -23,11 +24,12 @@ interface IntegrationProps { } export interface IntegrationSettings { - spreadsheetId: string; - spreadsheetName: string; - airtableBaseId: string; - airtableBaseName: string; + spreadsheetId?: string; + spreadsheetName?: string; + airtableBaseId?: string; + airtableBaseName?: string; data: string; + integrationType: "googleSheets" | "airtable"; } // Helper functions to replace js-cookie functionality @@ -50,123 +52,126 @@ export const IntegrationSettingsModal = ({ handleClose, }: IntegrationProps) => { const { t } = useTranslation(); + const [selectedIntegrationType, setSelectedIntegrationType] = useState< + "googleSheets" | "airtable" | null + >(null); const [settings, setSettings] = useState({ spreadsheetId: "", spreadsheetName: "", airtableBaseId: "", airtableBaseName: "", data: "", + integrationType: "googleSheets", }); - const [spreadsheets, setSpreadsheets] = useState<{ id: string; name: string }[]>([]); - const [airtableBases, setAirtableBases] = useState<{ id: string; name: string }[]>([]); + const [airtableTables, setAirtableTables] = useState<{ id: string; name: string }[]>([]); + const [spreadsheets, setSpreadsheets] = useState< + { id: string; name: string }[] + >([]); + const [airtableBases, setAirtableBases] = useState< + { id: string; name: string }[] + >([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const { recordingId, notify } = useGlobalInfoStore(); const [recording, setRecording] = useState(null); - // Authenticate with Google const authenticateWithGoogle = () => { - const redirectUri = `${window.location.origin}/google/callback`; - window.location.href = `${apiUrl}/auth/google?robotId=${recordingId}&redirect_uri=${encodeURIComponent(redirectUri)}`; + window.location.href = `${apiUrl}/auth/google?robotId=${recordingId}`; }; - // Authenticate with Airtable const authenticateWithAirtable = () => { - const redirectUri = `${window.location.origin}/airtable/callback`; - window.location.href = `${apiUrl}/auth/airtable?robotId=${recordingId}&redirect_uri=${encodeURIComponent(redirectUri)}`; + window.location.href = `${apiUrl}/auth/airtable?robotId=${recordingId}`; }; - // Handle Google OAuth callback - const handleGoogleCallback = async () => { + const handleIntegrationType = (type: "googleSheets" | "airtable") => { + setSelectedIntegrationType(type); + setSettings({ + ...settings, + integrationType: type, + }); + }; + + const fetchAirtableTables = async (baseId: string) => { try { - const urlParams = new URLSearchParams(window.location.search); - const code = urlParams.get("code"); - - if (!code) { - setError(t("integration_settings.errors.no_auth_code")); - return; - } - const response = await axios.get( - `${apiUrl}/auth/google/callback?code=${code}&robotId=${recordingId}` + `${apiUrl}/auth/airtable/tables?baseId=${baseId}&robotId=${recordingId}`, + { + withCredentials: true, + } + ); + setAirtableTables(response.data); + } catch (error: any) { + console.error( + "Error fetching Airtable tables:", + error.response?.data?.message || error.message + ); + notify( + "error", + t("integration_settings.errors.fetch_error", { + message: error.response?.data?.message || error.message, + }) ); - - if (response.data.accessToken) { - notify("success", t("integration_settings.notifications.google_auth_success")); - await fetchSpreadsheetFiles(); - } - - // Clear URL parameters - window.history.replaceState({}, document.title, window.location.pathname); - } catch (error) { - setError(t("integration_settings.errors.google_auth_error")); } }; - // Handle Airtable OAuth callback - const handleAirtableCallback = async () => { - try { - const urlParams = new URLSearchParams(window.location.search); - const code = urlParams.get("code"); - - if (!code) { - setError(t("integration_settings.errors.no_auth_code")); - return; - } - - const response = await axios.get( - `${apiUrl}/auth/airtable/callback?code=${code}&robotId=${recordingId}` - ); - - if (response.data.accessToken) { - notify("success", t("integration_settings.notifications.airtable_auth_success")); - await fetchAirtableBases(); - } - - // Clear URL parameters - window.history.replaceState({}, document.title, window.location.pathname); - } catch (error) { - setError(t("integration_settings.errors.airtable_auth_error")); - } - }; - - // Fetch Google Sheets const fetchSpreadsheetFiles = async () => { try { - setLoading(true); - const response = await axios.get(`${apiUrl}/auth/gsheets/files?robotId=${recordingId}`, { - withCredentials: true, - }); + const response = await axios.get( + `${apiUrl}/auth/gsheets/files?robotId=${recordingId}`, + { + withCredentials: true, + } + ); setSpreadsheets(response.data); } catch (error: any) { - console.error("Error fetching spreadsheet files:", error.response?.data?.message || error.message); - notify("error", t("integration_settings.errors.fetch_error", { message: error.response?.data?.message || error.message })); - } finally { - setLoading(false); + console.error( + "Error fetching spreadsheet files:", + error.response?.data?.message || error.message + ); + notify( + "error", + t("integration_settings.errors.fetch_error", { + message: error.response?.data?.message || error.message, + }) + ); } }; - // Fetch Airtable Bases + console.log("recordingId", recordingId); + const fetchAirtableBases = async () => { try { - setLoading(true); - const response = await axios.get(`${apiUrl}/auth/airtable/bases?robotId=${recordingId}`, { - withCredentials: true, - }); + const response = await axios.get( + `${apiUrl}/auth/airtable/bases?robotId=${recordingId}`, + { + withCredentials: true, + } + ); + setAirtableBases(response.data); + + console.log("Airtable bases:", response.data); + } catch (error: any) { - console.error("Error fetching Airtable bases:", error.response?.data?.message || error.message); - notify("error", t("integration_settings.errors.fetch_error", { message: error.response?.data?.message || error.message })); - } finally { - setLoading(false); + console.error( + "Error fetching Airtable bases:", + error.response?.data?.message || error.message + ); + notify( + "error", + t("integration_settings.errors.fetch_error", { + message: error.response?.data?.message || error.message, + }) + ); } }; - // Handle Google Sheet selection const handleSpreadsheetSelect = (e: React.ChangeEvent) => { - const selectedSheet = spreadsheets.find((sheet) => sheet.id === e.target.value); + const selectedSheet = spreadsheets.find( + (sheet) => sheet.id === e.target.value + ); if (selectedSheet) { setSettings({ ...settings, @@ -176,9 +181,10 @@ export const IntegrationSettingsModal = ({ } }; - // Handle Airtable Base selection const handleAirtableBaseSelect = (e: React.ChangeEvent) => { - const selectedBase = airtableBases.find((base) => base.id === e.target.value); + const selectedBase = airtableBases.find( + (base) => base.id === e.target.value + ); if (selectedBase) { setSettings({ ...settings, @@ -188,10 +194,8 @@ export const IntegrationSettingsModal = ({ } }; - // Update Google Sheet ID const updateGoogleSheetId = async () => { try { - setLoading(true); const response = await axios.post( `${apiUrl}/auth/gsheets/update`, { @@ -201,19 +205,18 @@ export const IntegrationSettingsModal = ({ }, { withCredentials: true } ); - notify("success", t("integration_settings.notifications.sheet_selected")); + notify(`success`, t("integration_settings.notifications.sheet_selected")); console.log("Google Sheet ID updated:", response.data); } catch (error: any) { - console.error("Error updating Google Sheet ID:", error.response?.data?.message || error.message); - } finally { - setLoading(false); + console.error( + "Error updating Google Sheet ID:", + error.response?.data?.message || error.message + ); } }; - // Update Airtable Base ID const updateAirtableBaseId = async () => { try { - setLoading(true); const response = await axios.post( `${apiUrl}/auth/airtable/update`, { @@ -223,21 +226,25 @@ export const IntegrationSettingsModal = ({ }, { withCredentials: true } ); - notify("success", t("integration_settings.notifications.base_selected")); + notify(`success`, t("integration_settings.notifications.base_selected")); console.log("Airtable Base ID updated:", response.data); } catch (error: any) { - console.error("Error updating Airtable Base ID:", error.response?.data?.message || error.message); - } finally { - setLoading(false); + console.error( + "Error updating Airtable Base ID:", + error.response?.data?.message || error.message + ); } }; - // Remove Integration const removeIntegration = async () => { try { - setLoading(true); + const endpoint = + selectedIntegrationType === "googleSheets" + ? "/auth/gsheets/remove" + : "/auth/airtable/remove"; + await axios.post( - `${apiUrl}/auth/gsheets/remove`, + `${apiUrl}${endpoint}`, { robotId: recordingId }, { withCredentials: true } ); @@ -245,204 +252,305 @@ export const IntegrationSettingsModal = ({ setRecording(null); setSpreadsheets([]); setAirtableBases([]); + setSelectedIntegrationType(null); setSettings({ spreadsheetId: "", spreadsheetName: "", airtableBaseId: "", airtableBaseName: "", data: "", + integrationType: "googleSheets", }); - notify("success", t("integration_settings.notifications.integration_removed")); } catch (error: any) { - console.error("Error removing integration:", error.response?.data?.message || error.message); - } finally { - setLoading(false); + console.error( + "Error removing integration:", + error.response?.data?.message || error.message + ); } }; useEffect(() => { - const checkAuthCallback = () => { - const path = window.location.pathname; - const urlParams = new URLSearchParams(window.location.search); - const code = urlParams.get("code"); + const status = getCookie("robot_auth_status"); + const message = getCookie("robot_auth_message"); - if (code) { - if (path.includes("/google/callback")) { - handleGoogleCallback(); - } else if (path.includes("/airtable/callback")) { - handleAirtableCallback(); - } - } - }; + if (status === "success" && message) { + notify("success", message); + removeCookie("robot_auth_status"); + removeCookie("robot_auth_message"); + } - checkAuthCallback(); + const urlParams = new URLSearchParams(window.location.search); + const code = urlParams.get("code"); + if (code) { + // Determine which authentication callback to handle + // You'll need to implement similar callback logic for Airtable + } - // Cleanup function - return () => { - window.history.replaceState({}, document.title, window.location.pathname); - }; - }, []); - - useEffect(() => { const fetchRecordingInfo = async () => { if (!recordingId) return; const recording = await getStoredRecording(recordingId); if (recording) { setRecording(recording); + // Determine integration type based on existing integration + if (recording.google_sheet_id) { + setSelectedIntegrationType("googleSheets"); + } else if (recording.airtable_base_id) { + setSelectedIntegrationType("airtable"); + } } }; fetchRecordingInfo(); }, [recordingId]); + // Initial integration type selection + if (!selectedIntegrationType) { + return ( + +
+ + {t("integration_settings.title_select_integration")} + +
+ + +
+
+
+ ); + } + return ( -
- {t("integration_settings.title")} +
+ + {selectedIntegrationType === "googleSheets" + ? t("integration_settings.title_google") + : t("integration_settings.title_airtable")} + - {recording && (recording.google_sheet_id || recording.airtable_base_id) ? ( + {recording && + (recording.google_sheet_id || recording.airtable_base_id ? ( + <> + + + {t("integration_settings.alerts.success.title")} + + {selectedIntegrationType === "googleSheets" ? ( + <> + {t("integration_settings.alerts.success.content", { + sheetName: recording.google_sheet_name, + })} + + {t("integration_settings.alerts.success.here")} + + + ) : ( + <> + {t("integration_settings.alerts.success.content", { + sheetName: recording.airtable_base_name, + })} + + {t("integration_settings.alerts.success.here")} + + + )} +
+ + {t("integration_settings.alerts.success.note")} + {" "} + {t("integration_settings.alerts.success.sync_limitation")} +
+ + + ) : null)} + + {!recording?.[ + selectedIntegrationType === "googleSheets" + ? "google_sheet_email" + : "airtable_email" + ] ? ( <> - {recording.google_sheet_id && ( - - {t("integration_settings.alerts.success.title")} - {t("integration_settings.alerts.success.content", { sheetName: recording.google_sheet_name })} - - {t("integration_settings.alerts.success.here")} - . -
- {t("integration_settings.alerts.success.note")} {t("integration_settings.alerts.success.sync_limitation")} -
- )} - - {recording.airtable_base_id && ( - - {t("integration_settings.alerts.success.title")} - {t("integration_settings.alerts.success.content", { sheetName: recording.airtable_base_name })} - - {t("integration_settings.alerts.success.here")} - . -
- {t("integration_settings.alerts.success.note")} {t("integration_settings.alerts.success.sync_limitation")} -
- )} - - ) : ( <> - {!recording?.google_sheet_email && !recording?.airtable_email ? ( + {recording[ + selectedIntegrationType === "googleSheets" + ? "google_sheet_email" + : "airtable_email" + ] && ( + + {t("integration_settings.descriptions.authenticated_as", { + email: + recording[ + selectedIntegrationType === "googleSheets" + ? "google_sheet_email" + : "airtable_email" + ], + })} + + )} + + {loading ? ( + + ) : error ? ( + {error} + ) : (selectedIntegrationType === "googleSheets" + ? spreadsheets + : airtableBases + ).length === 0 ? ( <> -

{t("integration_settings.descriptions.sync_info")}

- - +
+ + +
) : ( <> - {(recording.google_sheet_email || recording.airtable_email) && ( - - {t("integration_settings.descriptions.authenticated_as", { - email: recording.google_sheet_email || recording.airtable_email, + + {(selectedIntegrationType === "googleSheets" + ? spreadsheets + : airtableBases + ).map((item) => ( + + {item.name} + + ))} + + + {(selectedIntegrationType === "googleSheets" + ? settings.spreadsheetId + : settings.airtableBaseId) && ( + + {t("integration_settings.fields.selected_sheet", { + name: + selectedIntegrationType === "googleSheets" + ? spreadsheets.find( + (s) => s.id === settings.spreadsheetId + )?.name + : airtableBases.find( + (b) => b.id === settings.airtableBaseId + )?.name, + id: + selectedIntegrationType === "googleSheets" + ? settings.spreadsheetId + : settings.airtableBaseId, })} )} - {loading ? ( - - ) : error ? ( - {error} - ) : ( - <> - {recording.google_sheet_email && ( - <> - - {spreadsheets.map((sheet) => ( - - {sheet.name} - - ))} - - - {settings.spreadsheetId && ( - - {t("integration_settings.fields.selected_sheet", { - name: spreadsheets.find((s) => s.id === settings.spreadsheetId)?.name, - id: settings.spreadsheetId, - })} - - )} - - - - )} - - {recording.airtable_email && ( - <> - - {airtableBases.map((base) => ( - - {base.name} - - ))} - - - {settings.airtableBaseId && ( - - {t("integration_settings.fields.selected_base", { - name: airtableBases.find((b) => b.id === settings.airtableBaseId)?.name, - id: settings.airtableBaseId, - })} - - )} - - - - )} - - )} + )} @@ -462,4 +570,4 @@ export const modalStyle = { height: "fit-content", display: "block", padding: "20px", -}; \ No newline at end of file +}; From ae700764b9a3926d56c8960163eb7cc0b6be2a76 Mon Sep 17 00:00:00 2001 From: AmitChauhan63390 Date: Mon, 27 Jan 2025 21:29:11 +0530 Subject: [PATCH 04/20] integration modal changed --- server/src/routes/auth.ts | 11 +- .../integration/IntegrationSettings.tsx | 714 +++++++++--------- 2 files changed, 353 insertions(+), 372 deletions(-) diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index c2994942..268134f4 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -672,8 +672,17 @@ router.get("/airtable/callback", async (req, res) => { if (err) console.error('Session cleanup error:', err); }); + res.cookie("airtable_auth_status", "success", { + httpOnly: false, + maxAge: 60000, + }); // 1-minute expiration + res.cookie("airtable_auth_message", "Robot successfully authenticated", { + httpOnly: false, + maxAge: 60000, + }); + res.redirect( - `${process.env.PUBLIC_URL}/robots/${state}/integrate?success=true` + `${process.env.PUBLIC_URL}/robots/${state}/integrate || http://localhost:5173/robots/${state}/integrate` ); } catch (error: any) { diff --git a/src/components/integration/IntegrationSettings.tsx b/src/components/integration/IntegrationSettings.tsx index 1d0534d1..9dd3bdc9 100644 --- a/src/components/integration/IntegrationSettings.tsx +++ b/src/components/integration/IntegrationSettings.tsx @@ -6,10 +6,9 @@ import { CircularProgress, Alert, AlertTitle, - Chip, + Button, + TextField, } from "@mui/material"; -import Button from "@mui/material/Button"; -import TextField from "@mui/material/TextField"; import axios from "axios"; import { useGlobalInfoStore } from "../../context/globalInfo"; import { getStoredRecording } from "../../api/storage"; @@ -52,9 +51,6 @@ export const IntegrationSettingsModal = ({ handleClose, }: IntegrationProps) => { const { t } = useTranslation(); - const [selectedIntegrationType, setSelectedIntegrationType] = useState< - "googleSheets" | "airtable" | null - >(null); const [settings, setSettings] = useState({ spreadsheetId: "", spreadsheetName: "", @@ -64,114 +60,61 @@ export const IntegrationSettingsModal = ({ integrationType: "googleSheets", }); - const [airtableTables, setAirtableTables] = useState<{ id: string; name: string }[]>([]); - const [spreadsheets, setSpreadsheets] = useState< - { id: string; name: string }[] - >([]); - const [airtableBases, setAirtableBases] = useState< - { id: string; name: string }[] - >([]); + const [spreadsheets, setSpreadsheets] = useState<{ id: string; name: string }[]>([]); + const [airtableBases, setAirtableBases] = useState<{ id: string; name: string }[]>([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const { recordingId, notify } = useGlobalInfoStore(); const [recording, setRecording] = useState(null); + const [airtableAuthStatus, setAirtableAuthStatus] = useState(null); + + // Authenticate with Google Sheets const authenticateWithGoogle = () => { window.location.href = `${apiUrl}/auth/google?robotId=${recordingId}`; }; + // Authenticate with Airtable const authenticateWithAirtable = () => { window.location.href = `${apiUrl}/auth/airtable?robotId=${recordingId}`; }; - const handleIntegrationType = (type: "googleSheets" | "airtable") => { - setSelectedIntegrationType(type); - setSettings({ - ...settings, - integrationType: type, - }); - }; - - const fetchAirtableTables = async (baseId: string) => { - try { - const response = await axios.get( - `${apiUrl}/auth/airtable/tables?baseId=${baseId}&robotId=${recordingId}`, - { - withCredentials: true, - } - ); - setAirtableTables(response.data); - } catch (error: any) { - console.error( - "Error fetching Airtable tables:", - error.response?.data?.message || error.message - ); - notify( - "error", - t("integration_settings.errors.fetch_error", { - message: error.response?.data?.message || error.message, - }) - ); - } - }; - + // Fetch Google Sheets files const fetchSpreadsheetFiles = async () => { try { const response = await axios.get( `${apiUrl}/auth/gsheets/files?robotId=${recordingId}`, - { - withCredentials: true, - } + { withCredentials: true } ); setSpreadsheets(response.data); } catch (error: any) { - console.error( - "Error fetching spreadsheet files:", - error.response?.data?.message || error.message - ); - notify( - "error", - t("integration_settings.errors.fetch_error", { - message: error.response?.data?.message || error.message, - }) - ); + console.error("Error fetching spreadsheet files:", error); + notify("error", t("integration_settings.errors.fetch_error", { + message: error.response?.data?.message || error.message, + })); } }; - console.log("recordingId", recordingId); - + // Fetch Airtable bases const fetchAirtableBases = async () => { try { const response = await axios.get( `${apiUrl}/auth/airtable/bases?robotId=${recordingId}`, - { - withCredentials: true, - } + { withCredentials: true } ); - setAirtableBases(response.data); - - console.log("Airtable bases:", response.data); - } catch (error: any) { - console.error( - "Error fetching Airtable bases:", - error.response?.data?.message || error.message - ); - notify( - "error", - t("integration_settings.errors.fetch_error", { - message: error.response?.data?.message || error.message, - }) - ); + console.error("Error fetching Airtable bases:", error); + notify("error", t("integration_settings.errors.fetch_error", { + message: error.response?.data?.message || error.message, + })); } }; + // Handle Google Sheets selection const handleSpreadsheetSelect = (e: React.ChangeEvent) => { - const selectedSheet = spreadsheets.find( - (sheet) => sheet.id === e.target.value - ); + const selectedSheet = spreadsheets.find((sheet) => sheet.id === e.target.value); if (selectedSheet) { setSettings({ ...settings, @@ -181,10 +124,9 @@ export const IntegrationSettingsModal = ({ } }; + // Handle Airtable base selection const handleAirtableBaseSelect = (e: React.ChangeEvent) => { - const selectedBase = airtableBases.find( - (base) => base.id === e.target.value - ); + const selectedBase = airtableBases.find((base) => base.id === e.target.value); if (selectedBase) { setSettings({ ...settings, @@ -194,9 +136,10 @@ export const IntegrationSettingsModal = ({ } }; + // Update Google Sheets integration const updateGoogleSheetId = async () => { try { - const response = await axios.post( + await axios.post( `${apiUrl}/auth/gsheets/update`, { spreadsheetId: settings.spreadsheetId, @@ -205,19 +148,19 @@ export const IntegrationSettingsModal = ({ }, { withCredentials: true } ); - notify(`success`, t("integration_settings.notifications.sheet_selected")); - console.log("Google Sheet ID updated:", response.data); + notify("success", t("integration_settings.notifications.sheet_selected")); } catch (error: any) { - console.error( - "Error updating Google Sheet ID:", - error.response?.data?.message || error.message - ); + console.error("Error updating Google Sheet ID:", error); + notify("error", t("integration_settings.errors.update_error", { + message: error.response?.data?.message || error.message, + })); } }; - const updateAirtableBaseId = async () => { + // Update Airtable integration + const updateAirtableBase = async () => { try { - const response = await axios.post( + await axios.post( `${apiUrl}/auth/airtable/update`, { baseId: settings.airtableBaseId, @@ -226,332 +169,361 @@ export const IntegrationSettingsModal = ({ }, { withCredentials: true } ); - notify(`success`, t("integration_settings.notifications.base_selected")); - console.log("Airtable Base ID updated:", response.data); + notify("success", t("integration_settings.notifications.base_selected")); } catch (error: any) { - console.error( - "Error updating Airtable Base ID:", - error.response?.data?.message || error.message - ); + console.error("Error updating Airtable base:", error); + notify("error", t("integration_settings.errors.update_error", { + message: error.response?.data?.message || error.message, + })); } }; - const removeIntegration = async () => { + // Remove Google Sheets integration + const removeGoogleSheetsIntegration = async () => { try { - const endpoint = - selectedIntegrationType === "googleSheets" - ? "/auth/gsheets/remove" - : "/auth/airtable/remove"; - await axios.post( - `${apiUrl}${endpoint}`, + `${apiUrl}/auth/gsheets/remove`, { robotId: recordingId }, { withCredentials: true } ); - - setRecording(null); setSpreadsheets([]); - setAirtableBases([]); - setSelectedIntegrationType(null); - setSettings({ - spreadsheetId: "", - spreadsheetName: "", - airtableBaseId: "", - airtableBaseName: "", - data: "", - integrationType: "googleSheets", - }); + setSettings({ ...settings, spreadsheetId: "", spreadsheetName: "" }); + notify("success", t("integration_settings.notifications.integration_removed")); } catch (error: any) { - console.error( - "Error removing integration:", - error.response?.data?.message || error.message - ); + console.error("Error removing Google Sheets integration:", error); + notify("error", t("integration_settings.errors.remove_error", { + message: error.response?.data?.message || error.message, + })); } }; + // Remove Airtable integration + const removeAirtableIntegration = async () => { + try { + await axios.post( + `${apiUrl}/auth/airtable/remove`, + { robotId: recordingId }, + { withCredentials: true } + ); + setAirtableBases([]); + setSettings({ ...settings, airtableBaseId: "", airtableBaseName: "" }); + notify("success", t("integration_settings.notifications.integration_removed")); + } catch (error: any) { + console.error("Error removing Airtable integration:", error); + notify("error", t("integration_settings.errors.remove_error", { + message: error.response?.data?.message || error.message, + })); + } + }; + + // Handle OAuth callback for Airtable + const handleAirtableOAuthCallback = async () => { + try { + const response = await axios.get(`${apiUrl}/auth/airtable/callback`); + if (response.data.success) { + setAirtableAuthStatus(true); + fetchAirtableBases(); // Fetch bases after successful authentication + } + } catch (error) { + setError("Error authenticating with Airtable"); + } + }; + + // Fetch recording info on component mount useEffect(() => { - const status = getCookie("robot_auth_status"); - const message = getCookie("robot_auth_message"); - - if (status === "success" && message) { - notify("success", message); - removeCookie("robot_auth_status"); - removeCookie("robot_auth_message"); - } - - const urlParams = new URLSearchParams(window.location.search); - const code = urlParams.get("code"); - if (code) { - // Determine which authentication callback to handle - // You'll need to implement similar callback logic for Airtable - } - const fetchRecordingInfo = async () => { if (!recordingId) return; const recording = await getStoredRecording(recordingId); if (recording) { setRecording(recording); - // Determine integration type based on existing integration if (recording.google_sheet_id) { - setSelectedIntegrationType("googleSheets"); + setSettings({ ...settings, integrationType: "googleSheets" }); } else if (recording.airtable_base_id) { - setSelectedIntegrationType("airtable"); + setSettings({ ...settings, integrationType: "airtable" }); } } }; - fetchRecordingInfo(); }, [recordingId]); - // Initial integration type selection - if (!selectedIntegrationType) { - return ( - -
- - {t("integration_settings.title_select_integration")} - -
- - -
+ // Handle Airtable authentication status + useEffect(() => { + const status = getCookie("airtable_auth_status"); + const message = getCookie("airtable_auth_message"); + + if (status === "success" && message) { + notify("success", message); + removeCookie("airtable_auth_status"); + removeCookie("airtable_auth_message"); + setAirtableAuthStatus(true); + fetchAirtableBases(); // Fetch bases after successful authentication + } + + const urlParams = new URLSearchParams(window.location.search); + const code = urlParams.get("code"); + if (code) { + handleAirtableOAuthCallback(); + } + }, [recordingId]); + + console.log(recording) + + + const [selectedIntegrationType, setSelectedIntegrationType] = useState< + "googleSheets" | "airtable" | null +>(null); + +// Add this UI at the top of the modal return statement +if (!selectedIntegrationType) { + return ( + +
+ + {t("integration_settings.title_select_integration")} + +
+ {/* Google Sheets Button */} + + + {/* Airtable Button */} +
- - ); - } +
+
+ ); +} + + return ( -
+
- {selectedIntegrationType === "googleSheets" - ? t("integration_settings.title_google") - : t("integration_settings.title_airtable")} + {t("integration_settings.title")} - {recording && - (recording.google_sheet_id || recording.airtable_base_id ? ( - <> - - - {t("integration_settings.alerts.success.title")} - - {selectedIntegrationType === "googleSheets" ? ( - <> - {t("integration_settings.alerts.success.content", { - sheetName: recording.google_sheet_name, - })} - - {t("integration_settings.alerts.success.here")} - - - ) : ( - <> - {t("integration_settings.alerts.success.content", { - sheetName: recording.airtable_base_name, - })} - - {t("integration_settings.alerts.success.here")} - - - )} -
- - {t("integration_settings.alerts.success.note")} - {" "} - {t("integration_settings.alerts.success.sync_limitation")} -
- - - ) : null)} - - {!recording?.[ - selectedIntegrationType === "googleSheets" - ? "google_sheet_email" - : "airtable_email" - ] ? ( + {/* Google Sheets Integration */} + {settings.integrationType === "googleSheets" && ( <> -

{t("integration_settings.descriptions.sync_info")}

- - - ) : ( - <> - {recording[ - selectedIntegrationType === "googleSheets" - ? "google_sheet_email" - : "airtable_email" - ] && ( - - {t("integration_settings.descriptions.authenticated_as", { - email: - recording[ - selectedIntegrationType === "googleSheets" - ? "google_sheet_email" - : "airtable_email" - ], - })} - - )} - - {loading ? ( - - ) : error ? ( - {error} - ) : (selectedIntegrationType === "googleSheets" - ? spreadsheets - : airtableBases - ).length === 0 ? ( + {recording?.google_sheet_id ? ( <> -
- - -
+ {t("integration_settings.alerts.success.here")} + + + ) : ( <> - - {(selectedIntegrationType === "googleSheets" - ? spreadsheets - : airtableBases - ).map((item) => ( - - {item.name} - - ))} - - - {(selectedIntegrationType === "googleSheets" - ? settings.spreadsheetId - : settings.airtableBaseId) && ( - - {t("integration_settings.fields.selected_sheet", { - name: - selectedIntegrationType === "googleSheets" - ? spreadsheets.find( - (s) => s.id === settings.spreadsheetId - )?.name - : airtableBases.find( - (b) => b.id === settings.airtableBaseId - )?.name, - id: - selectedIntegrationType === "googleSheets" - ? settings.spreadsheetId - : settings.airtableBaseId, - })} - + {!recording?.google_sheet_email ? ( + <> +

{t("integration_settings.descriptions.sync_info")}

+ + + ) : ( + <> + + {t("integration_settings.descriptions.authenticated_as", { + email: recording.google_sheet_email, + })} + + {loading ? ( + + ) : error ? ( + {error} + ) : spreadsheets.length === 0 ? ( + + ) : ( + <> + + {spreadsheets.map((sheet) => ( + + {sheet.name} + + ))} + + + + )} + )} + + )} + + )} + {/* Airtable Integration */} + {settings.integrationType === "airtable" && ( + <> + {recording?.airtable_base_id ? ( + <> + + {t("integration_settings.alerts.airtable_success.title")} + {t("integration_settings.alerts.airtable_success.content", { + baseName: recording.airtable_base_name, + })} + + {t("integration_settings.alerts.airtable_success.here")} + + + ) : ( + <> + {!recording?.airtable_access_token ? ( + <> +

{t("integration_settings.descriptions.airtable_sync_info")}

+ + + ) : ( + <> + + {t("integration_settings.descriptions.authenticated_as", { + email: "hghghg", + })} + + {loading ? ( + + ) : error ? ( + {error} + ) : airtableBases.length === 0 ? ( + + ) : ( + <> + + {airtableBases.map((base) => ( + + {base.name} + + ))} + + + + )} + + )} + )} )} @@ -570,4 +542,4 @@ export const modalStyle = { height: "fit-content", display: "block", padding: "20px", -}; +}; \ No newline at end of file From 6a854f953122a36658db34278ed2683bee42520f Mon Sep 17 00:00:00 2001 From: AmitChauhan63390 Date: Thu, 30 Jan 2025 15:56:00 +0530 Subject: [PATCH 05/20] proper integration but writing data to airtable yet to be fixed --- server/src/routes/auth.ts | 49 ++++- server/src/routes/storage.ts | 10 + .../integrations/airtable.ts | 199 ++++++++++++++++++ .../integration/IntegrationSettings.tsx | 123 ++++++++++- 4 files changed, 371 insertions(+), 10 deletions(-) create mode 100644 server/src/workflow-management/integrations/airtable.ts diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 268134f4..86749313 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -733,8 +733,8 @@ router.get("/airtable/bases", async (req: AuthenticatedRequest, res) => { }); // Update robot with selected base -router.post("/airtable/update", requireSignIn, async (req: AuthenticatedRequest, res) => { - const { baseId, baseName, robotId } = req.body; +router.post("/airtable/update", async (req: AuthenticatedRequest, res) => { + const { baseId, robotId , tableName} = req.body; if (!baseId || !robotId) { return res.status(400).json({ message: "Base ID and Robot ID are required" }); @@ -751,7 +751,8 @@ router.post("/airtable/update", requireSignIn, async (req: AuthenticatedRequest, await robot.update({ airtable_base_id: baseId, - airtable_table_name: baseName, + airtable_table_name: tableName, + }); capture("maxun-oss-airtable-integration-created", { @@ -803,3 +804,45 @@ router.post("/airtable/remove", requireSignIn, async (req: AuthenticatedRequest, } }); + + +// Fetch tables from an Airtable base +router.get("/airtable/tables", async (req: AuthenticatedRequest, res) => { + try { + const { baseId, robotId } = req.query; + + if (!baseId || !robotId) { + return res.status(400).json({ message: "Base ID and Robot ID are required" }); + } + + const robot = await Robot.findOne({ + where: { "recording_meta.id": robotId.toString() }, + raw: true, + }); + + if (!robot?.airtable_access_token) { + return res.status(400).json({ message: "Robot not authenticated with Airtable" }); + } + + const response = await fetch(`https://api.airtable.com/v0/meta/bases/${baseId}/tables`, { + headers: { + 'Authorization': `Bearer ${robot.airtable_access_token}` + } + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error.message || 'Failed to fetch tables'); + } + + const data = await response.json(); + res.json(data.tables.map((table: any) => ({ + id: table.id, + name: table.name, + fields: table.fields + }))); + + } catch (error: any) { + res.status(500).json({ message: error.message }); + } +}); \ No newline at end of file diff --git a/server/src/routes/storage.ts b/server/src/routes/storage.ts index ddadf240..0fdd69ce 100644 --- a/server/src/routes/storage.ts +++ b/server/src/routes/storage.ts @@ -20,6 +20,7 @@ import { capture } from "../utils/analytics"; import { tryCatch } from 'bullmq'; import { WorkflowFile } from 'maxun-core'; import { Page } from 'playwright'; +import { airtableUpdateTasks, processAirtableUpdates } from '../workflow-management/integrations/airtable'; chromium.use(stealthPlugin()); export const router = Router(); @@ -514,6 +515,15 @@ router.post('/runs/run/:id', requireSignIn, async (req: AuthenticatedRequest, re status: 'pending', retries: 5, }; + + airtableUpdateTasks[plainRun.runId] = { + robotId: plainRun.robotMetaId, + runId: plainRun.runId, + status: 'pending', + retries: 5, + }; + + processAirtableUpdates(); processGoogleSheetUpdates(); } catch (err: any) { logger.log('error', `Failed to update Google Sheet for run: ${plainRun.runId}: ${err.message}`); diff --git a/server/src/workflow-management/integrations/airtable.ts b/server/src/workflow-management/integrations/airtable.ts new file mode 100644 index 00000000..9ace046c --- /dev/null +++ b/server/src/workflow-management/integrations/airtable.ts @@ -0,0 +1,199 @@ +import Airtable from "airtable"; +import axios from "axios"; +import logger from "../../logger"; +import Run from "../../models/Run"; +import Robot from "../../models/Robot"; + +interface AirtableUpdateTask { + robotId: string; + runId: string; + status: 'pending' | 'completed' | 'failed'; + retries: number; +} + +const MAX_RETRIES = 5; +const BASE_API_DELAY = 2000; + +export let airtableUpdateTasks: { [runId: string]: AirtableUpdateTask } = {}; + +export async function updateAirtable(robotId: string, runId: string) { + try { + const run = await Run.findOne({ where: { runId } }); + if (!run) throw new Error(`Run not found for runId: ${runId}`); + + const plainRun = run.toJSON(); + if (plainRun.status !== 'success') { + console.log('Run status is not success'); + return; + } + + let data: { [key: string]: any }[] = []; + if (plainRun.serializableOutput?.['item-0']) { + data = plainRun.serializableOutput['item-0'] as { [key: string]: any }[]; + } else if (plainRun.binaryOutput?.['item-0']) { + data = [{ "File URL": plainRun.binaryOutput['item-0'] }]; + } + + const robot = await Robot.findOne({ where: { 'recording_meta.id': robotId } }); + if (!robot) throw new Error(`Robot not found for robotId: ${robotId}`); + + const plainRobot = robot.toJSON(); + if (plainRobot.airtable_base_id && plainRobot.airtable_table_name) { + console.log(`Writing to Airtable base ${plainRobot.airtable_base_id}`); + await writeDataToAirtable( + robotId, + plainRobot.airtable_base_id, + plainRobot.airtable_table_name, + data + ); + console.log(`Data written to Airtable for ${robotId}`); + } + } catch (error: any) { + console.error(`Airtable update failed: ${error.message}`); + throw error; + } +} + +export async function writeDataToAirtable( + robotId: string, + baseId: string, + tableName: string, + data: any[] +) { + try { + const robot = await Robot.findOne({ where: { 'recording_meta.id': robotId } }); + if (!robot) throw new Error('Robot not found'); + + const accessToken = robot.get('airtable_access_token'); + if (!accessToken) throw new Error('Airtable not connected'); + + const airtable = new Airtable({ apiKey: accessToken }); + const base = airtable.base(baseId); + + const existingFields = await getExistingFields(base, tableName); + const dataFields = [...new Set(data.flatMap(row => Object.keys(row)))]; + const missingFields = dataFields.filter(field => !existingFields.includes(field)); + + for (const field of missingFields) { + const sampleValue = data.find(row => row[field])?.[field]; + if (sampleValue) { + await createAirtableField(baseId, tableName, field, sampleValue, accessToken); + } + } + + const batchSize = 10; + for (let i = 0; i < data.length; i += batchSize) { + const batch = data.slice(i, i + batchSize); + await retryableAirtableWrite(base, tableName, batch); + } + + logger.log('info', `Successfully wrote ${data.length} records to Airtable`); + } catch (error: any) { + logger.log('error', `Airtable write failed: ${error.message}`); + throw error; + } +} + +async function getExistingFields(base: Airtable.Base, tableName: string): Promise { + try { + const records = await base(tableName).select({ maxRecords: 1 }).firstPage(); + return records[0] ? Object.keys(records[0].fields) : []; + } catch (error) { + return []; + } +} + +async function createAirtableField( + baseId: string, + tableName: string, + fieldName: string, + sampleValue: any, + accessToken: string, + retries = MAX_RETRIES +): Promise { + try { + let fieldType = inferFieldType(sampleValue); + + // Fallback if field type is unknown + if (!fieldType) { + fieldType = 'singleLineText'; + logger.log('warn', `Unknown field type for ${fieldName}, defaulting to singleLineText`); + } + + console.log(`Creating field: ${fieldName}, Type: ${fieldType}`); + + await axios.post( + `https://api.airtable.com/v0/meta/bases/${baseId}/tables/${tableName}/fields`, + { name: fieldName, type: fieldType }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + } + } + ); + + logger.log('info', `Created field: ${fieldName} (${fieldType})`); + } catch (error: any) { + if (retries > 0 && error.response?.status === 429) { + await delay(BASE_API_DELAY * (MAX_RETRIES - retries + 2)); + return createAirtableField(baseId, tableName, fieldName, sampleValue, accessToken, retries - 1); + } + throw new Error(`Field creation failed: ${error.response?.data?.error?.message || 'Unknown error'}`); + } +} + +function inferFieldType(value: any): string { + if (typeof value === 'number') return 'number'; + if (typeof value === 'boolean') return 'checkbox'; + if (value instanceof Date) return 'dateTime'; + if (Array.isArray(value)) return 'multipleSelects'; + return 'singleLineText'; +} + +async function retryableAirtableWrite( + base: Airtable.Base, + tableName: string, + batch: any[], + retries = MAX_RETRIES +): Promise { + try { + await base(tableName).create(batch.map(row => ({ fields: row }))); + } catch (error) { + if (retries > 0) { + await delay(BASE_API_DELAY); + return retryableAirtableWrite(base, tableName, batch, retries - 1); + } + throw error; + } +} + +function delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +export const processAirtableUpdates = async () => { + while (true) { + let hasPendingTasks = false; + + for (const runId in airtableUpdateTasks) { + const task = airtableUpdateTasks[runId]; + if (task.status !== 'pending') continue; + + hasPendingTasks = true; + try { + await updateAirtable(task.robotId, task.runId); + delete airtableUpdateTasks[runId]; + } catch (error: any) { + task.retries += 1; + if (task.retries >= MAX_RETRIES) { + task.status = 'failed'; + logger.log('error', `Permanent failure for run ${runId}`); + } + } + } + + if (!hasPendingTasks) break; + await delay(5000); + } +}; diff --git a/src/components/integration/IntegrationSettings.tsx b/src/components/integration/IntegrationSettings.tsx index 9dd3bdc9..3fc62979 100644 --- a/src/components/integration/IntegrationSettings.tsx +++ b/src/components/integration/IntegrationSettings.tsx @@ -15,6 +15,7 @@ import { getStoredRecording } from "../../api/storage"; import { apiUrl } from "../../apiConfig.js"; import Cookies from "js-cookie"; import { useTranslation } from "react-i18next"; +import { SignalCellularConnectedNoInternet0BarSharp } from "@mui/icons-material"; interface IntegrationProps { isOpen: boolean; @@ -27,8 +28,11 @@ export interface IntegrationSettings { spreadsheetName?: string; airtableBaseId?: string; airtableBaseName?: string; + airtableTableName?: string, + airtableTableId?: string, data: string; integrationType: "googleSheets" | "airtable"; + } // Helper functions to replace js-cookie functionality @@ -56,12 +60,17 @@ export const IntegrationSettingsModal = ({ spreadsheetName: "", airtableBaseId: "", airtableBaseName: "", + airtableTableName: "", + airtableTableId: "", + data: "", integrationType: "googleSheets", + }); const [spreadsheets, setSpreadsheets] = useState<{ id: string; name: string }[]>([]); const [airtableBases, setAirtableBases] = useState<{ id: string; name: string }[]>([]); + const [airtableTables, setAirtableTables] = useState<{ id: string; name: string }[]>([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -112,6 +121,23 @@ export const IntegrationSettingsModal = ({ } }; + + const fetchAirtableTables = async (baseId: string, recordingId: string) => { + try { + const response = await axios.get( + `${apiUrl}/auth/airtable/tables?robotId=${recordingId}&baseId=${baseId}`, + { withCredentials: true } + ); + setAirtableTables(response.data); + } + catch (error: any) { + console.error("Error fetching Airtable tables:", error); + notify("error", t("integration_settings.errors.fetch_error", { + message: error.response?.data?.message || error.message, + })); + } + } + // Handle Google Sheets selection const handleSpreadsheetSelect = (e: React.ChangeEvent) => { const selectedSheet = spreadsheets.find((sheet) => sheet.id === e.target.value); @@ -125,17 +151,74 @@ export const IntegrationSettingsModal = ({ }; // Handle Airtable base selection - const handleAirtableBaseSelect = (e: React.ChangeEvent) => { + const handleAirtableBaseSelect = async (e: React.ChangeEvent) => { const selectedBase = airtableBases.find((base) => base.id === e.target.value); + console.log(selectedBase); + if (selectedBase) { - setSettings({ - ...settings, + // Update local state + setSettings((prevSettings) => ({ + ...prevSettings, airtableBaseId: selectedBase.id, airtableBaseName: selectedBase.name, - }); + })); + + // Fetch tables for the selected base + if (recordingId) { + await fetchAirtableTables(selectedBase.id, recordingId); + } else { + console.error("Recording ID is null"); + } + + + + // try { + // // Ensure recordingId is available + // if (!recordingId) { + // throw new Error("Recording ID is missing"); + // } + + // // Make API call to update the base in the database + // const response = await axios.post( + // `${apiUrl}/auth/airtable/update`, + // { + // baseId: selectedBase.id, + // baseName: selectedBase.name, + // robotId: recordingId, // Use recordingId from the global context + // }, + // { withCredentials: true } + // ); + + // if (response.status !== 200) { + // throw new Error("Failed to update Airtable base in the database"); + // } + + // console.log("Airtable base updated successfully:", response.data); + // } catch (error) { + // console.error("Error updating Airtable base:", error); + // notify("error", t("integration_settings.errors.update_error", { + // message: error instanceof Error ? error.message : "Unknown error", + // })); + // } } }; + const handleAirtabletableSelect = (e: React.ChangeEvent) => { + console.log( e.target.value); + const selectedTable = airtableTables.find((table) => table.id === e.target.value); + if (selectedTable) { + setSettings((prevSettings) => ({ + ...prevSettings, + airtableTableId: e.target.value, + + airtableTableName: selectedTable?.name||"", + + })); + } + }; + + + // Update Google Sheets integration const updateGoogleSheetId = async () => { try { @@ -159,6 +242,10 @@ export const IntegrationSettingsModal = ({ // Update Airtable integration const updateAirtableBase = async () => { + console.log(settings.airtableBaseId); + console.log(settings.airtableTableName); + console.log(recordingId); + console.log(settings.airtableBaseName); try { await axios.post( `${apiUrl}/auth/airtable/update`, @@ -166,6 +253,7 @@ export const IntegrationSettingsModal = ({ baseId: settings.airtableBaseId, baseName: settings.airtableBaseName, robotId: recordingId, + tableName: settings.airtableTableName, }, { withCredentials: true } ); @@ -206,7 +294,7 @@ export const IntegrationSettingsModal = ({ { withCredentials: true } ); setAirtableBases([]); - setSettings({ ...settings, airtableBaseId: "", airtableBaseName: "" }); + setSettings({ ...settings, airtableBaseId: "", airtableBaseName: "", airtableTableName:"" }); notify("success", t("integration_settings.notifications.integration_removed")); } catch (error: any) { console.error("Error removing Airtable integration:", error); @@ -239,7 +327,13 @@ export const IntegrationSettingsModal = ({ if (recording.google_sheet_id) { setSettings({ ...settings, integrationType: "googleSheets" }); } else if (recording.airtable_base_id) { - setSettings({ ...settings, integrationType: "airtable" }); + setSettings(prev => ({ + ...prev, + airtableBaseId: recording.airtable_base_id || "", + airtableBaseName: recording.airtable_base_name || "", + airtableTableName: recording.airtable_table_name || "", + integrationType: recording.airtable_base_id ? "airtable" : "googleSheets" + })); } } }; @@ -475,7 +569,7 @@ if (!selectedIntegrationType) { <> {t("integration_settings.descriptions.authenticated_as", { - email: "hghghg", + email: "amit63390@gmail.com", })} {loading ? ( @@ -507,6 +601,21 @@ if (!selectedIntegrationType) { ))} + + {airtableTables.map((table) => ( + + {table.name} + + ))} + ) : ( <> {!recording?.google_sheet_email ? ( <> -

{t("integration_settings.descriptions.sync_info")}

+

{t("integration_settings.google.descriptions.sync_info")}

) : ( <> - {t("integration_settings.descriptions.authenticated_as", { + {t("integration_settings.google.descriptions.authenticated_as", { email: recording.google_sheet_email, })} @@ -492,14 +453,14 @@ if (!selectedIntegrationType) { color="primary" onClick={fetchSpreadsheetFiles} > - {t("integration_settings.buttons.fetch_sheets")} + {t("integration_settings.google.buttons.fetch_sheets")} ) : ( <> - {t("integration_settings.buttons.submit")} + {t("integration_settings.google.buttons.submit")} )} @@ -535,19 +496,24 @@ if (!selectedIntegrationType) { {/* Airtable Integration */} {settings.integrationType === "airtable" && ( <> + + {t("integration_settings.airtable.title")} + + {recording?.airtable_base_id ? ( <> - - {t("integration_settings.alerts.airtable_success.title")} - {t("integration_settings.alerts.airtable_success.content", { + + {t("integration_settings.airtable.alerts.success.title")} + {t("integration_settings.airtable.alerts.success.content", { baseName: recording.airtable_base_name, + tableName: recording.airtable_table_name })} - {t("integration_settings.alerts.airtable_success.here")} + {t("integration_settings.airtable.alerts.success.here")} ) : ( <> {!recording?.airtable_access_token ? ( <> -

{t("integration_settings.descriptions.airtable_sync_info")}

+

{t("integration_settings.airtable.descriptions.sync_info")}

) : ( <> - {t("integration_settings.descriptions.authenticated_as", { - email: "", - })} + {t("integration_settings.airtable.descriptions.authenticated_as")} {loading ? ( @@ -586,17 +550,17 @@ if (!selectedIntegrationType) { ) : airtableBases.length === 0 ? ( ) : ( <> )} From 2ec9bbd026f511e551cd77562869e40c1126f29b Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 26 Feb 2025 14:47:23 +0530 Subject: [PATCH 16/20] feat: add sign in middleware --- server/src/routes/auth.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 92e7a7e3..09db64ab 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -584,7 +584,7 @@ router.post( // Airtable OAuth Routes -router.get("/airtable", (req: Request, res) => { +router.get("/airtable", requireSignIn, (req: Request, res) => { const authenticatedReq = req as AuthenticatedRequest; const { robotId } = authenticatedReq.query; if (!robotId) { @@ -708,7 +708,7 @@ router.get("/airtable/callback", async (req: Request, res) => { }); // Get Airtable bases -router.get("/airtable/bases", async (req: Request, res) => { +router.get("/airtable/bases", requireSignIn, async (req: Request, res) => { const authenticatedReq = req as AuthenticatedRequest; try { const { robotId } = authenticatedReq.query; @@ -748,7 +748,7 @@ router.get("/airtable/bases", async (req: Request, res) => { }); // Update robot with selected base -router.post("/airtable/update", async (req: Request, res) => { +router.post("/airtable/update", requireSignIn, async (req: Request, res) => { const authenticatedReq = req as AuthenticatedRequest; const { baseId, robotId , baseName, tableName, tableId} = req.body; @@ -827,7 +827,7 @@ router.post("/airtable/remove", requireSignIn, async (req: Request, res) => { // Fetch tables from an Airtable base -router.get("/airtable/tables", async (req: Request, res) => { +router.get("/airtable/tables", requireSignIn,async (req: Request, res) => { const authenticatedReq = req as AuthenticatedRequest; try { const { baseId, robotId } = authenticatedReq.query; From 4b05e7bac66530da3eb5d6529ab72734c4b212b3 Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 26 Feb 2025 14:49:00 +0530 Subject: [PATCH 17/20] feat: add translation for integration --- public/locales/de.json | 88 ++++++++++++++++++++++++++------- public/locales/en.json | 107 ++++++++++++++++++++++++++++++----------- public/locales/es.json | 88 ++++++++++++++++++++++++++------- public/locales/ja.json | 90 ++++++++++++++++++++++++++-------- public/locales/zh.json | 90 ++++++++++++++++++++++++++-------- 5 files changed, 362 insertions(+), 101 deletions(-) diff --git a/public/locales/de.json b/public/locales/de.json index 613ddfef..fdc4e667 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -287,29 +287,81 @@ } }, "integration_settings": { - "title": "Mit Google Sheet integrieren", + "title": "Integrationseinstellungen", + "title_select_integration": "Integrationstyp auswählen", "descriptions": { - "sync_info": "Wenn Sie diese Option aktivieren, werden bei jeder erfolgreichen Ausführung dieses Roboters die erfassten Daten in Ihrem Google Sheet ergänzt.", "authenticated_as": "Authentifiziert als: {{email}}" }, - "alerts": { - "success": { - "title": "Google Sheet erfolgreich integriert.", - "content": "Jedes Mal, wenn dieser Roboter eine erfolgreiche Ausführung erstellt, werden die erfassten Daten Ihrem Google Sheet {{sheetName}} hinzugefügt. Sie können die Datenaktualisierungen", - "here": "hier", - "note": "Hinweis:", - "sync_limitation": "Die vor der Integration mit Google Sheets extrahierten Daten werden nicht im Google Sheet synchronisiert. Nur die nach der Integration extrahierten Daten werden synchronisiert." + "buttons": { + "submit": "Absenden", + "remove_integration": "Integration entfernen" + }, + "google": { + "title": "Mit Google Sheet integrieren", + "descriptions": { + "sync_info": "Wenn Sie diese Option aktivieren, werden bei jeder erfolgreichen Ausführung dieses Roboters die erfassten Daten in Ihrem Google Sheet ergänzt.", + "authenticated_as": "Authentifiziert als: {{email}}" + }, + "alerts": { + "success": { + "title": "Google Sheet erfolgreich integriert.", + "content": "Jedes Mal, wenn dieser Roboter eine erfolgreiche Ausführung erstellt, werden die erfassten Daten Ihrem Google Sheet {{sheetName}} hinzugefügt. Sie können die Datenaktualisierungen", + "here": "hier", + "note": "Hinweis:", + "sync_limitation": "Die vor der Integration mit Google Sheets extrahierten Daten werden nicht im Google Sheet synchronisiert. Nur die nach der Integration extrahierten Daten werden synchronisiert." + } + }, + "buttons": { + "authenticate": "Mit Google authentifizieren", + "fetch_sheets": "Google Sheets abrufen", + "remove_integration": "Integration entfernen", + "submit": "Absenden" + }, + "fields": { + "select_sheet": "Google Sheet auswählen", + "selected_sheet": "Ausgewähltes Sheet: {{name}} (ID: {{id}})" } }, - "buttons": { - "authenticate": "Mit Google authentifizieren", - "fetch_sheets": "Google Sheets abrufen", - "remove_integration": "Integration entfernen", - "submit": "Absenden" - }, - "fields": { - "select_sheet": "Google Sheet auswählen", - "selected_sheet": "Ausgewähltes Sheet: {{name}} (ID: {{id}})" + "airtable": { + "title": "Mit Airtable verbinden", + "descriptions": { + "sync_info": "Verbinden Sie Ihren Roboter mit einer Airtable-Basis, um Daten automatisch zu synchronisieren.", + "authenticated_as": "Mit Airtable authentifiziert" + }, + "alerts": { + "success": { + "title": "Mit Airtable verbunden", + "content": "Ihr Roboter ist jetzt mit der Basis: {{baseName}} und Tabelle: {{tableName}} verbunden. Sie können darauf zugreifen ", + "here": "hier", + "note": "Hinweis:", + "sync_limitation": "Nur nach der Integration erfasste Daten werden mit Airtable synchronisiert." + } + }, + "buttons": { + "authenticate": "Mit Airtable verbinden", + "fetch_bases": "Airtable-Basen abrufen", + "fetch_tables": "Airtable-Tabellen abrufen", + "remove_integration": "Airtable-Integration entfernen", + "submit": "Basis und Tabelle auswählen" + }, + "fields": { + "select_base": "Airtable-Basis auswählen", + "select_table": "Airtable-Tabelle auswählen", + "selected_base": "Ausgewählte Basis: {{name}}", + "selected_table": "Ausgewählte Tabelle: {{name}}" + }, + "errors": { + "auth_error": "Fehler bei der Authentifizierung mit Airtable", + "fetch_error": "Fehler beim Abrufen von Airtable-Basen: {{message}}", + "fetch_tables_error": "Fehler beim Abrufen von Airtable-Tabellen: {{message}}", + "update_error": "Fehler beim Aktualisieren der Airtable-Basis: {{message}}", + "remove_error": "Fehler beim Entfernen der Airtable-Integration: {{message}}" + }, + "notifications": { + "base_selected": "Airtable-Basis erfolgreich ausgewählt", + "table_selected": "Airtable-Tabelle erfolgreich ausgewählt", + "integration_removed": "Airtable-Integration erfolgreich entfernt" + } } }, "robot_duplication": { diff --git a/public/locales/en.json b/public/locales/en.json index 85bd3148..19c2ea7e 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -289,38 +289,91 @@ } }, "integration_settings": { - "title": "Integrate with Google Sheet", + "title": "Integration Settings", + "title_select_integration": "Select Integration Type", "descriptions": { - "sync_info": "If you enable this option, every time this robot runs a task successfully, its captured data will be appended to your Google Sheet.", "authenticated_as": "Authenticated as: {{email}}" }, - "alerts": { - "success": { - "title": "Google Sheet Integrated Successfully.", - "content": "Every time this robot creates a successful run, its captured data is appended to your {{sheetName}} Google Sheet. You can check the data updates", - "here": "here", - "note": "Note:", - "sync_limitation": "The data extracted before integrating with Google Sheets will not be synced in the Google Sheet. Only the data extracted after the integration will be synced." + "buttons": { + "submit": "Submit", + "remove_integration": "Remove Integration" + }, + "google": { + "title": "Integrate with Google Sheet", + "descriptions": { + "sync_info": "If you enable this option, every time this robot runs a task successfully, its captured data will be appended to your Google Sheet.", + "authenticated_as": "Authenticated as: {{email}}" + }, + "alerts": { + "success": { + "title": "Google Sheet Integrated Successfully.", + "content": "Every time this robot creates a successful run, its captured data is appended to your {{sheetName}} Google Sheet. You can check the data updates", + "here": "here", + "note": "Note:", + "sync_limitation": "The data extracted before integrating with Google Sheets will not be synced in the Google Sheet. Only the data extracted after the integration will be synced." + } + }, + "buttons": { + "authenticate": "Authenticate with Google", + "fetch_sheets": "Fetch Google Spreadsheets", + "remove_integration": "Remove Integration", + "submit": "Submit" + }, + "fields": { + "select_sheet": "Select Google Sheet", + "selected_sheet": "Selected Sheet: {{name}} (ID: {{id}})" + }, + "errors": { + "auth_error": "Error authenticating with Google", + "fetch_error": "Error fetching spreadsheet files: {{message}}", + "update_error": "Error updating Google Sheet ID: {{message}}", + "remove_error": "Error removing Google Sheets integration: {{message}}" + }, + "notifications": { + "sheet_selected": "Google Sheet selected successfully", + "integration_removed": "Google Sheets integration removed successfully" } }, - "buttons": { - "authenticate": "Authenticate with Google", - "fetch_sheets": "Fetch Google Spreadsheets", - "remove_integration": "Remove Integration", - "submit": "Submit" - }, - "fields": { - "select_sheet": "Select Google Sheet", - "selected_sheet": "Selected Sheet: {{name}} (ID: {{id}})" - }, - "errors": { - "auth_error": "Error authenticating with Google", - "fetch_error": "Error fetching spreadsheet files: {{message}}", - "update_error": "Error updating Google Sheet ID: {{message}}", - "remove_error": "Error removing Google Sheets integration: {{message}}" - }, - "notifications": { - "sheet_selected": "Google Sheet selected successfully" + "airtable": { + "title": "Integrate with Airtable", + "descriptions": { + "sync_info": "If you enable this option, every time this robot runs a task successfully, its captured data will be appended to your Airtable.", + "authenticated_as": "Authenticated with Airtable" + }, + "alerts": { + "success": { + "title": "Connected to Airtable", + "content": "Your robot is now connected to the base: {{baseName}} and table: {{tableName}}. You can access it ", + "here": "here", + "note": "Note:", + "sync_limitation": "Only data captured after integration will be synced to Airtable." + } + }, + "buttons": { + "authenticate": "Connect with Airtable", + "fetch_bases": "Fetch Airtable Bases", + "fetch_tables": "Fetch Airtable Tables", + "remove_integration": "Remove Airtable Integration", + "submit": "Select Base and Table" + }, + "fields": { + "select_base": "Select Airtable Base", + "select_table": "Select Airtable Table", + "selected_base": "Selected Base: {{name}}", + "selected_table": "Selected Table: {{name}}" + }, + "errors": { + "auth_error": "Error authenticating with Airtable", + "fetch_error": "Error fetching Airtable bases: {{message}}", + "fetch_tables_error": "Error fetching Airtable tables: {{message}}", + "update_error": "Error updating Airtable base: {{message}}", + "remove_error": "Error removing Airtable integration: {{message}}" + }, + "notifications": { + "base_selected": "Airtable base selected successfully", + "table_selected": "Airtable table selected successfully", + "integration_removed": "Airtable integration removed successfully" + } } }, "robot_duplication": { diff --git a/public/locales/es.json b/public/locales/es.json index 46bded05..4e6aadd4 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -288,29 +288,81 @@ } }, "integration_settings": { - "title": "Integrar con Google Sheet", + "title": "Configuración de integración", + "title_select_integration": "Seleccionar tipo de integración", "descriptions": { - "sync_info": "Si habilitas esta opción, cada vez que este robot ejecute una tarea exitosamente, sus datos capturados se añadirán a tu Google Sheet.", "authenticated_as": "Autenticado como: {{email}}" }, - "alerts": { - "success": { - "title": "Google Sheet integrado exitosamente.", - "content": "Cada vez que este robot crea una ejecución exitosa, sus datos capturados se añaden a tu Google Sheet {{sheetName}}. Puedes verificar las actualizaciones de datos", - "here": "aquí", - "note": "Nota:", - "sync_limitation": "Los datos extraídos antes de la integración con Google Sheets no se sincronizarán en el Google Sheet. Solo se sincronizarán los datos extraídos después de la integración." + "buttons": { + "submit": "Enviar", + "remove_integration": "Eliminar integración" + }, + "google": { + "title": "Integrar con Google Sheet", + "descriptions": { + "sync_info": "Si habilitas esta opción, cada vez que este robot ejecute una tarea exitosamente, sus datos capturados se añadirán a tu Google Sheet.", + "authenticated_as": "Autenticado como: {{email}}" + }, + "alerts": { + "success": { + "title": "Google Sheet integrado exitosamente.", + "content": "Cada vez que este robot crea una ejecución exitosa, sus datos capturados se añaden a tu Google Sheet {{sheetName}}. Puedes verificar las actualizaciones de datos", + "here": "aquí", + "note": "Nota:", + "sync_limitation": "Los datos extraídos antes de la integración con Google Sheets no se sincronizarán en el Google Sheet. Solo se sincronizarán los datos extraídos después de la integración." + } + }, + "buttons": { + "authenticate": "Autenticar con Google", + "fetch_sheets": "Obtener Google Sheets", + "remove_integration": "Eliminar integración", + "submit": "Enviar" + }, + "fields": { + "select_sheet": "Seleccionar Google Sheet", + "selected_sheet": "Hoja seleccionada: {{name}} (ID: {{id}})" } }, - "buttons": { - "authenticate": "Autenticar con Google", - "fetch_sheets": "Obtener Google Sheets", - "remove_integration": "Eliminar integración", - "submit": "Enviar" - }, - "fields": { - "select_sheet": "Seleccionar Google Sheet", - "selected_sheet": "Hoja seleccionada: {{name}} (ID: {{id}})" + "airtable": { + "title": "Integrar con Airtable", + "descriptions": { + "sync_info": "Conecte su robot a una base de Airtable para sincronizar datos automáticamente.", + "authenticated_as": "Autenticado con Airtable" + }, + "alerts": { + "success": { + "title": "Conectado a Airtable", + "content": "Su robot ahora está conectado a la base: {{baseName}} y tabla: {{tableName}}. Puede acceder a ella ", + "here": "aquí", + "note": "Nota:", + "sync_limitation": "Solo los datos capturados después de la integración se sincronizarán con Airtable." + } + }, + "buttons": { + "authenticate": "Conectar con Airtable", + "fetch_bases": "Obtener bases de Airtable", + "fetch_tables": "Obtener tablas de Airtable", + "remove_integration": "Eliminar integración de Airtable", + "submit": "Seleccionar base y tabla" + }, + "fields": { + "select_base": "Seleccionar base de Airtable", + "select_table": "Seleccionar tabla de Airtable", + "selected_base": "Base seleccionada: {{name}}", + "selected_table": "Tabla seleccionada: {{name}}" + }, + "errors": { + "auth_error": "Error al autenticar con Airtable", + "fetch_error": "Error al obtener bases de Airtable: {{message}}", + "fetch_tables_error": "Error al obtener tablas de Airtable: {{message}}", + "update_error": "Error al actualizar la base de Airtable: {{message}}", + "remove_error": "Error al eliminar la integración de Airtable: {{message}}" + }, + "notifications": { + "base_selected": "Base de Airtable seleccionada con éxito", + "table_selected": "Tabla de Airtable seleccionada con éxito", + "integration_removed": "Integración de Airtable eliminada con éxito" + } } }, "robot_duplication": { diff --git a/public/locales/ja.json b/public/locales/ja.json index ad78c8f5..83f2b731 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -288,29 +288,81 @@ } }, "integration_settings": { - "title": "Google Sheetと連携", + "title": "連携設定", + "title_select_integration": "連携タイプを選択", "descriptions": { - "sync_info": "このオプションを有効にすると、このロボットがタスクを正常に実行するたびに、取得したデータがGoogle Sheetに追加されます。", - "authenticated_as": "認証済みユーザー: {{email}}" - }, - "alerts": { - "success": { - "title": "Google Sheetの連携が完了しました。", - "content": "このロボットが正常に実行を完了するたびに、取得したデータはGoogle Sheet {{sheetName}}に追加されます。データの更新は", - "here": "こちら", - "note": "注意:", - "sync_limitation": "Google Sheetsとの連携前に抽出されたデータは同期されません。連携後に抽出されたデータのみが同期されます。" - } + "authenticated_as": "認証済みユーザー:{{email}}" }, "buttons": { - "authenticate": "Googleで認証", - "fetch_sheets": "Google Sheetsを取得", - "remove_integration": "連携を解除", - "submit": "送信" + "submit": "送信", + "remove_integration": "連携を削除" }, - "fields": { - "select_sheet": "Google Sheetを選択", - "selected_sheet": "選択したシート: {{name}} (ID: {{id}})" + "google": { + "title": "Google Sheetと連携", + "descriptions": { + "sync_info": "このオプションを有効にすると、このロボットがタスクを正常に実行するたびに、取得したデータがGoogle Sheetに追加されます。", + "authenticated_as": "認証済みユーザー: {{email}}" + }, + "alerts": { + "success": { + "title": "Google Sheetの連携が完了しました。", + "content": "このロボットが正常に実行を完了するたびに、取得したデータはGoogle Sheet {{sheetName}}に追加されます。データの更新は", + "here": "こちら", + "note": "注意:", + "sync_limitation": "Google Sheetsとの連携前に抽出されたデータは同期されません。連携後に抽出されたデータのみが同期されます。" + } + }, + "buttons": { + "authenticate": "Googleで認証", + "fetch_sheets": "Google Sheetsを取得", + "remove_integration": "連携を解除", + "submit": "送信" + }, + "fields": { + "select_sheet": "Google Sheetを選択", + "selected_sheet": "選択したシート: {{name}} (ID: {{id}})" + } + }, + "airtable": { + "title": "Airtableと連携する", + "descriptions": { + "sync_info": "ロボットをAirtableベースに接続して、データを自動的に同期します。", + "authenticated_as": "Airtableで認証済み" + }, + "alerts": { + "success": { + "title": "Airtableに接続しました", + "content": "ロボットがベース:{{baseName}}、テーブル:{{tableName}}に接続されました。アクセスするには", + "here": "こちら", + "note": "注意:", + "sync_limitation": "統合後に収集されたデータのみがAirtableに同期されます。" + } + }, + "buttons": { + "authenticate": "Airtableと接続", + "fetch_bases": "Airtableベースを取得", + "fetch_tables": "Airtableテーブルを取得", + "remove_integration": "Airtable連携を削除", + "submit": "ベースとテーブルを選択" + }, + "fields": { + "select_base": "Airtableベースを選択", + "select_table": "Airtableテーブルを選択", + "selected_base": "選択されたベース:{{name}}", + "selected_table": "選択されたテーブル:{{name}}" + }, + "errors": { + "auth_error": "Airtableとの認証エラー", + "fetch_error": "Airtableベースの取得エラー:{{message}}", + "fetch_tables_error": "Airtableテーブルの取得エラー:{{message}}", + "update_error": "Airtableベースの更新エラー:{{message}}", + "remove_error": "Airtable連携の削除エラー:{{message}}" + }, + "notifications": { + "base_selected": "Airtableベースが正常に選択されました", + "table_selected": "Airtableテーブルが正常に選択されました", + "integration_removed": "Airtable連携が正常に削除されました" + } } }, "robot_duplication": { diff --git a/public/locales/zh.json b/public/locales/zh.json index cd1a4f9f..dea02a75 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -288,29 +288,81 @@ } }, "integration_settings": { - "title": "与Google Sheet集成", + "title": "集成设置", + "title_select_integration": "选择集成类型", "descriptions": { - "sync_info": "如果启用此选项,每次机器人成功运行任务时,捕获的数据都会追加到您的Google Sheet中。", - "authenticated_as": "已验证身份: {{email}}" - }, - "alerts": { - "success": { - "title": "Google Sheet集成成功。", - "content": "每次此机器人创建成功运行时,捕获的数据都会追加到您的Google Sheet {{sheetName}}中。您可以查看数据更新", - "here": "在此处", - "note": "注意:", - "sync_limitation": "与Google Sheets集成之前提取的数据将不会同步到Google Sheet中。只有集成后提取的数据才会同步。" - } + "authenticated_as": "已验证身份:{{email}}" }, "buttons": { - "authenticate": "使用Google验证", - "fetch_sheets": "获取Google Sheets", - "remove_integration": "移除集成", - "submit": "提交" + "submit": "提交", + "remove_integration": "移除集成" }, - "fields": { - "select_sheet": "选择Google Sheet", - "selected_sheet": "已选择表格: {{name}} (ID: {{id}})" + "google": { + "title": "与Google Sheet集成", + "descriptions": { + "sync_info": "如果启用此选项,每次机器人成功运行任务时,捕获的数据都会追加到您的Google Sheet中。", + "authenticated_as": "已验证身份: {{email}}" + }, + "alerts": { + "success": { + "title": "Google Sheet集成成功。", + "content": "每次此机器人创建成功运行时,捕获的数据都会追加到您的Google Sheet {{sheetName}}中。您可以查看数据更新", + "here": "在此处", + "note": "注意:", + "sync_limitation": "与Google Sheets集成之前提取的数据将不会同步到Google Sheet中。只有集成后提取的数据才会同步。" + } + }, + "buttons": { + "authenticate": "使用Google验证", + "fetch_sheets": "获取Google Sheets", + "remove_integration": "移除集成", + "submit": "提交" + }, + "fields": { + "select_sheet": "选择Google Sheet", + "selected_sheet": "已选择表格: {{name}} (ID: {{id}})" + } + }, + "airtable": { + "title": "与Airtable集成", + "descriptions": { + "sync_info": "将您的机器人连接到Airtable数据库以自动同步数据。", + "authenticated_as": "已通过Airtable认证" + }, + "alerts": { + "success": { + "title": "已连接到Airtable", + "content": "您的机器人现已连接到数据库:{{baseName}}和表格:{{tableName}}。您可以在", + "here": "这里", + "note": "注意:", + "sync_limitation": "只有集成后捕获的数据才会同步到Airtable。" + } + }, + "buttons": { + "authenticate": "连接Airtable", + "fetch_bases": "获取Airtable数据库", + "fetch_tables": "获取Airtable表格", + "remove_integration": "移除Airtable集成", + "submit": "选择数据库和表格" + }, + "fields": { + "select_base": "选择Airtable数据库", + "select_table": "选择Airtable表格", + "selected_base": "已选数据库:{{name}}", + "selected_table": "已选表格:{{name}}" + }, + "errors": { + "auth_error": "Airtable认证错误", + "fetch_error": "获取Airtable数据库错误:{{message}}", + "fetch_tables_error": "获取Airtable表格错误:{{message}}", + "update_error": "更新Airtable数据库错误:{{message}}", + "remove_error": "移除Airtable集成错误:{{message}}" + }, + "notifications": { + "base_selected": "已成功选择Airtable数据库", + "table_selected": "已成功选择Airtable表格", + "integration_removed": "已成功移除Airtable集成" + } } }, "robot_duplication": { From 905c81f2666636f35af4b442b7d76bc1a3b1a830 Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 26 Feb 2025 14:50:52 +0530 Subject: [PATCH 18/20] feat: add require sign in middleware --- server/src/routes/auth.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 5c267663..4a435cd6 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -677,7 +677,7 @@ router.get("/airtable", requireSignIn, (req: Request, res) => { res.redirect(`https://airtable.com/oauth2/v1/authorize?${params}`); }); -router.get("/airtable/callback", async (req: Request, res) => { +router.get("/airtable/callback", requireSignIn, async (req: Request, res) => { const authenticatedReq = req as AuthenticatedRequest; const baseUrl = process.env.PUBLIC_URL || "http://localhost:5173"; @@ -890,7 +890,7 @@ router.post("/airtable/remove", requireSignIn, async (req: Request, res) => { // Fetch tables from an Airtable base -router.get("/airtable/tables", requireSignIn,async (req: Request, res) => { +router.get("/airtable/tables", requireSignIn, async (req: Request, res) => { const authenticatedReq = req as AuthenticatedRequest; try { const { baseId, robotId } = authenticatedReq.query; From 6f92f6aae1e5c67445031a9893cd90c82b65f540 Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 26 Feb 2025 14:52:47 +0530 Subject: [PATCH 19/20] feat: rm trailing quotes --- src/components/robot/Recordings.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/robot/Recordings.tsx b/src/components/robot/Recordings.tsx index 62923cf5..7b062323 100644 --- a/src/components/robot/Recordings.tsx +++ b/src/components/robot/Recordings.tsx @@ -65,13 +65,13 @@ export const Recordings = ({ if (authStatus === 'success' && robotId) { notify(authStatus, t("recordingtable.notifications.auth_success")); - handleNavigate(`/robots/${robotId}/integrate`, robotId, "", []);'' + handleNavigate(`/robots/${robotId}/integrate`, robotId, "", []); } if (airtableAuthStatus === 'success' && robotId) { notify(airtableAuthStatus, t("recordingtable.notifications.auth_success")); - handleNavigate(`/robots/${robotId}/integrate`, robotId, "", []);'' + handleNavigate(`/robots/${robotId}/integrate`, robotId, "", []); } }, []); From 10ec273fbdbdf83af3810a98ca38c6dc6b40f542 Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 26 Feb 2025 15:01:37 +0530 Subject: [PATCH 20/20] feat: add integration settings translations --- public/locales/de.json | 28 +++++++++++------ public/locales/es.json | 56 ++++++++++++++++++++-------------- public/locales/ja.json | 68 ++++++++++++++++++++++++------------------ public/locales/zh.json | 68 ++++++++++++++++++++++++------------------ 4 files changed, 130 insertions(+), 90 deletions(-) diff --git a/public/locales/de.json b/public/locales/de.json index d86773b8..71ba81a1 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -317,13 +317,13 @@ "google": { "title": "Mit Google Sheet integrieren", "descriptions": { - "sync_info": "Wenn Sie diese Option aktivieren, werden bei jeder erfolgreichen Ausführung dieses Roboters die erfassten Daten in Ihrem Google Sheet ergänzt.", + "sync_info": "Wenn Sie diese Option aktivieren, werden bei jeder erfolgreichen Ausführung einer Aufgabe durch diesen Roboter die erfassten Daten an Ihr Google Sheet angehängt.", "authenticated_as": "Authentifiziert als: {{email}}" }, "alerts": { "success": { "title": "Google Sheet erfolgreich integriert.", - "content": "Jedes Mal, wenn dieser Roboter eine erfolgreiche Ausführung erstellt, werden die erfassten Daten Ihrem Google Sheet {{sheetName}} hinzugefügt. Sie können die Datenaktualisierungen", + "content": "Jedes Mal, wenn dieser Roboter einen erfolgreichen Lauf erstellt, werden seine erfassten Daten an Ihr Google Sheet {{sheetName}} angehängt. Sie können die Datenaktualisierungen überprüfen", "here": "hier", "note": "Hinweis:", "sync_limitation": "Die vor der Integration mit Google Sheets extrahierten Daten werden nicht im Google Sheet synchronisiert. Nur die nach der Integration extrahierten Daten werden synchronisiert." @@ -331,19 +331,29 @@ }, "buttons": { "authenticate": "Mit Google authentifizieren", - "fetch_sheets": "Google Sheets abrufen", + "fetch_sheets": "Google Tabellenkalkulationen abrufen", "remove_integration": "Integration entfernen", "submit": "Absenden" }, "fields": { "select_sheet": "Google Sheet auswählen", "selected_sheet": "Ausgewähltes Sheet: {{name}} (ID: {{id}})" + }, + "errors": { + "auth_error": "Fehler bei der Authentifizierung mit Google", + "fetch_error": "Fehler beim Abrufen der Tabellenkalkulationsdateien: {{message}}", + "update_error": "Fehler beim Aktualisieren der Google Sheet-ID: {{message}}", + "remove_error": "Fehler beim Entfernen der Google Sheets-Integration: {{message}}" + }, + "notifications": { + "sheet_selected": "Google Sheet erfolgreich ausgewählt", + "integration_removed": "Google Sheets-Integration erfolgreich entfernt" } }, "airtable": { - "title": "Mit Airtable verbinden", + "title": "Mit Airtable integrieren", "descriptions": { - "sync_info": "Verbinden Sie Ihren Roboter mit einer Airtable-Basis, um Daten automatisch zu synchronisieren.", + "sync_info": "Wenn Sie diese Option aktivieren, werden bei jeder erfolgreichen Ausführung einer Aufgabe durch diesen Roboter die erfassten Daten an Ihre Airtable angehängt.", "authenticated_as": "Mit Airtable authentifiziert" }, "alerts": { @@ -357,14 +367,14 @@ }, "buttons": { "authenticate": "Mit Airtable verbinden", - "fetch_bases": "Airtable-Basen abrufen", - "fetch_tables": "Airtable-Tabellen abrufen", + "fetch_bases": "Airtable Basen abrufen", + "fetch_tables": "Airtable Tabellen abrufen", "remove_integration": "Airtable-Integration entfernen", "submit": "Basis und Tabelle auswählen" }, "fields": { - "select_base": "Airtable-Basis auswählen", - "select_table": "Airtable-Tabelle auswählen", + "select_base": "Airtable Basis auswählen", + "select_table": "Airtable Tabelle auswählen", "selected_base": "Ausgewählte Basis: {{name}}", "selected_table": "Ausgewählte Tabelle: {{name}}" }, diff --git a/public/locales/es.json b/public/locales/es.json index 79270cd3..8cb4c302 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -306,45 +306,55 @@ } }, "integration_settings": { - "title": "Configuración de integración", - "title_select_integration": "Seleccionar tipo de integración", + "title": "Ajustes de Integración", + "title_select_integration": "Seleccionar Tipo de Integración", "descriptions": { "authenticated_as": "Autenticado como: {{email}}" }, "buttons": { "submit": "Enviar", - "remove_integration": "Eliminar integración" + "remove_integration": "Eliminar Integración" }, "google": { "title": "Integrar con Google Sheet", "descriptions": { - "sync_info": "Si habilitas esta opción, cada vez que este robot ejecute una tarea exitosamente, sus datos capturados se añadirán a tu Google Sheet.", + "sync_info": "Si habilita esta opción, cada vez que este robot ejecute una tarea correctamente, sus datos capturados se añadirán a su Google Sheet.", "authenticated_as": "Autenticado como: {{email}}" }, "alerts": { "success": { - "title": "Google Sheet integrado exitosamente.", - "content": "Cada vez que este robot crea una ejecución exitosa, sus datos capturados se añaden a tu Google Sheet {{sheetName}}. Puedes verificar las actualizaciones de datos", + "title": "Google Sheet Integrado Correctamente.", + "content": "Cada vez que este robot crea una ejecución exitosa, sus datos capturados se añaden a su Google Sheet {{sheetName}}. Puede comprobar las actualizaciones de datos", "here": "aquí", "note": "Nota:", - "sync_limitation": "Los datos extraídos antes de la integración con Google Sheets no se sincronizarán en el Google Sheet. Solo se sincronizarán los datos extraídos después de la integración." + "sync_limitation": "Los datos extraídos antes de la integración con Google Sheets no se sincronizarán en la Google Sheet. Solo los datos extraídos después de la integración se sincronizarán." } }, "buttons": { "authenticate": "Autenticar con Google", - "fetch_sheets": "Obtener Google Sheets", - "remove_integration": "Eliminar integración", + "fetch_sheets": "Obtener Hojas de Cálculo de Google", + "remove_integration": "Eliminar Integración", "submit": "Enviar" }, "fields": { "select_sheet": "Seleccionar Google Sheet", - "selected_sheet": "Hoja seleccionada: {{name}} (ID: {{id}})" + "selected_sheet": "Hoja Seleccionada: {{name}} (ID: {{id}})" + }, + "errors": { + "auth_error": "Error al autenticar con Google", + "fetch_error": "Error al obtener archivos de hojas de cálculo: {{message}}", + "update_error": "Error al actualizar ID de Google Sheet: {{message}}", + "remove_error": "Error al eliminar la integración de Google Sheets: {{message}}" + }, + "notifications": { + "sheet_selected": "Google Sheet seleccionado correctamente", + "integration_removed": "Integración de Google Sheets eliminada correctamente" } }, "airtable": { "title": "Integrar con Airtable", "descriptions": { - "sync_info": "Conecte su robot a una base de Airtable para sincronizar datos automáticamente.", + "sync_info": "Si habilita esta opción, cada vez que este robot ejecute una tarea correctamente, sus datos capturados se añadirán a su Airtable.", "authenticated_as": "Autenticado con Airtable" }, "alerts": { @@ -358,28 +368,28 @@ }, "buttons": { "authenticate": "Conectar con Airtable", - "fetch_bases": "Obtener bases de Airtable", - "fetch_tables": "Obtener tablas de Airtable", - "remove_integration": "Eliminar integración de Airtable", - "submit": "Seleccionar base y tabla" + "fetch_bases": "Obtener Bases de Airtable", + "fetch_tables": "Obtener Tablas de Airtable", + "remove_integration": "Eliminar Integración de Airtable", + "submit": "Seleccionar Base y Tabla" }, "fields": { - "select_base": "Seleccionar base de Airtable", - "select_table": "Seleccionar tabla de Airtable", - "selected_base": "Base seleccionada: {{name}}", - "selected_table": "Tabla seleccionada: {{name}}" + "select_base": "Seleccionar Base de Airtable", + "select_table": "Seleccionar Tabla de Airtable", + "selected_base": "Base Seleccionada: {{name}}", + "selected_table": "Tabla Seleccionada: {{name}}" }, "errors": { "auth_error": "Error al autenticar con Airtable", "fetch_error": "Error al obtener bases de Airtable: {{message}}", "fetch_tables_error": "Error al obtener tablas de Airtable: {{message}}", - "update_error": "Error al actualizar la base de Airtable: {{message}}", + "update_error": "Error al actualizar base de Airtable: {{message}}", "remove_error": "Error al eliminar la integración de Airtable: {{message}}" }, "notifications": { - "base_selected": "Base de Airtable seleccionada con éxito", - "table_selected": "Tabla de Airtable seleccionada con éxito", - "integration_removed": "Integración de Airtable eliminada con éxito" + "base_selected": "Base de Airtable seleccionada correctamente", + "table_selected": "Tabla de Airtable seleccionada correctamente", + "integration_removed": "Integración de Airtable eliminada correctamente" } } }, diff --git a/public/locales/ja.json b/public/locales/ja.json index d96942dd..adbad166 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -309,77 +309,87 @@ "title": "連携設定", "title_select_integration": "連携タイプを選択", "descriptions": { - "authenticated_as": "認証済みユーザー:{{email}}" + "authenticated_as": "認証済み: {{email}}" }, "buttons": { "submit": "送信", - "remove_integration": "連携を削除" + "remove_integration": "連携を解除" }, "google": { - "title": "Google Sheetと連携", + "title": "Google シートと連携", "descriptions": { - "sync_info": "このオプションを有効にすると、このロボットがタスクを正常に実行するたびに、取得したデータがGoogle Sheetに追加されます。", - "authenticated_as": "認証済みユーザー: {{email}}" + "sync_info": "このオプションを有効にすると、このロボットがタスクを正常に実行するたびに、取得したデータがGoogle シートに追加されます。", + "authenticated_as": "認証済み: {{email}}" }, "alerts": { "success": { - "title": "Google Sheetの連携が完了しました。", - "content": "このロボットが正常に実行を完了するたびに、取得したデータはGoogle Sheet {{sheetName}}に追加されます。データの更新は", + "title": "Google シートの連携に成功しました。", + "content": "このロボットが正常に実行されるたびに、取得したデータが{{sheetName}}という名前のGoogle シートに追加されます。データの更新を確認できます", "here": "こちら", - "note": "注意:", - "sync_limitation": "Google Sheetsとの連携前に抽出されたデータは同期されません。連携後に抽出されたデータのみが同期されます。" + "note": "注意:", + "sync_limitation": "Google シートとの連携前に抽出されたデータはGoogle シートに同期されません。連携後に抽出されたデータのみが同期されます。" } }, "buttons": { "authenticate": "Googleで認証", - "fetch_sheets": "Google Sheetsを取得", + "fetch_sheets": "Google スプレッドシートを取得", "remove_integration": "連携を解除", "submit": "送信" }, "fields": { - "select_sheet": "Google Sheetを選択", + "select_sheet": "Google シートを選択", "selected_sheet": "選択したシート: {{name}} (ID: {{id}})" + }, + "errors": { + "auth_error": "Googleでの認証エラー", + "fetch_error": "スプレッドシートファイルの取得エラー: {{message}}", + "update_error": "Google シートIDの更新エラー: {{message}}", + "remove_error": "Google シート連携の解除エラー: {{message}}" + }, + "notifications": { + "sheet_selected": "Google シートが正常に選択されました", + "integration_removed": "Google シートの連携が正常に解除されました" } }, "airtable": { - "title": "Airtableと連携する", + "title": "Airtableと連携", "descriptions": { - "sync_info": "ロボットをAirtableベースに接続して、データを自動的に同期します。", + "sync_info": "このオプションを有効にすると、このロボットがタスクを正常に実行するたびに、取得したデータがAirtableに追加されます。", "authenticated_as": "Airtableで認証済み" }, "alerts": { - "success": { - "title": "Airtableに接続しました", - "content": "ロボットがベース:{{baseName}}、テーブル:{{tableName}}に接続されました。アクセスするには", - "here": "こちら", - "note": "注意:", - "sync_limitation": "統合後に収集されたデータのみがAirtableに同期されます。" - } + "success": { + "title": "Airtableに接続しました", + "content": "ロボットはベース: {{baseName}} とテーブル: {{tableName}} に接続されました。アクセスできます ", + "here": "こちら", + "note": "注意:", + "sync_limitation": "連携後に取得されたデータのみがAirtableに同期されます。" + } }, "buttons": { "authenticate": "Airtableと接続", "fetch_bases": "Airtableベースを取得", "fetch_tables": "Airtableテーブルを取得", - "remove_integration": "Airtable連携を削除", + "remove_integration": "Airtable連携を解除", "submit": "ベースとテーブルを選択" }, "fields": { "select_base": "Airtableベースを選択", "select_table": "Airtableテーブルを選択", - "selected_base": "選択されたベース:{{name}}", - "selected_table": "選択されたテーブル:{{name}}" + "selected_base": "選択したベース: {{name}}", + "selected_table": "選択したテーブル: {{name}}" }, "errors": { - "auth_error": "Airtableとの認証エラー", - "fetch_error": "Airtableベースの取得エラー:{{message}}", - "fetch_tables_error": "Airtableテーブルの取得エラー:{{message}}", - "update_error": "Airtableベースの更新エラー:{{message}}", - "remove_error": "Airtable連携の削除エラー:{{message}}" + "auth_error": "Airtableでの認証エラー", + "fetch_error": "Airtableベースの取得エラー: {{message}}", + "fetch_tables_error": "Airtableテーブルの取得エラー: {{message}}", + "update_error": "Airtableベースの更新エラー: {{message}}", + "remove_error": "Airtable連携の解除エラー: {{message}}" }, "notifications": { "base_selected": "Airtableベースが正常に選択されました", "table_selected": "Airtableテーブルが正常に選択されました", - "integration_removed": "Airtable連携が正常に削除されました" + "integration_removed": "Airtable連携が正常に解除されました" } } }, diff --git a/public/locales/zh.json b/public/locales/zh.json index 4c12871c..e5fd6152 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -309,77 +309,87 @@ "title": "集成设置", "title_select_integration": "选择集成类型", "descriptions": { - "authenticated_as": "已验证身份:{{email}}" + "authenticated_as": "已认证为:{{email}}" }, "buttons": { "submit": "提交", "remove_integration": "移除集成" }, "google": { - "title": "与Google Sheet集成", + "title": "与Google表格集成", "descriptions": { - "sync_info": "如果启用此选项,每次机器人成功运行任务时,捕获的数据都会追加到您的Google Sheet中。", - "authenticated_as": "已验证身份: {{email}}" + "sync_info": "如果启用此选项,每次机器人成功运行任务时,其捕获的数据将附加到您的Google表格中。", + "authenticated_as": "已认证为:{{email}}" }, "alerts": { "success": { - "title": "Google Sheet集成成功。", - "content": "每次此机器人创建成功运行时,捕获的数据都会追加到您的Google Sheet {{sheetName}}中。您可以查看数据更新", - "here": "在此处", + "title": "Google表格集成成功。", + "content": "每次此机器人创建成功运行时,其捕获的数据将附加到您的{{sheetName}} Google表格中。您可以检查数据更新", + "here": "在这里", "note": "注意:", - "sync_limitation": "与Google Sheets集成之前提取的数据将不会同步到Google Sheet中。只有集成后提取的数据才会同步。" + "sync_limitation": "在与Google表格集成之前提取的数据将不会在Google表格中同步。只有集成后提取的数据才会同步。" } }, "buttons": { - "authenticate": "使用Google验证", - "fetch_sheets": "获取Google Sheets", + "authenticate": "使用Google认证", + "fetch_sheets": "获取Google电子表格", "remove_integration": "移除集成", "submit": "提交" }, "fields": { - "select_sheet": "选择Google Sheet", - "selected_sheet": "已选择表格: {{name}} (ID: {{id}})" + "select_sheet": "选择Google表格", + "selected_sheet": "已选择表格:{{name}}(ID:{{id}})" + }, + "errors": { + "auth_error": "使用Google认证时出错", + "fetch_error": "获取电子表格文件时出错:{{message}}", + "update_error": "更新Google表格ID时出错:{{message}}", + "remove_error": "移除Google表格集成时出错:{{message}}" + }, + "notifications": { + "sheet_selected": "Google表格选择成功", + "integration_removed": "Google表格集成已成功移除" } }, "airtable": { "title": "与Airtable集成", "descriptions": { - "sync_info": "将您的机器人连接到Airtable数据库以自动同步数据。", - "authenticated_as": "已通过Airtable认证" + "sync_info": "如果启用此选项,每次机器人成功运行任务时,其捕获的数据将附加到您的Airtable中。", + "authenticated_as": "已使用Airtable认证" }, "alerts": { "success": { "title": "已连接到Airtable", - "content": "您的机器人现已连接到数据库:{{baseName}}和表格:{{tableName}}。您可以在", - "here": "这里", + "content": "您的机器人现已连接到基础:{{baseName}}和表格:{{tableName}}。您可以访问它", + "here": "在这里", "note": "注意:", "sync_limitation": "只有集成后捕获的数据才会同步到Airtable。" } }, "buttons": { "authenticate": "连接Airtable", - "fetch_bases": "获取Airtable数据库", + "fetch_bases": "获取Airtable基础", "fetch_tables": "获取Airtable表格", "remove_integration": "移除Airtable集成", - "submit": "选择数据库和表格" + "submit": "选择基础和表格" }, "fields": { - "select_base": "选择Airtable数据库", + "select_base": "选择Airtable基础", "select_table": "选择Airtable表格", - "selected_base": "已选数据库:{{name}}", - "selected_table": "已选表格:{{name}}" + "selected_base": "已选择基础:{{name}}", + "selected_table": "已选择表格:{{name}}" }, "errors": { - "auth_error": "Airtable认证错误", - "fetch_error": "获取Airtable数据库错误:{{message}}", - "fetch_tables_error": "获取Airtable表格错误:{{message}}", - "update_error": "更新Airtable数据库错误:{{message}}", - "remove_error": "移除Airtable集成错误:{{message}}" + "auth_error": "使用Airtable认证时出错", + "fetch_error": "获取Airtable基础时出错:{{message}}", + "fetch_tables_error": "获取Airtable表格时出错:{{message}}", + "update_error": "更新Airtable基础时出错:{{message}}", + "remove_error": "移除Airtable集成时出错:{{message}}" }, "notifications": { - "base_selected": "已成功选择Airtable数据库", - "table_selected": "已成功选择Airtable表格", - "integration_removed": "已成功移除Airtable集成" + "base_selected": "Airtable基础选择成功", + "table_selected": "Airtable表格选择成功", + "integration_removed": "Airtable集成已成功移除" } } },