mirror of
https://github.com/callumalpass/tasknotes.git
synced 2026-07-22 12:50:26 +00:00
- Bump version to 3.25.6 - Set minAppVersion to 1.9.0 - Include fix for timeEntries duplication (#968) - Include webcal:// URL support (#1013) - Include task recognition fix (#1022) - Include test improvements
2255 lines
74 KiB
TypeScript
2255 lines
74 KiB
TypeScript
import { Notice, Platform, Modal, Setting, setIcon, App } from "obsidian";
|
|
import TaskNotesPlugin from "../../main";
|
|
import { WebhookConfig } from "../../types";
|
|
import { TranslationKey } from "../../i18n";
|
|
import { loadAPIEndpoints } from "../../api/loadAPIEndpoints";
|
|
import {
|
|
createSectionHeader,
|
|
createTextSetting,
|
|
createToggleSetting,
|
|
createDropdownSetting,
|
|
createNumberSetting,
|
|
createHelpText,
|
|
createButtonSetting,
|
|
// createValidationNote
|
|
} from "../components/settingHelpers";
|
|
// import { ListEditorComponent, ListEditorItem } from '../components/ListEditorComponent';
|
|
import { showConfirmationModal } from "../../modals/ConfirmationModal";
|
|
import {
|
|
createCard,
|
|
createStatusBadge,
|
|
createCardInput,
|
|
createDeleteHeaderButton,
|
|
createCardUrlInput,
|
|
createCardNumberInput,
|
|
createInfoBadge,
|
|
showCardEmptyState,
|
|
normalizeCalendarUrl,
|
|
} from "../components/CardComponent";
|
|
|
|
// interface WebhookItem extends ListEditorItem, WebhookConfig {}
|
|
// interface ICSSubscriptionItem extends ListEditorItem {
|
|
// id: string;
|
|
// name: string;
|
|
// url: string;
|
|
// enabled: boolean;
|
|
// color: string;
|
|
// lastSync?: string;
|
|
// }
|
|
|
|
/**
|
|
* Helper function to format relative time (e.g., "2 hours ago", "5 minutes ago")
|
|
*/
|
|
function getRelativeTime(
|
|
date: Date,
|
|
translate: (key: TranslationKey, params?: Record<string, string | number>) => string
|
|
): string {
|
|
const now = new Date();
|
|
const diffMs = now.getTime() - date.getTime();
|
|
const diffSeconds = Math.floor(diffMs / 1000);
|
|
const diffMinutes = Math.floor(diffSeconds / 60);
|
|
const diffHours = Math.floor(diffMinutes / 60);
|
|
const diffDays = Math.floor(diffHours / 24);
|
|
|
|
if (diffDays > 0) {
|
|
return translate("settings.integrations.timeFormats.daysAgo", {
|
|
days: diffDays,
|
|
plural: diffDays > 1 ? "s" : "",
|
|
});
|
|
} else if (diffHours > 0) {
|
|
return translate("settings.integrations.timeFormats.hoursAgo", {
|
|
hours: diffHours,
|
|
plural: diffHours > 1 ? "s" : "",
|
|
});
|
|
} else if (diffMinutes > 0) {
|
|
return translate("settings.integrations.timeFormats.minutesAgo", {
|
|
minutes: diffMinutes,
|
|
plural: diffMinutes > 1 ? "s" : "",
|
|
});
|
|
} else {
|
|
return translate("settings.integrations.timeFormats.justNow");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Renders the Integrations tab - external connections and API settings
|
|
*/
|
|
export function renderIntegrationsTab(
|
|
container: HTMLElement,
|
|
plugin: TaskNotesPlugin,
|
|
save: () => void
|
|
): void {
|
|
container.empty();
|
|
|
|
const translate = (key: TranslationKey, params?: Record<string, string | number>) =>
|
|
plugin.i18n.translate(key, params);
|
|
|
|
// OAuth Calendar Integration Section
|
|
createSectionHeader(container, "OAuth Calendar Integration");
|
|
createHelpText(
|
|
container,
|
|
"Connect your Google Calendar or Microsoft Outlook to sync events directly into TaskNotes."
|
|
);
|
|
|
|
// TaskNotes License Card (appears before calendar cards)
|
|
// TEMPORARILY DISABLED FOR BETA RELEASE
|
|
// const licenseContainer = container.createDiv("tasknotes-license-container");
|
|
|
|
/* const renderLicenseCard = async () => {
|
|
licenseContainer.empty();
|
|
|
|
// Setup mode toggle
|
|
const modeToggleContainer = document.createElement("div");
|
|
modeToggleContainer.style.cssText = `
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-bottom: 16px;
|
|
padding: 4px;
|
|
background: var(--background-secondary);
|
|
border-radius: 6px;
|
|
width: fit-content;
|
|
`;
|
|
|
|
const createModeButton = (mode: "quick" | "advanced", label: string, icon: string) => {
|
|
const button = document.createElement("button");
|
|
button.className = "tasknotes-mode-toggle-button";
|
|
const isActive = plugin.settings.oauthSetupMode === mode;
|
|
|
|
button.style.cssText = `
|
|
padding: 8px 16px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 0.9em;
|
|
font-weight: 500;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
transition: all 0.2s;
|
|
background: ${isActive ? 'var(--interactive-accent)' : 'transparent'};
|
|
color: ${isActive ? 'var(--text-on-accent)' : 'var(--text-normal)'};
|
|
`;
|
|
|
|
// Add icon
|
|
const iconEl = document.createElement("span");
|
|
iconEl.textContent = icon;
|
|
button.appendChild(iconEl);
|
|
|
|
// Add label
|
|
const labelEl = document.createElement("span");
|
|
labelEl.textContent = label;
|
|
button.appendChild(labelEl);
|
|
|
|
button.addEventListener("click", () => {
|
|
plugin.settings.oauthSetupMode = mode;
|
|
save();
|
|
// Reload OAuth credentials when mode changes
|
|
if (plugin.oauthService) {
|
|
plugin.oauthService.loadClientIds();
|
|
}
|
|
renderLicenseCard(); // Re-render to update UI
|
|
// Also re-render calendar cards to show/hide credential inputs
|
|
renderGoogleCalendarCard();
|
|
renderMicrosoftCalendarCard();
|
|
});
|
|
|
|
button.addEventListener("mouseenter", () => {
|
|
if (!isActive) {
|
|
button.style.background = "var(--background-modifier-hover)";
|
|
}
|
|
});
|
|
|
|
button.addEventListener("mouseleave", () => {
|
|
if (!isActive) {
|
|
button.style.background = "transparent";
|
|
}
|
|
});
|
|
|
|
return button;
|
|
};
|
|
|
|
modeToggleContainer.appendChild(createModeButton("quick", "Quick Setup", ""));
|
|
modeToggleContainer.appendChild(createModeButton("advanced", "Advanced Setup", ""));
|
|
|
|
// Build content based on selected mode
|
|
const sections: any[] = [];
|
|
const mode = plugin.settings.oauthSetupMode;
|
|
|
|
if (mode === "quick") {
|
|
// Quick Setup mode - show license key input
|
|
const helpText = document.createElement("div");
|
|
helpText.style.cssText = `
|
|
font-size: 0.9em;
|
|
color: var(--text-muted);
|
|
line-height: 1.5;
|
|
padding: 12px;
|
|
background: var(--background-secondary);
|
|
border-radius: 6px;
|
|
border-left: 3px solid var(--interactive-accent);
|
|
`;
|
|
helpText.innerHTML = "<strong>Quick Setup</strong> - Enter your license key to connect calendars using OAuth Device Flow. This method displays a verification code that you enter on the provider's website. No OAuth application configuration is required on your part.";
|
|
|
|
const licenseKeyInput = createCardInput("text", "TN-XXXX-XXXX-XXXX-XXXX", plugin.settings.lemonSqueezyLicenseKey);
|
|
licenseKeyInput.addEventListener("blur", async () => {
|
|
const newKey = licenseKeyInput.value.trim();
|
|
plugin.settings.lemonSqueezyLicenseKey = newKey;
|
|
save();
|
|
|
|
// Validate license
|
|
if (plugin.licenseService && newKey) {
|
|
// Clear cache to force fresh validation
|
|
plugin.licenseService.clearCache();
|
|
const valid = await plugin.licenseService.validateLicense(newKey);
|
|
if (valid) {
|
|
new Notice("License activated successfully!");
|
|
// Reload OAuth credentials
|
|
if (plugin.oauthService) {
|
|
await plugin.oauthService.loadClientIds();
|
|
}
|
|
// Re-render to update status
|
|
renderLicenseCard();
|
|
} else {
|
|
new Notice("Invalid or expired license key");
|
|
// Re-render to update status
|
|
renderLicenseCard();
|
|
}
|
|
} else if (plugin.oauthService) {
|
|
// Key was cleared, reload OAuth credentials
|
|
await plugin.oauthService.loadClientIds();
|
|
// Re-render to update status
|
|
renderLicenseCard();
|
|
}
|
|
});
|
|
|
|
const getLicenseLink = document.createElement("a");
|
|
getLicenseLink.href = "https://tasknotes.lemonsqueezy.com";
|
|
getLicenseLink.target = "_blank";
|
|
getLicenseLink.style.fontSize = "0.9em";
|
|
getLicenseLink.style.color = "var(--interactive-accent)";
|
|
getLicenseLink.textContent = "Get License Key ($2/month)";
|
|
|
|
// Status indicator
|
|
const statusDiv = document.createElement("div");
|
|
statusDiv.style.fontSize = "0.85em";
|
|
statusDiv.style.marginTop = "0.5rem";
|
|
|
|
const currentKey = plugin.settings.lemonSqueezyLicenseKey;
|
|
if (currentKey && currentKey.trim()) {
|
|
// Check cached validation status
|
|
const cachedInfo = plugin.licenseService?.getCachedLicenseInfo();
|
|
if (cachedInfo && cachedInfo.key === currentKey && Date.now() < cachedInfo.validUntil) {
|
|
if (cachedInfo.valid) {
|
|
statusDiv.style.color = "var(--text-success)";
|
|
statusDiv.textContent = "License active - valid on unlimited devices";
|
|
} else {
|
|
statusDiv.style.color = "var(--text-error)";
|
|
statusDiv.textContent = "Invalid or expired license key";
|
|
}
|
|
} else {
|
|
statusDiv.style.color = "var(--text-muted)";
|
|
statusDiv.textContent = "License key entered (validation pending)";
|
|
}
|
|
} else {
|
|
statusDiv.style.color = "var(--text-muted)";
|
|
statusDiv.style.fontStyle = "italic";
|
|
statusDiv.textContent = "Enter your license key to enable instant calendar connection.";
|
|
}
|
|
|
|
sections.push({
|
|
rows: [
|
|
{ label: "", input: helpText, fullWidth: true }
|
|
]
|
|
});
|
|
|
|
const quickSetupGuideLink = document.createElement("a");
|
|
quickSetupGuideLink.href = "https://callumalpass.github.io/tasknotes/calendar-setup";
|
|
quickSetupGuideLink.target = "_blank";
|
|
quickSetupGuideLink.style.fontSize = "0.9em";
|
|
quickSetupGuideLink.style.color = "var(--interactive-accent)";
|
|
quickSetupGuideLink.style.marginTop = "0.5rem";
|
|
quickSetupGuideLink.style.display = "inline-block";
|
|
quickSetupGuideLink.textContent = "View Calendar Setup Guide";
|
|
|
|
sections.push({
|
|
rows: [
|
|
{ label: "License Key:", input: licenseKeyInput },
|
|
{ label: "", input: getLicenseLink, fullWidth: true },
|
|
{ label: "", input: statusDiv, fullWidth: true },
|
|
{ label: "", input: quickSetupGuideLink, fullWidth: true }
|
|
]
|
|
});
|
|
|
|
} else {
|
|
// Advanced Setup mode - show instructions only (credentials are per-calendar now)
|
|
const helpText = document.createElement("div");
|
|
helpText.style.cssText = `
|
|
font-size: 0.9em;
|
|
color: var(--text-muted);
|
|
line-height: 1.5;
|
|
padding: 12px;
|
|
background: var(--background-secondary);
|
|
border-radius: 6px;
|
|
border-left: 3px solid var(--color-orange);
|
|
`;
|
|
helpText.innerHTML = "<strong>Advanced Setup</strong> - Configure your own OAuth credentials for calendar integration. This requires creating an OAuth application with the calendar provider and entering the Client ID and Secret in each calendar card below. Initial setup takes approximately 15 minutes.<br><br><strong>Benefits of Advanced Setup:</strong><br>• No license subscription required<br>• Uses your own API quota allocation<br>• Direct connection between Obsidian and calendar provider<br>• Complete data privacy (no intermediary servers)";
|
|
|
|
const setupGuideLink = document.createElement("a");
|
|
setupGuideLink.href = "https://callumalpass.github.io/tasknotes/calendar-setup";
|
|
setupGuideLink.target = "_blank";
|
|
setupGuideLink.style.fontSize = "0.9em";
|
|
setupGuideLink.style.color = "var(--interactive-accent)";
|
|
setupGuideLink.textContent = "View Calendar Setup Guide";
|
|
|
|
sections.push({
|
|
rows: [
|
|
{ label: "", input: helpText, fullWidth: true },
|
|
{ label: "", input: setupGuideLink, fullWidth: true }
|
|
]
|
|
});
|
|
}
|
|
|
|
// Determine status badge
|
|
let statusBadge;
|
|
if (mode === "quick") {
|
|
const hasKey = plugin.settings.lemonSqueezyLicenseKey && plugin.settings.lemonSqueezyLicenseKey.trim();
|
|
statusBadge = hasKey ? createStatusBadge("License Active", "active") : createStatusBadge("No License", "inactive");
|
|
} else {
|
|
statusBadge = createStatusBadge("Advanced Mode", "default");
|
|
}
|
|
|
|
// Create the card
|
|
const card = createCard(licenseContainer, {
|
|
collapsible: true,
|
|
defaultCollapsed: false,
|
|
colorIndicator: {
|
|
color: mode === "quick" ? "#4A9EFF" : "#FF8C00" // Blue for quick, orange for advanced
|
|
},
|
|
header: {
|
|
primaryText: "OAuth Calendar Setup",
|
|
secondaryText: mode === "quick" ? "License-based instant setup" : "Bring your own OAuth credentials",
|
|
meta: [statusBadge]
|
|
},
|
|
content: {
|
|
sections: sections
|
|
}
|
|
});
|
|
|
|
// Insert mode toggle before the card
|
|
licenseContainer.insertBefore(modeToggleContainer, card);
|
|
}; */
|
|
|
|
// Initial render
|
|
// TEMPORARILY DISABLED FOR BETA RELEASE
|
|
// renderLicenseCard();
|
|
|
|
// Setup guide link (always visible)
|
|
const setupGuideContainer = container.createDiv("tasknotes-oauth-setup-guide");
|
|
setupGuideContainer.style.cssText = `
|
|
font-size: 0.9em;
|
|
color: var(--text-muted);
|
|
line-height: 1.5;
|
|
padding: 12px;
|
|
background: var(--background-secondary);
|
|
border-radius: 6px;
|
|
border-left: 3px solid var(--interactive-accent);
|
|
margin-bottom: 16px;
|
|
`;
|
|
|
|
const setupText = setupGuideContainer.createDiv();
|
|
const strong = setupText.createEl("strong");
|
|
strong.textContent = "OAuth Setup Required:";
|
|
setupText.appendText(" You'll need to create OAuth credentials with Google and/or Microsoft to connect your calendars. This takes approximately 15 minutes for initial setup.");
|
|
|
|
const setupGuideLink = setupGuideContainer.createEl("a", {
|
|
text: "View Calendar Setup Guide",
|
|
href: "https://callumalpass.github.io/tasknotes/calendar-setup",
|
|
attr: { target: "_blank" }
|
|
});
|
|
setupGuideLink.style.cssText = `
|
|
font-size: 0.9em;
|
|
color: var(--interactive-accent);
|
|
margin-top: 8px;
|
|
display: inline-block;
|
|
`;
|
|
|
|
// Google Calendar container for card-based UI
|
|
const googleCalendarContainer = container.createDiv("google-calendar-integration-container");
|
|
|
|
// Check connection status and render card
|
|
const renderGoogleCalendarCard = async () => {
|
|
googleCalendarContainer.empty();
|
|
|
|
if (!plugin.oauthService) {
|
|
const errorCard = createCard(googleCalendarContainer, {
|
|
header: {
|
|
primaryText: "Google Calendar",
|
|
secondaryText: "OAuth service not available",
|
|
meta: [createStatusBadge("Error", "inactive")]
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
const isConnected = await plugin.oauthService.isConnected("google");
|
|
const connection = isConnected ? await plugin.oauthService.getConnection("google") : null;
|
|
|
|
if (isConnected && connection) {
|
|
// Connected state card
|
|
const connectedDate = connection.connectedAt ? new Date(connection.connectedAt) : null;
|
|
const timeAgo = connectedDate ? getRelativeTime(connectedDate, translate) : "";
|
|
|
|
// Create info displays
|
|
const accountInfo = document.createElement("div");
|
|
accountInfo.style.fontSize = "0.9em";
|
|
accountInfo.style.color = "var(--text-muted)";
|
|
accountInfo.textContent = connection.userEmail || "Unknown account";
|
|
|
|
const connectedInfo = document.createElement("div");
|
|
connectedInfo.style.fontSize = "0.9em";
|
|
connectedInfo.style.color = "var(--text-muted)";
|
|
connectedInfo.textContent = connectedDate ? `Connected ${timeAgo}` : "Connected";
|
|
|
|
const lastRefreshInfo = document.createElement("div");
|
|
lastRefreshInfo.style.fontSize = "0.9em";
|
|
lastRefreshInfo.style.color = "var(--text-muted)";
|
|
if (connection.lastRefreshed) {
|
|
const lastRefreshDate = new Date(connection.lastRefreshed);
|
|
lastRefreshInfo.textContent = `Last refreshed ${getRelativeTime(lastRefreshDate, translate)}`;
|
|
} else {
|
|
lastRefreshInfo.textContent = "Never refreshed";
|
|
}
|
|
|
|
createCard(googleCalendarContainer, {
|
|
collapsible: true,
|
|
defaultCollapsed: false,
|
|
colorIndicator: {
|
|
color: "#4285F4" // Google blue
|
|
},
|
|
header: {
|
|
primaryText: "Google Calendar",
|
|
secondaryText: "OAuth 2.0 Connection",
|
|
meta: [createStatusBadge("Connected", "active")]
|
|
},
|
|
content: {
|
|
sections: [{
|
|
rows: [
|
|
{ label: "Account:", input: accountInfo },
|
|
{ label: "Status:", input: connectedInfo },
|
|
{ label: "Sync:", input: lastRefreshInfo }
|
|
]
|
|
}]
|
|
},
|
|
actions: {
|
|
buttons: [
|
|
{
|
|
text: "Refresh Now",
|
|
icon: "refresh-cw",
|
|
variant: "primary",
|
|
onClick: async () => {
|
|
try {
|
|
if (plugin.googleCalendarService) {
|
|
await plugin.googleCalendarService.refresh();
|
|
new Notice("Google Calendar refreshed successfully");
|
|
renderGoogleCalendarCard(); // Re-render to update timestamp
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to refresh:", error);
|
|
new Notice("Failed to refresh Google Calendar");
|
|
}
|
|
}
|
|
},
|
|
{
|
|
text: "Disconnect",
|
|
icon: "log-out",
|
|
variant: "warning",
|
|
onClick: async () => {
|
|
try {
|
|
await plugin.oauthService!.disconnect("google");
|
|
new Notice("Disconnected from Google Calendar");
|
|
renderGoogleCalendarCard(); // Re-render to show disconnected state
|
|
} catch (error) {
|
|
console.error("Failed to disconnect:", error);
|
|
new Notice("Failed to disconnect from Google Calendar");
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
});
|
|
} else {
|
|
// Disconnected state card
|
|
const helpText = document.createElement("div");
|
|
helpText.style.fontSize = "0.9em";
|
|
helpText.style.color = "var(--text-muted)";
|
|
helpText.style.lineHeight = "1.5";
|
|
helpText.innerHTML = "Connect your Google Calendar account to sync events directly into TaskNotes. Events will automatically refresh every 15 minutes.";
|
|
|
|
// Build sections based on setup mode
|
|
const sections: any[] = [
|
|
{
|
|
rows: [
|
|
{ label: "Info:", input: helpText, fullWidth: true }
|
|
]
|
|
}
|
|
];
|
|
|
|
// Only show credential inputs in Advanced mode
|
|
// TEMPORARILY FORCING ADVANCED MODE FOR BETA RELEASE
|
|
if (true) { // if (plugin.settings.oauthSetupMode === "advanced") {
|
|
// Create credential input fields
|
|
const clientIdInput = createCardInput("text", "your-client-id.apps.googleusercontent.com", plugin.settings.googleOAuthClientId);
|
|
clientIdInput.addEventListener("blur", async () => {
|
|
plugin.settings.googleOAuthClientId = clientIdInput.value.trim();
|
|
save();
|
|
if (plugin.oauthService) {
|
|
await plugin.oauthService.loadClientIds();
|
|
}
|
|
});
|
|
|
|
const clientSecretInput = createCardInput("text", "your-client-secret", plugin.settings.googleOAuthClientSecret);
|
|
clientSecretInput.setAttribute("type", "password");
|
|
clientSecretInput.addEventListener("blur", async () => {
|
|
plugin.settings.googleOAuthClientSecret = clientSecretInput.value.trim();
|
|
save();
|
|
if (plugin.oauthService) {
|
|
await plugin.oauthService.loadClientIds();
|
|
}
|
|
});
|
|
|
|
const credentialNote = document.createElement("div");
|
|
credentialNote.style.fontSize = "0.85em";
|
|
credentialNote.style.color = "var(--text-muted)";
|
|
credentialNote.style.fontStyle = "italic";
|
|
credentialNote.style.marginTop = "0.5rem";
|
|
credentialNote.textContent = "Enter your OAuth app credentials from Google Cloud Console.";
|
|
|
|
sections.push({
|
|
rows: [
|
|
{ label: "Client ID:", input: clientIdInput },
|
|
{ label: "Client Secret:", input: clientSecretInput },
|
|
{ label: "", input: credentialNote, fullWidth: true }
|
|
]
|
|
});
|
|
} else {
|
|
// Quick mode - show reminder about license
|
|
// TEMPORARILY DISABLED FOR BETA RELEASE
|
|
/* const quickModeNote = document.createElement("div");
|
|
quickModeNote.style.fontSize = "0.85em";
|
|
quickModeNote.style.color = "var(--text-accent)";
|
|
quickModeNote.style.fontStyle = "italic";
|
|
quickModeNote.style.padding = "8px";
|
|
quickModeNote.style.background = "var(--background-secondary)";
|
|
quickModeNote.style.borderRadius = "4px";
|
|
quickModeNote.textContent = "Note: A valid license key is required above. Click Connect to authenticate using OAuth Device Flow.";
|
|
|
|
sections.push({
|
|
rows: [
|
|
{ label: "", input: quickModeNote, fullWidth: true }
|
|
]
|
|
}); */
|
|
}
|
|
|
|
createCard(googleCalendarContainer, {
|
|
collapsible: true,
|
|
defaultCollapsed: false,
|
|
colorIndicator: {
|
|
color: "#9AA0A6" // Google gray
|
|
},
|
|
header: {
|
|
primaryText: "Google Calendar",
|
|
secondaryText: "OAuth 2.0 Connection",
|
|
meta: [createStatusBadge("Not Connected", "inactive")]
|
|
},
|
|
content: {
|
|
sections: sections
|
|
},
|
|
actions: {
|
|
buttons: [
|
|
{
|
|
text: "Connect Google Calendar",
|
|
icon: "link",
|
|
variant: "primary",
|
|
onClick: async () => {
|
|
try {
|
|
await plugin.oauthService!.authenticate("google");
|
|
new Notice("Google Calendar connected successfully!");
|
|
renderGoogleCalendarCard(); // Re-render to show connected state
|
|
} catch (error) {
|
|
console.error("Failed to connect:", error);
|
|
new Notice(`Failed to connect: ${error.message}`);
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
// Initial render
|
|
renderGoogleCalendarCard();
|
|
|
|
// Microsoft Calendar container for card-based UI
|
|
const microsoftCalendarContainer = container.createDiv("microsoft-calendar-integration-container");
|
|
|
|
// Check connection status and render card
|
|
const renderMicrosoftCalendarCard = async () => {
|
|
microsoftCalendarContainer.empty();
|
|
|
|
if (!plugin.oauthService) {
|
|
createCard(microsoftCalendarContainer, {
|
|
header: {
|
|
primaryText: "Microsoft Outlook Calendar",
|
|
secondaryText: "OAuth service not available",
|
|
meta: [createStatusBadge("Error", "inactive")]
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
const isConnected = await plugin.oauthService.isConnected("microsoft");
|
|
const connection = isConnected ? await plugin.oauthService.getConnection("microsoft") : null;
|
|
|
|
if (isConnected && connection) {
|
|
// Connected state card
|
|
const connectedDate = connection.connectedAt ? new Date(connection.connectedAt) : null;
|
|
const timeAgo = connectedDate ? getRelativeTime(connectedDate, translate) : "";
|
|
|
|
// Create info displays
|
|
const accountInfo = document.createElement("div");
|
|
accountInfo.style.fontSize = "0.9em";
|
|
accountInfo.style.color = "var(--text-muted)";
|
|
accountInfo.textContent = connection.userEmail || "Unknown account";
|
|
|
|
const connectedInfo = document.createElement("div");
|
|
connectedInfo.style.fontSize = "0.9em";
|
|
connectedInfo.style.color = "var(--text-muted)";
|
|
connectedInfo.textContent = connectedDate ? `Connected ${timeAgo}` : "Connected";
|
|
|
|
const lastRefreshInfo = document.createElement("div");
|
|
lastRefreshInfo.style.fontSize = "0.9em";
|
|
lastRefreshInfo.style.color = "var(--text-muted)";
|
|
if (connection.lastRefreshed) {
|
|
const lastRefreshDate = new Date(connection.lastRefreshed);
|
|
lastRefreshInfo.textContent = `Last refreshed ${getRelativeTime(lastRefreshDate, translate)}`;
|
|
} else {
|
|
lastRefreshInfo.textContent = "Never refreshed";
|
|
}
|
|
|
|
createCard(microsoftCalendarContainer, {
|
|
collapsible: true,
|
|
defaultCollapsed: false,
|
|
colorIndicator: {
|
|
color: "#0078D4" // Microsoft blue
|
|
},
|
|
header: {
|
|
primaryText: "Microsoft Outlook Calendar",
|
|
secondaryText: "OAuth 2.0 Connection",
|
|
meta: [createStatusBadge("Connected", "active")]
|
|
},
|
|
content: {
|
|
sections: [{
|
|
rows: [
|
|
{ label: "Account:", input: accountInfo },
|
|
{ label: "Status:", input: connectedInfo },
|
|
{ label: "Sync:", input: lastRefreshInfo }
|
|
]
|
|
}]
|
|
},
|
|
actions: {
|
|
buttons: [
|
|
{
|
|
text: "Disconnect",
|
|
icon: "log-out",
|
|
variant: "warning",
|
|
onClick: async () => {
|
|
try {
|
|
await plugin.oauthService!.disconnect("microsoft");
|
|
new Notice("Disconnected from Microsoft Calendar");
|
|
renderMicrosoftCalendarCard();
|
|
} catch (error) {
|
|
console.error("Failed to disconnect:", error);
|
|
new Notice("Failed to disconnect from Microsoft Calendar");
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
});
|
|
} else {
|
|
// Disconnected state card
|
|
const helpText = document.createElement("div");
|
|
helpText.style.fontSize = "0.9em";
|
|
helpText.style.color = "var(--text-muted)";
|
|
helpText.style.lineHeight = "1.5";
|
|
helpText.innerHTML = "Connect your Microsoft Outlook calendar to sync events directly into TaskNotes.";
|
|
|
|
// Build sections based on setup mode
|
|
const sections: any[] = [
|
|
{
|
|
rows: [
|
|
{ label: "Info:", input: helpText, fullWidth: true }
|
|
]
|
|
}
|
|
];
|
|
|
|
// Only show credential inputs in Advanced mode
|
|
// TEMPORARILY FORCING ADVANCED MODE FOR BETA RELEASE
|
|
if (true) { // if (plugin.settings.oauthSetupMode === "advanced") {
|
|
// Create credential input fields
|
|
const clientIdInput = createCardInput("text", "your-microsoft-client-id", plugin.settings.microsoftOAuthClientId);
|
|
clientIdInput.addEventListener("blur", async () => {
|
|
plugin.settings.microsoftOAuthClientId = clientIdInput.value.trim();
|
|
save();
|
|
if (plugin.oauthService) {
|
|
await plugin.oauthService.loadClientIds();
|
|
}
|
|
});
|
|
|
|
const clientSecretInput = createCardInput("text", "your-microsoft-client-secret", plugin.settings.microsoftOAuthClientSecret);
|
|
clientSecretInput.setAttribute("type", "password");
|
|
clientSecretInput.addEventListener("blur", async () => {
|
|
plugin.settings.microsoftOAuthClientSecret = clientSecretInput.value.trim();
|
|
save();
|
|
if (plugin.oauthService) {
|
|
await plugin.oauthService.loadClientIds();
|
|
}
|
|
});
|
|
|
|
const credentialNote = document.createElement("div");
|
|
credentialNote.style.fontSize = "0.85em";
|
|
credentialNote.style.color = "var(--text-muted)";
|
|
credentialNote.style.fontStyle = "italic";
|
|
credentialNote.style.marginTop = "0.5rem";
|
|
credentialNote.textContent = "Enter your OAuth app credentials from Azure Portal.";
|
|
|
|
sections.push({
|
|
rows: [
|
|
{ label: "Client ID:", input: clientIdInput },
|
|
{ label: "Client Secret:", input: clientSecretInput },
|
|
{ label: "", input: credentialNote, fullWidth: true }
|
|
]
|
|
});
|
|
} else {
|
|
// Quick mode - show reminder about license
|
|
// TEMPORARILY DISABLED FOR BETA RELEASE
|
|
/* const quickModeNote = document.createElement("div");
|
|
quickModeNote.style.fontSize = "0.85em";
|
|
quickModeNote.style.color = "var(--text-accent)";
|
|
quickModeNote.style.fontStyle = "italic";
|
|
quickModeNote.style.padding = "8px";
|
|
quickModeNote.style.background = "var(--background-secondary)";
|
|
quickModeNote.style.borderRadius = "4px";
|
|
quickModeNote.textContent = "Note: A valid license key is required above. Click Connect to authenticate using OAuth Device Flow.";
|
|
|
|
sections.push({
|
|
rows: [
|
|
{ label: "", input: quickModeNote, fullWidth: true }
|
|
]
|
|
}); */
|
|
}
|
|
|
|
createCard(microsoftCalendarContainer, {
|
|
collapsible: true,
|
|
defaultCollapsed: false,
|
|
colorIndicator: {
|
|
color: "#737373" // Microsoft gray
|
|
},
|
|
header: {
|
|
primaryText: "Microsoft Outlook Calendar",
|
|
secondaryText: "OAuth 2.0 Connection",
|
|
meta: [createStatusBadge("Not Connected", "inactive")]
|
|
},
|
|
content: {
|
|
sections: sections
|
|
},
|
|
actions: {
|
|
buttons: [
|
|
{
|
|
text: "Connect Microsoft Calendar",
|
|
icon: "link",
|
|
variant: "primary",
|
|
onClick: async () => {
|
|
try {
|
|
await plugin.oauthService!.authenticate("microsoft");
|
|
new Notice("Microsoft Calendar connected successfully!");
|
|
renderMicrosoftCalendarCard();
|
|
} catch (error) {
|
|
console.error("Failed to connect:", error);
|
|
new Notice(`Failed to connect: ${error.message}`);
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
// Initial render
|
|
renderMicrosoftCalendarCard();
|
|
|
|
// Calendar Subscriptions Section (ICS)
|
|
createSectionHeader(container, translate("settings.integrations.calendarSubscriptions.header"));
|
|
createHelpText(container, translate("settings.integrations.calendarSubscriptions.description"));
|
|
|
|
// Default settings for ICS integration
|
|
createTextSetting(container, {
|
|
name: translate("settings.integrations.calendarSubscriptions.defaultNoteTemplate.name"),
|
|
desc: translate(
|
|
"settings.integrations.calendarSubscriptions.defaultNoteTemplate.description"
|
|
),
|
|
placeholder: translate(
|
|
"settings.integrations.calendarSubscriptions.defaultNoteTemplate.placeholder"
|
|
),
|
|
getValue: () => plugin.settings.icsIntegration.defaultNoteTemplate,
|
|
setValue: async (value: string) => {
|
|
plugin.settings.icsIntegration.defaultNoteTemplate = value;
|
|
save();
|
|
},
|
|
});
|
|
|
|
createTextSetting(container, {
|
|
name: translate("settings.integrations.calendarSubscriptions.defaultNoteFolder.name"),
|
|
desc: translate(
|
|
"settings.integrations.calendarSubscriptions.defaultNoteFolder.description"
|
|
),
|
|
placeholder: translate(
|
|
"settings.integrations.calendarSubscriptions.defaultNoteFolder.placeholder"
|
|
),
|
|
getValue: () => plugin.settings.icsIntegration.defaultNoteFolder,
|
|
setValue: async (value: string) => {
|
|
plugin.settings.icsIntegration.defaultNoteFolder = value;
|
|
save();
|
|
},
|
|
});
|
|
|
|
createDropdownSetting(container, {
|
|
name: translate("settings.integrations.calendarSubscriptions.filenameFormat.name"),
|
|
desc: translate("settings.integrations.calendarSubscriptions.filenameFormat.description"),
|
|
options: [
|
|
{
|
|
value: "title",
|
|
label: translate(
|
|
"settings.integrations.calendarSubscriptions.filenameFormat.options.title"
|
|
),
|
|
},
|
|
{
|
|
value: "zettel",
|
|
label: translate(
|
|
"settings.integrations.calendarSubscriptions.filenameFormat.options.zettel"
|
|
),
|
|
},
|
|
{
|
|
value: "timestamp",
|
|
label: translate(
|
|
"settings.integrations.calendarSubscriptions.filenameFormat.options.timestamp"
|
|
),
|
|
},
|
|
{
|
|
value: "custom",
|
|
label: translate(
|
|
"settings.integrations.calendarSubscriptions.filenameFormat.options.custom"
|
|
),
|
|
},
|
|
],
|
|
getValue: () => plugin.settings.icsIntegration.icsNoteFilenameFormat,
|
|
setValue: async (value: string) => {
|
|
plugin.settings.icsIntegration.icsNoteFilenameFormat = value as any;
|
|
save();
|
|
// Re-render to show custom template field if needed
|
|
renderIntegrationsTab(container, plugin, save);
|
|
},
|
|
});
|
|
|
|
if (plugin.settings.icsIntegration.icsNoteFilenameFormat === "custom") {
|
|
createTextSetting(container, {
|
|
name: translate("settings.integrations.calendarSubscriptions.customTemplate.name"),
|
|
desc: translate(
|
|
"settings.integrations.calendarSubscriptions.customTemplate.description"
|
|
),
|
|
placeholder: translate(
|
|
"settings.integrations.calendarSubscriptions.customTemplate.placeholder"
|
|
),
|
|
getValue: () => plugin.settings.icsIntegration.customICSNoteFilenameTemplate,
|
|
setValue: async (value: string) => {
|
|
plugin.settings.icsIntegration.customICSNoteFilenameTemplate = value;
|
|
save();
|
|
},
|
|
});
|
|
}
|
|
|
|
// ICS Subscriptions List - Add proper section header
|
|
createSectionHeader(container, translate("settings.integrations.subscriptionsList.header"));
|
|
const icsContainer = container.createDiv("ics-subscriptions-container");
|
|
renderICSSubscriptionsList(icsContainer, plugin, save);
|
|
|
|
// Add subscription button
|
|
createButtonSetting(container, {
|
|
name: translate("settings.integrations.subscriptionsList.addSubscription.name"),
|
|
desc: translate("settings.integrations.subscriptionsList.addSubscription.description"),
|
|
buttonText: translate("settings.integrations.subscriptionsList.addSubscription.buttonText"),
|
|
onClick: async () => {
|
|
// Create a new subscription with temporary values
|
|
const newSubscription = {
|
|
name: translate("settings.integrations.subscriptionsList.newCalendarName"),
|
|
url: "",
|
|
color: "#6366f1",
|
|
enabled: false, // Start disabled until user fills in details
|
|
type: "remote" as const,
|
|
refreshInterval: 60,
|
|
};
|
|
|
|
if (!plugin.icsSubscriptionService) {
|
|
new Notice(
|
|
translate("settings.integrations.subscriptionsList.notices.serviceUnavailable")
|
|
);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await plugin.icsSubscriptionService.addSubscription(newSubscription);
|
|
new Notice(translate("settings.integrations.subscriptionsList.notices.addSuccess"));
|
|
// Re-render to show the new subscription card
|
|
renderICSSubscriptionsList(icsContainer, plugin, save);
|
|
} catch (error) {
|
|
console.error("Error adding subscription:", error);
|
|
new Notice(translate("settings.integrations.subscriptionsList.notices.addFailure"));
|
|
}
|
|
},
|
|
});
|
|
|
|
// Refresh all subscriptions button
|
|
createButtonSetting(container, {
|
|
name: translate("settings.integrations.subscriptionsList.refreshAll.name"),
|
|
desc: translate("settings.integrations.subscriptionsList.refreshAll.description"),
|
|
buttonText: translate("settings.integrations.subscriptionsList.refreshAll.buttonText"),
|
|
onClick: async () => {
|
|
if (plugin.icsSubscriptionService) {
|
|
try {
|
|
await plugin.icsSubscriptionService.refreshAllSubscriptions();
|
|
new Notice(
|
|
translate("settings.integrations.subscriptionsList.notices.refreshSuccess")
|
|
);
|
|
} catch (error) {
|
|
console.error("Error refreshing subscriptions:", error);
|
|
new Notice(
|
|
translate("settings.integrations.subscriptionsList.notices.refreshFailure")
|
|
);
|
|
}
|
|
}
|
|
},
|
|
});
|
|
|
|
// Automatic ICS Export Section
|
|
createSectionHeader(container, translate("settings.integrations.autoExport.header"));
|
|
createHelpText(container, translate("settings.integrations.autoExport.description"));
|
|
|
|
createToggleSetting(container, {
|
|
name: translate("settings.integrations.autoExport.enable.name"),
|
|
desc: translate("settings.integrations.autoExport.enable.description"),
|
|
getValue: () => plugin.settings.icsIntegration.enableAutoExport,
|
|
setValue: async (value: boolean) => {
|
|
plugin.settings.icsIntegration.enableAutoExport = value;
|
|
save();
|
|
new Notice(translate("settings.integrations.autoExport.notices.reloadRequired"));
|
|
// Re-render to show/hide export settings
|
|
renderIntegrationsTab(container, plugin, save);
|
|
},
|
|
});
|
|
|
|
if (plugin.settings.icsIntegration.enableAutoExport) {
|
|
createTextSetting(container, {
|
|
name: translate("settings.integrations.autoExport.filePath.name"),
|
|
desc: translate("settings.integrations.autoExport.filePath.description"),
|
|
placeholder: translate("settings.integrations.autoExport.filePath.placeholder"),
|
|
getValue: () => plugin.settings.icsIntegration.autoExportPath,
|
|
setValue: async (value: string) => {
|
|
plugin.settings.icsIntegration.autoExportPath = value || "tasknotes-calendar.ics";
|
|
save();
|
|
},
|
|
});
|
|
|
|
createNumberSetting(container, {
|
|
name: translate("settings.integrations.autoExport.interval.name"),
|
|
desc: translate("settings.integrations.autoExport.interval.description"),
|
|
placeholder: translate("settings.integrations.autoExport.interval.placeholder"),
|
|
min: 5,
|
|
max: 1440, // 24 hours max
|
|
getValue: () => plugin.settings.icsIntegration.autoExportInterval,
|
|
setValue: async (value: number) => {
|
|
plugin.settings.icsIntegration.autoExportInterval = Math.max(5, value || 60);
|
|
save();
|
|
// Restart the auto export service with new interval
|
|
if (plugin.autoExportService) {
|
|
plugin.autoExportService.updateInterval(
|
|
plugin.settings.icsIntegration.autoExportInterval
|
|
);
|
|
}
|
|
},
|
|
});
|
|
|
|
// Show current export status
|
|
const statusContainer = container.createDiv("auto-export-status");
|
|
statusContainer.style.marginTop = "10px";
|
|
statusContainer.style.padding = "10px";
|
|
statusContainer.style.backgroundColor = "var(--background-secondary)";
|
|
statusContainer.style.borderRadius = "4px";
|
|
|
|
statusContainer.empty();
|
|
|
|
if (plugin.autoExportService) {
|
|
const lastExport = plugin.autoExportService.getLastExportTime();
|
|
const nextExport = plugin.autoExportService.getNextExportTime();
|
|
|
|
const titleDiv = statusContainer.createDiv();
|
|
titleDiv.style.fontWeight = "500";
|
|
titleDiv.style.marginBottom = "5px";
|
|
titleDiv.textContent = translate("settings.integrations.autoExport.status.title") + ":";
|
|
|
|
const statusDiv = statusContainer.createDiv();
|
|
statusDiv.style.fontSize = "0.9em";
|
|
statusDiv.style.opacity = "0.8";
|
|
|
|
const lastExportText = lastExport
|
|
? translate("settings.integrations.autoExport.status.lastExport", { time: lastExport.toLocaleString() })
|
|
: translate("settings.integrations.autoExport.status.noExports");
|
|
const nextExportText = nextExport
|
|
? translate("settings.integrations.autoExport.status.nextExport", { time: nextExport.toLocaleString() })
|
|
: translate("settings.integrations.autoExport.status.notScheduled");
|
|
|
|
statusDiv.textContent = lastExportText + "\n" + nextExportText;
|
|
statusDiv.style.whiteSpace = "pre-line";
|
|
} else {
|
|
const errorDiv = statusContainer.createDiv();
|
|
errorDiv.style.fontWeight = "500";
|
|
errorDiv.style.color = "var(--text-warning)";
|
|
errorDiv.textContent = translate("settings.integrations.autoExport.status.serviceNotInitialized");
|
|
}
|
|
|
|
// Manual export trigger button
|
|
createButtonSetting(container, {
|
|
name: translate("settings.integrations.autoExport.exportNow.name"),
|
|
desc: translate("settings.integrations.autoExport.exportNow.description"),
|
|
buttonText: translate("settings.integrations.autoExport.exportNow.buttonText"),
|
|
onClick: async () => {
|
|
if (plugin.autoExportService) {
|
|
try {
|
|
await plugin.autoExportService.exportNow();
|
|
new Notice(
|
|
translate("settings.integrations.autoExport.notices.exportSuccess")
|
|
);
|
|
// Re-render to update status
|
|
renderIntegrationsTab(container, plugin, save);
|
|
} catch (error) {
|
|
console.error("Manual export failed:", error);
|
|
new Notice(
|
|
translate("settings.integrations.autoExport.notices.exportFailure")
|
|
);
|
|
}
|
|
} else {
|
|
new Notice(
|
|
translate("settings.integrations.autoExport.notices.serviceUnavailable")
|
|
);
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
// HTTP API Section (Skip on mobile)
|
|
if (!Platform.isMobile) {
|
|
createSectionHeader(container, translate("settings.integrations.httpApi.header"));
|
|
createHelpText(container, translate("settings.integrations.httpApi.description"));
|
|
|
|
createToggleSetting(container, {
|
|
name: translate("settings.integrations.httpApi.enable.name"),
|
|
desc: translate("settings.integrations.httpApi.enable.description"),
|
|
getValue: () => plugin.settings.enableAPI,
|
|
setValue: async (value: boolean) => {
|
|
plugin.settings.enableAPI = value;
|
|
save();
|
|
// Re-render to show API settings
|
|
renderIntegrationsTab(container, plugin, save);
|
|
},
|
|
});
|
|
|
|
if (plugin.settings.enableAPI) {
|
|
createNumberSetting(container, {
|
|
name: translate("settings.integrations.httpApi.port.name"),
|
|
desc: translate("settings.integrations.httpApi.port.description"),
|
|
placeholder: translate("settings.integrations.httpApi.port.placeholder"),
|
|
min: 1024,
|
|
max: 65535,
|
|
getValue: () => plugin.settings.apiPort,
|
|
setValue: async (value: number) => {
|
|
plugin.settings.apiPort = value;
|
|
save();
|
|
},
|
|
});
|
|
|
|
createTextSetting(container, {
|
|
name: translate("settings.integrations.httpApi.authToken.name"),
|
|
desc: translate("settings.integrations.httpApi.authToken.description"),
|
|
placeholder: translate("settings.integrations.httpApi.authToken.placeholder"),
|
|
getValue: () => plugin.settings.apiAuthToken,
|
|
setValue: async (value: string) => {
|
|
plugin.settings.apiAuthToken = value;
|
|
save();
|
|
},
|
|
});
|
|
|
|
// API endpoint info
|
|
const apiInfoContainer = container.createDiv("tasknotes-settings__help-section");
|
|
const apiHeader = apiInfoContainer.createDiv("tasknotes-settings__collapsible-header");
|
|
const apiHeaderContent = apiHeader.createDiv(
|
|
"tasknotes-settings__collapsible-header-content"
|
|
);
|
|
const apiToggleIcon = apiHeaderContent.createSpan(
|
|
"tasknotes-settings__collapsible-icon"
|
|
);
|
|
apiToggleIcon.textContent = translate(
|
|
"settings.integrations.httpApi.endpoints.expandIcon"
|
|
);
|
|
apiHeaderContent.createSpan({
|
|
text: translate("settings.integrations.httpApi.endpoints.header"),
|
|
cls: "tasknotes-settings__collapsible-title",
|
|
});
|
|
|
|
const apiEndpointsContent = apiInfoContainer.createDiv(
|
|
"tasknotes-settings__collapsible-content"
|
|
);
|
|
apiEndpointsContent.style.display = "none"; // Start collapsed
|
|
|
|
// Toggle functionality
|
|
apiHeader.addEventListener("click", () => {
|
|
const isExpanded = apiEndpointsContent.style.display !== "none";
|
|
apiEndpointsContent.style.display = isExpanded ? "none" : "block";
|
|
apiToggleIcon.textContent = isExpanded
|
|
? translate("settings.integrations.httpApi.endpoints.expandIcon")
|
|
: translate("settings.integrations.httpApi.endpoints.collapseIcon");
|
|
});
|
|
|
|
// Fetch live API documentation
|
|
loadAPIEndpoints(apiEndpointsContent, plugin.settings.apiPort);
|
|
}
|
|
|
|
// Webhooks Section
|
|
createSectionHeader(container, translate("settings.integrations.webhooks.header"));
|
|
|
|
// Webhook description
|
|
const webhookDescEl = container.createDiv("setting-item-description");
|
|
webhookDescEl.createEl("p", {
|
|
text: translate("settings.integrations.webhooks.description.overview"),
|
|
});
|
|
webhookDescEl.createEl("p", {
|
|
text: translate("settings.integrations.webhooks.description.usage"),
|
|
});
|
|
|
|
// Webhook management
|
|
renderWebhookList(container, plugin, save);
|
|
|
|
// Add webhook button
|
|
createButtonSetting(container, {
|
|
name: translate("settings.integrations.webhooks.addWebhook.name"),
|
|
desc: translate("settings.integrations.webhooks.addWebhook.description"),
|
|
buttonText: translate("settings.integrations.webhooks.addWebhook.buttonText"),
|
|
onClick: async () => {
|
|
const modal = new WebhookModal(
|
|
plugin.app,
|
|
async (webhookConfig: Partial<WebhookConfig>) => {
|
|
// Generate ID and secret
|
|
const webhook: WebhookConfig = {
|
|
id: `wh_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
|
|
url: webhookConfig.url || "",
|
|
events: webhookConfig.events || [],
|
|
secret: generateWebhookSecret(),
|
|
active: true,
|
|
createdAt: new Date().toISOString(),
|
|
failureCount: 0,
|
|
successCount: 0,
|
|
transformFile: webhookConfig.transformFile,
|
|
corsHeaders: webhookConfig.corsHeaders,
|
|
};
|
|
|
|
if (!plugin.settings.webhooks) {
|
|
plugin.settings.webhooks = [];
|
|
}
|
|
|
|
plugin.settings.webhooks.push(webhook);
|
|
save();
|
|
|
|
// Re-render webhook list to show the new webhook
|
|
renderWebhookList(
|
|
container.querySelector(".tasknotes-webhooks-container")
|
|
?.parentElement || container,
|
|
plugin,
|
|
save
|
|
);
|
|
|
|
// Show success message with secret
|
|
new SecretNoticeModal(plugin.app, webhook.secret).open();
|
|
new Notice(translate("settings.integrations.webhooks.notices.created"));
|
|
}
|
|
);
|
|
modal.open();
|
|
},
|
|
});
|
|
}
|
|
|
|
// Other Integrations Section
|
|
createSectionHeader(container, translate("settings.integrations.otherIntegrations.header"));
|
|
createHelpText(container, translate("settings.integrations.otherIntegrations.description"));
|
|
}
|
|
|
|
function renderICSSubscriptionsList(
|
|
container: HTMLElement,
|
|
plugin: TaskNotesPlugin,
|
|
save: () => void
|
|
): void {
|
|
container.empty();
|
|
|
|
const translate = (key: TranslationKey, params?: Record<string, string | number>) =>
|
|
plugin.i18n.translate(key, params);
|
|
|
|
if (!plugin.icsSubscriptionService) {
|
|
createHelpText(
|
|
container,
|
|
translate("settings.integrations.subscriptionsList.notices.serviceUnavailable")
|
|
);
|
|
return;
|
|
}
|
|
|
|
const subscriptions = plugin.icsSubscriptionService.getSubscriptions();
|
|
if (subscriptions.length === 0) {
|
|
// Empty state with consistent styling
|
|
const emptyState = container.createDiv("tasknotes-webhooks-empty-state");
|
|
emptyState.createSpan("tasknotes-webhooks-empty-icon");
|
|
emptyState.createSpan({
|
|
text: translate("settings.integrations.subscriptionsList.emptyState"),
|
|
cls: "tasknotes-webhooks-empty-text",
|
|
});
|
|
return;
|
|
}
|
|
|
|
subscriptions.forEach((subscription) => {
|
|
// Create input elements
|
|
const enabledToggle = createCardInput("checkbox");
|
|
enabledToggle.checked = subscription.enabled;
|
|
|
|
const nameInput = createCardInput("text", "Calendar name", subscription.name);
|
|
|
|
// Create type dropdown
|
|
const typeSelect = document.createElement("select");
|
|
typeSelect.className = "tasknotes-settings__card-input";
|
|
|
|
const remoteOption = document.createElement("option");
|
|
remoteOption.value = "remote";
|
|
remoteOption.textContent = "Remote URL";
|
|
remoteOption.selected = subscription.type === "remote";
|
|
typeSelect.appendChild(remoteOption);
|
|
|
|
const localOption = document.createElement("option");
|
|
localOption.value = "local";
|
|
localOption.textContent = "Local File";
|
|
localOption.selected = subscription.type === "local";
|
|
typeSelect.appendChild(localOption);
|
|
|
|
// Create input based on type
|
|
let sourceInput: HTMLElement;
|
|
if (subscription.type === "remote") {
|
|
sourceInput = createCardUrlInput("ICS/iCal URL", subscription.url);
|
|
} else {
|
|
const fileInput = createCardInput(
|
|
"text",
|
|
"Local file path (e.g., Calendar.ics)",
|
|
subscription.filePath || ""
|
|
);
|
|
fileInput.setAttribute("placeholder", "Calendar.ics");
|
|
sourceInput = fileInput;
|
|
}
|
|
|
|
const colorInput = createCardInput("color", "", subscription.color);
|
|
const refreshInput = createCardNumberInput(5, 1440, 5, subscription.refreshInterval || 60);
|
|
|
|
// Update handlers
|
|
const updateSubscription = async (updates: Partial<typeof subscription>) => {
|
|
try {
|
|
await plugin.icsSubscriptionService!.updateSubscription(subscription.id, updates);
|
|
save();
|
|
renderICSSubscriptionsList(container, plugin, save);
|
|
} catch (error) {
|
|
console.error("Error updating subscription:", error);
|
|
new Notice(
|
|
translate("settings.integrations.subscriptionsList.notices.updateFailure")
|
|
);
|
|
// Revert changes if needed
|
|
renderICSSubscriptionsList(container, plugin, save);
|
|
}
|
|
};
|
|
|
|
// Update handlers
|
|
enabledToggle.addEventListener("change", () =>
|
|
updateSubscription({ enabled: enabledToggle.checked })
|
|
);
|
|
nameInput.addEventListener("blur", () =>
|
|
updateSubscription({ name: nameInput.value.trim() })
|
|
);
|
|
colorInput.addEventListener("change", () =>
|
|
updateSubscription({ color: colorInput.value })
|
|
);
|
|
refreshInput.addEventListener("blur", () => {
|
|
const minutes = parseInt(refreshInput.value) || 60;
|
|
updateSubscription({ refreshInterval: minutes });
|
|
});
|
|
|
|
// Type change handler - re-render the subscription list to update input type
|
|
typeSelect.addEventListener("change", async () => {
|
|
const newType = typeSelect.value as "remote" | "local";
|
|
|
|
// Update the subscription object
|
|
subscription.type = newType;
|
|
if (newType === "remote") {
|
|
subscription.url = subscription.filePath || ""; // Transfer old local path to url if exists
|
|
subscription.filePath = undefined;
|
|
} else {
|
|
subscription.filePath = subscription.url || ""; // Transfer old url to local path if exists
|
|
subscription.url = undefined;
|
|
}
|
|
save();
|
|
|
|
// Dynamically replace the source input element
|
|
const card = typeSelect.closest(".tasknotes-settings__card");
|
|
if (card) {
|
|
const sourceInputContainer = card.querySelector(
|
|
".tasknotes-settings__card-config-row:nth-child(4)"
|
|
); // Assuming it's the 4th row
|
|
if (sourceInputContainer) {
|
|
const oldSourceInput = sourceInputContainer.querySelector("input");
|
|
if (oldSourceInput) {
|
|
oldSourceInput.remove();
|
|
}
|
|
|
|
let newSourceInput: HTMLElement;
|
|
if (newType === "remote") {
|
|
newSourceInput = createCardUrlInput("ICS/iCal URL", subscription.url);
|
|
} else {
|
|
const fileInput = createCardInput(
|
|
"text",
|
|
"Local file path (e.g., Calendar.ics)",
|
|
subscription.filePath || ""
|
|
);
|
|
fileInput.setAttribute("placeholder", "Calendar.ics");
|
|
newSourceInput = fileInput;
|
|
}
|
|
|
|
// Re-add event listener for the new input
|
|
newSourceInput.addEventListener("blur", () => {
|
|
const value = (newSourceInput as HTMLInputElement).value.trim();
|
|
if (subscription.type === "remote") {
|
|
// Normalize webcal:// and webcals:// URLs to http:// and https://
|
|
const normalizedUrl = normalizeCalendarUrl(value);
|
|
updateSubscription({ url: normalizedUrl });
|
|
} else {
|
|
updateSubscription({ filePath: value });
|
|
}
|
|
});
|
|
|
|
sourceInputContainer.appendChild(newSourceInput);
|
|
|
|
// Update the label for the source input
|
|
const labelElement = sourceInputContainer.querySelector(
|
|
".tasknotes-settings__card-config-label"
|
|
);
|
|
if (labelElement) {
|
|
labelElement.textContent = newType === "remote" ? "URL:" : "File Path:";
|
|
}
|
|
|
|
// Update the secondary text in the header
|
|
const secondaryText = card.querySelector(
|
|
".tasknotes-settings__card-secondary-text"
|
|
);
|
|
if (secondaryText) {
|
|
secondaryText.textContent =
|
|
newType === "remote" ? "Remote Calendar" : "Local File";
|
|
}
|
|
|
|
// Update the type badge
|
|
const typeBadge = card.querySelector(
|
|
".tasknotes-settings__card-meta .info-badge"
|
|
); // Assuming info-badge is the class for type badge
|
|
if (typeBadge) {
|
|
typeBadge.textContent = newType === "remote" ? "Remote" : "Local File";
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Source input handler (URL or file path)
|
|
sourceInput.addEventListener("blur", () => {
|
|
const value = (sourceInput as HTMLInputElement).value.trim();
|
|
if (subscription.type === "remote") {
|
|
// Normalize webcal:// and webcals:// URLs to http:// and https://
|
|
const normalizedUrl = normalizeCalendarUrl(value);
|
|
updateSubscription({ url: normalizedUrl });
|
|
} else {
|
|
updateSubscription({ filePath: value });
|
|
}
|
|
});
|
|
|
|
// Create meta badges
|
|
const statusBadge = createStatusBadge(
|
|
subscription.enabled ? "Enabled" : "Disabled",
|
|
subscription.enabled ? "active" : "inactive"
|
|
);
|
|
|
|
const typeBadge = createInfoBadge(subscription.type === "remote" ? "Remote" : "Local File");
|
|
|
|
const metaBadges = [statusBadge, typeBadge];
|
|
|
|
// Add last sync badge if available
|
|
if (subscription.lastFetched) {
|
|
const lastSyncDate = new Date(subscription.lastFetched);
|
|
const timeAgo = getRelativeTime(lastSyncDate, translate);
|
|
const syncBadge = createInfoBadge(`Synced ${timeAgo}`);
|
|
metaBadges.push(syncBadge);
|
|
}
|
|
|
|
// Add error badge if there's an error
|
|
if (subscription.lastError) {
|
|
const errorBadge = createStatusBadge("Error", "inactive");
|
|
errorBadge.title = subscription.lastError; // Show error on hover
|
|
metaBadges.push(errorBadge);
|
|
}
|
|
|
|
// Build content rows
|
|
const contentRows: { label: string; input: HTMLElement; fullWidth?: boolean }[] = [
|
|
{ label: "Enabled:", input: enabledToggle },
|
|
{ label: "Name:", input: nameInput },
|
|
{ label: "Type:", input: typeSelect },
|
|
{
|
|
label: subscription.type === "remote" ? "URL:" : "File Path:",
|
|
input: sourceInput,
|
|
},
|
|
{ label: "Color:", input: colorInput },
|
|
{ label: "Refresh (min):", input: refreshInput },
|
|
];
|
|
|
|
createCard(container, {
|
|
id: subscription.id,
|
|
collapsible: true,
|
|
defaultCollapsed: true,
|
|
colorIndicator: {
|
|
color: subscription.color,
|
|
},
|
|
header: {
|
|
primaryText: subscription.name,
|
|
secondaryText: subscription.type === "remote" ? "Remote Calendar" : "Local File",
|
|
meta: metaBadges,
|
|
actions: [
|
|
createDeleteHeaderButton(async () => {
|
|
const confirmed = await showConfirmationModal(plugin.app, {
|
|
title: translate(
|
|
"settings.integrations.subscriptionsList.confirmDelete.title"
|
|
),
|
|
message: translate(
|
|
"settings.integrations.subscriptionsList.confirmDelete.message",
|
|
{ name: subscription.name }
|
|
),
|
|
confirmText: translate(
|
|
"settings.integrations.subscriptionsList.confirmDelete.confirmText"
|
|
),
|
|
cancelText: translate("common.cancel"),
|
|
isDestructive: true,
|
|
});
|
|
|
|
if (confirmed) {
|
|
try {
|
|
await plugin.icsSubscriptionService!.removeSubscription(
|
|
subscription.id
|
|
);
|
|
new Notice(
|
|
translate(
|
|
"settings.integrations.subscriptionsList.notices.deleteSuccess",
|
|
{ name: subscription.name }
|
|
)
|
|
);
|
|
save();
|
|
renderICSSubscriptionsList(container, plugin, save);
|
|
} catch (error) {
|
|
console.error("Error deleting subscription:", error);
|
|
new Notice(
|
|
translate(
|
|
"settings.integrations.subscriptionsList.notices.deleteFailure"
|
|
)
|
|
);
|
|
}
|
|
}
|
|
}, "Delete subscription"),
|
|
],
|
|
},
|
|
content: {
|
|
sections: [{ rows: contentRows }],
|
|
},
|
|
actions: {
|
|
buttons: [
|
|
{
|
|
text: translate("settings.integrations.subscriptionsList.refreshNow"),
|
|
icon: "refresh-cw",
|
|
variant: subscription.enabled ? "primary" : "secondary",
|
|
disabled: !subscription.enabled,
|
|
onClick: async () => {
|
|
if (!subscription.enabled) {
|
|
new Notice(
|
|
translate(
|
|
"settings.integrations.subscriptionsList.notices.enableFirst"
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await plugin.icsSubscriptionService!.refreshSubscription(
|
|
subscription.id
|
|
);
|
|
new Notice(
|
|
translate(
|
|
"settings.integrations.subscriptionsList.notices.refreshSuccess",
|
|
{ name: subscription.name }
|
|
)
|
|
);
|
|
// Re-render to show updated sync time
|
|
renderICSSubscriptionsList(container, plugin, save);
|
|
} catch (error) {
|
|
console.error("Error refreshing subscription:", error);
|
|
new Notice(
|
|
translate(
|
|
"settings.integrations.subscriptionsList.notices.refreshFailure"
|
|
)
|
|
);
|
|
}
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
function renderWebhookList(
|
|
container: HTMLElement,
|
|
plugin: TaskNotesPlugin,
|
|
save: () => void
|
|
): void {
|
|
const translate = (key: TranslationKey, params?: Record<string, string | number>) =>
|
|
plugin.i18n.translate(key, params);
|
|
|
|
// Clear existing webhook content
|
|
const existingContainer = container.querySelector(".tasknotes-webhooks-container");
|
|
if (existingContainer) {
|
|
existingContainer.remove();
|
|
}
|
|
|
|
const webhooksContainer = container.createDiv("tasknotes-webhooks-container");
|
|
|
|
if (!plugin.settings.webhooks || plugin.settings.webhooks.length === 0) {
|
|
showCardEmptyState(
|
|
webhooksContainer,
|
|
translate("settings.integrations.webhooks.emptyState.message"),
|
|
translate("settings.integrations.webhooks.emptyState.buttonText"),
|
|
() => {
|
|
// This is a bit of a hack, but it's the easiest way to trigger the add webhook modal
|
|
const addWebhookButton = container
|
|
.closest(".settings-tab-content")
|
|
?.querySelector("button.tn-btn--primary");
|
|
if (addWebhookButton) {
|
|
(addWebhookButton as HTMLElement).click();
|
|
}
|
|
}
|
|
);
|
|
return;
|
|
}
|
|
|
|
plugin.settings.webhooks.forEach((webhook, index) => {
|
|
const statusBadge = createStatusBadge(
|
|
webhook.active ? "Active" : "Inactive",
|
|
webhook.active ? "active" : "inactive"
|
|
);
|
|
|
|
const successBadge = createInfoBadge(`Success: ${webhook.successCount || 0}`);
|
|
const failureBadge = createInfoBadge(`Failed: ${webhook.failureCount || 0}`);
|
|
|
|
// Create inputs for inline editing
|
|
const urlInput = createCardUrlInput("Webhook URL", webhook.url);
|
|
const activeToggle = createCardInput("checkbox");
|
|
activeToggle.checked = webhook.active;
|
|
|
|
// Update handlers
|
|
urlInput.addEventListener("blur", () => {
|
|
if (urlInput.value.trim() !== webhook.url) {
|
|
webhook.url = urlInput.value.trim();
|
|
save();
|
|
new Notice(translate("settings.integrations.webhooks.notices.urlUpdated"));
|
|
}
|
|
});
|
|
|
|
activeToggle.addEventListener("change", () => {
|
|
webhook.active = activeToggle.checked;
|
|
save();
|
|
|
|
// Update the status badge in place instead of re-rendering entire list
|
|
const card = activeToggle.closest(".tasknotes-settings__card");
|
|
if (card) {
|
|
const statusBadge = card.querySelector(
|
|
".tasknotes-settings__card-status-badge--active, .tasknotes-settings__card-status-badge--inactive"
|
|
);
|
|
if (statusBadge) {
|
|
statusBadge.textContent = webhook.active ? "Active" : "Inactive";
|
|
statusBadge.className = webhook.active
|
|
? "tasknotes-settings__card-status-badge tasknotes-settings__card-status-badge--active"
|
|
: "tasknotes-settings__card-status-badge tasknotes-settings__card-status-badge--inactive";
|
|
}
|
|
|
|
// Update test button disabled state
|
|
const testButton = card.querySelector('[aria-label*="Test"]') as HTMLButtonElement;
|
|
if (testButton) {
|
|
testButton.disabled = !webhook.active || !webhook.url;
|
|
}
|
|
}
|
|
|
|
new Notice(
|
|
webhook.active
|
|
? translate("settings.integrations.webhooks.notices.enabled")
|
|
: translate("settings.integrations.webhooks.notices.disabled")
|
|
);
|
|
});
|
|
|
|
// Format webhook creation date
|
|
const createdDate = webhook.createdAt ? new Date(webhook.createdAt) : null;
|
|
const createdText = createdDate
|
|
? translate("settings.integrations.webhooks.statusLabels.created", {
|
|
timeAgo: getRelativeTime(createdDate, translate),
|
|
})
|
|
: "Creation date unknown";
|
|
|
|
// Create events display as a formatted string
|
|
const eventsDisplay = document.createElement("div");
|
|
eventsDisplay.style.display = "flex";
|
|
eventsDisplay.style.flexWrap = "wrap";
|
|
eventsDisplay.style.gap = "0.5rem";
|
|
eventsDisplay.style.alignItems = "center";
|
|
eventsDisplay.style.minHeight = "1.5rem";
|
|
eventsDisplay.style.lineHeight = "1.5rem";
|
|
|
|
if (webhook.events.length === 0) {
|
|
const noEventsSpan = document.createElement("span");
|
|
noEventsSpan.textContent = translate(
|
|
"settings.integrations.webhooks.eventsDisplay.noEvents"
|
|
);
|
|
noEventsSpan.style.color = "var(--text-muted)";
|
|
noEventsSpan.style.fontStyle = "italic";
|
|
noEventsSpan.style.lineHeight = "1.5rem";
|
|
eventsDisplay.appendChild(noEventsSpan);
|
|
} else {
|
|
webhook.events.forEach((event) => {
|
|
const eventBadge = createInfoBadge(event);
|
|
eventBadge.style.marginBottom = "0";
|
|
eventBadge.style.flexShrink = "0";
|
|
eventsDisplay.appendChild(eventBadge);
|
|
});
|
|
}
|
|
|
|
// Create transform file display if exists
|
|
const transformDisplay = webhook.transformFile
|
|
? (() => {
|
|
const span = document.createElement("span");
|
|
span.textContent = webhook.transformFile;
|
|
span.style.fontFamily = "monospace";
|
|
span.style.fontSize = "0.85rem";
|
|
span.style.color = "var(--text-muted)";
|
|
span.style.lineHeight = "1.5rem";
|
|
span.style.padding = "0.25rem 0.5rem";
|
|
span.style.background = "var(--background-modifier-form-field)";
|
|
span.style.borderRadius = "4px";
|
|
span.style.border = "1px solid var(--background-modifier-border)";
|
|
return span;
|
|
})()
|
|
: (() => {
|
|
const span = document.createElement("span");
|
|
span.textContent = translate(
|
|
"settings.integrations.webhooks.transformDisplay.noTransform"
|
|
);
|
|
span.style.color = "var(--text-muted)";
|
|
span.style.fontStyle = "italic";
|
|
span.style.lineHeight = "1.5rem";
|
|
return span;
|
|
})();
|
|
|
|
createCard(webhooksContainer, {
|
|
id: webhook.id,
|
|
collapsible: true,
|
|
defaultCollapsed: true,
|
|
header: {
|
|
primaryText: translate("settings.integrations.webhooks.cardHeader"),
|
|
secondaryText: createdText,
|
|
meta: [statusBadge, successBadge, failureBadge],
|
|
actions: [
|
|
createDeleteHeaderButton(async () => {
|
|
const confirmed = await showConfirmationModal(plugin.app, {
|
|
title: translate("settings.integrations.webhooks.confirmDelete.title"),
|
|
message: translate(
|
|
"settings.integrations.webhooks.confirmDelete.message",
|
|
{ url: webhook.url }
|
|
),
|
|
confirmText: translate(
|
|
"settings.integrations.webhooks.confirmDelete.confirmText"
|
|
),
|
|
cancelText: translate("common.cancel"),
|
|
isDestructive: true,
|
|
});
|
|
|
|
if (confirmed) {
|
|
plugin.settings.webhooks.splice(index, 1);
|
|
save();
|
|
renderWebhookList(container, plugin, save);
|
|
new Notice(translate("settings.integrations.webhooks.notices.deleted"));
|
|
}
|
|
}),
|
|
],
|
|
},
|
|
content: {
|
|
sections: [
|
|
{
|
|
rows: [
|
|
{
|
|
label: translate(
|
|
"settings.integrations.webhooks.cardFields.active"
|
|
),
|
|
input: activeToggle,
|
|
},
|
|
{
|
|
label: translate("settings.integrations.webhooks.cardFields.url"),
|
|
input: urlInput,
|
|
},
|
|
{
|
|
label: translate(
|
|
"settings.integrations.webhooks.cardFields.events"
|
|
),
|
|
input: eventsDisplay,
|
|
},
|
|
{
|
|
label: translate(
|
|
"settings.integrations.webhooks.cardFields.transform"
|
|
),
|
|
input: transformDisplay,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
actions: {
|
|
buttons: [
|
|
{
|
|
text: translate("settings.integrations.webhooks.editEvents"),
|
|
icon: "settings",
|
|
variant: "secondary",
|
|
onClick: async () => {
|
|
const modal = new WebhookEditModal(
|
|
plugin.app,
|
|
webhook,
|
|
async (updatedConfig: Partial<WebhookConfig>) => {
|
|
Object.assign(webhook, updatedConfig);
|
|
save();
|
|
renderWebhookList(container, plugin, save);
|
|
new Notice(
|
|
translate("settings.integrations.webhooks.notices.updated")
|
|
);
|
|
}
|
|
);
|
|
modal.open();
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Generate secure webhook secret
|
|
*/
|
|
function generateWebhookSecret(): string {
|
|
return Array.from(crypto.getRandomValues(new Uint8Array(32)))
|
|
.map((b) => b.toString(16).padStart(2, "0"))
|
|
.join("");
|
|
}
|
|
|
|
/**
|
|
* Modal for displaying webhook secret after creation
|
|
*/
|
|
class SecretNoticeModal extends Modal {
|
|
private secret: string;
|
|
|
|
constructor(app: App, secret: string) {
|
|
super(app);
|
|
this.secret = secret;
|
|
}
|
|
|
|
onOpen(): void {
|
|
const { contentEl } = this;
|
|
contentEl.empty();
|
|
contentEl.addClass("tasknotes-webhook-modal");
|
|
|
|
const notice = contentEl.createDiv({ cls: "tasknotes-webhook-secret-notice" });
|
|
|
|
const title = notice.createDiv({ cls: "tasknotes-webhook-secret-title" });
|
|
const titleIcon = title.createSpan();
|
|
setIcon(titleIcon, "shield-check");
|
|
title.createSpan({ text: "Webhook Secret Generated" });
|
|
|
|
const content = notice.createDiv({ cls: "tasknotes-webhook-secret-content" });
|
|
content.createEl("p", {
|
|
text: "Your webhook secret has been generated. Save this secret as you won't be able to view it again:",
|
|
});
|
|
content.createEl("code", { text: this.secret, cls: "tasknotes-webhook-secret-code" });
|
|
content.createEl("p", {
|
|
text: "Use this secret to verify webhook payloads in your receiving application.",
|
|
});
|
|
|
|
const buttonContainer = contentEl.createDiv({ cls: "tasknotes-webhook-modal-buttons" });
|
|
const closeBtn = buttonContainer.createEl("button", {
|
|
text: "Got it",
|
|
cls: "tasknotes-webhook-modal-btn save",
|
|
});
|
|
closeBtn.onclick = () => this.close();
|
|
}
|
|
|
|
onClose(): void {
|
|
const { contentEl } = this;
|
|
contentEl.empty();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Modal for editing existing webhooks
|
|
*/
|
|
class WebhookEditModal extends Modal {
|
|
private selectedEvents: string[];
|
|
private transformFile: string;
|
|
private corsHeaders: boolean;
|
|
private onSubmit: (config: Partial<WebhookConfig>) => void;
|
|
|
|
constructor(
|
|
app: App,
|
|
webhook: WebhookConfig,
|
|
onSubmit: (config: Partial<WebhookConfig>) => void
|
|
) {
|
|
super(app);
|
|
this.selectedEvents = [...webhook.events];
|
|
this.transformFile = webhook.transformFile || "";
|
|
this.corsHeaders = webhook.corsHeaders ?? true;
|
|
this.onSubmit = onSubmit;
|
|
}
|
|
|
|
onOpen(): void {
|
|
const { contentEl } = this;
|
|
contentEl.empty();
|
|
contentEl.addClass("tasknotes-webhook-modal");
|
|
|
|
// Modal header with icon
|
|
const header = contentEl.createDiv({ cls: "tasknotes-webhook-modal-header" });
|
|
const headerIcon = header.createSpan({ cls: "tasknotes-webhook-modal-icon" });
|
|
setIcon(headerIcon, "webhook");
|
|
header.createEl("h2", { text: "Edit Webhook", cls: "tasknotes-webhook-modal-title" });
|
|
|
|
// Events selection section
|
|
const eventsSection = contentEl.createDiv({ cls: "tasknotes-webhook-modal-section" });
|
|
const eventsHeader = eventsSection.createDiv({
|
|
cls: "tasknotes-webhook-modal-subsection-header",
|
|
});
|
|
const eventsIcon = eventsHeader.createSpan();
|
|
setIcon(eventsIcon, "zap");
|
|
eventsHeader.createEl("h3", { text: "Events to subscribe to" });
|
|
|
|
const eventsGrid = eventsSection.createDiv({ cls: "tasknotes-webhook-events-list" });
|
|
const availableEvents = [
|
|
{ id: "task.created", label: "Task Created", desc: "When new tasks are created" },
|
|
{ id: "task.updated", label: "Task Updated", desc: "When tasks are modified" },
|
|
{
|
|
id: "task.completed",
|
|
label: "Task Completed",
|
|
desc: "When tasks are marked complete",
|
|
},
|
|
{ id: "task.deleted", label: "Task Deleted", desc: "When tasks are deleted" },
|
|
{ id: "task.archived", label: "Task Archived", desc: "When tasks are archived" },
|
|
{ id: "task.unarchived", label: "Task Unarchived", desc: "When tasks are unarchived" },
|
|
{ id: "time.started", label: "Time Started", desc: "When time tracking starts" },
|
|
{ id: "time.stopped", label: "Time Stopped", desc: "When time tracking stops" },
|
|
{
|
|
id: "pomodoro.started",
|
|
label: "Pomodoro Started",
|
|
desc: "When pomodoro sessions begin",
|
|
},
|
|
{
|
|
id: "pomodoro.completed",
|
|
label: "Pomodoro Completed",
|
|
desc: "When pomodoro sessions finish",
|
|
},
|
|
{
|
|
id: "pomodoro.interrupted",
|
|
label: "Pomodoro Interrupted",
|
|
desc: "When pomodoro sessions are stopped",
|
|
},
|
|
{
|
|
id: "recurring.instance.completed",
|
|
label: "Recurring Instance Completed",
|
|
desc: "When recurring task instances complete",
|
|
},
|
|
{
|
|
id: "reminder.triggered",
|
|
label: "Reminder Triggered",
|
|
desc: "When task reminders activate",
|
|
},
|
|
];
|
|
|
|
availableEvents.forEach((event) => {
|
|
new Setting(eventsGrid)
|
|
.setName(event.label)
|
|
.setDesc(event.desc)
|
|
.addToggle((toggle) => {
|
|
toggle.toggleEl.setAttribute(
|
|
"aria-label",
|
|
`Subscribe to ${event.label} events`
|
|
);
|
|
return toggle
|
|
.setValue(this.selectedEvents.includes(event.id))
|
|
.onChange((value) => {
|
|
if (value) {
|
|
this.selectedEvents.push(event.id);
|
|
} else {
|
|
const index = this.selectedEvents.indexOf(event.id);
|
|
if (index > -1) {
|
|
this.selectedEvents.splice(index, 1);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
// Transform file section
|
|
const transformSection = contentEl.createDiv({ cls: "tasknotes-webhook-modal-section" });
|
|
const transformHeader = transformSection.createDiv({
|
|
cls: "tasknotes-webhook-modal-subsection-header",
|
|
});
|
|
const transformIcon = transformHeader.createSpan();
|
|
setIcon(transformIcon, "file-code");
|
|
transformHeader.createEl("h3", { text: "Transform Configuration (Optional)" });
|
|
|
|
new Setting(transformSection)
|
|
.setName("Transform File")
|
|
.setDesc("Path to a .js or .json file in your vault that transforms webhook payloads")
|
|
.addText((text) => {
|
|
text.inputEl.setAttribute("aria-label", "Transform file path");
|
|
return text
|
|
.setPlaceholder("discord-transform.js")
|
|
.setValue(this.transformFile)
|
|
.onChange((value) => {
|
|
this.transformFile = value;
|
|
});
|
|
});
|
|
|
|
// CORS headers section
|
|
const corsSection = contentEl.createDiv({ cls: "tasknotes-webhook-modal-section" });
|
|
const corsHeader = corsSection.createDiv({
|
|
cls: "tasknotes-webhook-modal-subsection-header",
|
|
});
|
|
const corsIcon = corsHeader.createSpan();
|
|
setIcon(corsIcon, "settings");
|
|
corsHeader.createEl("h3", { text: "Headers Configuration" });
|
|
|
|
new Setting(corsSection)
|
|
.setName("Include custom headers")
|
|
.setDesc(
|
|
"Include TaskNotes headers (event type, signature, delivery ID). Turn off for Discord, Slack, and other services with strict CORS policies."
|
|
)
|
|
.addToggle((toggle) => {
|
|
toggle.toggleEl.setAttribute("aria-label", "Include custom headers");
|
|
return toggle.setValue(this.corsHeaders).onChange((value) => {
|
|
this.corsHeaders = value;
|
|
});
|
|
});
|
|
|
|
// Buttons section
|
|
const buttonContainer = contentEl.createDiv({ cls: "tasknotes-webhook-modal-buttons" });
|
|
|
|
const cancelBtn = buttonContainer.createEl("button", {
|
|
text: "Cancel",
|
|
cls: "tasknotes-webhook-modal-btn cancel",
|
|
attr: { "aria-label": "Cancel webhook editing" },
|
|
});
|
|
const cancelIcon = cancelBtn.createSpan({ cls: "tasknotes-webhook-modal-btn-icon" });
|
|
setIcon(cancelIcon, "x");
|
|
cancelBtn.onclick = () => this.close();
|
|
|
|
const saveBtn = buttonContainer.createEl("button", {
|
|
text: "Save Changes",
|
|
cls: "tasknotes-webhook-modal-btn save mod-cta",
|
|
attr: { "aria-label": "Save webhook changes" },
|
|
});
|
|
const saveIcon = saveBtn.createSpan({ cls: "tasknotes-webhook-modal-btn-icon" });
|
|
setIcon(saveIcon, "save");
|
|
|
|
saveBtn.onclick = () => {
|
|
if (this.selectedEvents.length === 0) {
|
|
new Notice("Please select at least one event");
|
|
return;
|
|
}
|
|
|
|
this.onSubmit({
|
|
events: this.selectedEvents as any[],
|
|
transformFile: this.transformFile.trim() || undefined,
|
|
corsHeaders: this.corsHeaders,
|
|
});
|
|
|
|
this.close();
|
|
};
|
|
}
|
|
|
|
onClose(): void {
|
|
const { contentEl } = this;
|
|
contentEl.empty();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Modal for adding/editing webhooks
|
|
*/
|
|
class WebhookModal extends Modal {
|
|
private url = "";
|
|
private selectedEvents: string[] = [];
|
|
private transformFile = "";
|
|
private corsHeaders = true;
|
|
private onSubmit: (config: Partial<WebhookConfig>) => void;
|
|
|
|
constructor(app: App, onSubmit: (config: Partial<WebhookConfig>) => void) {
|
|
super(app);
|
|
this.onSubmit = onSubmit;
|
|
}
|
|
|
|
onOpen(): void {
|
|
const { contentEl } = this;
|
|
contentEl.empty();
|
|
contentEl.addClass("tasknotes-webhook-modal");
|
|
|
|
// Modal header with icon
|
|
const header = contentEl.createDiv({ cls: "tasknotes-webhook-modal-header" });
|
|
const headerIcon = header.createSpan({ cls: "tasknotes-webhook-modal-icon" });
|
|
setIcon(headerIcon, "webhook");
|
|
header.createEl("h2", { text: "Add Webhook", cls: "tasknotes-webhook-modal-title" });
|
|
|
|
// URL input section
|
|
const urlSection = contentEl.createDiv({ cls: "tasknotes-webhook-modal-section" });
|
|
new Setting(urlSection)
|
|
.setName("Webhook URL")
|
|
.setDesc("The endpoint where webhook payloads will be sent")
|
|
.addText((text) => {
|
|
text.inputEl.setAttribute("aria-label", "Webhook URL");
|
|
return text
|
|
.setPlaceholder("https://your-service.com/webhook")
|
|
.setValue(this.url)
|
|
.onChange((value) => {
|
|
this.url = value;
|
|
});
|
|
});
|
|
|
|
// Events selection section
|
|
const eventsSection = contentEl.createDiv({ cls: "tasknotes-webhook-modal-section" });
|
|
const eventsHeader = eventsSection.createDiv({
|
|
cls: "tasknotes-webhook-modal-subsection-header",
|
|
});
|
|
const eventsIcon = eventsHeader.createSpan();
|
|
setIcon(eventsIcon, "zap");
|
|
eventsHeader.createEl("h3", { text: "Events to subscribe to" });
|
|
|
|
const eventsGrid = eventsSection.createDiv({ cls: "tasknotes-webhook-events-list" });
|
|
const availableEvents = [
|
|
{ id: "task.created", label: "Task Created", desc: "When new tasks are created" },
|
|
{ id: "task.updated", label: "Task Updated", desc: "When tasks are modified" },
|
|
{
|
|
id: "task.completed",
|
|
label: "Task Completed",
|
|
desc: "When tasks are marked complete",
|
|
},
|
|
{ id: "task.deleted", label: "Task Deleted", desc: "When tasks are deleted" },
|
|
{ id: "task.archived", label: "Task Archived", desc: "When tasks are archived" },
|
|
{ id: "task.unarchived", label: "Task Unarchived", desc: "When tasks are unarchived" },
|
|
{ id: "time.started", label: "Time Started", desc: "When time tracking starts" },
|
|
{ id: "time.stopped", label: "Time Stopped", desc: "When time tracking stops" },
|
|
{
|
|
id: "pomodoro.started",
|
|
label: "Pomodoro Started",
|
|
desc: "When pomodoro sessions begin",
|
|
},
|
|
{
|
|
id: "pomodoro.completed",
|
|
label: "Pomodoro Completed",
|
|
desc: "When pomodoro sessions finish",
|
|
},
|
|
{
|
|
id: "pomodoro.interrupted",
|
|
label: "Pomodoro Interrupted",
|
|
desc: "When pomodoro sessions are stopped",
|
|
},
|
|
{
|
|
id: "recurring.instance.completed",
|
|
label: "Recurring Instance Completed",
|
|
desc: "When recurring task instances complete",
|
|
},
|
|
{
|
|
id: "reminder.triggered",
|
|
label: "Reminder Triggered",
|
|
desc: "When task reminders activate",
|
|
},
|
|
];
|
|
|
|
availableEvents.forEach((event) => {
|
|
new Setting(eventsGrid)
|
|
.setName(event.label)
|
|
.setDesc(event.desc)
|
|
.addToggle((toggle) => {
|
|
toggle.toggleEl.setAttribute(
|
|
"aria-label",
|
|
`Subscribe to ${event.label} events`
|
|
);
|
|
return toggle
|
|
.setValue(this.selectedEvents.includes(event.id))
|
|
.onChange((value) => {
|
|
if (value) {
|
|
this.selectedEvents.push(event.id);
|
|
} else {
|
|
const index = this.selectedEvents.indexOf(event.id);
|
|
if (index > -1) {
|
|
this.selectedEvents.splice(index, 1);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
// Transform file section
|
|
const transformSection = contentEl.createDiv({ cls: "tasknotes-webhook-modal-section" });
|
|
const transformHeader = transformSection.createDiv({
|
|
cls: "tasknotes-webhook-modal-subsection-header",
|
|
});
|
|
const transformIcon = transformHeader.createSpan();
|
|
setIcon(transformIcon, "file-code");
|
|
transformHeader.createEl("h3", { text: "Transform Configuration (Optional)" });
|
|
|
|
new Setting(transformSection)
|
|
.setName("Transform File")
|
|
.setDesc("Path to a .js or .json file in your vault that transforms webhook payloads")
|
|
.addText((text) => {
|
|
text.inputEl.setAttribute("aria-label", "Transform file path");
|
|
return text
|
|
.setPlaceholder("discord-transform.js")
|
|
.setValue(this.transformFile)
|
|
.onChange((value) => {
|
|
this.transformFile = value;
|
|
});
|
|
});
|
|
|
|
// Transform help section
|
|
const transformHelp = transformSection.createDiv({
|
|
cls: "tasknotes-webhook-transform-help",
|
|
});
|
|
const helpHeader = transformHelp.createDiv({ cls: "tasknotes-webhook-help-header" });
|
|
const helpIcon = helpHeader.createSpan();
|
|
setIcon(helpIcon, "info");
|
|
helpHeader.createSpan({ text: "Transform files allow you to customize webhook payloads:" });
|
|
|
|
const helpList = transformHelp.createEl("ul", { cls: "tasknotes-webhook-help-list" });
|
|
const jsLi = helpList.createEl("li");
|
|
jsLi.createEl("strong", { text: ".js files:" });
|
|
jsLi.appendText(" Custom JavaScript transforms");
|
|
|
|
const jsonLi = helpList.createEl("li");
|
|
jsonLi.createEl("strong", { text: ".json files:" });
|
|
jsonLi.appendText(" Templates with ");
|
|
jsonLi.createEl("code", { text: "${data.task.title}" });
|
|
|
|
const emptyLi = helpList.createEl("li");
|
|
emptyLi.createEl("strong", { text: "Leave empty:" });
|
|
emptyLi.appendText(" Send raw data");
|
|
|
|
const helpExample = transformHelp.createDiv({ cls: "tasknotes-webhook-help-example" });
|
|
helpExample.createEl("strong", { text: "Example:" });
|
|
helpExample.appendText(" ");
|
|
helpExample.createEl("code", { text: "discord-transform.js" });
|
|
|
|
// CORS headers section
|
|
const corsSection = contentEl.createDiv({ cls: "tasknotes-webhook-modal-section" });
|
|
const corsHeader = corsSection.createDiv({
|
|
cls: "tasknotes-webhook-modal-subsection-header",
|
|
});
|
|
const corsIcon = corsHeader.createSpan();
|
|
setIcon(corsIcon, "settings");
|
|
corsHeader.createEl("h3", { text: "Headers Configuration" });
|
|
|
|
new Setting(corsSection)
|
|
.setName("Include custom headers")
|
|
.setDesc(
|
|
"Include TaskNotes headers (event type, signature, delivery ID). Turn off for Discord, Slack, and other services with strict CORS policies."
|
|
)
|
|
.addToggle((toggle) => {
|
|
toggle.toggleEl.setAttribute("aria-label", "Include custom headers");
|
|
return toggle.setValue(this.corsHeaders).onChange((value) => {
|
|
this.corsHeaders = value;
|
|
});
|
|
});
|
|
|
|
// Buttons section
|
|
const buttonContainer = contentEl.createDiv({ cls: "tasknotes-webhook-modal-buttons" });
|
|
|
|
const cancelBtn = buttonContainer.createEl("button", {
|
|
text: "Cancel",
|
|
cls: "tasknotes-webhook-modal-btn cancel",
|
|
attr: { "aria-label": "Cancel webhook creation" },
|
|
});
|
|
const cancelIcon = cancelBtn.createSpan({ cls: "tasknotes-webhook-modal-btn-icon" });
|
|
setIcon(cancelIcon, "x");
|
|
cancelBtn.onclick = () => this.close();
|
|
|
|
const saveBtn = buttonContainer.createEl("button", {
|
|
text: "Add Webhook",
|
|
cls: "tasknotes-webhook-modal-btn save mod-cta",
|
|
attr: { "aria-label": "Create webhook" },
|
|
});
|
|
const saveIcon = saveBtn.createSpan({ cls: "tasknotes-webhook-modal-btn-icon" });
|
|
setIcon(saveIcon, "plus");
|
|
|
|
saveBtn.onclick = () => {
|
|
if (!this.url.trim()) {
|
|
new Notice("Webhook URL is required");
|
|
return;
|
|
}
|
|
|
|
if (this.selectedEvents.length === 0) {
|
|
new Notice("Please select at least one event");
|
|
return;
|
|
}
|
|
|
|
this.onSubmit({
|
|
url: this.url.trim(),
|
|
events: this.selectedEvents as any[],
|
|
transformFile: this.transformFile.trim() || undefined,
|
|
corsHeaders: this.corsHeaders,
|
|
});
|
|
|
|
this.close();
|
|
};
|
|
}
|
|
|
|
onClose(): void {
|
|
const { contentEl } = this;
|
|
contentEl.empty();
|
|
}
|
|
}
|