@rashidazarang/airtable-mcp 2.1.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +10 -1
- package/.github/ISSUE_TEMPLATE/bug-report.yml +0 -173
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -38
- package/.github/ISSUE_TEMPLATE/custom.md +0 -10
- package/.github/ISSUE_TEMPLATE/feature-request.yml +0 -209
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
- package/.github/ISSUE_TEMPLATE/security-report.yml +0 -216
- package/.github/pull_request_template.md +0 -245
- package/.github/workflows/ci-cd.yml +0 -408
- package/.github/workflows/security-audit.yml +0 -316
- package/API_DOCUMENTATION.md +0 -897
- package/CAPABILITY_REPORT.md +0 -118
- package/CLAUDE_INTEGRATION.md +0 -96
- package/CODE_OF_CONDUCT.md +0 -181
- package/CONTRIBUTING.md +0 -81
- package/DEVELOPMENT.md +0 -190
- package/Dockerfile +0 -39
- package/Dockerfile.node +0 -20
- package/Dockerfile.production +0 -127
- package/IMPROVEMENT_PROPOSAL.md +0 -371
- package/INSTALLATION.md +0 -183
- package/ISSUE_RESPONSES.md +0 -171
- package/MCP_REVIEW_SUMMARY.md +0 -142
- package/QUICK_START.md +0 -60
- package/RELEASE_NOTES_v1.2.0.md +0 -50
- package/RELEASE_NOTES_v1.2.1.md +0 -40
- package/RELEASE_NOTES_v1.2.2.md +0 -48
- package/RELEASE_NOTES_v1.2.3.md +0 -105
- package/RELEASE_NOTES_v1.2.4.md +0 -60
- package/RELEASE_NOTES_v1.4.0.md +0 -104
- package/RELEASE_NOTES_v1.5.0.md +0 -185
- package/RELEASE_NOTES_v1.6.0.md +0 -248
- package/SECURITY_NOTICE.md +0 -40
- package/airtable-clipper/CHANGELOG.md +0 -198
- package/airtable-clipper/CHROME_STORE_SUBMISSION.md +0 -343
- package/airtable-clipper/LAUNCH_STRATEGY.md +0 -495
- package/airtable-clipper/LICENSE +0 -21
- package/airtable-clipper/OAUTH_SETUP.md +0 -51
- package/airtable-clipper/PRIVACY_POLICY.md +0 -187
- package/airtable-clipper/README.md +0 -575
- package/airtable-clipper/SUBMIT_TO_CHROME_STORE.md +0 -273
- package/airtable-clipper/build.sh +0 -85
- package/airtable-clipper/docs/QUICK_START.md +0 -99
- package/airtable-clipper/docs/SETUP.md +0 -291
- package/airtable-clipper/extension/background.js +0 -337
- package/airtable-clipper/extension/base-setup.html +0 -324
- package/airtable-clipper/extension/base-setup.js +0 -471
- package/airtable-clipper/extension/content.js +0 -771
- package/airtable-clipper/extension/icons/README.md +0 -69
- package/airtable-clipper/extension/icons/icon-16.png +0 -3
- package/airtable-clipper/extension/manifest.json +0 -73
- package/airtable-clipper/extension/popup.html +0 -144
- package/airtable-clipper/extension/popup.js +0 -475
- package/airtable-clipper/extension/styles/content.css +0 -229
- package/airtable-clipper/extension/styles/popup.css +0 -477
- package/airtable-clipper/privacy-policy.md +0 -63
- package/airtable-clipper/releases/v1.0.0/background.js +0 -337
- package/airtable-clipper/releases/v1.0.0/base-setup.html +0 -324
- package/airtable-clipper/releases/v1.0.0/base-setup.js +0 -471
- package/airtable-clipper/releases/v1.0.0/content.js +0 -771
- package/airtable-clipper/releases/v1.0.0/icons/README.md +0 -69
- package/airtable-clipper/releases/v1.0.0/icons/icon-128.png +0 -2
- package/airtable-clipper/releases/v1.0.0/icons/icon-16.png +0 -3
- package/airtable-clipper/releases/v1.0.0/icons/icon-32.png +0 -2
- package/airtable-clipper/releases/v1.0.0/icons/icon-48.png +0 -2
- package/airtable-clipper/releases/v1.0.0/manifest.json +0 -73
- package/airtable-clipper/releases/v1.0.0/popup.html +0 -144
- package/airtable-clipper/releases/v1.0.0/popup.js +0 -475
- package/airtable-clipper/releases/v1.0.0/sidepanel.html +0 -25
- package/airtable-clipper/releases/v1.0.0/styles/content.css +0 -229
- package/airtable-clipper/releases/v1.0.0/styles/popup.css +0 -477
- package/airtable-clipper/releases/v1.0.1/background.js +0 -337
- package/airtable-clipper/releases/v1.0.1/base-setup.html +0 -324
- package/airtable-clipper/releases/v1.0.1/base-setup.js +0 -471
- package/airtable-clipper/releases/v1.0.1/content.js +0 -771
- package/airtable-clipper/releases/v1.0.1/icons/README.md +0 -69
- package/airtable-clipper/releases/v1.0.1/icons/icon-128.png +0 -2
- package/airtable-clipper/releases/v1.0.1/icons/icon-16.png +0 -3
- package/airtable-clipper/releases/v1.0.1/icons/icon-32.png +0 -2
- package/airtable-clipper/releases/v1.0.1/icons/icon-48.png +0 -2
- package/airtable-clipper/releases/v1.0.1/manifest.json +0 -70
- package/airtable-clipper/releases/v1.0.1/popup.html +0 -157
- package/airtable-clipper/releases/v1.0.1/popup.js +0 -562
- package/airtable-clipper/releases/v1.0.1/sidepanel.html +0 -25
- package/airtable-clipper/releases/v1.0.1/styles/content.css +0 -229
- package/airtable-clipper/releases/v1.0.1/styles/popup.css +0 -647
- package/airtable-clipper/releases/v1.0.2/background.js +0 -337
- package/airtable-clipper/releases/v1.0.2/base-setup.html +0 -324
- package/airtable-clipper/releases/v1.0.2/base-setup.js +0 -471
- package/airtable-clipper/releases/v1.0.2/content.js +0 -771
- package/airtable-clipper/releases/v1.0.2/icons/README.md +0 -69
- package/airtable-clipper/releases/v1.0.2/icons/icon-128.png +0 -2
- package/airtable-clipper/releases/v1.0.2/icons/icon-16.png +0 -3
- package/airtable-clipper/releases/v1.0.2/icons/icon-32.png +0 -2
- package/airtable-clipper/releases/v1.0.2/icons/icon-48.png +0 -2
- package/airtable-clipper/releases/v1.0.2/manifest.json +0 -62
- package/airtable-clipper/releases/v1.0.2/popup.html +0 -157
- package/airtable-clipper/releases/v1.0.2/popup.js +0 -567
- package/airtable-clipper/releases/v1.0.2/sidepanel.html +0 -25
- package/airtable-clipper/releases/v1.0.2/styles/content.css +0 -229
- package/airtable-clipper/releases/v1.0.2/styles/popup.css +0 -647
- package/airtable-clipper/terms-of-service.md +0 -124
- package/airtable-clipper/test-credentials.md +0 -61
- package/airtable-clipper/test-extension/background.js +0 -337
- package/airtable-clipper/test-extension/base-setup.html +0 -324
- package/airtable-clipper/test-extension/base-setup.js +0 -471
- package/airtable-clipper/test-extension/content.js +0 -873
- package/airtable-clipper/test-extension/icons/README.md +0 -69
- package/airtable-clipper/test-extension/icons/icon-128.png +0 -2
- package/airtable-clipper/test-extension/icons/icon-16.png +0 -3
- package/airtable-clipper/test-extension/icons/icon-32.png +0 -2
- package/airtable-clipper/test-extension/icons/icon-48.png +0 -2
- package/airtable-clipper/test-extension/manifest.json +0 -72
- package/airtable-clipper/test-extension/popup.html +0 -274
- package/airtable-clipper/test-extension/popup.js +0 -729
- package/airtable-clipper/test-extension/sidepanel.html +0 -25
- package/airtable-clipper/test-extension/styles/content.css +0 -229
- package/airtable-clipper/test-extension/styles/popup.css +0 -794
- package/airtable_mcp/__init__.py +0 -5
- package/airtable_mcp/src/server.py +0 -329
- package/airtable_mcp_v2.js +0 -1505
- package/airtable_mcp_v2_oauth.js +0 -1048
- package/airtable_mcp_v3_advanced.js +0 -1161
- package/cleanup.sh +0 -71
- package/docker-compose.production.yml +0 -366
- package/helm/airtable-mcp/Chart.yaml +0 -122
- package/helm/airtable-mcp/values.yaml +0 -538
- package/index.js +0 -179
- package/inspector.py +0 -148
- package/inspector_server.py +0 -337
- package/k8s/deployment.yaml +0 -402
- package/k8s/namespace.yaml +0 -108
- package/k8s/service.yaml +0 -194
- package/monitoring/alerts.yml +0 -289
- package/monitoring/prometheus.yml +0 -224
- package/publish-steps.txt +0 -27
- package/quick_test.sh +0 -30
- package/requirements.txt +0 -10
- package/setup.py +0 -29
- package/simple_airtable_server.py +0 -151
- package/smithery.yaml +0 -45
- package/test_all_features.sh +0 -146
- package/test_all_operations.sh +0 -120
- package/test_client.py +0 -70
- package/test_enhanced_features.js +0 -389
- package/test_mcp_comprehensive.js +0 -163
- package/test_mock_server.js +0 -180
- package/test_v1.4.0_final.sh +0 -131
- package/test_v1.5.0_comprehensive.sh +0 -96
- package/test_v1.5.0_final.sh +0 -224
- package/test_v1.6.0_comprehensive.sh +0 -187
- package/test_webhooks.sh +0 -105
|
@@ -1,729 +0,0 @@
|
|
|
1
|
-
// Airtable Clipper - Simple Popup Controller
|
|
2
|
-
import { AirtableClient, AirtableError } from './lib/airtable-client.js';
|
|
3
|
-
|
|
4
|
-
class PopupController {
|
|
5
|
-
constructor() {
|
|
6
|
-
this.client = null;
|
|
7
|
-
this.currentTab = null;
|
|
8
|
-
this.isConnected = false;
|
|
9
|
-
this.init();
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
async init() {
|
|
13
|
-
try {
|
|
14
|
-
// Get current tab
|
|
15
|
-
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
16
|
-
this.currentTab = tabs[0];
|
|
17
|
-
|
|
18
|
-
// Check connection status
|
|
19
|
-
await this.checkConnection();
|
|
20
|
-
|
|
21
|
-
// Update UI for current page
|
|
22
|
-
this.updateUIForCurrentPage();
|
|
23
|
-
|
|
24
|
-
// Bind all event listeners
|
|
25
|
-
this.bindEvents();
|
|
26
|
-
} catch (error) {
|
|
27
|
-
console.error('Init failed:', error);
|
|
28
|
-
this.showToast('Failed to initialize extension', 'error');
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async checkConnection() {
|
|
33
|
-
try {
|
|
34
|
-
const result = await chrome.storage.local.get(['airtableToken', 'baseId']);
|
|
35
|
-
|
|
36
|
-
if (result.airtableToken && result.baseId) {
|
|
37
|
-
// Test the connection
|
|
38
|
-
this.client = new AirtableClient(result.airtableToken, result.baseId);
|
|
39
|
-
const testResult = await this.client.testConnection();
|
|
40
|
-
|
|
41
|
-
if (testResult.success) {
|
|
42
|
-
this.isConnected = true;
|
|
43
|
-
this.showConnectedView();
|
|
44
|
-
} else {
|
|
45
|
-
this.showSetupView();
|
|
46
|
-
}
|
|
47
|
-
} else {
|
|
48
|
-
this.showSetupView();
|
|
49
|
-
}
|
|
50
|
-
} catch (error) {
|
|
51
|
-
console.error('Connection check failed:', error);
|
|
52
|
-
this.showSetupView();
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
showSetupView() {
|
|
57
|
-
document.getElementById('setupContainer').style.display = 'block';
|
|
58
|
-
document.getElementById('connectedContainer').style.display = 'none';
|
|
59
|
-
this.isConnected = false;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
showConnectedView() {
|
|
63
|
-
document.getElementById('setupContainer').style.display = 'none';
|
|
64
|
-
document.getElementById('connectedContainer').style.display = 'block';
|
|
65
|
-
this.isConnected = true;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
updateUIForCurrentPage() {
|
|
69
|
-
if (!this.currentTab || !this.isConnected) return;
|
|
70
|
-
|
|
71
|
-
try {
|
|
72
|
-
const url = new URL(this.currentTab.url);
|
|
73
|
-
const domain = url.hostname.replace('www.', '');
|
|
74
|
-
|
|
75
|
-
// Update page info
|
|
76
|
-
document.getElementById('pageTitle').textContent = this.currentTab.title || 'Current Page';
|
|
77
|
-
document.getElementById('pageUrl').textContent = domain;
|
|
78
|
-
|
|
79
|
-
// Set page icon based on domain
|
|
80
|
-
let icon = '🌐';
|
|
81
|
-
if (domain.includes('linkedin.com')) {
|
|
82
|
-
icon = '💼';
|
|
83
|
-
document.getElementById('linkedinBtn').style.display = 'block';
|
|
84
|
-
document.getElementById('saveTarget').textContent = 'to Contacts';
|
|
85
|
-
} else {
|
|
86
|
-
document.getElementById('linkedinBtn').style.display = 'none';
|
|
87
|
-
document.getElementById('saveTarget').textContent = 'to Web Clips';
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
document.getElementById('pageIcon').textContent = icon;
|
|
91
|
-
} catch (error) {
|
|
92
|
-
console.error('Failed to update UI for page:', error);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
bindEvents() {
|
|
97
|
-
console.log('Binding events...');
|
|
98
|
-
|
|
99
|
-
// Setup view events
|
|
100
|
-
const oauthBtn = document.getElementById('oauthBtn');
|
|
101
|
-
const manualBtn = document.getElementById('manualConnectBtn');
|
|
102
|
-
const confirmBtn = document.getElementById('confirmBaseBtn');
|
|
103
|
-
const setupBtn = document.getElementById('setupBtn');
|
|
104
|
-
|
|
105
|
-
console.log('Found buttons:', {
|
|
106
|
-
oauth: !!oauthBtn,
|
|
107
|
-
manual: !!manualBtn,
|
|
108
|
-
confirm: !!confirmBtn,
|
|
109
|
-
setup: !!setupBtn
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
oauthBtn?.addEventListener('click', () => {
|
|
113
|
-
console.log('OAuth button clicked');
|
|
114
|
-
this.handleOAuthConnect();
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
manualBtn?.addEventListener('click', () => {
|
|
118
|
-
console.log('Manual connect button clicked');
|
|
119
|
-
this.handleManualConnect();
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
confirmBtn?.addEventListener('click', () => {
|
|
123
|
-
console.log('Confirm base button clicked');
|
|
124
|
-
this.handleConfirmBase();
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
setupBtn?.addEventListener('click', () => {
|
|
128
|
-
console.log('Setup button clicked');
|
|
129
|
-
this.handleSetupDatabase();
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
// Connected view events
|
|
133
|
-
document.getElementById('saveBtn')?.addEventListener('click', () => this.handleQuickSave());
|
|
134
|
-
document.getElementById('linkedinBtn')?.addEventListener('click', () => this.handleLinkedInSave());
|
|
135
|
-
document.getElementById('bulkBtn')?.addEventListener('click', () => this.handleBulkMode());
|
|
136
|
-
document.getElementById('settingsBtn')?.addEventListener('click', () => this.showSettings());
|
|
137
|
-
|
|
138
|
-
// Settings events
|
|
139
|
-
document.getElementById('closeSettings')?.addEventListener('click', () => this.hideSettings());
|
|
140
|
-
document.getElementById('disconnectBtn')?.addEventListener('click', () => this.handleDisconnect());
|
|
141
|
-
|
|
142
|
-
// Auto-save settings
|
|
143
|
-
document.getElementById('autoSave')?.addEventListener('change', () => this.saveSettings());
|
|
144
|
-
document.getElementById('notifications')?.addEventListener('change', () => this.saveSettings());
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async handleConnect() {
|
|
148
|
-
const connectBtn = document.getElementById('connectBtn');
|
|
149
|
-
const btnText = connectBtn.querySelector('.btn-text');
|
|
150
|
-
const btnSpinner = document.getElementById('connectSpinner');
|
|
151
|
-
|
|
152
|
-
const token = document.getElementById('airtableToken').value.trim();
|
|
153
|
-
const baseId = document.getElementById('baseId').value.trim();
|
|
154
|
-
|
|
155
|
-
if (!token || !baseId) {
|
|
156
|
-
this.showToast('Please enter both token and base ID', 'error');
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Show loading state
|
|
161
|
-
connectBtn.disabled = true;
|
|
162
|
-
btnText.textContent = 'Connecting...';
|
|
163
|
-
btnSpinner.style.display = 'block';
|
|
164
|
-
|
|
165
|
-
try {
|
|
166
|
-
// Test connection
|
|
167
|
-
const testClient = new AirtableClient(token, baseId);
|
|
168
|
-
const result = await testClient.testConnection();
|
|
169
|
-
|
|
170
|
-
if (result.success) {
|
|
171
|
-
// Save credentials
|
|
172
|
-
await chrome.storage.local.set({
|
|
173
|
-
airtableToken: token,
|
|
174
|
-
baseId: baseId
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
this.client = testClient;
|
|
178
|
-
this.isConnected = true;
|
|
179
|
-
this.showConnectedView();
|
|
180
|
-
this.updateUIForCurrentPage();
|
|
181
|
-
this.showToast('Connected successfully!', 'success');
|
|
182
|
-
} else {
|
|
183
|
-
throw new Error(result.error || 'Connection test failed');
|
|
184
|
-
}
|
|
185
|
-
} catch (error) {
|
|
186
|
-
console.error('Connection failed:', error);
|
|
187
|
-
let errorMessage = 'Connection failed';
|
|
188
|
-
|
|
189
|
-
if (error instanceof AirtableError) {
|
|
190
|
-
if (error.message.includes('401')) {
|
|
191
|
-
errorMessage = 'Invalid token. Please check your Personal Access Token.';
|
|
192
|
-
} else if (error.message.includes('404')) {
|
|
193
|
-
errorMessage = 'Base not found. Please check your Base ID.';
|
|
194
|
-
} else {
|
|
195
|
-
errorMessage = error.message;
|
|
196
|
-
}
|
|
197
|
-
} else if (error.message) {
|
|
198
|
-
errorMessage = error.message;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
this.showToast(errorMessage, 'error');
|
|
202
|
-
} finally {
|
|
203
|
-
// Reset button state
|
|
204
|
-
connectBtn.disabled = false;
|
|
205
|
-
btnText.textContent = 'Connect to Airtable';
|
|
206
|
-
btnSpinner.style.display = 'none';
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
async handleOAuthConnect() {
|
|
211
|
-
const connectBtn = document.getElementById('oauthBtn');
|
|
212
|
-
const btnText = connectBtn.querySelector('.btn-text');
|
|
213
|
-
const btnSpinner = document.getElementById('oauthSpinner');
|
|
214
|
-
|
|
215
|
-
// Show loading state
|
|
216
|
-
connectBtn.disabled = true;
|
|
217
|
-
btnText.textContent = 'Connecting...';
|
|
218
|
-
btnSpinner.style.display = 'block';
|
|
219
|
-
|
|
220
|
-
try {
|
|
221
|
-
// Import and use OAuth
|
|
222
|
-
const { AirtableOAuth } = await import('./lib/airtable-oauth.js');
|
|
223
|
-
const oauth = new AirtableOAuth();
|
|
224
|
-
|
|
225
|
-
// Check if OAuth is available
|
|
226
|
-
if (!chrome.identity || !chrome.identity.launchWebAuthFlow) {
|
|
227
|
-
throw new Error('OAuth not available in this context. Please use manual connection.');
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Authenticate
|
|
231
|
-
const tokens = await oauth.authenticate();
|
|
232
|
-
|
|
233
|
-
// Get user's bases
|
|
234
|
-
const basesData = await oauth.getUserBases();
|
|
235
|
-
|
|
236
|
-
if (basesData && basesData.bases && basesData.bases.length > 0) {
|
|
237
|
-
// Show base selector
|
|
238
|
-
this.populateBaseOptions(basesData.bases);
|
|
239
|
-
document.getElementById('baseSelector').style.display = 'block';
|
|
240
|
-
this.showToast('Authentication successful! Please select a base.', 'success');
|
|
241
|
-
} else {
|
|
242
|
-
throw new Error('No Airtable bases found');
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
} catch (error) {
|
|
246
|
-
console.error('OAuth failed:', error);
|
|
247
|
-
this.showToast(`OAuth failed: ${error.message}. Try manual connection below.`, 'error');
|
|
248
|
-
|
|
249
|
-
// Expand manual auth section
|
|
250
|
-
const manualAuth = document.querySelector('.manual-auth');
|
|
251
|
-
if (manualAuth) {
|
|
252
|
-
manualAuth.open = true;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Focus on token input
|
|
256
|
-
setTimeout(() => {
|
|
257
|
-
document.getElementById('tokenInput')?.focus();
|
|
258
|
-
}, 500);
|
|
259
|
-
|
|
260
|
-
} finally {
|
|
261
|
-
// Reset button state
|
|
262
|
-
connectBtn.disabled = false;
|
|
263
|
-
btnText.textContent = 'Connect with Airtable';
|
|
264
|
-
btnSpinner.style.display = 'none';
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
populateBaseOptions(bases) {
|
|
269
|
-
const baseSelect = document.getElementById('baseSelect');
|
|
270
|
-
baseSelect.innerHTML = '<option value="">Choose a base...</option>';
|
|
271
|
-
|
|
272
|
-
bases.forEach(base => {
|
|
273
|
-
const option = document.createElement('option');
|
|
274
|
-
option.value = base.id;
|
|
275
|
-
option.textContent = base.name;
|
|
276
|
-
baseSelect.appendChild(option);
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
// Store bases for later use
|
|
280
|
-
this.availableBases = bases;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
async handleConfirmBase() {
|
|
284
|
-
const baseSelect = document.getElementById('baseSelect');
|
|
285
|
-
const selectedBaseId = baseSelect.value;
|
|
286
|
-
|
|
287
|
-
if (!selectedBaseId) {
|
|
288
|
-
this.showToast('Please select a base', 'error');
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const selectedBase = this.availableBases.find(base => base.id === selectedBaseId);
|
|
293
|
-
|
|
294
|
-
try {
|
|
295
|
-
// Save base selection
|
|
296
|
-
await chrome.storage.local.set({
|
|
297
|
-
selectedBaseId: selectedBaseId,
|
|
298
|
-
selectedBaseName: selectedBase.name,
|
|
299
|
-
useOAuth: true
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
// Create client with OAuth token
|
|
303
|
-
const { AirtableOAuth } = await import('./lib/airtable-oauth.js');
|
|
304
|
-
const oauth = new AirtableOAuth();
|
|
305
|
-
const token = await oauth.getAccessToken();
|
|
306
|
-
this.client = new AirtableClient(token, selectedBaseId);
|
|
307
|
-
|
|
308
|
-
// Switch to connected view
|
|
309
|
-
this.isConnected = true;
|
|
310
|
-
this.showConnectedView();
|
|
311
|
-
this.updateUIForCurrentPage();
|
|
312
|
-
this.showToast(`Connected to ${selectedBase.name}!`, 'success');
|
|
313
|
-
} catch (error) {
|
|
314
|
-
console.error('Base confirmation failed:', error);
|
|
315
|
-
this.showToast(`Failed to connect: ${error.message}`, 'error');
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
async handleManualConnect() {
|
|
320
|
-
const connectBtn = document.getElementById('manualConnectBtn');
|
|
321
|
-
const btnText = connectBtn.querySelector('.btn-text');
|
|
322
|
-
const btnSpinner = document.getElementById('manualSpinner');
|
|
323
|
-
|
|
324
|
-
const token = document.getElementById('tokenInput').value.trim();
|
|
325
|
-
const baseId = document.getElementById('baseInput').value.trim();
|
|
326
|
-
|
|
327
|
-
if (!token || !baseId) {
|
|
328
|
-
this.showToast('Please enter both token and base ID', 'error');
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// Show loading state
|
|
333
|
-
connectBtn.disabled = true;
|
|
334
|
-
btnText.textContent = 'Connecting...';
|
|
335
|
-
btnSpinner.style.display = 'block';
|
|
336
|
-
|
|
337
|
-
try {
|
|
338
|
-
console.log('Testing connection with token:', token.substring(0, 10) + '...', 'and base:', baseId);
|
|
339
|
-
|
|
340
|
-
// Test connection
|
|
341
|
-
const testClient = new AirtableClient(token, baseId);
|
|
342
|
-
const result = await testClient.testConnection();
|
|
343
|
-
|
|
344
|
-
console.log('Connection test result:', result);
|
|
345
|
-
|
|
346
|
-
if (result && result.success) {
|
|
347
|
-
// Save credentials
|
|
348
|
-
await chrome.storage.local.set({
|
|
349
|
-
airtableToken: token,
|
|
350
|
-
baseId: baseId
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
this.client = testClient;
|
|
354
|
-
this.isConnected = true;
|
|
355
|
-
this.showConnectedView();
|
|
356
|
-
this.updateUIForCurrentPage();
|
|
357
|
-
this.showToast('Connected successfully!', 'success');
|
|
358
|
-
} else {
|
|
359
|
-
const errorMsg = result?.message || result?.error || 'Connection test failed';
|
|
360
|
-
throw new Error(errorMsg);
|
|
361
|
-
}
|
|
362
|
-
} catch (error) {
|
|
363
|
-
console.error('Connection failed:', error);
|
|
364
|
-
let errorMessage = 'Connection failed: ';
|
|
365
|
-
|
|
366
|
-
if (error.message) {
|
|
367
|
-
if (error.message.includes('401')) {
|
|
368
|
-
errorMessage += 'Invalid token. Please check your Personal Access Token.';
|
|
369
|
-
} else if (error.message.includes('404')) {
|
|
370
|
-
errorMessage += 'Base not found. Please check your Base ID.';
|
|
371
|
-
} else {
|
|
372
|
-
errorMessage += error.message;
|
|
373
|
-
}
|
|
374
|
-
} else {
|
|
375
|
-
errorMessage += 'Unknown error occurred';
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
this.showToast(errorMessage, 'error');
|
|
379
|
-
} finally {
|
|
380
|
-
// Reset button state
|
|
381
|
-
connectBtn.disabled = false;
|
|
382
|
-
btnText.textContent = 'Connect';
|
|
383
|
-
btnSpinner.style.display = 'none';
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
async handleQuickSave() {
|
|
388
|
-
if (!this.isConnected || !this.currentTab) {
|
|
389
|
-
this.showToast('Not connected to Airtable', 'error');
|
|
390
|
-
return;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
const saveBtn = document.getElementById('saveBtn');
|
|
394
|
-
const originalText = saveBtn.querySelector('.btn-text').textContent;
|
|
395
|
-
|
|
396
|
-
saveBtn.disabled = true;
|
|
397
|
-
saveBtn.querySelector('.btn-text').textContent = 'Saving...';
|
|
398
|
-
|
|
399
|
-
try {
|
|
400
|
-
let saveData;
|
|
401
|
-
let tableName;
|
|
402
|
-
|
|
403
|
-
// Handle LinkedIn pages specially
|
|
404
|
-
if (this.currentTab.url.includes('linkedin.com') && this.currentTab.url.includes('/in/')) {
|
|
405
|
-
console.log('LinkedIn profile detected, extracting profile data...');
|
|
406
|
-
|
|
407
|
-
try {
|
|
408
|
-
// Try to inject content script if needed
|
|
409
|
-
if (chrome.scripting && chrome.scripting.executeScript) {
|
|
410
|
-
await chrome.scripting.executeScript({
|
|
411
|
-
target: { tabId: this.currentTab.id },
|
|
412
|
-
files: ['content.js']
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// Extract LinkedIn profile data
|
|
417
|
-
const response = await chrome.tabs.sendMessage(this.currentTab.id, {
|
|
418
|
-
action: 'extractLinkedInProfile'
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
if (response && response.success && response.data) {
|
|
422
|
-
saveData = response.data;
|
|
423
|
-
tableName = 'Contacts';
|
|
424
|
-
console.log('LinkedIn profile data extracted:', saveData);
|
|
425
|
-
} else {
|
|
426
|
-
throw new Error('Failed to extract LinkedIn profile data');
|
|
427
|
-
}
|
|
428
|
-
} catch (error) {
|
|
429
|
-
console.log('Content script injection failed, using fallback extraction');
|
|
430
|
-
// Use fallback extraction method
|
|
431
|
-
const fallbackData = await this.extractLinkedInDataFallback();
|
|
432
|
-
if (fallbackData) {
|
|
433
|
-
saveData = fallbackData;
|
|
434
|
-
tableName = 'Contacts';
|
|
435
|
-
} else {
|
|
436
|
-
throw new Error('Failed to extract LinkedIn profile data');
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
} else {
|
|
440
|
-
// Regular web page
|
|
441
|
-
saveData = {
|
|
442
|
-
'Title': this.currentTab.title || 'Untitled',
|
|
443
|
-
'Website URL': this.currentTab.url,
|
|
444
|
-
'Domain': new URL(this.currentTab.url).hostname,
|
|
445
|
-
'Saved At': new Date().toISOString().split('T')[0]
|
|
446
|
-
};
|
|
447
|
-
tableName = 'Web Clips';
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
console.log('Saving to table:', tableName, 'with data:', saveData);
|
|
451
|
-
|
|
452
|
-
// Save to Airtable with duplicate detection
|
|
453
|
-
const result = await this.client.createRecord(tableName, saveData);
|
|
454
|
-
|
|
455
|
-
if (result.success) {
|
|
456
|
-
if (result.action === 'updated') {
|
|
457
|
-
this.showToast(`Already exists in ${tableName} - no duplicate created`, 'warning');
|
|
458
|
-
} else {
|
|
459
|
-
this.showToast(`Saved to ${tableName}!`, 'success');
|
|
460
|
-
}
|
|
461
|
-
} else {
|
|
462
|
-
throw new Error(result.error || 'Failed to save');
|
|
463
|
-
}
|
|
464
|
-
} catch (error) {
|
|
465
|
-
console.error('Quick save failed:', error);
|
|
466
|
-
this.showToast(`Failed to save: ${error.message}`, 'error');
|
|
467
|
-
} finally {
|
|
468
|
-
saveBtn.disabled = false;
|
|
469
|
-
saveBtn.querySelector('.btn-text').textContent = originalText;
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
async handleLinkedInSave() {
|
|
474
|
-
if (!this.isConnected || !this.currentTab) {
|
|
475
|
-
this.showToast('Not connected to Airtable', 'error');
|
|
476
|
-
return;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
if (!this.currentTab.url.includes('linkedin.com')) {
|
|
480
|
-
this.showToast('This only works on LinkedIn profiles', 'error');
|
|
481
|
-
return;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
const linkedinBtn = document.getElementById('linkedinBtn');
|
|
485
|
-
const originalText = linkedinBtn.querySelector('.action-title').textContent;
|
|
486
|
-
|
|
487
|
-
linkedinBtn.disabled = true;
|
|
488
|
-
linkedinBtn.querySelector('.action-title').textContent = 'Saving...';
|
|
489
|
-
|
|
490
|
-
try {
|
|
491
|
-
// Try to extract profile data via content script
|
|
492
|
-
let response;
|
|
493
|
-
|
|
494
|
-
try {
|
|
495
|
-
// First try sending message in case content script is already injected
|
|
496
|
-
response = await chrome.tabs.sendMessage(this.currentTab.id, {
|
|
497
|
-
action: 'extractLinkedInProfile'
|
|
498
|
-
});
|
|
499
|
-
} catch (error) {
|
|
500
|
-
console.log('Content script not found, injecting...', error);
|
|
501
|
-
|
|
502
|
-
try {
|
|
503
|
-
// Check if chrome.scripting is available
|
|
504
|
-
if (!chrome.scripting || !chrome.scripting.executeScript) {
|
|
505
|
-
throw new Error('Chrome scripting API not available');
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// Inject content script if needed
|
|
509
|
-
await chrome.scripting.executeScript({
|
|
510
|
-
target: { tabId: this.currentTab.id },
|
|
511
|
-
files: ['content.js']
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
// Wait for script to load then try again
|
|
515
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
516
|
-
|
|
517
|
-
response = await chrome.tabs.sendMessage(this.currentTab.id, {
|
|
518
|
-
action: 'extractLinkedInProfile'
|
|
519
|
-
});
|
|
520
|
-
} catch (scriptError) {
|
|
521
|
-
console.error('Failed to inject content script:', scriptError);
|
|
522
|
-
|
|
523
|
-
// Fallback: extract data directly in popup (basic extraction)
|
|
524
|
-
const fallbackData = await this.extractLinkedInDataFallback();
|
|
525
|
-
if (fallbackData) {
|
|
526
|
-
response = { success: true, data: fallbackData };
|
|
527
|
-
} else {
|
|
528
|
-
throw new Error('Could not extract LinkedIn profile data. Please try refreshing the page.');
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
if (response && response.success && response.data) {
|
|
534
|
-
const profileData = response.data;
|
|
535
|
-
|
|
536
|
-
// Save to Contacts table with duplicate detection
|
|
537
|
-
const saveResult = await this.client.createRecord('Contacts', profileData);
|
|
538
|
-
|
|
539
|
-
if (saveResult.success) {
|
|
540
|
-
if (saveResult.action === 'updated') {
|
|
541
|
-
this.showToast('LinkedIn profile already exists - no duplicate created', 'warning');
|
|
542
|
-
} else {
|
|
543
|
-
this.showToast('LinkedIn profile saved!', 'success');
|
|
544
|
-
}
|
|
545
|
-
} else {
|
|
546
|
-
throw new Error(saveResult.error || 'Failed to save profile');
|
|
547
|
-
}
|
|
548
|
-
} else {
|
|
549
|
-
throw new Error('Failed to extract profile data');
|
|
550
|
-
}
|
|
551
|
-
} catch (error) {
|
|
552
|
-
console.error('LinkedIn save failed:', error);
|
|
553
|
-
this.showToast(`Failed to save profile: ${error.message}`, 'error');
|
|
554
|
-
} finally {
|
|
555
|
-
linkedinBtn.disabled = false;
|
|
556
|
-
linkedinBtn.querySelector('.action-title').textContent = originalText;
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
// Fallback method to extract basic LinkedIn data from page URL and title
|
|
561
|
-
async extractLinkedInDataFallback() {
|
|
562
|
-
try {
|
|
563
|
-
if (!this.currentTab.url.includes('linkedin.com/in/')) {
|
|
564
|
-
return null;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// Extract name from URL or page title
|
|
568
|
-
let name = '';
|
|
569
|
-
const urlMatch = this.currentTab.url.match(/linkedin\.com\/in\/([^/?]+)/);
|
|
570
|
-
if (urlMatch) {
|
|
571
|
-
name = urlMatch[1].replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// Try to get name from page title
|
|
575
|
-
if (this.currentTab.title) {
|
|
576
|
-
const titleMatch = this.currentTab.title.match(/^([^|]+)/);
|
|
577
|
-
if (titleMatch) {
|
|
578
|
-
const titleName = titleMatch[1].trim();
|
|
579
|
-
if (titleName && titleName !== 'LinkedIn') {
|
|
580
|
-
name = titleName;
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
if (!name) {
|
|
586
|
-
return null;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
return {
|
|
590
|
-
'Name': name,
|
|
591
|
-
'LinkedIn URL': this.currentTab.url,
|
|
592
|
-
'Date Added': new Date().toISOString().split('T')[0],
|
|
593
|
-
'Source': 'LinkedIn',
|
|
594
|
-
'Notes': 'Extracted via fallback method - please update manually'
|
|
595
|
-
};
|
|
596
|
-
} catch (error) {
|
|
597
|
-
console.error('Fallback extraction failed:', error);
|
|
598
|
-
return null;
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
async handleBulkMode() {
|
|
603
|
-
if (!this.isConnected || !this.currentTab) {
|
|
604
|
-
this.showToast('Not connected to Airtable', 'error');
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
try {
|
|
609
|
-
// Inject content script for bulk selection
|
|
610
|
-
await chrome.scripting.executeScript({
|
|
611
|
-
target: { tabId: this.currentTab.id },
|
|
612
|
-
files: ['content.js']
|
|
613
|
-
});
|
|
614
|
-
|
|
615
|
-
// Send message to activate bulk mode
|
|
616
|
-
await chrome.tabs.sendMessage(this.currentTab.id, {
|
|
617
|
-
action: 'enterBulkMode'
|
|
618
|
-
});
|
|
619
|
-
|
|
620
|
-
this.showToast('Bulk mode activated! Select elements on the page.', 'success');
|
|
621
|
-
window.close(); // Close popup so user can interact with page
|
|
622
|
-
} catch (error) {
|
|
623
|
-
console.error('Bulk mode failed:', error);
|
|
624
|
-
this.showToast(`Failed to activate bulk mode: ${error.message}`, 'error');
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
async handleSetupDatabase() {
|
|
629
|
-
try {
|
|
630
|
-
const result = await chrome.runtime.sendMessage({
|
|
631
|
-
action: 'openDatabaseSetup'
|
|
632
|
-
});
|
|
633
|
-
|
|
634
|
-
if (result.success) {
|
|
635
|
-
this.showToast('Database setup opened!', 'success');
|
|
636
|
-
window.close();
|
|
637
|
-
} else {
|
|
638
|
-
throw new Error('Failed to open setup');
|
|
639
|
-
}
|
|
640
|
-
} catch (error) {
|
|
641
|
-
console.error('Database setup failed:', error);
|
|
642
|
-
this.showToast('Database setup will be available soon', 'info');
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
showSettings() {
|
|
647
|
-
document.getElementById('settingsPanel').style.display = 'block';
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
hideSettings() {
|
|
651
|
-
document.getElementById('settingsPanel').style.display = 'none';
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
async handleDisconnect() {
|
|
655
|
-
try {
|
|
656
|
-
// Clear all possible authentication data
|
|
657
|
-
await chrome.storage.local.remove([
|
|
658
|
-
// Manual token data
|
|
659
|
-
'airtableToken', 'baseId',
|
|
660
|
-
// OAuth data
|
|
661
|
-
'selectedBaseId', 'selectedBaseName', 'useOAuth',
|
|
662
|
-
'airtable_access_token', 'airtable_refresh_token',
|
|
663
|
-
'airtable_token_expires', 'airtable_token_type', 'airtable_scope'
|
|
664
|
-
]);
|
|
665
|
-
|
|
666
|
-
this.client = null;
|
|
667
|
-
this.isConnected = false;
|
|
668
|
-
this.showSetupView();
|
|
669
|
-
this.showToast('Disconnected successfully', 'success');
|
|
670
|
-
} catch (error) {
|
|
671
|
-
console.error('Disconnect failed:', error);
|
|
672
|
-
this.showToast('Failed to disconnect', 'error');
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
async saveSettings() {
|
|
677
|
-
try {
|
|
678
|
-
const settings = {
|
|
679
|
-
autoSave: document.getElementById('autoSave').checked,
|
|
680
|
-
notifications: document.getElementById('notifications').checked,
|
|
681
|
-
defaultTable: document.getElementById('defaultTable').value,
|
|
682
|
-
linkedinTable: document.getElementById('linkedinTable').value
|
|
683
|
-
};
|
|
684
|
-
|
|
685
|
-
await chrome.storage.local.set({ settings });
|
|
686
|
-
this.showToast('Settings saved', 'success');
|
|
687
|
-
} catch (error) {
|
|
688
|
-
console.error('Save settings failed:', error);
|
|
689
|
-
this.showToast('Failed to save settings', 'error');
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
showToast(message, type = 'success') {
|
|
694
|
-
const toast = document.getElementById('toast');
|
|
695
|
-
const toastIcon = document.getElementById('toastIcon');
|
|
696
|
-
const toastMessage = document.getElementById('toastMessage');
|
|
697
|
-
|
|
698
|
-
// Set icon based on type
|
|
699
|
-
const icons = {
|
|
700
|
-
success: '✅',
|
|
701
|
-
error: '❌',
|
|
702
|
-
warning: '🔄',
|
|
703
|
-
info: 'ℹ️'
|
|
704
|
-
};
|
|
705
|
-
|
|
706
|
-
toastIcon.textContent = icons[type] || icons.success;
|
|
707
|
-
toastMessage.textContent = message;
|
|
708
|
-
|
|
709
|
-
// Show toast
|
|
710
|
-
toast.classList.add('show');
|
|
711
|
-
|
|
712
|
-
// Hide after 3 seconds
|
|
713
|
-
setTimeout(() => {
|
|
714
|
-
toast.classList.remove('show');
|
|
715
|
-
}, 3000);
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
// Initialize when DOM is ready
|
|
720
|
-
document.addEventListener('DOMContentLoaded', () => {
|
|
721
|
-
new PopupController();
|
|
722
|
-
});
|
|
723
|
-
|
|
724
|
-
// Also initialize immediately in case DOMContentLoaded already fired
|
|
725
|
-
if (document.readyState === 'loading') {
|
|
726
|
-
document.addEventListener('DOMContentLoaded', () => new PopupController());
|
|
727
|
-
} else {
|
|
728
|
-
new PopupController();
|
|
729
|
-
}
|