feat(webhook): ajout de la fonctionnalité d'envoi de webhooks

- Remplacement de l'action de compteur par une action d'envoi de webhook
- Ajout d'une interface utilisateur pour configurer les paramètres du webhook (URL, méthode, headers, corps)
- Implémentation des services de validation, de construction de requêtes et d'exécution de requêtes
- Ajout de la gestion des erreurs et des messages de validation
- Mise à jour du fichier README avec des instructions d'utilisation et des exemples de configuration
- Ajout de nouveaux icônes pour l'action d'envoi de webhook
This commit is contained in:
Mr¤KayJayDee
2025-07-10 15:15:12 +02:00
parent 22c2a5c3a6
commit a48df5509c
18 changed files with 1495 additions and 74 deletions

View File

@@ -1,19 +0,0 @@
<!DOCTYPE html>
<html>
<head lang="en">
<title>Increment Counter Settings</title>
<meta charset="utf-8" />
<script src="https://sdpi-components.dev/releases/v4/sdpi-components.js"></script>
</head>
<body>
<!--
Learn more about property inspector components at https://sdpi-components.dev/docs/components
-->
<sdpi-item label="Increment By">
<sdpi-range setting="incrementBy" min="1" max="5" step="1" default="1" showlabels></sdpi-range>
</sdpi-item>
</body>
</html>

View File

@@ -0,0 +1,137 @@
<!DOCTYPE html>
<html>
<head lang="en">
<title>Send Webhook Settings</title>
<meta charset="utf-8" />
<script src="https://sdpi-components.dev/releases/v4/sdpi-components.js"></script>
<style>
.validation-error {
color: #ff6b6b;
font-size: 12px;
margin-top: 4px;
padding: 4px;
background-color: #ffe6e6;
border-left: 3px solid #ff6b6b;
border-radius: 3px;
display: none;
}
.validation-success {
color: #51cf66;
font-size: 12px;
margin-top: 4px;
padding: 4px;
background-color: #e6ffe6;
border-left: 3px solid #51cf66;
border-radius: 3px;
display: none;
}
.button-group {
display: flex;
gap: 8px;
margin-top: 8px;
}
.flex-item {
flex: 1;
}
.json-container {
position: relative;
}
.verify-button-container {
margin-top: 16px;
text-align: center;
}
.config-status {
margin-top: 8px;
padding: 8px;
border-radius: 4px;
font-size: 12px;
display: none;
}
.config-status.success {
background-color: #e6ffe6;
color: #51cf66;
border: 1px solid #51cf66;
}
.config-status.error {
background-color: #ffe6e6;
color: #ff6b6b;
border: 1px solid #ff6b6b;
}
</style>
</head>
<body>
<!--
Learn more about property inspector components at https://sdpi-components.dev/docs/components
-->
<sdpi-item label="URL">
<sdpi-textfield setting="url" placeholder="https://webhook.site/your-unique-url" required></sdpi-textfield>
<div id="url-error" class="validation-error"></div>
</sdpi-item>
<sdpi-item label="HTTP Method">
<sdpi-select setting="method" default="GET">
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="PATCH">PATCH</option>
<option value="DELETE">DELETE</option>
</sdpi-select>
</sdpi-item>
<sdpi-item label="Headers (JSON)">
<div class="json-container">
<sdpi-textarea id="headers-textarea" setting="headers" placeholder='{"Content-Type": "application/json"}'
rows="4"></sdpi-textarea>
<div class="button-group">
<sdpi-button id="beautify-headers-btn" class="flex-item">
Beautify JSON
</sdpi-button>
<sdpi-button id="validate-headers-btn" class="flex-item">
Validate JSON
</sdpi-button>
</div>
</div>
<div id="headers-error" class="validation-error"></div>
<div id="headers-success" class="validation-success"></div>
</sdpi-item>
<sdpi-item label="Body">
<div class="json-container">
<sdpi-textarea id="body-textarea" setting="body" placeholder='{"message": "Hello from Stream Deck!"}'
rows="6"></sdpi-textarea>
<div class="button-group">
<sdpi-button id="beautify-body-btn" class="flex-item">
Beautify JSON
</sdpi-button>
<sdpi-button id="validate-body-btn" class="flex-item">
Validate JSON
</sdpi-button>
</div>
</div>
<div id="body-error" class="validation-error"></div>
<div id="body-success" class="validation-success"></div>
</sdpi-item>
<div class="verify-button-container">
<sdpi-item>
<sdpi-button id="verify-config-btn">
Verify Configuration
</sdpi-button>
</sdpi-item>
<div id="config-status" class="config-status"></div>
</div>
<script src="send-webhook.js"></script>
</body>
</html>

View File

@@ -0,0 +1,333 @@
/**
* Stream Deck Property Inspector JavaScript for Send Webhook Action
* Simple and focused - fixes timeout and duplicate alert issues
*/
// Store timeout IDs to clear them properly
const validationTimeouts = {};
// Utility function to show validation messages with proper timeout handling
function showValidationMessage(elementId, message, isError = true, duration = 5000) {
const element = document.getElementById(elementId);
if (!element) return;
// Clear existing timeout for this element
if (validationTimeouts[elementId]) {
clearTimeout(validationTimeouts[elementId]);
delete validationTimeouts[elementId];
}
element.textContent = message;
element.style.display = message ? 'block' : 'none';
// Set new timeout if duration > 0
if (duration > 0 && message) {
validationTimeouts[elementId] = setTimeout(() => {
element.style.display = 'none';
delete validationTimeouts[elementId];
}, duration);
}
}
// Hide validation message immediately
function hideValidationMessage(elementId) {
showValidationMessage(elementId, '', true, 0);
}
// JSON validation function
function validateJSON(jsonString, fieldName) {
if (!jsonString || jsonString.trim() === '') {
return { isValid: true, message: `${fieldName} is empty (optional)` };
}
try {
const parsed = JSON.parse(jsonString);
// Additional validation for headers - must be an object
if (fieldName === 'Headers' && (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))) {
return {
isValid: false,
message: 'Headers must be a JSON object (e.g., {"Content-Type": "application/json"})'
};
}
return { isValid: true, message: `Valid JSON`, data: parsed };
} catch (error) {
return {
isValid: false,
message: `Invalid JSON: ${error.message}`
};
}
}
// JSON beautification function
function beautifyJSON(jsonString) {
if (!jsonString || jsonString.trim() === '') {
return { success: false, message: 'No content to beautify' };
}
try {
const parsed = JSON.parse(jsonString);
const beautified = JSON.stringify(parsed, null, 2);
return { success: true, beautified };
} catch (error) {
return { success: false, message: `Cannot beautify: ${error.message}` };
}
}
// URL validation function
function validateURL(urlString) {
if (!urlString || urlString.trim() === '') {
return { isValid: false, message: 'URL is required' };
}
try {
const url = new URL(urlString.trim());
// Check for supported protocols
if (!['http:', 'https:'].includes(url.protocol)) {
return {
isValid: false,
message: 'URL must use HTTP or HTTPS protocol'
};
}
return { isValid: true, message: 'Valid URL' };
} catch (error) {
return {
isValid: false,
message: 'Invalid URL format'
};
}
}
// Get current settings from the form
function getCurrentSettings() {
const urlField = document.querySelector('sdpi-textfield[setting="url"]');
const methodField = document.querySelector('sdpi-select[setting="method"]');
const headersField = document.querySelector('sdpi-textarea[setting="headers"]');
const bodyField = document.querySelector('sdpi-textarea[setting="body"]');
return {
url: urlField ? urlField.value : '',
method: methodField ? methodField.value : 'GET',
headers: headersField ? headersField.value : '',
body: bodyField ? bodyField.value : ''
};
}
// Handle field validation - shows only one alert at a time
function handleFieldValidation(fieldName, displayName) {
const field = document.querySelector(`sdpi-textarea[setting="${fieldName}"]`);
if (!field) return;
const validation = validateJSON(field.value, displayName);
const errorId = `${fieldName}-error`;
const successId = `${fieldName}-success`;
if (validation.isValid) {
showValidationMessage(successId, validation.message, false);
hideValidationMessage(errorId);
} else {
showValidationMessage(errorId, validation.message, true);
hideValidationMessage(successId);
}
}
// Handle JSON beautification
function handleJsonBeautify(fieldName) {
const field = document.querySelector(`sdpi-textarea[setting="${fieldName}"]`);
if (!field) return;
const result = beautifyJSON(field.value);
const errorId = `${fieldName}-error`;
const successId = `${fieldName}-success`;
if (result.success) {
field.value = result.beautified;
// Trigger change event to save settings
field.dispatchEvent(new Event('change', { bubbles: true }));
showValidationMessage(successId, 'JSON beautified successfully!', false);
hideValidationMessage(errorId);
} else {
showValidationMessage(errorId, result.message, true);
hideValidationMessage(successId);
}
}
// Comprehensive configuration verification
function verifyConfiguration() {
const settings = getCurrentSettings();
const errors = [];
const warnings = [];
// Validate URL
const urlValidation = validateURL(settings.url);
if (!urlValidation.isValid) {
errors.push(`URL: ${urlValidation.message}`);
}
// Validate Headers JSON
const headersValidation = validateJSON(settings.headers, 'Headers');
if (!headersValidation.isValid) {
errors.push(`Headers: ${headersValidation.message}`);
}
// Validate Body JSON (only if not empty)
if (settings.body && settings.body.trim() !== '') {
const bodyValidation = validateJSON(settings.body, 'Body');
if (!bodyValidation.isValid) {
errors.push(`Body: ${bodyValidation.message}`);
}
}
// Check method-specific requirements
const methodsWithBody = ['POST', 'PUT', 'PATCH'];
if (methodsWithBody.includes(settings.method)) {
if (!settings.body || settings.body.trim() === '') {
warnings.push(`${settings.method} requests typically include a body`);
}
} else if (settings.method === 'GET' && settings.body && settings.body.trim() !== '') {
warnings.push('GET requests typically do not include a body');
}
// Check for common header requirements
if (settings.body && settings.body.trim() !== '') {
try {
const headers = settings.headers ? JSON.parse(settings.headers) : {};
if (!headers['Content-Type'] && !headers['content-type']) {
warnings.push('Consider adding Content-Type header when sending a body');
}
} catch (e) {
// Headers validation already handled above
}
}
showConfigurationResults(errors, warnings);
}
// Show configuration verification results
function showConfigurationResults(errors, warnings) {
const statusElement = document.getElementById('config-status');
if (!statusElement) return;
let message = '';
let className = '';
if (errors.length > 0) {
className = 'error';
message = `Configuration Issues:\n${errors.join('\n')}`;
if (warnings.length > 0) {
message += `\n\nWarnings:\n${warnings.join('\n')}`;
}
} else if (warnings.length > 0) {
className = 'error'; // Show warnings as yellow/orange
message = `Configuration Warnings:\n${warnings.join('\n')}`;
} else {
className = 'success';
message = '✓ Configuration is valid and ready to use!';
}
statusElement.className = `config-status ${className}`;
statusElement.textContent = message;
statusElement.style.display = 'block';
// Hide after 10 seconds for success, keep visible for errors
if (className === 'success') {
setTimeout(() => {
statusElement.style.display = 'none';
}, 10000);
}
}
// Event handlers setup
function setupEventHandlers() {
// Headers validation button
const validateHeadersBtn = document.getElementById('validate-headers-btn');
if (validateHeadersBtn) {
validateHeadersBtn.addEventListener('click', () => {
handleFieldValidation('headers', 'Headers');
});
}
// Headers beautify button
const beautifyHeadersBtn = document.getElementById('beautify-headers-btn');
if (beautifyHeadersBtn) {
beautifyHeadersBtn.addEventListener('click', () => {
handleJsonBeautify('headers');
});
}
// Body validation button
const validateBodyBtn = document.getElementById('validate-body-btn');
if (validateBodyBtn) {
validateBodyBtn.addEventListener('click', () => {
handleFieldValidation('body', 'Body');
});
}
// Body beautify button
const beautifyBodyBtn = document.getElementById('beautify-body-btn');
if (beautifyBodyBtn) {
beautifyBodyBtn.addEventListener('click', () => {
handleJsonBeautify('body');
});
}
// Configuration verification button
const verifyConfigBtn = document.getElementById('verify-config-btn');
if (verifyConfigBtn) {
verifyConfigBtn.addEventListener('click', verifyConfiguration);
}
// Auto-validate URL on change
const urlField = document.querySelector('sdpi-textfield[setting="url"]');
if (urlField) {
urlField.addEventListener('change', () => {
const validation = validateURL(urlField.value);
if (!validation.isValid && urlField.value.trim() !== '') {
showValidationMessage('url-error', validation.message, true, 3000);
} else {
hideValidationMessage('url-error');
}
});
}
// Auto-validate JSON fields on change
const headersField = document.querySelector('sdpi-textarea[setting="headers"]');
if (headersField) {
headersField.addEventListener('change', () => {
if (headersField.value.trim() !== '') {
const validation = validateJSON(headersField.value, 'Headers');
if (!validation.isValid) {
showValidationMessage('headers-error', validation.message, true, 3000);
hideValidationMessage('headers-success');
} else {
hideValidationMessage('headers-error');
}
}
});
}
const bodyField = document.querySelector('sdpi-textarea[setting="body"]');
if (bodyField) {
bodyField.addEventListener('change', () => {
if (bodyField.value.trim() !== '') {
const validation = validateJSON(bodyField.value, 'Body');
if (!validation.isValid) {
showValidationMessage('body-error', validation.message, true, 3000);
hideValidationMessage('body-success');
} else {
hideValidationMessage('body-error');
}
}
});
}
}
// Initialize when DOM is loaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', setupEventHandlers);
} else {
setupEventHandlers();
}