Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@
web-ext-artifacts
node_modules
coverage

# Germini CLI Key
/keys.json
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Easily manage and trigger webhooks directly from your browser! Compatible with F
- **🌍 Localization:** Available in multiple languages (see `_locales/`).
- **📤 Export/Import:** Backup or restore your webhooks using JSON files.
- **🗂️ Group Webhooks:** Organize webhooks into groups for clarity and easier management.
- **🧪 Test Webhooks:** Test your webhooks right from the options page to ensure they are configured correctly.

## 🛠️ Getting Started

Expand All @@ -37,6 +38,9 @@ Easily manage and trigger webhooks directly from your browser! Compatible with F
**🗑️ Delete a Webhook:**
- Find the webhook, click "Delete".

**🧪 Test a Webhook:**
- When adding or editing a webhook, click the 'Test' button to send a test payload to your URL.

**🗂️ Organize into Groups:**
- Use the group management dialog to add, delete, rename, or reorder groups via drag-and-drop.

Expand Down
12 changes: 12 additions & 0 deletions _locales/de/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -240,5 +240,17 @@
"optionsImportInfo": {
"message": "Beim Import werden vorhandene Webhooks ersetzt.",
"description": "Hinweistext neben dem Import-Button."
},
"optionsTestButton": {
"message": "Testen",
"description": "Text für den Test-Button."
},
"optionsTestSuccess": {
"message": "Test-Webhook erfolgreich gesendet.",
"description": "Erfolgsmeldung für den Test-Webhook."
},
"optionsTestError": {
"message": "Fehler beim Senden des Test-Webhooks: ",
"description": "Fehlermeldung für den Test-Webhook."
}
}
12 changes: 12 additions & 0 deletions _locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -240,5 +240,17 @@
"optionsImportInfo": {
"message": "Importing replaces all existing webhooks.",
"description": "Information text shown next to the import button."
},
"optionsTestButton": {
"message": "Test",
"description": "Text for the test button."
},
"optionsTestSuccess": {
"message": "Test webhook sent successfully.",
"description": "Success message for the test webhook."
},
"optionsTestError": {
"message": "Error sending test webhook: ",
"description": "Error message for the test webhook."
}
}
10 changes: 10 additions & 0 deletions options/options.css
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,16 @@ button:hover {
background-color: #dc2626;
}

/* Test webhook button styling */
#test-webhook-btn {
background-color: #f59e0b;
color: white;
}

#test-webhook-btn:hover {
background-color: #d97706;
}

#add-new-webhook-btn {
margin-bottom: 20px;
}
Expand Down
2 changes: 2 additions & 0 deletions options/options.html
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,9 @@ <h2>__MSG_optionsAddWebhookHeader__</h2>
</div>
</div>
<button type="submit">__MSG_optionsSaveButton__</button>
<button type="button" id="test-webhook-btn" class="hidden">__MSG_optionsTestButton__</button>
<button type="button" id="cancel-edit-btn" class="hidden">__MSG_optionsCancelEditButton__</button>
<p id="form-status-message" class="status-message"></p>
</form>

<h2>__MSG_optionsStoredWebhooksHeader__</h2>
Expand Down
48 changes: 48 additions & 0 deletions options/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,8 @@ const headerValueInput = document.getElementById("header-value");
const addHeaderBtn = document.getElementById("add-header-btn");
const cancelEditBtn = document.getElementById("cancel-edit-btn");
const showAddWebhookBtn = document.getElementById("add-new-webhook-btn");
const testWebhookBtn = document.getElementById("test-webhook-btn");
const formStatusMessage = document.getElementById("form-status-message");
const manageGroupsBtn = document.getElementById("manage-groups-btn");
const customPayloadInput = document.getElementById("webhook-custom-payload");
const variablesAutocomplete = document.getElementById("variables-autocomplete");
Expand Down Expand Up @@ -514,6 +516,7 @@ showAddWebhookBtn.addEventListener('click', () => {
form.classList.remove('hidden');
showAddWebhookBtn.classList.add('hidden');
cancelEditBtn.classList.remove('hidden');
testWebhookBtn.classList.remove('hidden');
labelInput.focus();
});

Expand Down Expand Up @@ -892,6 +895,7 @@ webhookList.addEventListener("click", async (e) => {
headers = Array.isArray(webhook.headers) ? [...webhook.headers] : [];
renderHeaders();
cancelEditBtn.classList.remove("hidden");
testWebhookBtn.classList.remove("hidden");
form.classList.remove('hidden');
showAddWebhookBtn.classList.add('hidden');
// Always set to save button when entering edit mode
Expand All @@ -917,6 +921,7 @@ webhookList.addEventListener("click", async (e) => {
headers = Array.isArray(webhook.headers) ? [...webhook.headers] : [];
renderHeaders();
cancelEditBtn.classList.remove("hidden");
testWebhookBtn.classList.remove("hidden");
form.classList.remove('hidden');
showAddWebhookBtn.classList.add('hidden');
form.querySelector('button[type="submit"]').textContent = browser.i18n.getMessage("optionsSaveButton") || "Save Webhook";
Expand All @@ -942,6 +947,9 @@ cancelEditBtn.addEventListener("click", () => {
headers = [];
renderHeaders();
cancelEditBtn.classList.add("hidden");
testWebhookBtn.classList.add("hidden");
formStatusMessage.textContent = "";
formStatusMessage.className = "status-message";
form.querySelector('button[type="submit"]').textContent = browser.i18n.getMessage("optionsSaveButton") || "Save Webhook";
// Collapse custom payload section
updateCustomPayloadVisibility();
Expand Down Expand Up @@ -1077,3 +1085,43 @@ if (typeof module !== "undefined" && module.exports) {
handleImport,
};
}

testWebhookBtn.addEventListener('click', async () => {
const url = urlInput.value.trim();
if (!url) {
alert('URL is required to send a test webhook.');
return;
}

// Create webhook object with all current form settings
const webhook = {
url: url,
method: methodSelect.value,
headers: [...headers],
identifier: identifierInput.value.trim() || undefined,
customPayload: customPayloadInput.value.trim() || undefined,
urlFilter: urlFilterInput.value.trim() || undefined,
groupId: groupSelect.value || undefined
};

// Add test header to identify this as a test webhook
webhook.headers = [...webhook.headers, { key: 'x-webhook-test', value: 'true' }];

formStatusMessage.className = 'status-message';
testWebhookBtn.disabled = true;

try {
await sendWebhook(webhook, false); // Use false to send real data instead of test payload
formStatusMessage.textContent = browser.i18n.getMessage('optionsTestSuccess');
formStatusMessage.classList.add('success');
} catch (error) {
formStatusMessage.textContent = browser.i18n.getMessage('optionsTestError') + error.message;
formStatusMessage.classList.add('error');
} finally {
setTimeout(() => {
testWebhookBtn.disabled = false;
formStatusMessage.textContent = '';
formStatusMessage.className = 'status-message';
}, 2500);
}
});
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

121 changes: 1 addition & 120 deletions popup/popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,126 +114,7 @@ document
statusMessage.className = "";

try {
// Get info about the active tab
const tabs = await browserAPI.tabs.query({
active: true,
currentWindow: true,
});
if (tabs.length === 0) {
throw new Error(browserAPI.i18n.getMessage("popupErrorNoActiveTab"));
}
const activeTab = tabs[0];
const currentUrl = activeTab.url;

// Get browser and platform info
const browserInfo = await browserAPI.runtime.getBrowserInfo?.() || {};
const platformInfo = await browserAPI.runtime.getPlatformInfo?.() || {};

// Create default payload
let payload = {
tab: {
title: activeTab.title,
url: currentUrl,
id: activeTab.id,
windowId: activeTab.windowId,
index: activeTab.index,
pinned: activeTab.pinned,
audible: activeTab.audible,
mutedInfo: activeTab.mutedInfo,
incognito: activeTab.incognito,
status: activeTab.status,
},
browser: browserInfo,
platform: platformInfo,
triggeredAt: new Date().toISOString(),
};

if (webhook && webhook.identifier) {
payload.identifier = webhook.identifier;
}

// Use custom payload if available
// The custom payload is a JSON string that can contain placeholders like {{tab.title}}
// These placeholders will be replaced with actual values before sending the webhook
if (webhook && webhook.customPayload) {
try {
// Create variable replacements map
const replacements = {
"{{tab.title}}": activeTab.title,
"{{tab.url}}": currentUrl,
"{{tab.id}}": activeTab.id,
"{{tab.windowId}}": activeTab.windowId,
"{{tab.index}}": activeTab.index,
"{{tab.pinned}}": activeTab.pinned,
"{{tab.audible}}": activeTab.audible,
"{{tab.incognito}}": activeTab.incognito,
"{{tab.status}}": activeTab.status,
"{{browser}}": JSON.stringify(browserInfo),
"{{platform.arch}}": platformInfo.arch || "unknown",
"{{platform.os}}": platformInfo.os || "unknown",
"{{platform.version}}": platformInfo.version,
"{{triggeredAt}}": new Date().toISOString(),
"{{identifier}}": webhook.identifier || ""
};

// Replace placeholders in custom payload
let customPayloadStr = webhook.customPayload;
Object.entries(replacements).forEach(([placeholder, value]) => {
// Handle different types of values
// For string values in JSON, we need to handle them differently based on context
// If the placeholder is inside quotes in the JSON, we should not add quotes again
const isPlaceholderInQuotes = customPayloadStr.match(new RegExp(`"[^"]*${placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[^"]*"`, 'g'));

const replaceValue = typeof value === 'string'
? (isPlaceholderInQuotes ? value.replace(/"/g, '\\"') : `"${value.replace(/"/g, '\\"')}"`)
: (value === undefined ? 'null' : JSON.stringify(value));

customPayloadStr = customPayloadStr.replace(
new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'),
replaceValue
);
});

// Parse the resulting JSON
const customPayload = JSON.parse(customPayloadStr);

// Use the custom payload instead of the default one
payload = customPayload;
} catch (error) {
throw new Error(browserAPI.i18n.getMessage("popupErrorCustomPayloadJsonParseError", error.message));
}
}
// Prepare headers
let headers = { "Content-Type": "application/json" };
if (webhook && Array.isArray(webhook.headers)) {
webhook.headers.forEach(h => {
if (h.key && h.value) headers[h.key] = h.value;
});
}
// Determine method
const method = webhook && webhook.method ? webhook.method : "POST";
// Prepare fetch options
const fetchOpts = {
method,
headers,
};
if (method === "POST") {
fetchOpts.body = JSON.stringify(payload);
} else if (method === "GET") {
// For GET, append payload as query param
const urlObj = new URL(url);
urlObj.searchParams.set("payload", encodeURIComponent(JSON.stringify(payload)));
fetchOpts.body = undefined;
// Overwrite url for fetch
fetchOpts._url = urlObj.toString();
}
// Send the request
const fetchUrl = fetchOpts._url || url;
const response = await fetch(fetchUrl, fetchOpts);

if (!response.ok) {
throw new Error(browserAPI.i18n.getMessage("popupErrorHttp", response.status));
}
await window.sendWebhook(webhook, false);

// Success feedback
statusMessage.textContent = browserAPI.i18n.getMessage("popupStatusSuccess");
Expand Down
2 changes: 2 additions & 0 deletions tests/exportImport.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ describe('export and import logic', () => {
</div>
<button type="button" id="cancel-edit-btn" class="hidden"></button>
<button type="submit"></button>
<button type="button" id="test-webhook-btn" class="hidden">__MSG_optionsTestButton__</button>
<p id="form-status-message" class="status-message"></p>
</form>

<!-- Group Management Modal -->
Expand Down
Loading