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
13 changes: 12 additions & 1 deletion options/options.html
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,18 @@ <h2>__MSG_optionsAddWebhookHeader__</h2>
<input type="text" id="webhook-identifier" placeholder="Optional identifier" />
</div>
<div class="form-group">
<div class="collapsible-header">
<div class="collapsible-header" id="url-filter-header">
<label for="webhook-url-filter">URL Filter (optional)</label>
<button type="button" id="toggle-url-filter" class="toggle-btn" aria-expanded="false">
<span class="toggle-icon">+</span>
</button>
</div>
<div id="url-filter-content" class="collapsible-content collapsed">
<input type="text" id="webhook-url-filter" placeholder="Show button when URL contains this text" />
</div>
</div>
<div class="form-group">
<div class="collapsible-header" id="custom-payload-header">
<label for="webhook-custom-payload">Custom Payload (optional)</label>
<button type="button" id="toggle-custom-payload" class="toggle-btn" aria-expanded="false">
<span class="toggle-icon">+</span>
Expand Down
62 changes: 58 additions & 4 deletions options/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ const customPayloadInput = document.getElementById("webhook-custom-payload");
const variablesAutocomplete = document.getElementById("variables-autocomplete");
const toggleCustomPayloadBtn = document.getElementById("toggle-custom-payload");
const customPayloadContent = document.getElementById("custom-payload-content");
const urlFilterInput = document.getElementById("webhook-url-filter");
const toggleUrlFilterBtn = document.getElementById("toggle-url-filter");
const urlFilterContent = document.getElementById("url-filter-content");
let headers = [];

showAddWebhookBtn.addEventListener('click', () => {
Expand Down Expand Up @@ -251,6 +254,7 @@ form.addEventListener("submit", async (e) => {
const url = urlInput.value.trim();
const method = methodSelect.value;
const identifier = identifierInput.value.trim();
const urlFilter = urlFilterInput.value.trim();
const customPayload = customPayloadInput.value.trim();
let { webhooks = [] } = await browser.storage.sync.get("webhooks");

Expand All @@ -264,7 +268,8 @@ form.addEventListener("submit", async (e) => {
method,
headers: [...headers],
identifier,
customPayload: customPayload || null
customPayload: customPayload || null,
urlFilter: urlFilter || ""
} : wh
);
editWebhookId = null;
Expand All @@ -278,7 +283,8 @@ form.addEventListener("submit", async (e) => {
method,
headers: [...headers],
identifier,
customPayload: customPayload || null
customPayload: customPayload || null,
urlFilter: urlFilter || ""
};
webhooks.push(newWebhook);
}
Expand All @@ -288,6 +294,7 @@ form.addEventListener("submit", async (e) => {
urlInput.value = "";
methodSelect.value = "POST";
identifierInput.value = "";
urlFilterInput.value = "";
customPayloadInput.value = "";
headerKeyInput.value = "";
headerValueInput.value = "";
Expand All @@ -297,6 +304,7 @@ form.addEventListener("submit", async (e) => {
form.querySelector('button[type="submit"]').textContent = browser.i18n.getMessage("optionsSaveButton") || "Save Webhook";
// Collapse custom payload section
updateCustomPayloadVisibility();
updateUrlFilterVisibility();
form.classList.add('hidden');
showAddWebhookBtn.classList.remove('hidden');
loadWebhooks();
Expand Down Expand Up @@ -380,6 +388,7 @@ webhookList.addEventListener("click", async (e) => {
urlInput.value = webhook.url;
methodSelect.value = webhook.method || "POST";
identifierInput.value = webhook.identifier || "";
urlFilterInput.value = webhook.urlFilter || "";
customPayloadInput.value = webhook.customPayload || "";
headers = Array.isArray(webhook.headers) ? [...webhook.headers] : [];
renderHeaders();
Expand All @@ -391,6 +400,7 @@ webhookList.addEventListener("click", async (e) => {

// Update custom payload section visibility based on content
updateCustomPayloadVisibility();
updateUrlFilterVisibility();

labelInput.focus();
}
Expand All @@ -404,6 +414,7 @@ cancelEditBtn.addEventListener("click", () => {
urlInput.value = "";
methodSelect.value = "POST";
identifierInput.value = "";
urlFilterInput.value = "";
customPayloadInput.value = "";
headerKeyInput.value = "";
headerValueInput.value = "";
Expand All @@ -413,6 +424,7 @@ cancelEditBtn.addEventListener("click", () => {
form.querySelector('button[type="submit"]').textContent = browser.i18n.getMessage("optionsSaveButton") || "Save Webhook";
// Collapse custom payload section
updateCustomPayloadVisibility();
updateUrlFilterVisibility();
form.classList.add('hidden');
showAddWebhookBtn.classList.remove('hidden');
});
Expand All @@ -433,14 +445,43 @@ function toggleCustomPayloadSection() {
toggleCustomPayloadBtn.addEventListener('click', toggleCustomPayloadSection);

// Event listener for collapsible header (title click)
const collapsibleHeader = document.querySelector('.collapsible-header');
collapsibleHeader.addEventListener('click', (e) => {
const customPayloadHeader = document.getElementById('custom-payload-header');
customPayloadHeader.addEventListener('click', (e) => {
// Only toggle if the click is not on the toggle button itself
if (!e.target.closest('#toggle-custom-payload')) {
toggleCustomPayloadSection();
}
});

// Toggle URL filter section
function toggleUrlFilterSection() {
const isExpanded = toggleUrlFilterBtn.getAttribute('aria-expanded') === 'true';
toggleUrlFilterBtn.setAttribute('aria-expanded', !isExpanded);

if (isExpanded) {
urlFilterContent.classList.add('collapsed');
} else {
urlFilterContent.classList.remove('collapsed');
}
}

toggleUrlFilterBtn.addEventListener('click', toggleUrlFilterSection);

const urlFilterHeader = document.getElementById('url-filter-header');
urlFilterHeader.addEventListener('click', (e) => {
if (!e.target.closest('#toggle-url-filter')) {
toggleUrlFilterSection();
}
});

// Keep URL filter section expanded when typing
urlFilterInput.addEventListener('input', () => {
if (urlFilterInput.value.trim() !== '') {
toggleUrlFilterBtn.setAttribute('aria-expanded', 'true');
urlFilterContent.classList.remove('collapsed');
}
});

// Event listener for custom payload input to keep section expanded when typing
customPayloadInput.addEventListener('input', () => {
if (customPayloadInput.value.trim() !== '') {
Expand All @@ -464,6 +505,18 @@ function updateCustomPayloadVisibility() {
}
}

function updateUrlFilterVisibility() {
const hasContent = urlFilterInput.value.trim() !== '';

if (hasContent) {
toggleUrlFilterBtn.setAttribute('aria-expanded', 'true');
urlFilterContent.classList.remove('collapsed');
} else {
toggleUrlFilterBtn.setAttribute('aria-expanded', 'false');
urlFilterContent.classList.add('collapsed');
}
}

// Show webhooks on page load
document.addEventListener("DOMContentLoaded", () => {
// Replace i18n placeholders
Expand All @@ -478,6 +531,7 @@ document.addEventListener("DOMContentLoaded", () => {

// Initialize custom payload section (collapsed by default)
updateCustomPayloadVisibility();
updateUrlFilterVisibility();

// Load webhooks
loadWebhooks();
Expand Down
11 changes: 8 additions & 3 deletions popup/popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@ document.addEventListener("DOMContentLoaded", async () => {

// Load webhooks from storage
const { webhooks = [] } = await browser.storage.sync.get("webhooks");
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
const currentUrl = tabs[0]?.url || "";
const visibleWebhooks = webhooks.filter(
(wh) => !wh.urlFilter || currentUrl.includes(wh.urlFilter)
);

if (webhooks.length === 0) {
if (visibleWebhooks.length === 0) {
// Use textContent instead of innerHTML for security
const p = document.createElement("p");
p.className = "no-hooks-msg";
Expand All @@ -21,7 +26,7 @@ document.addEventListener("DOMContentLoaded", async () => {
buttonsContainer.appendChild(p);
} else {
// Create a button for each webhook
webhooks.forEach((webhook) => {
visibleWebhooks.forEach((webhook) => {
const button = document.createElement("button");
button.textContent = webhook.label;
button.dataset.url = webhook.url;
Expand All @@ -31,7 +36,7 @@ document.addEventListener("DOMContentLoaded", async () => {
buttonsContainer.appendChild(button);
});
// Store webhooks in a map for quick lookup by id
window._webhookMap = Object.fromEntries(webhooks.map(w => [w.id, w]));
window._webhookMap = Object.fromEntries(visibleWebhooks.map(w => [w.id, w]));
}
});

Expand Down
14 changes: 12 additions & 2 deletions tests/options.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,15 @@ describe('options page', () => {
<input id="header-key" />
<input id="header-value" />
<button type="button" id="add-header-btn"></button>
<div class="collapsible-header">
<div class="collapsible-header" id="url-filter-header">
<button type="button" id="toggle-url-filter" class="toggle-btn" aria-expanded="false">
<span class="toggle-icon">+</span>
</button>
</div>
<div id="url-filter-content" class="collapsible-content collapsed">
<input id="webhook-url-filter" />
</div>
<div class="collapsible-header" id="custom-payload-header">
<button type="button" id="toggle-custom-payload" class="toggle-btn" aria-expanded="false">
<span class="toggle-icon">+</span>
</button>
Expand Down Expand Up @@ -116,6 +124,7 @@ describe('options page', () => {
document.getElementById('webhook-label').value = 'Test Webhook';
document.getElementById('webhook-url').value = 'https://example.com/webhook';
document.getElementById('webhook-custom-payload').value = customPayload;
document.getElementById('webhook-url-filter').value = 'example.com';

// Set identifier value
document.getElementById('webhook-identifier').value = 'test-identifier';
Expand Down Expand Up @@ -169,7 +178,8 @@ describe('options page', () => {
{ key: 'Authorization', value: 'Bearer token123' }
],
identifier: 'test-identifier',
customPayload
customPayload,
urlFilter: 'example.com'
}]
});
});
Expand Down
19 changes: 18 additions & 1 deletion tests/popup.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe('popup script', () => {
global.browser = {
storage: { sync: { get: jest.fn() } },
i18n: { getMessage: jest.fn((key) => key) },
tabs: { query: jest.fn() },
tabs: { query: jest.fn().mockResolvedValue([{ title: 't', url: 'https://example.com', id: 1, windowId: 1, index: 0, pinned: false, audible: false, mutedInfo: null, incognito: false, status: 'complete' }]) },
runtime: {
openOptionsPage: jest.fn(),
getBrowserInfo: jest.fn().mockResolvedValue({}),
Expand Down Expand Up @@ -105,4 +105,21 @@ describe('popup script', () => {
const sentPayload = JSON.parse(fetchOptions.body);
expect(sentPayload).toEqual({ message: 'Custom message with Test Page' });
});

test('filters webhooks based on urlFilter', async () => {
const hooks = [
{ id: '1', label: 'A', url: 'https://hook1.test', urlFilter: 'example.com' },
{ id: '2', label: 'B', url: 'https://hook2.test', urlFilter: 'other.com' }
];
browser.storage.sync.get.mockResolvedValue({ webhooks: hooks });
browser.tabs.query.mockResolvedValue([{ title: 't', url: 'https://example.com', id: 1, windowId: 1, index: 0, pinned: false, audible: false, mutedInfo: null, incognito: false, status: 'complete' }]);

require('../popup/popup.js');
document.dispatchEvent(new dom.window.Event('DOMContentLoaded'));
await new Promise(setImmediate);

const btns = document.querySelectorAll('button.webhook-btn');
expect(btns.length).toBe(1);
expect(btns[0].textContent).toBe('A');
});
});