From 92a129917f19e7956ac547d91c2c15190dc3406e Mon Sep 17 00:00:00 2001 From: devant-cloud-editor Date: Tue, 10 Mar 2026 07:10:50 +0000 Subject: [PATCH 01/18] initial implementation --- .../trello_summary_email/.gitignore | 11 + .../trello_summary_email/Ballerina.toml | 9 + .../trello_summary_email/agents.bal | 0 .../trello_summary_email/automation.bal | 69 +++ .../trello_summary_email/config.bal | 36 ++ .../trello_summary_email/connections.bal | 21 + .../trello_summary_email/data_mappings.bal | 0 .../trello_summary_email/functions.bal | 424 ++++++++++++++++++ .../trello_summary_email/main.bal | 36 ++ .../trello_summary_email/types.bal | 34 ++ 10 files changed, 640 insertions(+) create mode 100644 ballerina-integrator/trello_summary_email/.gitignore create mode 100644 ballerina-integrator/trello_summary_email/Ballerina.toml create mode 100644 ballerina-integrator/trello_summary_email/agents.bal create mode 100644 ballerina-integrator/trello_summary_email/automation.bal create mode 100644 ballerina-integrator/trello_summary_email/config.bal create mode 100644 ballerina-integrator/trello_summary_email/connections.bal create mode 100644 ballerina-integrator/trello_summary_email/data_mappings.bal create mode 100644 ballerina-integrator/trello_summary_email/functions.bal create mode 100644 ballerina-integrator/trello_summary_email/main.bal create mode 100644 ballerina-integrator/trello_summary_email/types.bal diff --git a/ballerina-integrator/trello_summary_email/.gitignore b/ballerina-integrator/trello_summary_email/.gitignore new file mode 100644 index 00000000..3908323d --- /dev/null +++ b/ballerina-integrator/trello_summary_email/.gitignore @@ -0,0 +1,11 @@ +# Ballerina generates this directory during the compilation of a package. +# It contains compiler-generated artifacts and the final executable if this is an application package. +target/ + +# Ballerina maintains the compiler-generated source code here. +# Remove this if you want to commit generated sources. +generated/ + +# Contains configuration values used during development time. +# See https://ballerina.io/learn/provide-values-to-configurable-variables/ for more details. +Config.toml \ No newline at end of file diff --git a/ballerina-integrator/trello_summary_email/Ballerina.toml b/ballerina-integrator/trello_summary_email/Ballerina.toml new file mode 100644 index 00000000..11ddd34f --- /dev/null +++ b/ballerina-integrator/trello_summary_email/Ballerina.toml @@ -0,0 +1,9 @@ +[package] +org = "wso2" +name = "trello_summary_email" +version = "0.1.0" +distribution = "2201.13.1" +title = "trello-summary-email" + +[build-options] +sticky = true \ No newline at end of file diff --git a/ballerina-integrator/trello_summary_email/agents.bal b/ballerina-integrator/trello_summary_email/agents.bal new file mode 100644 index 00000000..e69de29b diff --git a/ballerina-integrator/trello_summary_email/automation.bal b/ballerina-integrator/trello_summary_email/automation.bal new file mode 100644 index 00000000..058cb4ea --- /dev/null +++ b/ballerina-integrator/trello_summary_email/automation.bal @@ -0,0 +1,69 @@ +import ballerina/log; +import ballerina/task; + +// Job to send Trello summary +class TrelloSummaryJob { + + *task:Job; + + public function execute() { + error? result = sendTrelloSummary(); + if result is error { + log:printError("Failed to send Trello summary", 'error = result); + } else { + log:printInfo("Trello summary sent successfully"); + } + } +} + +// Main function to send Trello summary +function sendTrelloSummary() returns error? { + log:printInfo("Starting Trello summary generation..."); + + // Fetch cards + CardSummary[] cards = check fetchTrelloCards(); + log:printInfo(string `Fetched ${cards.length().toString()} cards`); + + if cards.length() == 0 { + log:printInfo("No cards found matching the criteria. Skipping email."); + return; + } + + // Count overdue cards + int overdueCount = countOverdueCards(cards); + + // Group cards + GroupedSummary[] groupedSummaries = groupCards(cards); + log:printInfo(string `Grouped cards into ${groupedSummaries.length().toString()} groups`); + + // Generate email content + string emailContent = generateEmailContent(groupedSummaries, cards.length(), overdueCount); + + // Send email + check sendEmailSummary(emailContent); + log:printInfo("Email sent successfully"); +} + +// Parse cron expression and convert to frequency in seconds +function parseCronToFrequency(string cron) returns decimal|error { + // Simple cron parser for common patterns + // Format: minute hour day month dayOfWeek + string[] parts = re ` `.split(cron); + + if parts.length() != 5 { + return error("Invalid cron expression format"); + } + + string minutePart = parts[0]; + string hourPart = parts[1]; + + // For simplicity, calculate based on daily frequency + // This is a basic implementation - for production use a proper cron library + if minutePart == "*" && hourPart == "*" { + return 3600; // Every hour + } else if minutePart != "*" && hourPart == "*" { + return 3600; // Every hour at specific minute + } else { + return 86400; // Daily + } +} diff --git a/ballerina-integrator/trello_summary_email/config.bal b/ballerina-integrator/trello_summary_email/config.bal new file mode 100644 index 00000000..4c3aec46 --- /dev/null +++ b/ballerina-integrator/trello_summary_email/config.bal @@ -0,0 +1,36 @@ +configurable record { + string key; + string token; + string[] boardIds; + string[] listIds; +} trelloConfig = ?; + +configurable record { + string apiKey; + string serverPrefix; + string listId; + string fromName; + string fromAddress; + string subjectPrefix = "Trello Cards Summary"; +} mailchimpConfig = ?; + +configurable record { + string cron = "0 9 * * 1"; +} scheduleConfig = {}; + +configurable record { + string[] labels = []; + string[] members = []; + boolean includeDueDateFilter = false; + int dueDateDaysAhead = 7; +} filterConfig = {}; + +configurable record { + SummaryGrouping grouping = LIST; + boolean highlightOverdueCards = true; + boolean showCardAge = true; + int staleCardDays = 30; + boolean showAttachmentCount = true; + boolean showChecklistProgress = true; +} summaryConfig = {}; + diff --git a/ballerina-integrator/trello_summary_email/connections.bal b/ballerina-integrator/trello_summary_email/connections.bal new file mode 100644 index 00000000..b84b9b40 --- /dev/null +++ b/ballerina-integrator/trello_summary_email/connections.bal @@ -0,0 +1,21 @@ +import ballerina/http; +import ballerinax/mailchimp; +import ballerinax/trello; + +final trello:Client trelloClient = check new ({ + 'key: trelloConfig.key, + token: trelloConfig.token +}); + +final http:Client trelloHttpClient = check new ("https://api.trello.com/1"); +#Had to use a separate client for Trello API calls as the trello:Client does not support all endpoints needed for fetching card details and attachments. + +final mailchimp:Client mailchimpClient = check new ( + config = { + auth: { + username: "anystring", + password: mailchimpConfig.apiKey + } + }, + serviceUrl = string `https://${mailchimpConfig.serverPrefix}.api.mailchimp.com/3.0` +); diff --git a/ballerina-integrator/trello_summary_email/data_mappings.bal b/ballerina-integrator/trello_summary_email/data_mappings.bal new file mode 100644 index 00000000..e69de29b diff --git a/ballerina-integrator/trello_summary_email/functions.bal b/ballerina-integrator/trello_summary_email/functions.bal new file mode 100644 index 00000000..6b7ef3ad --- /dev/null +++ b/ballerina-integrator/trello_summary_email/functions.bal @@ -0,0 +1,424 @@ +import ballerina/time; +import ballerinax/mailchimp; +import ballerinax/trello; + +function fetchListCardsAsJson(string listId) returns json[]|error { + json cardsJson = check trelloHttpClient->/lists/[listId]/cards.get( + 'key = trelloConfig.key, + token = trelloConfig.token, + fields = "id,name,url,desc,dateLastActivity,due,idMembers,labels,badges" + ); + + return check cardsJson.ensureType(); +} + +// Fetch all cards from specified boards and lists +function fetchTrelloCards() returns CardSummary[]|error { + CardSummary[] allCards = []; + + foreach string boardId in trelloConfig.boardIds { + trello:Board board = check trelloClient->/boards/[boardId].get( + checklists = "none", + cards = "none", + customFields = false, + cardPluginData = false, + memberships = "none", + labels = "none", + tags = false, + boardStars = "none", + lists = "all", + members = "none", + organization = false, + organizationPluginData = false, + pluginData = false, + myPrefs = false, + fields = "name", + actions = "none" + ); + string boardName = board.name ?: "Unknown Board"; + + // Get lists from board - workaround for ambiguous resource access + json boardJson = board.toJson(); + json listsJson = check boardJson.lists; + json[] listsArray = check listsJson.ensureType(); + + foreach json listJson in listsArray { + string listId = check listJson.id; + string listName = check listJson.name; + + // Filter by list IDs if specified + if (trelloConfig.listIds.length() > 0 && trelloConfig.listIds.indexOf(listId) is ()) { + continue; + } + + // Fetch as raw JSON to tolerate Trello payload shape changes. + json[] cardsArray = check fetchListCardsAsJson(listId); + + foreach json cardJson in cardsArray { + CardSummary? cardSummary = check processCardFromJson(cardJson, listName, boardName); + if cardSummary is CardSummary { + allCards.push(cardSummary); + } + } + } + } + + return allCards; +} + +// Process a single card from JSON and apply filters +function processCardFromJson(json cardJson, string listName, string boardName) returns CardSummary?|error { + string cardId = check cardJson.id; + string cardName = check cardJson.name; + string cardUrl = check cardJson.url; + string? cardDesc = check cardJson.desc; + string description = cardDesc is string ? cardDesc : ""; + + // Calculate card age + int cardAgeDays = 0; + boolean isStale = false; + string? dateLastActivity = check cardJson.dateLastActivity; + if dateLastActivity is string { + time:Utc lastActivityUtc = check time:utcFromString(dateLastActivity); + time:Utc currentTime = time:utcNow(); + decimal secondsDiff = time:utcDiffSeconds(currentTime, lastActivityUtc); + cardAgeDays = (secondsDiff / 86400); + isStale = cardAgeDays >= summaryConfig.staleCardDays; + } + + // Get attachment count and checklist progress from badges + int attachmentCount = 0; + int checklistItemsTotal = 0; + int checklistItemsCompleted = 0; + decimal checklistCompletionPercentage = 0.0; + + json? badgesJson = check cardJson.badges; + if badgesJson is json { + int? attachments = check badgesJson.attachments; + int? checkItems = check badgesJson.checkItems; + int? checkItemsChecked = check badgesJson.checkItemsChecked; + + attachmentCount = attachments ?: 0; + checklistItemsTotal = checkItems ?: 0; + checklistItemsCompleted = checkItemsChecked ?: 0; + + if checklistItemsTotal > 0 { + checklistCompletionPercentage = (checklistItemsCompleted / checklistItemsTotal) * 100.0; + } + } + + // Extract labels + string[] labelNames = []; + json? labelsJson = check cardJson.labels; + if labelsJson is json[] { + foreach json labelJson in labelsJson { + string? labelName = check labelJson.name; + if labelName is string && labelName.trim().length() > 0 { + labelNames.push(labelName); + } + } + } + + // Apply label filter + if filterConfig.labels.length() > 0 { + boolean hasMatchingLabel = false; + foreach string filterLabel in filterConfig.labels { + if labelNames.indexOf(filterLabel) !is () { + hasMatchingLabel = true; + break; + } + } + if !hasMatchingLabel { + return (); + } + } + + // Extract member information + string[] memberNames = []; + json? membersJson = check cardJson.idMembers; + if membersJson is json[] { + foreach json memberIdJson in membersJson { + string? memberIdStr = memberIdJson.toString(); + if memberIdStr is string { + trello:InlineResponse2001|error memberInfo = trelloClient->/members/[memberIdStr].get(); + if memberInfo is trello:InlineResponse2001 { + string? fullName = memberInfo?.fullName; + if fullName is string { + memberNames.push(fullName); + } + } + } + } + } + + // Apply member filter + if filterConfig.members.length() > 0 { + boolean hasMatchingMember = false; + foreach string filterMember in filterConfig.members { + if memberNames.indexOf(filterMember) !is () { + hasMatchingMember = true; + break; + } + } + if !hasMatchingMember { + return (); + } + } + + // Parse due date + time:Civil? dueDate = (); + boolean isOverdue = false; + string? dueDateStr = check cardJson.due; + + if dueDateStr is string { + time:Utc dueDateUtc = check time:utcFromString(dueDateStr); + dueDate = time:utcToCivil(dueDateUtc); + + // Check if overdue + time:Utc currentTime = time:utcNow(); + if dueDateUtc < currentTime { + isOverdue = true; + } + + // Apply due date filter + if filterConfig.includeDueDateFilter { + time:Utc futureTime = time:utcAddSeconds(currentTime, filterConfig.dueDateDaysAhead * 24 * 60 * 60); + if dueDateUtc > futureTime { + return (); + } + } + } + + return { + id: cardId, + name: cardName, + listName: listName, + boardName: boardName, + url: cardUrl, + labels: labelNames, + members: memberNames, + dueDate: dueDate, + isOverdue: isOverdue, + description: description, + cardAgeDays: cardAgeDays, + isStale: isStale, + attachmentCount: attachmentCount, + checklistItemsTotal: checklistItemsTotal, + checklistItemsCompleted: checklistItemsCompleted, + checklistCompletionPercentage: checklistCompletionPercentage + }; +} + +// Group cards based on configuration +function groupCards(CardSummary[] cards) returns GroupedSummary[] { + map groupMap = {}; + + foreach CardSummary card in cards { + string[] groupKeys = []; + + match summaryConfig.grouping { + LIST => { + groupKeys.push(card.listName); + } + MEMBER => { + if card.members.length() > 0 { + groupKeys = card.members; + } else { + groupKeys.push("Unassigned"); + } + } + LABEL => { + if card.labels.length() > 0 { + groupKeys = card.labels; + } else { + groupKeys.push("No Labels"); + } + } + } + + foreach string groupKey in groupKeys { + if !groupMap.hasKey(groupKey) { + groupMap[groupKey] = []; + } + CardSummary[] existingCards = groupMap.get(groupKey); + existingCards.push(card); + groupMap[groupKey] = existingCards; + } + } + + GroupedSummary[] groupedSummaries = []; + foreach string groupName in groupMap.keys() { + CardSummary[] groupCards = groupMap.get(groupName); + groupedSummaries.push({ + groupName: groupName, + cards: groupCards + }); + } + + return groupedSummaries; +} + +// Generate HTML email content +function generateEmailContent(GroupedSummary[] groupedSummaries, int totalCards, int overdueCount) returns string { + string html = string ` + + + + + +
+

📋 Trello Cards Summary

+
+ Total Cards: ${totalCards.toString()}
`; + + if summaryConfig.highlightOverdueCards && overdueCount > 0 { + html += string ` ⚠️ Overdue Cards: ${overdueCount.toString()}
`; + } + + html += string ` Grouped By: ${summaryConfig.grouping.toString()}
+ Generated: ${time:utcToString(time:utcNow())} +
`; + + foreach GroupedSummary group in groupedSummaries { + html += string ` +

${group.groupName} (${group.cards.length().toString()} cards)

`; + + foreach CardSummary card in group.cards { + string overdueIndicator = summaryConfig.highlightOverdueCards && card.isOverdue ? " ⚠️ OVERDUE" : ""; + + html += string ` +
+
${card.name}${overdueIndicator}
+
Board: ${card.boardName} | List: ${card.listName}
`; + + if card.dueDate is time:Civil { + time:Civil dueDate = card.dueDate; + string dueDateStr = string `${dueDate.year}-${dueDate.month.toString().padZero(2)}-${dueDate.day.toString().padZero(2)}`; + html += string ` +
Due Date: ${dueDateStr}
`; + } + + if card.labels.length() > 0 { + html += string ` +
Labels: `; + foreach string label in card.labels { + html += string `${label}`; + } + html += "
"; + } + + if card.members.length() > 0 { + html += string ` +
Members: `; + foreach string member in card.members { + html += string `${member}`; + } + html += "
"; + } + + if card.description.trim().length() > 0 { + string truncatedDesc = card.description.length() > 200 ? card.description.substring(0, 200) + "..." : card.description; + html += string ` +
${truncatedDesc}
`; + } + + // Show card age + if summaryConfig.showCardAge { + html += string ` +
Card Age: ${card.cardAgeDays.toString()} days
`; + } + + // Show attachment count + if summaryConfig.showAttachmentCount && card.attachmentCount > 0 { + html += string ` +
Attachments: ${card.attachmentCount.toString()}
`; + } + + // Show checklist progress + if summaryConfig.showChecklistProgress && card.checklistItemsTotal > 0 { + string checklistPercentage = formatPercentageTwoDecimals(card.checklistCompletionPercentage); + html += string ` +
Checklist: ${card.checklistItemsCompleted.toString()}/${card.checklistItemsTotal.toString()} (${checklistPercentage}%)
`; + } + + html += string ` +
`; + } + } + + html += string ` +
+ +`; + + return html; +} + +function formatPercentageTwoDecimals(decimal value) returns string { + decimal rounded = value.round(2); + int wholePart = rounded; + int decimalPart = ((rounded - wholePart) * 100); + + return string `${wholePart.toString()}.${decimalPart.toString().padZero(2)}`; +} + +// Send email with summary using Mailchimp +function sendEmailSummary(string htmlContent) returns error? { + string subject = string `${mailchimpConfig.subjectPrefix} - ${time:utcToString(time:utcNow())}`; + + // Create a campaign + mailchimp:Campaign1 campaign = check mailchimpClient->postCampaigns({ + 'type: "regular", + recipients: { + list_id: mailchimpConfig.listId + }, + settings: { + subject_line: subject, + from_name: mailchimpConfig.fromName, + reply_to: mailchimpConfig.fromAddress, + title: subject + } + }); + + string? campaignId = campaign?.id; + if campaignId is () { + return error("Failed to create campaign: Campaign ID is null"); + } + + // Set campaign content + _ = check mailchimpClient->putCampaignsIdContent( + campaignId = campaignId, + payload = { + html: htmlContent + } + ); + + // Send the campaign + _ = check mailchimpClient->postCampaignsIdActionsSend(campaignId = campaignId); +} + +// Calculate overdue count +function countOverdueCards(CardSummary[] cards) returns int { + int count = 0; + foreach CardSummary card in cards { + if card.isOverdue { + count += 1; + } + } + return count; +} diff --git a/ballerina-integrator/trello_summary_email/main.bal b/ballerina-integrator/trello_summary_email/main.bal new file mode 100644 index 00000000..3d5a68b7 --- /dev/null +++ b/ballerina-integrator/trello_summary_email/main.bal @@ -0,0 +1,36 @@ +import ballerina/log; +import ballerina/task; + +public function main() returns error? { + log:printInfo("Starting Trello Card Summary Automation"); + log:printInfo(string `Schedule: ${scheduleConfig.cron}`); + log:printInfo(string `Grouping: ${summaryConfig.grouping.toString()}`); + log:printInfo(string `Mailchimp List: ${mailchimpConfig.listId}`); + + // Send immediately on startup for testing + log:printInfo("Sending initial summary immediately for testing..."); + error? initialResult = sendTrelloSummary(); + if initialResult is error { + log:printError("Failed to send initial summary", 'error = initialResult); + } else { + log:printInfo("Initial summary sent successfully"); + } + + // Parse cron schedule to frequency + decimal frequency = check parseCronToFrequency(scheduleConfig.cron); + + // Schedule the job + task:JobId jobId = check task:scheduleJobRecurByFrequency( + job = new TrelloSummaryJob(), + interval = frequency + ); + + log:printInfo(string `Job scheduled with ID: ${jobId.id.toString()}`); + log:printInfo("Automation is running. Press Ctrl+C to stop."); + + // Keep the program running + while true { + // Sleep to keep the program alive + // The scheduled job will run in the background + } +} diff --git a/ballerina-integrator/trello_summary_email/types.bal b/ballerina-integrator/trello_summary_email/types.bal new file mode 100644 index 00000000..a703914f --- /dev/null +++ b/ballerina-integrator/trello_summary_email/types.bal @@ -0,0 +1,34 @@ +import ballerina/time; + +// Summary grouping options +public enum SummaryGrouping { + LIST, + MEMBER, + LABEL +} + +// Card summary record +public type CardSummary record {| + string id; + string name; + string listName; + string boardName; + string url; + string[] labels; + string[] members; + time:Civil? dueDate; + boolean isOverdue; + string description; + int cardAgeDays; + boolean isStale; + int attachmentCount; + int checklistItemsTotal; + int checklistItemsCompleted; + decimal checklistCompletionPercentage; +|}; + +// Grouped summary +public type GroupedSummary record {| + string groupName; + CardSummary[] cards; +|}; From 3d2d82238dbd2eeb28d385d6c4119a3221cd10fe Mon Sep 17 00:00:00 2001 From: Anjana Edirisinghe Date: Tue, 10 Mar 2026 12:42:25 +0530 Subject: [PATCH 02/18] directory rename --- .../{trello_summary_email => trello-summary-email}/.gitignore | 0 .../{trello_summary_email => trello-summary-email}/Ballerina.toml | 0 .../{trello_summary_email => trello-summary-email}/agents.bal | 0 .../{trello_summary_email => trello-summary-email}/automation.bal | 0 .../{trello_summary_email => trello-summary-email}/config.bal | 0 .../connections.bal | 0 .../data_mappings.bal | 0 .../{trello_summary_email => trello-summary-email}/functions.bal | 0 .../{trello_summary_email => trello-summary-email}/main.bal | 0 .../{trello_summary_email => trello-summary-email}/types.bal | 0 10 files changed, 0 insertions(+), 0 deletions(-) rename ballerina-integrator/{trello_summary_email => trello-summary-email}/.gitignore (100%) rename ballerina-integrator/{trello_summary_email => trello-summary-email}/Ballerina.toml (100%) rename ballerina-integrator/{trello_summary_email => trello-summary-email}/agents.bal (100%) rename ballerina-integrator/{trello_summary_email => trello-summary-email}/automation.bal (100%) rename ballerina-integrator/{trello_summary_email => trello-summary-email}/config.bal (100%) rename ballerina-integrator/{trello_summary_email => trello-summary-email}/connections.bal (100%) rename ballerina-integrator/{trello_summary_email => trello-summary-email}/data_mappings.bal (100%) rename ballerina-integrator/{trello_summary_email => trello-summary-email}/functions.bal (100%) rename ballerina-integrator/{trello_summary_email => trello-summary-email}/main.bal (100%) rename ballerina-integrator/{trello_summary_email => trello-summary-email}/types.bal (100%) diff --git a/ballerina-integrator/trello_summary_email/.gitignore b/ballerina-integrator/trello-summary-email/.gitignore similarity index 100% rename from ballerina-integrator/trello_summary_email/.gitignore rename to ballerina-integrator/trello-summary-email/.gitignore diff --git a/ballerina-integrator/trello_summary_email/Ballerina.toml b/ballerina-integrator/trello-summary-email/Ballerina.toml similarity index 100% rename from ballerina-integrator/trello_summary_email/Ballerina.toml rename to ballerina-integrator/trello-summary-email/Ballerina.toml diff --git a/ballerina-integrator/trello_summary_email/agents.bal b/ballerina-integrator/trello-summary-email/agents.bal similarity index 100% rename from ballerina-integrator/trello_summary_email/agents.bal rename to ballerina-integrator/trello-summary-email/agents.bal diff --git a/ballerina-integrator/trello_summary_email/automation.bal b/ballerina-integrator/trello-summary-email/automation.bal similarity index 100% rename from ballerina-integrator/trello_summary_email/automation.bal rename to ballerina-integrator/trello-summary-email/automation.bal diff --git a/ballerina-integrator/trello_summary_email/config.bal b/ballerina-integrator/trello-summary-email/config.bal similarity index 100% rename from ballerina-integrator/trello_summary_email/config.bal rename to ballerina-integrator/trello-summary-email/config.bal diff --git a/ballerina-integrator/trello_summary_email/connections.bal b/ballerina-integrator/trello-summary-email/connections.bal similarity index 100% rename from ballerina-integrator/trello_summary_email/connections.bal rename to ballerina-integrator/trello-summary-email/connections.bal diff --git a/ballerina-integrator/trello_summary_email/data_mappings.bal b/ballerina-integrator/trello-summary-email/data_mappings.bal similarity index 100% rename from ballerina-integrator/trello_summary_email/data_mappings.bal rename to ballerina-integrator/trello-summary-email/data_mappings.bal diff --git a/ballerina-integrator/trello_summary_email/functions.bal b/ballerina-integrator/trello-summary-email/functions.bal similarity index 100% rename from ballerina-integrator/trello_summary_email/functions.bal rename to ballerina-integrator/trello-summary-email/functions.bal diff --git a/ballerina-integrator/trello_summary_email/main.bal b/ballerina-integrator/trello-summary-email/main.bal similarity index 100% rename from ballerina-integrator/trello_summary_email/main.bal rename to ballerina-integrator/trello-summary-email/main.bal diff --git a/ballerina-integrator/trello_summary_email/types.bal b/ballerina-integrator/trello-summary-email/types.bal similarity index 100% rename from ballerina-integrator/trello_summary_email/types.bal rename to ballerina-integrator/trello-summary-email/types.bal From fd8c7d35f3d2129fad917519be2e78e6e6a7e187 Mon Sep 17 00:00:00 2001 From: Anjana Edirisinghe Date: Tue, 10 Mar 2026 14:33:39 +0530 Subject: [PATCH 03/18] Added documentation with flowchart and setup guides --- .../trello-summary-email/.choreo/diagram.md | 18 +++ .../.choreo/instructions.md | 56 ++++++++ .../trello-summary-email/README.md | 136 ++++++++++++++++++ .../trello-summary-email/functions.bal | 18 +-- 4 files changed, 219 insertions(+), 9 deletions(-) create mode 100644 ballerina-integrator/trello-summary-email/.choreo/diagram.md create mode 100644 ballerina-integrator/trello-summary-email/.choreo/instructions.md create mode 100644 ballerina-integrator/trello-summary-email/README.md diff --git a/ballerina-integrator/trello-summary-email/.choreo/diagram.md b/ballerina-integrator/trello-summary-email/.choreo/diagram.md new file mode 100644 index 00000000..c508b28a --- /dev/null +++ b/ballerina-integrator/trello-summary-email/.choreo/diagram.md @@ -0,0 +1,18 @@ +flowchart TD + A(["Begin"]):::startNode + B["Fetch Cards from
Trello Boards & Lists"]:::processNode + C{"Are there Cards?"}:::decisionNode + D["Apply Filters
(Labels / Members / Due Date)"]:::processNode + E{"Cards remaining
after filtering?"}:::decisionNode + F["Group Cards
(by List / Member / Label)"]:::processNode + G["Generate HTML
Email Content"]:::processNode + H["Create Mailchimp
Email Campaign"]:::processNode + I["Send Campaign to
Mailchimp Audience"]:::processNode + J(["Complete"]):::endNode + K(["Skip - No Cards"]):::endNode + + A --> B --> C + C -- Yes --> D --> E + C -- No --> K + E -- Yes --> F --> G --> H --> I --> J + E -- No --> K \ No newline at end of file diff --git a/ballerina-integrator/trello-summary-email/.choreo/instructions.md b/ballerina-integrator/trello-summary-email/.choreo/instructions.md new file mode 100644 index 00000000..05731a68 --- /dev/null +++ b/ballerina-integrator/trello-summary-email/.choreo/instructions.md @@ -0,0 +1,56 @@ +## What It Does + +- Fetches cards from specified Trello boards and lists using the Trello API +- Applies optional filters by label, member, or due date range +- Groups cards by **List**, **Member**, or **Label** +- Generates a formatted HTML email summarising all cards, including overdue status, card age, attachments, and checklist progress +- Creates and sends a Mailchimp email campaign to a configured audience list on a cron schedule (default: every Monday at 9:00 AM) + +
+ +Trello Setup Guide + +1. A Trello account with at least one board containing cards +2. Trello API credentials: + - API Key — available from [https://trello.com/app-key](https://trello.com/app-key) + - Token — generate a token from the same page +3. The **Board IDs** of the boards to include + - Open a board in Trello, click **Share**, and copy the short link. The ID is the alphanumeric segment: `https://trello.com/b//...` +4. (Optional) **List IDs** to filter specific lists — leave empty to include all lists on the board + +
+ +
+ +Mailchimp Setup Guide + +1. A Mailchimp account with a configured audience (list) +2. Mailchimp API credentials: + - API Key — found under **Profile → Extras → API Keys** + - Server Prefix — the prefix shown in your Mailchimp URL (e.g., `us21`) + - List ID — found under **Audience → Settings → Audience name and defaults** +3. A configured sender name and reply-to email address + +
+ +
+ +Additional Configurations + +1. `scheduleConfig.cron` + - Cron expression controlling when the summary is sent (default: `0 9 * * 1` — every Monday at 9:00 AM) +2. `filterConfig.labels` + - Filter cards by label name. Leave empty to include all labels. +3. `filterConfig.members` + - Filter cards by member full name. Leave empty to include all members. +4. `filterConfig.includeDueDateFilter` + - Set to `true` to only include cards due within the next `dueDateDaysAhead` days. +5. `summaryConfig.grouping` + - How to group cards in the email. Possible values: + - `LIST` (default) + - `MEMBER` + - `LABEL` +6. `summaryConfig.staleCardDays` + - Cards with no activity for this many days are considered stale (default: `30`). + +
diff --git a/ballerina-integrator/trello-summary-email/README.md b/ballerina-integrator/trello-summary-email/README.md new file mode 100644 index 00000000..86f786e3 --- /dev/null +++ b/ballerina-integrator/trello-summary-email/README.md @@ -0,0 +1,136 @@ +# Trello Summary Email + +## Description + +This integration fetches cards from one or more Trello boards and lists, generates a grouped HTML summary, and sends it as an email campaign through Mailchimp on a configurable schedule. It is designed to give teams a regular digest of active Trello cards, highlighting overdue items, card ages, attachment counts, and checklist progress. + +### What It Does + +- Fetches cards from specified Trello boards and lists using the Trello API +- Applies optional filters by label, member, or due date range +- Groups cards by **List**, **Member**, or **Label** +- Generates a formatted HTML email with: + - Total card count and overdue card count + - Per-card details: board, list, due date, labels, members, description, card age, attachments, and checklist progress +- Creates and sends a Mailchimp email campaign to a configured audience list +- Runs automatically on a configurable cron schedule (default: every Monday at 9:00 AM) + +## Prerequisites + +Before running this integration, you need: + +### Trello Setup + +1. A Trello account with at least one board containing cards +2. Trello API credentials: + - **API Key** – available from [https://trello.com/app-key](https://trello.com/app-key) + - **Token** – generate a token from the same page +3. The **Board IDs** of the boards you want to include + - Open a board in Trello, click **Share**, and copy the short link. The ID is the alphanumeric part (e.g., `https://trello.com/b//...`) +4. (Optional) The **List IDs** of specific lists to filter — leave empty to include all lists on the board + +### Mailchimp Setup + +1. A Mailchimp account with a configured audience (list) +2. Mailchimp API credentials: + - **API Key** – found under **Profile → Extras → API Keys** + - **Server Prefix** – the prefix shown in your Mailchimp URL (e.g., `us21`) + - **List ID** – the audience to send the campaign to (found under **Audience → Settings → Audience name and defaults**) +3. A configured sender name and email address for the campaign + +## Configuration + +Create a `Config.toml` file in the project root with the following values: + +```toml +[trelloConfig] +key = "" +token = "" +boardIds = ["", ""] +listIds = [] # Leave empty to include all lists + +[mailchimpConfig] +apiKey = "" +serverPrefix = "" # e.g., "us21" +listId = "" +fromName = "" +fromAddress = "" +subjectPrefix = "Trello Cards Summary" # Optional, has default + +[scheduleConfig] +cron = "0 9 * * 1" # Every Monday at 9:00 AM (default) + +[filterConfig] +labels = [] # Filter by label names; empty means no filter +members = [] # Filter by member full names; empty means no filter +includeDueDateFilter = false # Set to true to only include cards due within dueDateDaysAhead +dueDateDaysAhead = 7 + +[summaryConfig] +grouping = "LIST" # Group by: "LIST", "MEMBER", or "LABEL" +highlightOverdueCards = true +showCardAge = true +staleCardDays = 30 # Cards inactive for this many days are considered stale +showAttachmentCount = true +showChecklistProgress = true +``` + +### Configuration Reference + +#### `trelloConfig` + +| Field | Description | +|---|---| +| `key` | Your Trello API key | +| `token` | Your Trello API token | +| `boardIds` | List of Trello board IDs to fetch cards from | +| `listIds` | List of specific list IDs to include; leave empty for all lists | + +#### `mailchimpConfig` + +| Field | Description | +|---|---| +| `apiKey` | Your Mailchimp API key | +| `serverPrefix` | Mailchimp data center prefix (e.g., `us21`) | +| `listId` | Mailchimp audience list ID to send the campaign to | +| `fromName` | Sender display name for the email campaign | +| `fromAddress` | Sender reply-to email address | +| `subjectPrefix` | Prefix for the email subject line (default: `Trello Cards Summary`) | + +#### `scheduleConfig` + +| Field | Description | +|---|---| +| `cron` | Cron expression for the schedule (default: `0 9 * * 1` — Mondays at 9 AM) | + +#### `filterConfig` + +| Field | Description | +|---|---| +| `labels` | Only include cards matching these label names; empty means all labels | +| `members` | Only include cards assigned to these members (by full name); empty means all members | +| `includeDueDateFilter` | If `true`, only include cards due within the next `dueDateDaysAhead` days | +| `dueDateDaysAhead` | Number of days ahead to use for the due date filter (default: `7`) | + +#### `summaryConfig` + +| Field | Description | +|---|---| +| `grouping` | How to group cards in the email: `LIST`, `MEMBER`, or `LABEL` | +| `highlightOverdueCards` | If `true`, overdue cards are flagged in the email | +| `showCardAge` | If `true`, each card shows the number of days since last activity | +| `staleCardDays` | Cards with no activity for this many days are considered stale (default: `30`) | +| `showAttachmentCount` | If `true`, shows the attachment count on each card | +| `showChecklistProgress` | If `true`, shows checklist completion progress on each card | + +## Deploying on Devant + +1. Sign in to your Devant account. +2. Create a new Integration and follow the instructions in the [Devant Documentation](https://wso2.com/devant/docs/references/import-a-repository/) to import this repository. +3. Select the **Technology** as `WSO2 Integrator: BI`. +4. Choose the **Integration** type as `Automation` and click **Create**. +5. Once the build is successful, click **Configure to Continue** and set up the required environment variables for Trello and Mailchimp credentials. +6. Click **Schedule** to schedule the automation. +7. In the **BY INTERVAL** tab, configure the desired schedule (e.g., weekly on Monday mornings). +8. Click **Update**. +9. Once tested, you may promote the integration to production. Make sure to set the relevant environment variables in the production environment as well. diff --git a/ballerina-integrator/trello-summary-email/functions.bal b/ballerina-integrator/trello-summary-email/functions.bal index 6b7ef3ad..44c21143 100644 --- a/ballerina-integrator/trello-summary-email/functions.bal +++ b/ballerina-integrator/trello-summary-email/functions.bal @@ -17,7 +17,7 @@ function fetchTrelloCards() returns CardSummary[]|error { CardSummary[] allCards = []; foreach string boardId in trelloConfig.boardIds { - trello:Board board = check trelloClient->/boards/[boardId].get( + trello:Board board = check trelloClient->/boards/[boardId].get( checklists = "none", cards = "none", customFields = false, @@ -248,10 +248,10 @@ function groupCards(CardSummary[] cards) returns GroupedSummary[] { GroupedSummary[] groupedSummaries = []; foreach string groupName in groupMap.keys() { - CardSummary[] groupCards = groupMap.get(groupName); + CardSummary[] groupCardsList = groupMap.get(groupName); groupedSummaries.push({ groupName: groupName, - cards: groupCards + cards: groupCardsList }); } @@ -282,12 +282,12 @@ function generateEmailContent(GroupedSummary[] groupedSummaries, int totalCards,
-

📋 Trello Cards Summary

+

Trello Cards Summary

Total Cards: ${totalCards.toString()}
`; if summaryConfig.highlightOverdueCards && overdueCount > 0 { - html += string ` ⚠️ Overdue Cards: ${overdueCount.toString()}
`; + html += string ` Overdue Cards: ${overdueCount.toString()}
`; } html += string ` Grouped By: ${summaryConfig.grouping.toString()}
@@ -299,7 +299,7 @@ function generateEmailContent(GroupedSummary[] groupedSummaries, int totalCards,

${group.groupName} (${group.cards.length().toString()} cards)

`; foreach CardSummary card in group.cards { - string overdueIndicator = summaryConfig.highlightOverdueCards && card.isOverdue ? " ⚠️ OVERDUE" : ""; + string overdueIndicator = summaryConfig.highlightOverdueCards && card.isOverdue ? " OVERDUE" : ""; html += string `
@@ -338,19 +338,19 @@ function generateEmailContent(GroupedSummary[] groupedSummaries, int totalCards, } // Show card age - if summaryConfig.showCardAge { + if summaryConfig.showCardAge { html += string `
Card Age: ${card.cardAgeDays.toString()} days
`; } // Show attachment count - if summaryConfig.showAttachmentCount && card.attachmentCount > 0 { + if summaryConfig.showAttachmentCount && card.attachmentCount > 0 { html += string `
Attachments: ${card.attachmentCount.toString()}
`; } // Show checklist progress - if summaryConfig.showChecklistProgress && card.checklistItemsTotal > 0 { + if summaryConfig.showChecklistProgress && card.checklistItemsTotal > 0 { string checklistPercentage = formatPercentageTwoDecimals(card.checklistCompletionPercentage); html += string `
Checklist: ${card.checklistItemsCompleted.toString()}/${card.checklistItemsTotal.toString()} (${checklistPercentage}%)
`; From 831b1f9717b10e4e374999c8805763ea560e4fef Mon Sep 17 00:00:00 2001 From: Anjana Edirisinghe Date: Tue, 10 Mar 2026 14:44:08 +0530 Subject: [PATCH 04/18] config schema added --- .../.choreo/config-schema.json | 695 ++++++++++++++++++ .../trello-summary-email/.gitignore | 3 +- 2 files changed, 697 insertions(+), 1 deletion(-) create mode 100644 ballerina-integrator/trello-summary-email/.choreo/config-schema.json diff --git a/ballerina-integrator/trello-summary-email/.choreo/config-schema.json b/ballerina-integrator/trello-summary-email/.choreo/config-schema.json new file mode 100644 index 00000000..8878a926 --- /dev/null +++ b/ballerina-integrator/trello-summary-email/.choreo/config-schema.json @@ -0,0 +1,695 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "ballerina": { + "type": "object", + "properties": { + "http": { + "type": "object", + "properties": { + "maxActiveConnections": { + "type": "integer", + "description": "" + }, + "maxIdleConnections": { + "type": "integer", + "description": "" + }, + "waitTime": { + "type": "number", + "description": "" + }, + "maxActiveStreamsPerConnection": { + "type": "integer", + "description": "" + }, + "minEvictableIdleTime": { + "type": "number", + "description": "" + }, + "timeBetweenEvictionRuns": { + "type": "number", + "description": "" + }, + "minIdleTimeInStaleState": { + "type": "number", + "description": "" + }, + "timeBetweenStaleEviction": { + "type": "number", + "description": "" + }, + "defaultListenerPort": { + "type": "integer", + "description": "" + }, + "defaultListenerConfig": { + "type": "object", + "properties": { + "host": { + "type": "string" + }, + "http1Settings": { + "type": "object", + "properties": { + "keepAlive": { + "enum": [ + "AUTO", + "ALWAYS", + "NEVER" + ] + }, + "maxPipelinedRequests": { + "type": "integer" + } + }, + "additionalProperties": false, + "required": [ + "keepAlive", + "maxPipelinedRequests" + ], + "name": "ballerina/http:2.15.0:ListenerHttp1Settings" + }, + "secureSocket": { + "anyOf": [ + { + "type": "object", + "properties": { + "key": { + "anyOf": [ + { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "password": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "path", + "password" + ], + "name": "ballerina/crypto:2.9.2:KeyStore" + }, + { + "type": "object", + "properties": { + "certFile": { + "type": "string" + }, + "keyFile": { + "type": "string" + }, + "keyPassword": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "certFile", + "keyFile" + ], + "name": "ballerina/http:2.15.0:CertKey" + } + ] + }, + "mutualSsl": { + "type": "object", + "properties": { + "verifyClient": { + "enum": [ + "OPTIONAL", + "REQUIRE" + ] + }, + "cert": { + "anyOf": [ + { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "password": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "path", + "password" + ], + "name": "ballerina/crypto:2.9.2:TrustStore" + }, + { + "type": "string" + } + ] + } + }, + "additionalProperties": false, + "required": [ + "verifyClient", + "cert" + ] + }, + "protocol": { + "type": "object", + "properties": { + "name": { + "enum": [ + "DTLS", + "TLS", + "SSL" + ] + }, + "versions": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false, + "required": [ + "name", + "versions" + ] + }, + "certValidation": { + "type": "object", + "properties": { + "type": { + "enum": [ + "OCSP_STAPLING", + "OCSP_CRL" + ] + }, + "cacheSize": { + "type": "integer" + }, + "cacheValidityPeriod": { + "type": "integer" + } + }, + "additionalProperties": false, + "required": [ + "type", + "cacheSize", + "cacheValidityPeriod" + ] + }, + "ciphers": { + "type": "array", + "items": { + "type": "string" + } + }, + "shareSession": { + "type": "boolean" + }, + "handshakeTimeout": { + "type": "number" + }, + "sessionTimeout": { + "type": "number" + } + }, + "additionalProperties": false, + "required": [ + "key", + "ciphers", + "shareSession" + ], + "name": "ballerina/http:2.15.0:ListenerSecureSocket" + }, + { + "type": "" + } + ] + }, + "httpVersion": { + "enum": [ + "2.0", + "1.1", + "1.0" + ] + }, + "timeout": { + "type": "number" + }, + "server": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "" + } + ] + }, + "requestLimits": { + "type": "object", + "properties": { + "maxUriLength": { + "type": "integer" + }, + "maxHeaderSize": { + "type": "integer" + }, + "maxEntityBodySize": { + "type": "integer" + } + }, + "additionalProperties": false, + "required": [ + "maxUriLength", + "maxHeaderSize", + "maxEntityBodySize" + ], + "name": "ballerina/http:2.15.0:RequestLimitConfigs" + }, + "gracefulStopTimeout": { + "type": "number" + }, + "socketConfig": { + "type": "object", + "properties": { + "soBackLog": { + "type": "integer" + }, + "connectTimeOut": { + "type": "number" + }, + "receiveBufferSize": { + "type": "integer" + }, + "sendBufferSize": { + "type": "integer" + }, + "tcpNoDelay": { + "type": "boolean" + }, + "socketReuse": { + "type": "boolean" + }, + "keepAlive": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + "soBackLog", + "connectTimeOut", + "receiveBufferSize", + "sendBufferSize", + "tcpNoDelay", + "socketReuse", + "keepAlive" + ], + "name": "ballerina/http:2.15.0:ServerSocketConfig" + }, + "http2InitialWindowSize": { + "type": "integer" + }, + "minIdleTimeInStaleState": { + "type": "number" + }, + "timeBetweenStaleEviction": { + "type": "number" + } + }, + "additionalProperties": false, + "required": [ + "host", + "http1Settings", + "secureSocket", + "httpVersion", + "timeout", + "server", + "requestLimits", + "gracefulStopTimeout", + "socketConfig", + "http2InitialWindowSize", + "minIdleTimeInStaleState", + "timeBetweenStaleEviction" + ], + "name": "ballerina/http:2.15.0:ListenerConfiguration", + "description": "" + }, + "traceLogConsole": { + "type": "boolean", + "description": "" + }, + "traceLogAdvancedConfig": { + "type": "object", + "properties": { + "console": { + "type": "boolean" + }, + "path": { + "type": "string" + }, + "host": { + "type": "string" + }, + "port": { + "type": "integer" + } + }, + "additionalProperties": false, + "required": [ + "console" + ], + "name": "ballerina/http:2.15.0:TraceLogAdvancedConfiguration", + "description": "" + }, + "accessLogConfig": { + "type": "object", + "properties": { + "console": { + "type": "boolean" + }, + "format": { + "type": "string" + }, + "attributes": { + "type": "array", + "items": { + "type": "string" + } + }, + "path": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "console", + "format" + ], + "name": "ballerina/http:2.15.0:AccessLogConfiguration", + "description": "" + } + }, + "additionalProperties": false + }, + "task": { + "type": "object", + "properties": { + "globalSchedulerWorkerCount": { + "type": "integer", + "description": "" + }, + "globalSchedulerWaitingTime": { + "type": "number", + "description": "" + } + }, + "additionalProperties": false + }, + "log": { + "type": "object", + "properties": { + "destinations": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "enum": [ + "stderr", + "stdout" + ] + } + }, + "additionalProperties": false, + "required": [ + "type" + ], + "name": "ballerina/log:2.14.0:StandardDestination" + }, + { + "type": "object", + "properties": { + "type": { + "enum": [ + "file" + ] + }, + "path": { + "type": "string" + }, + "mode": { + "enum": [ + "APPEND", + "TRUNCATE" + ] + } + }, + "additionalProperties": false, + "required": [ + "type", + "path", + "mode" + ], + "name": "ballerina/log:2.14.0:FileOutputDestination" + } + ] + }, + "description": "" + }, + "modules": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "level": { + "enum": [ + "WARN", + "INFO", + "ERROR", + "DEBUG" + ] + } + }, + "additionalProperties": false, + "required": [ + "name", + "level" + ], + "name": "ballerina/log:2.14.0:Module" + }, + "description": "" + }, + "keyValues": { + "type": "object", + "properties": { + "msg": {}, + "error": {}, + "stackTrace": {}, + "module": {} + }, + "additionalProperties": false, + "name": "ballerina/log:2.14.0:AnydataKeyValues", + "description": "" + }, + "format": { + "enum": [ + "logfmt", + "json" + ], + "description": "" + }, + "level": { + "enum": [ + "WARN", + "INFO", + "ERROR", + "DEBUG" + ], + "description": "" + }, + "enableSensitiveDataMasking": { + "type": "boolean", + "description": "" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "wso2": { + "type": "object", + "properties": { + "trello_summary_email": { + "type": "object", + "properties": { + "trelloConfig": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "token": { + "type": "string" + }, + "boardIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "listIds": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false, + "required": [ + "key", + "token", + "boardIds", + "listIds" + ], + "description": "" + }, + "mailchimpConfig": { + "type": "object", + "properties": { + "apiKey": { + "type": "string" + }, + "serverPrefix": { + "type": "string" + }, + "listId": { + "type": "string" + }, + "fromName": { + "type": "string" + }, + "fromAddress": { + "type": "string" + }, + "subjectPrefix": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "apiKey", + "serverPrefix", + "listId", + "fromName", + "fromAddress", + "subjectPrefix" + ], + "description": "" + }, + "scheduleConfig": { + "type": "object", + "properties": { + "cron": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "cron" + ], + "description": "" + }, + "filterConfig": { + "type": "object", + "properties": { + "labels": { + "type": "array", + "items": { + "type": "string" + } + }, + "members": { + "type": "array", + "items": { + "type": "string" + } + }, + "includeDueDateFilter": { + "type": "boolean" + }, + "dueDateDaysAhead": { + "type": "integer" + } + }, + "additionalProperties": false, + "required": [ + "labels", + "members", + "includeDueDateFilter", + "dueDateDaysAhead" + ], + "description": "" + }, + "summaryConfig": { + "type": "object", + "properties": { + "grouping": { + "enum": [ + "LABEL", + "MEMBER", + "LIST" + ] + }, + "highlightOverdueCards": { + "type": "boolean" + }, + "showCardAge": { + "type": "boolean" + }, + "staleCardDays": { + "type": "integer" + }, + "showAttachmentCount": { + "type": "boolean" + }, + "showChecklistProgress": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + "grouping", + "highlightOverdueCards", + "showCardAge", + "staleCardDays", + "showAttachmentCount", + "showChecklistProgress" + ], + "description": "" + } + }, + "additionalProperties": false, + "required": [ + "trelloConfig", + "mailchimpConfig" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false +} diff --git a/ballerina-integrator/trello-summary-email/.gitignore b/ballerina-integrator/trello-summary-email/.gitignore index 3908323d..31794861 100644 --- a/ballerina-integrator/trello-summary-email/.gitignore +++ b/ballerina-integrator/trello-summary-email/.gitignore @@ -8,4 +8,5 @@ generated/ # Contains configuration values used during development time. # See https://ballerina.io/learn/provide-values-to-configurable-variables/ for more details. -Config.toml \ No newline at end of file +Config.toml +Dependencies.toml \ No newline at end of file From 1ba2a95f157e6d92c236ecc6f7fe1eebcec7dc9a Mon Sep 17 00:00:00 2001 From: Anjana Edirisinghe Date: Tue, 10 Mar 2026 15:14:04 +0530 Subject: [PATCH 05/18] added email html templated and updated documentation for clarity --- .../trello-summary-email/.choreo/diagram.md | 3 +- .../.choreo/instructions.md | 18 +- .../trello-summary-email/README.md | 10 +- .../trello-summary-email/functions.bal | 276 +++++++++++------- 4 files changed, 192 insertions(+), 115 deletions(-) diff --git a/ballerina-integrator/trello-summary-email/.choreo/diagram.md b/ballerina-integrator/trello-summary-email/.choreo/diagram.md index c508b28a..73952b57 100644 --- a/ballerina-integrator/trello-summary-email/.choreo/diagram.md +++ b/ballerina-integrator/trello-summary-email/.choreo/diagram.md @@ -1,4 +1,3 @@ -flowchart TD A(["Begin"]):::startNode B["Fetch Cards from
Trello Boards & Lists"]:::processNode C{"Are there Cards?"}:::decisionNode @@ -15,4 +14,4 @@ flowchart TD C -- Yes --> D --> E C -- No --> K E -- Yes --> F --> G --> H --> I --> J - E -- No --> K \ No newline at end of file + E -- No --> K diff --git a/ballerina-integrator/trello-summary-email/.choreo/instructions.md b/ballerina-integrator/trello-summary-email/.choreo/instructions.md index 05731a68..9add3403 100644 --- a/ballerina-integrator/trello-summary-email/.choreo/instructions.md +++ b/ballerina-integrator/trello-summary-email/.choreo/instructions.md @@ -1,9 +1,9 @@ ## What It Does -- Fetches cards from specified Trello boards and lists using the Trello API +- Fetches cards from specified Trello boards and lists - Applies optional filters by label, member, or due date range - Groups cards by **List**, **Member**, or **Label** -- Generates a formatted HTML email summarising all cards, including overdue status, card age, attachments, and checklist progress +- Generates a formatted email summarising all cards, including overdue status, card age, attachments, and checklist progress - Creates and sends a Mailchimp email campaign to a configured audience list on a cron schedule (default: every Monday at 9:00 AM)
@@ -12,11 +12,11 @@ 1. A Trello account with at least one board containing cards 2. Trello API credentials: - - API Key — available from [https://trello.com/app-key](https://trello.com/app-key) - - Token — generate a token from the same page + - API Key - available from [https://trello.com/app-key](https://trello.com/app-key) + - Token - generate a token from the same page 3. The **Board IDs** of the boards to include - Open a board in Trello, click **Share**, and copy the short link. The ID is the alphanumeric segment: `https://trello.com/b//...` -4. (Optional) **List IDs** to filter specific lists — leave empty to include all lists on the board +4. (Optional) **List IDs** to filter specific lists - leave empty to include all lists on the board
@@ -26,9 +26,9 @@ 1. A Mailchimp account with a configured audience (list) 2. Mailchimp API credentials: - - API Key — found under **Profile → Extras → API Keys** - - Server Prefix — the prefix shown in your Mailchimp URL (e.g., `us21`) - - List ID — found under **Audience → Settings → Audience name and defaults** + - API Key - found under **Profile → Extras → API Keys** + - Server Prefix - the prefix shown in your Mailchimp URL (e.g., `us21`) + - List ID - found under **Audience → Settings → Audience name and defaults** 3. A configured sender name and reply-to email address @@ -38,7 +38,7 @@ Additional Configurations 1. `scheduleConfig.cron` - - Cron expression controlling when the summary is sent (default: `0 9 * * 1` — every Monday at 9:00 AM) + - Cron expression controlling when the summary is sent (default: `0 9 * * 1` - every Monday at 9:00 AM) 2. `filterConfig.labels` - Filter cards by label name. Leave empty to include all labels. 3. `filterConfig.members` diff --git a/ballerina-integrator/trello-summary-email/README.md b/ballerina-integrator/trello-summary-email/README.md index 86f786e3..6062aace 100644 --- a/ballerina-integrator/trello-summary-email/README.md +++ b/ballerina-integrator/trello-summary-email/README.md @@ -1,15 +1,15 @@ -# Trello Summary Email +# Trello Summary Email Integration ## Description -This integration fetches cards from one or more Trello boards and lists, generates a grouped HTML summary, and sends it as an email campaign through Mailchimp on a configurable schedule. It is designed to give teams a regular digest of active Trello cards, highlighting overdue items, card ages, attachment counts, and checklist progress. +This integration fetches cards from one or more Trello boards and lists, generates a grouped summary, and sends it as an email campaign through Mailchimp on a configurable schedule. It is designed to give teams a regular digest of active Trello cards, highlighting overdue items, card ages, attachment counts, and checklist progress. ### What It Does -- Fetches cards from specified Trello boards and lists using the Trello API +- Fetches cards from specified Trello boards and lists - Applies optional filters by label, member, or due date range - Groups cards by **List**, **Member**, or **Label** -- Generates a formatted HTML email with: +- Generates a formatted email with: - Total card count and overdue card count - Per-card details: board, list, due date, labels, members, description, card age, attachments, and checklist progress - Creates and sends a Mailchimp email campaign to a configured audience list @@ -27,7 +27,7 @@ Before running this integration, you need: - **Token** – generate a token from the same page 3. The **Board IDs** of the boards you want to include - Open a board in Trello, click **Share**, and copy the short link. The ID is the alphanumeric part (e.g., `https://trello.com/b//...`) -4. (Optional) The **List IDs** of specific lists to filter — leave empty to include all lists on the board +4. (Optional) The **List IDs** of specific lists to filter - leave empty to include all lists on the board ### Mailchimp Setup diff --git a/ballerina-integrator/trello-summary-email/functions.bal b/ballerina-integrator/trello-summary-email/functions.bal index 44c21143..a2cbe5d9 100644 --- a/ballerina-integrator/trello-summary-email/functions.bal +++ b/ballerina-integrator/trello-summary-email/functions.bal @@ -258,115 +258,94 @@ function groupCards(CardSummary[] cards) returns GroupedSummary[] { return groupedSummaries; } -// Generate HTML email content -function generateEmailContent(GroupedSummary[] groupedSummaries, int totalCards, int overdueCount) returns string { - string html = string ` - - - - - -
-

Trello Cards Summary

-
- Total Cards: ${totalCards.toString()}
`; - - if summaryConfig.highlightOverdueCards && overdueCount > 0 { - html += string ` Overdue Cards: ${overdueCount.toString()}
`; - } - - html += string ` Grouped By: ${summaryConfig.grouping.toString()}
- Generated: ${time:utcToString(time:utcNow())} -
`; - - foreach GroupedSummary group in groupedSummaries { - html += string ` -

${group.groupName} (${group.cards.length().toString()} cards)

`; - - foreach CardSummary card in group.cards { - string overdueIndicator = summaryConfig.highlightOverdueCards && card.isOverdue ? " OVERDUE" : ""; - - html += string ` -
-
${card.name}${overdueIndicator}
-
Board: ${card.boardName} | List: ${card.listName}
`; - - if card.dueDate is time:Civil { - time:Civil dueDate = card.dueDate; - string dueDateStr = string `${dueDate.year}-${dueDate.month.toString().padZero(2)}-${dueDate.day.toString().padZero(2)}`; - html += string ` -
Due Date: ${dueDateStr}
`; - } +// Format a UTC timestamp as a human-readable string +function getFormattedTimestamp(time:Utc utcTime) returns string { + time:Civil civil = time:utcToCivil(utcTime); + return string `${civil.year}-${civil.month.toString().padZero(2)}-${civil.day.toString().padZero(2)} ${civil.hour.toString().padZero(2)}:${civil.minute.toString().padZero(2)} UTC`; +} - if card.labels.length() > 0 { - html += string ` -
Labels: `; - foreach string label in card.labels { - html += string `${label}`; - } - html += "
"; - } +// Render a single group of cards as HTML table rows +function getHtmlFormattedGroup(GroupedSummary group) returns string { + string cardsHtml = ""; + foreach CardSummary card in group.cards { + string leftBorder = card.isOverdue + ? " style=\"border-left: 3px solid #de350b; padding: 16px 30px; border-bottom: 1px solid #e1e4e8;\"" + : (card.isStale + ? " style=\"border-left: 3px solid #ff8b00; padding: 16px 30px; border-bottom: 1px solid #e1e4e8;\"" + : " style=\"padding: 16px 30px; border-bottom: 1px solid #e1e4e8;\""); + + string dueDateHtml = ""; + time:Civil? dueDate = card.dueDate; + if dueDate is time:Civil { + string dueDateStr = string `${dueDate.year}-${dueDate.month.toString().padZero(2)}-${dueDate.day.toString().padZero(2)}`; + string dueDateColor = card.isOverdue ? "#de350b" : "#586069"; + dueDateHtml = string `Due: ${dueDateStr}  `; + } - if card.members.length() > 0 { - html += string ` -
Members: `; - foreach string member in card.members { - html += string `${member}`; - } - html += "
"; - } + string labelsHtml = ""; + foreach string label in card.labels { + labelsHtml += string `${label}`; + } - if card.description.trim().length() > 0 { - string truncatedDesc = card.description.length() > 200 ? card.description.substring(0, 200) + "..." : card.description; - html += string ` -
${truncatedDesc}
`; - } + string membersHtml = ""; + foreach string member in card.members { + membersHtml += member + " "; + } - // Show card age - if summaryConfig.showCardAge { - html += string ` -
Card Age: ${card.cardAgeDays.toString()} days
`; - } + string cardAgeHtml = summaryConfig.showCardAge + ? string `Age: ${card.cardAgeDays.toString()} days  ` : ""; - // Show attachment count - if summaryConfig.showAttachmentCount && card.attachmentCount > 0 { - html += string ` -
Attachments: ${card.attachmentCount.toString()}
`; - } + string attachmentsHtml = summaryConfig.showAttachmentCount && card.attachmentCount > 0 + ? string `Attachments: ${card.attachmentCount.toString()}  ` : ""; - // Show checklist progress - if summaryConfig.showChecklistProgress && card.checklistItemsTotal > 0 { - string checklistPercentage = formatPercentageTwoDecimals(card.checklistCompletionPercentage); - html += string ` -
Checklist: ${card.checklistItemsCompleted.toString()}/${card.checklistItemsTotal.toString()} (${checklistPercentage}%)
`; - } + string checklistHtml = ""; + if summaryConfig.showChecklistProgress && card.checklistItemsTotal > 0 { + string pct = formatPercentageTwoDecimals(card.checklistCompletionPercentage); + checklistHtml = string `Checklist: ${card.checklistItemsCompleted.toString()}/${card.checklistItemsTotal.toString()} (${pct}%)`; + } - html += string ` -
`; + string descHtml = ""; + if card.description.trim().length() > 0 { + string truncatedDesc = card.description.length() > 200 + ? card.description.substring(0, 200) + "..." : card.description; + descHtml = string `
${truncatedDesc}
`; } - } - html += string ` -
- -`; + string overdueTag = summaryConfig.highlightOverdueCards && card.isOverdue + ? "OVERDUE" + : ""; + + string metaHtml = dueDateHtml + cardAgeHtml + attachmentsHtml + checklistHtml; + string labelsRow = labelsHtml.length() > 0 ? string `
${labelsHtml}
` : ""; + string membersRow = membersHtml.trim().length() > 0 + ? string `
Members: ${membersHtml.trim()}
` : ""; + + cardsHtml += string ` + + + ${card.name}${overdueTag} +
${metaHtml}
+ ${labelsRow} + ${membersRow} + ${descHtml} + + `; + } - return html; + return string ` + + + ${group.groupName} + (${group.cards.length().toString()} cards) + + + + + + ${cardsHtml} +
+ + `; } function formatPercentageTwoDecimals(decimal value) returns string { @@ -377,6 +356,105 @@ function formatPercentageTwoDecimals(decimal value) returns string { return string `${wholePart.toString()}.${decimalPart.toString().padZero(2)}`; } +// Generate HTML email content using a responsive table-based layout +function generateEmailContent(GroupedSummary[] groupedSummaries, int totalCards, int overdueCount) returns string { + int staleCount = 0; + foreach GroupedSummary group in groupedSummaries { + foreach CardSummary card in group.cards { + if card.isStale { + staleCount += 1; + } + } + } + + string groupedByStr = summaryConfig.grouping.toString(); + string generatedAt = getFormattedTimestamp(time:utcNow()); + + string groupsHtml = ""; + foreach GroupedSummary group in groupedSummaries { + groupsHtml += getHtmlFormattedGroup(group); + } + + string overdueCountDisplay = summaryConfig.highlightOverdueCards ? overdueCount.toString() : "-"; + + return string ` + + + + + + Trello Cards Summary + + + +
+ +
+ Trello Cards Summary: ${totalCards.toString()} total, ${overdueCount.toString()} overdue, grouped by ${groupedByStr} +
+ + + + + + + + + + + + + + ${groupsHtml} + + + + + + + + + +
+ +`; +} + // Send email with summary using Mailchimp function sendEmailSummary(string htmlContent) returns error? { string subject = string `${mailchimpConfig.subjectPrefix} - ${time:utcToString(time:utcNow())}`; From 713f407108ba31df4c62b5294165e5ec6846745b Mon Sep 17 00:00:00 2001 From: Anjana Edirisinghe Date: Wed, 11 Mar 2026 16:20:57 +0530 Subject: [PATCH 06/18] Refactor Trello summary template and enhanced cron handling. --- .../trello-summary-email/automation.bal | 194 ++++++++++++++++-- .../trello-summary-email/functions.bal | 36 +--- .../trello-summary-email/main.bal | 21 +- 3 files changed, 185 insertions(+), 66 deletions(-) diff --git a/ballerina-integrator/trello-summary-email/automation.bal b/ballerina-integrator/trello-summary-email/automation.bal index 058cb4ea..872dd2a7 100644 --- a/ballerina-integrator/trello-summary-email/automation.bal +++ b/ballerina-integrator/trello-summary-email/automation.bal @@ -1,7 +1,10 @@ import ballerina/log; import ballerina/task; +import ballerina/time; + +const int MAX_CRON_SEARCH_MINUTES = 366 * 24 * 60; +final int[] MONTH_OFFSETS = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4]; -// Job to send Trello summary class TrelloSummaryJob { *task:Job; @@ -13,14 +16,25 @@ class TrelloSummaryJob { } else { log:printInfo("Trello summary sent successfully"); } + + task:JobId|error nextJob = scheduleNextTrelloSummary(); + if nextJob is error { + log:printError("Failed to schedule the next Trello summary", 'error = nextJob); + } } } -// Main function to send Trello summary +function scheduleNextTrelloSummary() returns task:JobId|error { + time:Civil nextRun = check getNextCronOccurrence(scheduleConfig.cron, time:utcNow()); + task:JobId jobId = check task:scheduleOneTimeJob(new TrelloSummaryJob(), nextRun); + log:printInfo(string `Next Trello summary scheduled for ${formatScheduledTime(nextRun)} with ID: ${jobId.id.toString()}`); + return jobId; +} + function sendTrelloSummary() returns error? { log:printInfo("Starting Trello summary generation..."); - // Fetch cards + //Fetch cards CardSummary[] cards = check fetchTrelloCards(); log:printInfo(string `Fetched ${cards.length().toString()} cards`); @@ -29,41 +43,177 @@ function sendTrelloSummary() returns error? { return; } - // Count overdue cards int overdueCount = countOverdueCards(cards); - // Group cards GroupedSummary[] groupedSummaries = groupCards(cards); log:printInfo(string `Grouped cards into ${groupedSummaries.length().toString()} groups`); - // Generate email content string emailContent = generateEmailContent(groupedSummaries, cards.length(), overdueCount); - // Send email check sendEmailSummary(emailContent); log:printInfo("Email sent successfully"); } -// Parse cron expression and convert to frequency in seconds -function parseCronToFrequency(string cron) returns decimal|error { - // Simple cron parser for common patterns - // Format: minute hour day month dayOfWeek - string[] parts = re ` `.split(cron); +function getNextCronOccurrence(string cron, time:Utc fromTime) returns time:Civil|error { + string[] parts = re ` `.split(cron.trim()); if parts.length() != 5 { return error("Invalid cron expression format"); } - string minutePart = parts[0]; - string hourPart = parts[1]; + time:Utc candidateUtc = check getNextMinuteBoundary(fromTime); + int attempts = 0; + while attempts < MAX_CRON_SEARCH_MINUTES { + time:Civil candidate = time:utcToCivil(candidateUtc); + if check matchesCron(parts, candidate) { + return candidate; + } + + candidateUtc = time:utcAddSeconds(candidateUtc, 60); + attempts += 1; + } + + return error(string `Could not find a matching execution time for cron expression: ${cron}`); +} + +function getNextMinuteBoundary(time:Utc fromTime) returns time:Utc|error { + time:Civil civil = time:utcToCivil(fromTime); + time:Civil rounded = { + year: civil.year, + month: civil.month, + day: civil.day, + hour: civil.hour, + minute: civil.minute, + second: 0, + utcOffset: civil.utcOffset, + timeAbbrev: civil.timeAbbrev + }; + time:Utc roundedUtc = check time:utcFromCivil(rounded); + return time:utcAddSeconds(roundedUtc, 60); +} + +function matchesCron(string[] parts, time:Civil candidate) returns boolean|error { + if !(check matchesCronField(parts[0], candidate.minute, 0, 59)) { + return false; + } + if !(check matchesCronField(parts[1], candidate.hour, 0, 23)) { + return false; + } + if !(check matchesCronField(parts[3], candidate.month, 1, 12)) { + return false; + } + + return matchesDayFields(parts[2], parts[4], candidate); +} + +function matchesDayFields(string dayOfMonthField, string dayOfWeekField, time:Civil candidate) returns boolean|error { + boolean dayOfMonthMatches = check matchesCronField(dayOfMonthField, candidate.day, 1, 31); + int dayOfWeek = getDayOfWeek(candidate); + boolean dayOfWeekMatches = check matchesCronField(dayOfWeekField, dayOfWeek, 0, 6, allowSevenAsSunday = true); + + boolean isDayOfMonthWildcard = dayOfMonthField.trim() == "*"; + boolean isDayOfWeekWildcard = dayOfWeekField.trim() == "*"; + + if isDayOfMonthWildcard && isDayOfWeekWildcard { + return true; + } + if isDayOfMonthWildcard { + return dayOfWeekMatches; + } + if isDayOfWeekWildcard { + return dayOfMonthMatches; + } + + return dayOfMonthMatches || dayOfWeekMatches; +} + +function matchesCronField(string expression, int value, int min, int max, boolean allowSevenAsSunday = false) returns boolean|error { + foreach string rawSegment in re `,`.split(expression) { + string segment = rawSegment.trim(); + if segment.length() == 0 { + return error(string `Invalid cron field segment in: ${expression}`); + } + + if check matchesCronSegment(segment, value, min, max, allowSevenAsSunday) { + return true; + } + } + + return false; +} + +function matchesCronSegment(string segment, int value, int min, int max, boolean allowSevenAsSunday) returns boolean|error { + string base = segment; + int step = 1; + + if segment.indexOf("/") is int { + string[] stepParts = re `/`.split(segment); + if stepParts.length() != 2 { + return error(string `Invalid cron step segment: ${segment}`); + } + + base = stepParts[0].trim(); + step = check parseCronNumber(stepParts[1].trim(), 1, max, string `step in ${segment}`); + if step <= 0 { + return error(string `Cron step must be greater than zero: ${segment}`); + } + } + + if base == "*" { + return (value - min) % step == 0; + } + + if base.indexOf("-") is int { + string[] rangeParts = re `-`.split(base); + if rangeParts.length() != 2 { + return error(string `Invalid cron range segment: ${segment}`); + } + + int rangeStart = check parseCronFieldValue(rangeParts[0].trim(), min, max, allowSevenAsSunday, segment); + int rangeEnd = check parseCronFieldValue(rangeParts[1].trim(), min, max, allowSevenAsSunday, segment); + if rangeEnd < rangeStart { + return error(string `Invalid descending cron range: ${segment}`); + } + + return value >= rangeStart && value <= rangeEnd && (value - rangeStart) % step == 0; + } + + int exactValue = check parseCronFieldValue(base, min, max, allowSevenAsSunday, segment); + if step != 1 { + return value == exactValue; + } + return value == exactValue; +} + +function parseCronFieldValue(string token, int min, int max, boolean allowSevenAsSunday, string segment) returns int|error { + int value = check parseCronNumber(token, min, max, string `value in ${segment}`); + if allowSevenAsSunday && value == 7 { + return 0; + } + return value; +} - // For simplicity, calculate based on daily frequency - // This is a basic implementation - for production use a proper cron library - if minutePart == "*" && hourPart == "*" { - return 3600; // Every hour - } else if minutePart != "*" && hourPart == "*" { - return 3600; // Every hour at specific minute - } else { - return 86400; // Daily +function parseCronNumber(string token, int min, int max, string fieldName) returns int|error { + int|error parsed = int:fromString(token); + if parsed is error { + return error(string `Invalid cron ${fieldName}: ${token}`); } + if parsed < min || parsed > max { + return error(string `Cron ${fieldName} must be between ${min.toString()} and ${max.toString()}: ${token}`); + } + return parsed; +} + +function getDayOfWeek(time:Civil candidate) returns int { + int year = candidate.year; + int month = candidate.month; + if month < 3 { + year -= 1; + } + + return (year + year / 4 - year / 100 + year / 400 + MONTH_OFFSETS[candidate.month - 1] + candidate.day) % 7; +} + +function formatScheduledTime(time:Civil scheduledTime) returns string { + return string `${scheduledTime.year}-${scheduledTime.month.toString().padZero(2)}-${scheduledTime.day.toString().padZero(2)} ${scheduledTime.hour.toString().padZero(2)}:${scheduledTime.minute.toString().padZero(2)} UTC`; } diff --git a/ballerina-integrator/trello-summary-email/functions.bal b/ballerina-integrator/trello-summary-email/functions.bal index a2cbe5d9..4ef294c1 100644 --- a/ballerina-integrator/trello-summary-email/functions.bal +++ b/ballerina-integrator/trello-summary-email/functions.bal @@ -12,7 +12,6 @@ function fetchListCardsAsJson(string listId) returns json[]|error { return check cardsJson.ensureType(); } -// Fetch all cards from specified boards and lists function fetchTrelloCards() returns CardSummary[]|error { CardSummary[] allCards = []; @@ -37,7 +36,6 @@ function fetchTrelloCards() returns CardSummary[]|error { ); string boardName = board.name ?: "Unknown Board"; - // Get lists from board - workaround for ambiguous resource access json boardJson = board.toJson(); json listsJson = check boardJson.lists; json[] listsArray = check listsJson.ensureType(); @@ -46,12 +44,10 @@ function fetchTrelloCards() returns CardSummary[]|error { string listId = check listJson.id; string listName = check listJson.name; - // Filter by list IDs if specified if (trelloConfig.listIds.length() > 0 && trelloConfig.listIds.indexOf(listId) is ()) { continue; } - // Fetch as raw JSON to tolerate Trello payload shape changes. json[] cardsArray = check fetchListCardsAsJson(listId); foreach json cardJson in cardsArray { @@ -66,7 +62,6 @@ function fetchTrelloCards() returns CardSummary[]|error { return allCards; } -// Process a single card from JSON and apply filters function processCardFromJson(json cardJson, string listName, string boardName) returns CardSummary?|error { string cardId = check cardJson.id; string cardName = check cardJson.name; @@ -74,7 +69,6 @@ function processCardFromJson(json cardJson, string listName, string boardName) r string? cardDesc = check cardJson.desc; string description = cardDesc is string ? cardDesc : ""; - // Calculate card age int cardAgeDays = 0; boolean isStale = false; string? dateLastActivity = check cardJson.dateLastActivity; @@ -86,7 +80,6 @@ function processCardFromJson(json cardJson, string listName, string boardName) r isStale = cardAgeDays >= summaryConfig.staleCardDays; } - // Get attachment count and checklist progress from badges int attachmentCount = 0; int checklistItemsTotal = 0; int checklistItemsCompleted = 0; @@ -107,7 +100,6 @@ function processCardFromJson(json cardJson, string listName, string boardName) r } } - // Extract labels string[] labelNames = []; json? labelsJson = check cardJson.labels; if labelsJson is json[] { @@ -119,7 +111,6 @@ function processCardFromJson(json cardJson, string listName, string boardName) r } } - // Apply label filter if filterConfig.labels.length() > 0 { boolean hasMatchingLabel = false; foreach string filterLabel in filterConfig.labels { @@ -133,7 +124,6 @@ function processCardFromJson(json cardJson, string listName, string boardName) r } } - // Extract member information string[] memberNames = []; json? membersJson = check cardJson.idMembers; if membersJson is json[] { @@ -151,7 +141,6 @@ function processCardFromJson(json cardJson, string listName, string boardName) r } } - // Apply member filter if filterConfig.members.length() > 0 { boolean hasMatchingMember = false; foreach string filterMember in filterConfig.members { @@ -165,7 +154,6 @@ function processCardFromJson(json cardJson, string listName, string boardName) r } } - // Parse due date time:Civil? dueDate = (); boolean isOverdue = false; string? dueDateStr = check cardJson.due; @@ -174,13 +162,11 @@ function processCardFromJson(json cardJson, string listName, string boardName) r time:Utc dueDateUtc = check time:utcFromString(dueDateStr); dueDate = time:utcToCivil(dueDateUtc); - // Check if overdue time:Utc currentTime = time:utcNow(); if dueDateUtc < currentTime { isOverdue = true; } - // Apply due date filter if filterConfig.includeDueDateFilter { time:Utc futureTime = time:utcAddSeconds(currentTime, filterConfig.dueDateDaysAhead * 24 * 60 * 60); if dueDateUtc > futureTime { @@ -209,7 +195,6 @@ function processCardFromJson(json cardJson, string listName, string boardName) r }; } -// Group cards based on configuration function groupCards(CardSummary[] cards) returns GroupedSummary[] { map groupMap = {}; @@ -258,13 +243,11 @@ function groupCards(CardSummary[] cards) returns GroupedSummary[] { return groupedSummaries; } -// Format a UTC timestamp as a human-readable string function getFormattedTimestamp(time:Utc utcTime) returns string { time:Civil civil = time:utcToCivil(utcTime); return string `${civil.year}-${civil.month.toString().padZero(2)}-${civil.day.toString().padZero(2)} ${civil.hour.toString().padZero(2)}:${civil.minute.toString().padZero(2)} UTC`; } -// Render a single group of cards as HTML table rows function getHtmlFormattedGroup(GroupedSummary group) returns string { string cardsHtml = ""; foreach CardSummary card in group.cards { @@ -356,7 +339,6 @@ function formatPercentageTwoDecimals(decimal value) returns string { return string `${wholePart.toString()}.${decimalPart.toString().padZero(2)}`; } -// Generate HTML email content using a responsive table-based layout function generateEmailContent(GroupedSummary[] groupedSummaries, int totalCards, int overdueCount) returns string { int staleCount = 0; foreach GroupedSummary group in groupedSummaries { @@ -441,8 +423,13 @@ function generateEmailContent(GroupedSummary[] groupedSummaries, int totalCards, ${groupsHtml} - -

You are receiving this Trello summary because it was scheduled via the Trello Summary Email integration.

+ +

You are receiving this Trello summary because it was scheduled via the Trello Summary Email integration.

+

+ Unsubscribe +  •  + Update preferences +

@@ -455,11 +442,11 @@ function generateEmailContent(GroupedSummary[] groupedSummaries, int totalCards, `; } -// Send email with summary using Mailchimp function sendEmailSummary(string htmlContent) returns error? { - string subject = string `${mailchimpConfig.subjectPrefix} - ${time:utcToString(time:utcNow())}`; + time:Civil currentDate = time:utcToCivil(time:utcNow()); + string dateStr = string `${currentDate.year}-${currentDate.month.toString().padZero(2)}-${currentDate.day.toString().padZero(2)}`; + string subject = string `${mailchimpConfig.subjectPrefix} - ${dateStr}`; - // Create a campaign mailchimp:Campaign1 campaign = check mailchimpClient->postCampaigns({ 'type: "regular", recipients: { @@ -478,7 +465,6 @@ function sendEmailSummary(string htmlContent) returns error? { return error("Failed to create campaign: Campaign ID is null"); } - // Set campaign content _ = check mailchimpClient->putCampaignsIdContent( campaignId = campaignId, payload = { @@ -486,11 +472,9 @@ function sendEmailSummary(string htmlContent) returns error? { } ); - // Send the campaign _ = check mailchimpClient->postCampaignsIdActionsSend(campaignId = campaignId); } -// Calculate overdue count function countOverdueCards(CardSummary[] cards) returns int { int count = 0; foreach CardSummary card in cards { diff --git a/ballerina-integrator/trello-summary-email/main.bal b/ballerina-integrator/trello-summary-email/main.bal index 3d5a68b7..ea0a227c 100644 --- a/ballerina-integrator/trello-summary-email/main.bal +++ b/ballerina-integrator/trello-summary-email/main.bal @@ -1,4 +1,5 @@ import ballerina/log; +import ballerina/lang.runtime; import ballerina/task; public function main() returns error? { @@ -7,30 +8,14 @@ public function main() returns error? { log:printInfo(string `Grouping: ${summaryConfig.grouping.toString()}`); log:printInfo(string `Mailchimp List: ${mailchimpConfig.listId}`); - // Send immediately on startup for testing - log:printInfo("Sending initial summary immediately for testing..."); - error? initialResult = sendTrelloSummary(); - if initialResult is error { - log:printError("Failed to send initial summary", 'error = initialResult); - } else { - log:printInfo("Initial summary sent successfully"); - } - // Parse cron schedule to frequency - decimal frequency = check parseCronToFrequency(scheduleConfig.cron); - // Schedule the job - task:JobId jobId = check task:scheduleJobRecurByFrequency( - job = new TrelloSummaryJob(), - interval = frequency - ); + task:JobId jobId = check scheduleNextTrelloSummary(); log:printInfo(string `Job scheduled with ID: ${jobId.id.toString()}`); log:printInfo("Automation is running. Press Ctrl+C to stop."); - // Keep the program running while true { - // Sleep to keep the program alive - // The scheduled job will run in the background + runtime:sleep(60); } } From ae8bf28c6fc986da3d2726a707fe34024ae37caa Mon Sep 17 00:00:00 2001 From: Anjana Edirisinghe Date: Thu, 12 Mar 2026 11:11:28 +0530 Subject: [PATCH 07/18] enhanced cron schedule support and email subject date inclusion options --- .../.choreo/config-schema.json | 262 +++++++++--------- .../.choreo/instructions.md | 24 +- .../trello-summary-email/README.md | 2 + .../trello-summary-email/config.bal | 1 + .../trello-summary-email/functions.bal | 9 +- .../trello-summary-email/types.bal | 3 - 6 files changed, 155 insertions(+), 146 deletions(-) diff --git a/ballerina-integrator/trello-summary-email/.choreo/config-schema.json b/ballerina-integrator/trello-summary-email/.choreo/config-schema.json index 8878a926..17ebd1f7 100644 --- a/ballerina-integrator/trello-summary-email/.choreo/config-schema.json +++ b/ballerina-integrator/trello-summary-email/.choreo/config-schema.json @@ -5,6 +5,134 @@ "ballerina": { "type": "object", "properties": { + "log": { + "type": "object", + "properties": { + "destinations": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "enum": [ + "stderr", + "stdout" + ] + } + }, + "additionalProperties": false, + "required": [ + "type" + ], + "name": "ballerina/log:2.14.0:StandardDestination" + }, + { + "type": "object", + "properties": { + "type": { + "enum": [ + "file" + ] + }, + "path": { + "type": "string" + }, + "mode": { + "enum": [ + "APPEND", + "TRUNCATE" + ] + } + }, + "additionalProperties": false, + "required": [ + "type", + "path", + "mode" + ], + "name": "ballerina/log:2.14.0:FileOutputDestination" + } + ] + }, + "description": "" + }, + "modules": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "level": { + "enum": [ + "WARN", + "INFO", + "ERROR", + "DEBUG" + ] + } + }, + "additionalProperties": false, + "required": [ + "name", + "level" + ], + "name": "ballerina/log:2.14.0:Module" + }, + "description": "" + }, + "keyValues": { + "type": "object", + "properties": { + "msg": {}, + "error": {}, + "stackTrace": {}, + "module": {} + }, + "additionalProperties": false, + "name": "ballerina/log:2.14.0:AnydataKeyValues", + "description": "" + }, + "format": { + "enum": [ + "logfmt", + "json" + ], + "description": "" + }, + "level": { + "enum": [ + "WARN", + "INFO", + "ERROR", + "DEBUG" + ], + "description": "" + }, + "enableSensitiveDataMasking": { + "type": "boolean", + "description": "" + } + }, + "additionalProperties": false + }, + "task": { + "type": "object", + "properties": { + "globalSchedulerWorkerCount": { + "type": "integer", + "description": "" + }, + "globalSchedulerWaitingTime": { + "type": "number", + "description": "" + } + }, + "additionalProperties": false + }, "http": { "type": "object", "properties": { @@ -397,134 +525,6 @@ } }, "additionalProperties": false - }, - "task": { - "type": "object", - "properties": { - "globalSchedulerWorkerCount": { - "type": "integer", - "description": "" - }, - "globalSchedulerWaitingTime": { - "type": "number", - "description": "" - } - }, - "additionalProperties": false - }, - "log": { - "type": "object", - "properties": { - "destinations": { - "type": "array", - "items": { - "anyOf": [ - { - "type": "object", - "properties": { - "type": { - "enum": [ - "stderr", - "stdout" - ] - } - }, - "additionalProperties": false, - "required": [ - "type" - ], - "name": "ballerina/log:2.14.0:StandardDestination" - }, - { - "type": "object", - "properties": { - "type": { - "enum": [ - "file" - ] - }, - "path": { - "type": "string" - }, - "mode": { - "enum": [ - "APPEND", - "TRUNCATE" - ] - } - }, - "additionalProperties": false, - "required": [ - "type", - "path", - "mode" - ], - "name": "ballerina/log:2.14.0:FileOutputDestination" - } - ] - }, - "description": "" - }, - "modules": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "level": { - "enum": [ - "WARN", - "INFO", - "ERROR", - "DEBUG" - ] - } - }, - "additionalProperties": false, - "required": [ - "name", - "level" - ], - "name": "ballerina/log:2.14.0:Module" - }, - "description": "" - }, - "keyValues": { - "type": "object", - "properties": { - "msg": {}, - "error": {}, - "stackTrace": {}, - "module": {} - }, - "additionalProperties": false, - "name": "ballerina/log:2.14.0:AnydataKeyValues", - "description": "" - }, - "format": { - "enum": [ - "logfmt", - "json" - ], - "description": "" - }, - "level": { - "enum": [ - "WARN", - "INFO", - "ERROR", - "DEBUG" - ], - "description": "" - }, - "enableSensitiveDataMasking": { - "type": "boolean", - "description": "" - } - }, - "additionalProperties": false } }, "additionalProperties": false @@ -586,6 +586,9 @@ }, "subjectPrefix": { "type": "string" + }, + "includeDateInSubject": { + "type": "boolean" } }, "additionalProperties": false, @@ -595,7 +598,8 @@ "listId", "fromName", "fromAddress", - "subjectPrefix" + "subjectPrefix", + "includeDateInSubject" ], "description": "" }, diff --git a/ballerina-integrator/trello-summary-email/.choreo/instructions.md b/ballerina-integrator/trello-summary-email/.choreo/instructions.md index 9add3403..ec418bee 100644 --- a/ballerina-integrator/trello-summary-email/.choreo/instructions.md +++ b/ballerina-integrator/trello-summary-email/.choreo/instructions.md @@ -38,19 +38,21 @@ Additional Configurations 1. `scheduleConfig.cron` - - Cron expression controlling when the summary is sent (default: `0 9 * * 1` - every Monday at 9:00 AM) + - Cron expression controlling when the summary is sent (default: `0 9 * * 1` - every Monday at 9:00 AM) 2. `filterConfig.labels` - - Filter cards by label name. Leave empty to include all labels. + - Filter cards by label name. Leave empty to include all labels. 3. `filterConfig.members` - - Filter cards by member full name. Leave empty to include all members. + - Filter cards by member full name. Leave empty to include all members. 4. `filterConfig.includeDueDateFilter` - - Set to `true` to only include cards due within the next `dueDateDaysAhead` days. -5. `summaryConfig.grouping` - - How to group cards in the email. Possible values: - - `LIST` (default) - - `MEMBER` - - `LABEL` -6. `summaryConfig.staleCardDays` - - Cards with no activity for this many days are considered stale (default: `30`). + - Set to `true` to only include cards due within the next `dueDateDaysAhead` days. +5. `mailchimpConfig.includeDateInSubject` + - If `true` (default), today's date is included in the email subject (e.g., `Trello Cards Summary - 2026-03-12`). +6. `summaryConfig.grouping` + - How to group cards in the email. Possible values: + - `LIST` (default) + - `MEMBER` + - `LABEL` +7. `summaryConfig.staleCardDays` + - Cards with no activity for this many days are considered stale (default: `30`). diff --git a/ballerina-integrator/trello-summary-email/README.md b/ballerina-integrator/trello-summary-email/README.md index 6062aace..85229495 100644 --- a/ballerina-integrator/trello-summary-email/README.md +++ b/ballerina-integrator/trello-summary-email/README.md @@ -56,6 +56,7 @@ listId = "" fromName = "" fromAddress = "" subjectPrefix = "Trello Cards Summary" # Optional, has default +includeDateInSubject = true # If true, today's date is shown in the subject [scheduleConfig] cron = "0 9 * * 1" # Every Monday at 9:00 AM (default) @@ -96,6 +97,7 @@ showChecklistProgress = true | `fromName` | Sender display name for the email campaign | | `fromAddress` | Sender reply-to email address | | `subjectPrefix` | Prefix for the email subject line (default: `Trello Cards Summary`) | +| `includeDateInSubject` | If `true` (default), today's date is included in the email subject (e.g., `Trello Cards Summary - 2026-03-12`) | #### `scheduleConfig` diff --git a/ballerina-integrator/trello-summary-email/config.bal b/ballerina-integrator/trello-summary-email/config.bal index 4c3aec46..1d96752c 100644 --- a/ballerina-integrator/trello-summary-email/config.bal +++ b/ballerina-integrator/trello-summary-email/config.bal @@ -12,6 +12,7 @@ configurable record { string fromName; string fromAddress; string subjectPrefix = "Trello Cards Summary"; + boolean includeDateInSubject = true; } mailchimpConfig = ?; configurable record { diff --git a/ballerina-integrator/trello-summary-email/functions.bal b/ballerina-integrator/trello-summary-email/functions.bal index 4ef294c1..2b464156 100644 --- a/ballerina-integrator/trello-summary-email/functions.bal +++ b/ballerina-integrator/trello-summary-email/functions.bal @@ -443,9 +443,12 @@ function generateEmailContent(GroupedSummary[] groupedSummaries, int totalCards, } function sendEmailSummary(string htmlContent) returns error? { - time:Civil currentDate = time:utcToCivil(time:utcNow()); - string dateStr = string `${currentDate.year}-${currentDate.month.toString().padZero(2)}-${currentDate.day.toString().padZero(2)}`; - string subject = string `${mailchimpConfig.subjectPrefix} - ${dateStr}`; + string subject = mailchimpConfig.subjectPrefix; + if mailchimpConfig.includeDateInSubject { + time:Civil currentDate = time:utcToCivil(time:utcNow()); + string dateStr = string `${currentDate.year}-${currentDate.month.toString().padZero(2)}-${currentDate.day.toString().padZero(2)}`; + subject = string `${mailchimpConfig.subjectPrefix} - ${dateStr}`; + } mailchimp:Campaign1 campaign = check mailchimpClient->postCampaigns({ 'type: "regular", diff --git a/ballerina-integrator/trello-summary-email/types.bal b/ballerina-integrator/trello-summary-email/types.bal index a703914f..2a2c6f56 100644 --- a/ballerina-integrator/trello-summary-email/types.bal +++ b/ballerina-integrator/trello-summary-email/types.bal @@ -1,13 +1,11 @@ import ballerina/time; -// Summary grouping options public enum SummaryGrouping { LIST, MEMBER, LABEL } -// Card summary record public type CardSummary record {| string id; string name; @@ -27,7 +25,6 @@ public type CardSummary record {| decimal checklistCompletionPercentage; |}; -// Grouped summary public type GroupedSummary record {| string groupName; CardSummary[] cards; From ff06a9969e91fbc8303535896edd32905f071b95 Mon Sep 17 00:00:00 2001 From: Anjana Edirisinghe Date: Thu, 12 Mar 2026 11:35:51 +0530 Subject: [PATCH 08/18] removed manual cron configuration since it will handled via devant --- .../.choreo/config-schema.json | 473 +++++++++--------- .../.choreo/instructions.md | 6 +- .../trello-summary-email/README.md | 18 +- .../trello-summary-email/automation.bal | 193 ------- .../trello-summary-email/config.bal | 4 +- .../trello-summary-email/main.bal | 14 +- 6 files changed, 231 insertions(+), 477 deletions(-) diff --git a/ballerina-integrator/trello-summary-email/.choreo/config-schema.json b/ballerina-integrator/trello-summary-email/.choreo/config-schema.json index 17ebd1f7..3d67ec4e 100644 --- a/ballerina-integrator/trello-summary-email/.choreo/config-schema.json +++ b/ballerina-integrator/trello-summary-email/.choreo/config-schema.json @@ -2,137 +2,161 @@ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { - "ballerina": { + "wso2": { "type": "object", "properties": { - "log": { + "trello_summary_email": { "type": "object", "properties": { - "destinations": { - "type": "array", - "items": { - "anyOf": [ - { - "type": "object", - "properties": { - "type": { - "enum": [ - "stderr", - "stdout" - ] - } - }, - "additionalProperties": false, - "required": [ - "type" - ], - "name": "ballerina/log:2.14.0:StandardDestination" - }, - { - "type": "object", - "properties": { - "type": { - "enum": [ - "file" - ] - }, - "path": { - "type": "string" - }, - "mode": { - "enum": [ - "APPEND", - "TRUNCATE" - ] - } - }, - "additionalProperties": false, - "required": [ - "type", - "path", - "mode" - ], - "name": "ballerina/log:2.14.0:FileOutputDestination" - } - ] - }, - "description": "" - }, - "modules": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { + "trelloConfig": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "token": { + "type": "string" + }, + "boardIds": { + "type": "array", + "items": { "type": "string" - }, - "level": { - "enum": [ - "WARN", - "INFO", - "ERROR", - "DEBUG" - ] } }, - "additionalProperties": false, - "required": [ - "name", - "level" - ], - "name": "ballerina/log:2.14.0:Module" + "listIds": { + "type": "array", + "items": { + "type": "string" + } + } }, + "additionalProperties": false, + "required": [ + "key", + "token", + "boardIds", + "listIds" + ], "description": "" }, - "keyValues": { + "mailchimpConfig": { "type": "object", "properties": { - "msg": {}, - "error": {}, - "stackTrace": {}, - "module": {} + "apiKey": { + "type": "string" + }, + "serverPrefix": { + "type": "string" + }, + "listId": { + "type": "string" + }, + "fromName": { + "type": "string" + }, + "fromAddress": { + "type": "string" + }, + "subjectPrefix": { + "type": "string" + }, + "includeDateInSubject": { + "type": "boolean" + } }, "additionalProperties": false, - "name": "ballerina/log:2.14.0:AnydataKeyValues", - "description": "" - }, - "format": { - "enum": [ - "logfmt", - "json" + "required": [ + "apiKey", + "serverPrefix", + "listId", + "fromName", + "fromAddress", + "subjectPrefix", + "includeDateInSubject" ], "description": "" }, - "level": { - "enum": [ - "WARN", - "INFO", - "ERROR", - "DEBUG" + "filterConfig": { + "type": "object", + "properties": { + "labels": { + "type": "array", + "items": { + "type": "string" + } + }, + "members": { + "type": "array", + "items": { + "type": "string" + } + }, + "includeDueDateFilter": { + "type": "boolean" + }, + "dueDateDaysAhead": { + "type": "integer" + } + }, + "additionalProperties": false, + "required": [ + "labels", + "members", + "includeDueDateFilter", + "dueDateDaysAhead" ], "description": "" }, - "enableSensitiveDataMasking": { - "type": "boolean", - "description": "" - } - }, - "additionalProperties": false - }, - "task": { - "type": "object", - "properties": { - "globalSchedulerWorkerCount": { - "type": "integer", - "description": "" - }, - "globalSchedulerWaitingTime": { - "type": "number", + "summaryConfig": { + "type": "object", + "properties": { + "grouping": { + "enum": [ + "LABEL", + "MEMBER", + "LIST" + ] + }, + "highlightOverdueCards": { + "type": "boolean" + }, + "showCardAge": { + "type": "boolean" + }, + "staleCardDays": { + "type": "integer" + }, + "showAttachmentCount": { + "type": "boolean" + }, + "showChecklistProgress": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + "grouping", + "highlightOverdueCards", + "showCardAge", + "staleCardDays", + "showAttachmentCount", + "showChecklistProgress" + ], "description": "" } }, - "additionalProperties": false - }, + "additionalProperties": false, + "required": [ + "trelloConfig", + "mailchimpConfig" + ] + } + }, + "additionalProperties": false + }, + "ballerina": { + "type": "object", + "properties": { "http": { "type": "object", "properties": { @@ -525,171 +549,120 @@ } }, "additionalProperties": false - } - }, - "additionalProperties": false - }, - "wso2": { - "type": "object", - "properties": { - "trello_summary_email": { + }, + "log": { "type": "object", "properties": { - "trelloConfig": { - "type": "object", - "properties": { - "key": { - "type": "string" - }, - "token": { - "type": "string" - }, - "boardIds": { - "type": "array", - "items": { - "type": "string" - } - }, - "listIds": { - "type": "array", - "items": { - "type": "string" + "destinations": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "enum": [ + "stderr", + "stdout" + ] + } + }, + "additionalProperties": false, + "required": [ + "type" + ], + "name": "ballerina/log:2.14.0:StandardDestination" + }, + { + "type": "object", + "properties": { + "type": { + "enum": [ + "file" + ] + }, + "path": { + "type": "string" + }, + "mode": { + "enum": [ + "APPEND", + "TRUNCATE" + ] + } + }, + "additionalProperties": false, + "required": [ + "type", + "path", + "mode" + ], + "name": "ballerina/log:2.14.0:FileOutputDestination" } - } + ] }, - "additionalProperties": false, - "required": [ - "key", - "token", - "boardIds", - "listIds" - ], "description": "" }, - "mailchimpConfig": { - "type": "object", - "properties": { - "apiKey": { - "type": "string" - }, - "serverPrefix": { - "type": "string" - }, - "listId": { - "type": "string" - }, - "fromName": { - "type": "string" - }, - "fromAddress": { - "type": "string" - }, - "subjectPrefix": { - "type": "string" + "modules": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "level": { + "enum": [ + "WARN", + "INFO", + "ERROR", + "DEBUG" + ] + } }, - "includeDateInSubject": { - "type": "boolean" - } + "additionalProperties": false, + "required": [ + "name", + "level" + ], + "name": "ballerina/log:2.14.0:Module" }, - "additionalProperties": false, - "required": [ - "apiKey", - "serverPrefix", - "listId", - "fromName", - "fromAddress", - "subjectPrefix", - "includeDateInSubject" - ], "description": "" }, - "scheduleConfig": { + "keyValues": { "type": "object", "properties": { - "cron": { - "type": "string" - } + "msg": {}, + "error": {}, + "stackTrace": {}, + "module": {} }, "additionalProperties": false, - "required": [ - "cron" - ], + "name": "ballerina/log:2.14.0:AnydataKeyValues", "description": "" }, - "filterConfig": { - "type": "object", - "properties": { - "labels": { - "type": "array", - "items": { - "type": "string" - } - }, - "members": { - "type": "array", - "items": { - "type": "string" - } - }, - "includeDueDateFilter": { - "type": "boolean" - }, - "dueDateDaysAhead": { - "type": "integer" - } - }, - "additionalProperties": false, - "required": [ - "labels", - "members", - "includeDueDateFilter", - "dueDateDaysAhead" + "format": { + "enum": [ + "logfmt", + "json" ], "description": "" }, - "summaryConfig": { - "type": "object", - "properties": { - "grouping": { - "enum": [ - "LABEL", - "MEMBER", - "LIST" - ] - }, - "highlightOverdueCards": { - "type": "boolean" - }, - "showCardAge": { - "type": "boolean" - }, - "staleCardDays": { - "type": "integer" - }, - "showAttachmentCount": { - "type": "boolean" - }, - "showChecklistProgress": { - "type": "boolean" - } - }, - "additionalProperties": false, - "required": [ - "grouping", - "highlightOverdueCards", - "showCardAge", - "staleCardDays", - "showAttachmentCount", - "showChecklistProgress" + "level": { + "enum": [ + "WARN", + "INFO", + "ERROR", + "DEBUG" ], "description": "" + }, + "enableSensitiveDataMasking": { + "type": "boolean", + "description": "" } }, - "additionalProperties": false, - "required": [ - "trelloConfig", - "mailchimpConfig" - ] + "additionalProperties": false } }, "additionalProperties": false diff --git a/ballerina-integrator/trello-summary-email/.choreo/instructions.md b/ballerina-integrator/trello-summary-email/.choreo/instructions.md index ec418bee..27ac576e 100644 --- a/ballerina-integrator/trello-summary-email/.choreo/instructions.md +++ b/ballerina-integrator/trello-summary-email/.choreo/instructions.md @@ -4,7 +4,7 @@ - Applies optional filters by label, member, or due date range - Groups cards by **List**, **Member**, or **Label** - Generates a formatted email summarising all cards, including overdue status, card age, attachments, and checklist progress -- Creates and sends a Mailchimp email campaign to a configured audience list on a cron schedule (default: every Monday at 9:00 AM) +- Creates and sends a Mailchimp email campaign to a configured audience list when triggered by Devant automation
@@ -37,9 +37,7 @@ Additional Configurations -1. `scheduleConfig.cron` - - Cron expression controlling when the summary is sent (default: `0 9 * * 1` - every Monday at 9:00 AM) -2. `filterConfig.labels` +1. `filterConfig.labels` - Filter cards by label name. Leave empty to include all labels. 3. `filterConfig.members` - Filter cards by member full name. Leave empty to include all members. diff --git a/ballerina-integrator/trello-summary-email/README.md b/ballerina-integrator/trello-summary-email/README.md index 85229495..4f4be617 100644 --- a/ballerina-integrator/trello-summary-email/README.md +++ b/ballerina-integrator/trello-summary-email/README.md @@ -13,7 +13,7 @@ This integration fetches cards from one or more Trello boards and lists, generat - Total card count and overdue card count - Per-card details: board, list, due date, labels, members, description, card age, attachments, and checklist progress - Creates and sends a Mailchimp email campaign to a configured audience list -- Runs automatically on a configurable cron schedule (default: every Monday at 9:00 AM) +- Executes when triggered by Devant automation (scheduling is handled by Devant) ## Prerequisites @@ -58,9 +58,6 @@ fromAddress = "" subjectPrefix = "Trello Cards Summary" # Optional, has default includeDateInSubject = true # If true, today's date is shown in the subject -[scheduleConfig] -cron = "0 9 * * 1" # Every Monday at 9:00 AM (default) - [filterConfig] labels = [] # Filter by label names; empty means no filter members = [] # Filter by member full names; empty means no filter @@ -99,12 +96,6 @@ showChecklistProgress = true | `subjectPrefix` | Prefix for the email subject line (default: `Trello Cards Summary`) | | `includeDateInSubject` | If `true` (default), today's date is included in the email subject (e.g., `Trello Cards Summary - 2026-03-12`) | -#### `scheduleConfig` - -| Field | Description | -|---|---| -| `cron` | Cron expression for the schedule (default: `0 9 * * 1` — Mondays at 9 AM) | - #### `filterConfig` | Field | Description | @@ -132,7 +123,6 @@ showChecklistProgress = true 3. Select the **Technology** as `WSO2 Integrator: BI`. 4. Choose the **Integration** type as `Automation` and click **Create**. 5. Once the build is successful, click **Configure to Continue** and set up the required environment variables for Trello and Mailchimp credentials. -6. Click **Schedule** to schedule the automation. -7. In the **BY INTERVAL** tab, configure the desired schedule (e.g., weekly on Monday mornings). -8. Click **Update**. -9. Once tested, you may promote the integration to production. Make sure to set the relevant environment variables in the production environment as well. +6. Configure the schedule in Devant's automation scheduler (e.g., weekly on Monday mornings). +7. Devant will trigger this automation on the configured schedule and handle all scheduling. +8. Once tested, you may promote the integration to production. Make sure to set the relevant environment variables in the production environment as well. diff --git a/ballerina-integrator/trello-summary-email/automation.bal b/ballerina-integrator/trello-summary-email/automation.bal index 872dd2a7..0fcba442 100644 --- a/ballerina-integrator/trello-summary-email/automation.bal +++ b/ballerina-integrator/trello-summary-email/automation.bal @@ -1,35 +1,4 @@ import ballerina/log; -import ballerina/task; -import ballerina/time; - -const int MAX_CRON_SEARCH_MINUTES = 366 * 24 * 60; -final int[] MONTH_OFFSETS = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4]; - -class TrelloSummaryJob { - - *task:Job; - - public function execute() { - error? result = sendTrelloSummary(); - if result is error { - log:printError("Failed to send Trello summary", 'error = result); - } else { - log:printInfo("Trello summary sent successfully"); - } - - task:JobId|error nextJob = scheduleNextTrelloSummary(); - if nextJob is error { - log:printError("Failed to schedule the next Trello summary", 'error = nextJob); - } - } -} - -function scheduleNextTrelloSummary() returns task:JobId|error { - time:Civil nextRun = check getNextCronOccurrence(scheduleConfig.cron, time:utcNow()); - task:JobId jobId = check task:scheduleOneTimeJob(new TrelloSummaryJob(), nextRun); - log:printInfo(string `Next Trello summary scheduled for ${formatScheduledTime(nextRun)} with ID: ${jobId.id.toString()}`); - return jobId; -} function sendTrelloSummary() returns error? { log:printInfo("Starting Trello summary generation..."); @@ -54,166 +23,4 @@ function sendTrelloSummary() returns error? { log:printInfo("Email sent successfully"); } -function getNextCronOccurrence(string cron, time:Utc fromTime) returns time:Civil|error { - string[] parts = re ` `.split(cron.trim()); - - if parts.length() != 5 { - return error("Invalid cron expression format"); - } - - time:Utc candidateUtc = check getNextMinuteBoundary(fromTime); - int attempts = 0; - while attempts < MAX_CRON_SEARCH_MINUTES { - time:Civil candidate = time:utcToCivil(candidateUtc); - if check matchesCron(parts, candidate) { - return candidate; - } - - candidateUtc = time:utcAddSeconds(candidateUtc, 60); - attempts += 1; - } - - return error(string `Could not find a matching execution time for cron expression: ${cron}`); -} - -function getNextMinuteBoundary(time:Utc fromTime) returns time:Utc|error { - time:Civil civil = time:utcToCivil(fromTime); - time:Civil rounded = { - year: civil.year, - month: civil.month, - day: civil.day, - hour: civil.hour, - minute: civil.minute, - second: 0, - utcOffset: civil.utcOffset, - timeAbbrev: civil.timeAbbrev - }; - time:Utc roundedUtc = check time:utcFromCivil(rounded); - return time:utcAddSeconds(roundedUtc, 60); -} - -function matchesCron(string[] parts, time:Civil candidate) returns boolean|error { - if !(check matchesCronField(parts[0], candidate.minute, 0, 59)) { - return false; - } - if !(check matchesCronField(parts[1], candidate.hour, 0, 23)) { - return false; - } - if !(check matchesCronField(parts[3], candidate.month, 1, 12)) { - return false; - } - - return matchesDayFields(parts[2], parts[4], candidate); -} - -function matchesDayFields(string dayOfMonthField, string dayOfWeekField, time:Civil candidate) returns boolean|error { - boolean dayOfMonthMatches = check matchesCronField(dayOfMonthField, candidate.day, 1, 31); - int dayOfWeek = getDayOfWeek(candidate); - boolean dayOfWeekMatches = check matchesCronField(dayOfWeekField, dayOfWeek, 0, 6, allowSevenAsSunday = true); - - boolean isDayOfMonthWildcard = dayOfMonthField.trim() == "*"; - boolean isDayOfWeekWildcard = dayOfWeekField.trim() == "*"; - - if isDayOfMonthWildcard && isDayOfWeekWildcard { - return true; - } - if isDayOfMonthWildcard { - return dayOfWeekMatches; - } - if isDayOfWeekWildcard { - return dayOfMonthMatches; - } - - return dayOfMonthMatches || dayOfWeekMatches; -} - -function matchesCronField(string expression, int value, int min, int max, boolean allowSevenAsSunday = false) returns boolean|error { - foreach string rawSegment in re `,`.split(expression) { - string segment = rawSegment.trim(); - if segment.length() == 0 { - return error(string `Invalid cron field segment in: ${expression}`); - } - - if check matchesCronSegment(segment, value, min, max, allowSevenAsSunday) { - return true; - } - } - - return false; -} - -function matchesCronSegment(string segment, int value, int min, int max, boolean allowSevenAsSunday) returns boolean|error { - string base = segment; - int step = 1; - - if segment.indexOf("/") is int { - string[] stepParts = re `/`.split(segment); - if stepParts.length() != 2 { - return error(string `Invalid cron step segment: ${segment}`); - } - - base = stepParts[0].trim(); - step = check parseCronNumber(stepParts[1].trim(), 1, max, string `step in ${segment}`); - if step <= 0 { - return error(string `Cron step must be greater than zero: ${segment}`); - } - } - - if base == "*" { - return (value - min) % step == 0; - } - - if base.indexOf("-") is int { - string[] rangeParts = re `-`.split(base); - if rangeParts.length() != 2 { - return error(string `Invalid cron range segment: ${segment}`); - } - - int rangeStart = check parseCronFieldValue(rangeParts[0].trim(), min, max, allowSevenAsSunday, segment); - int rangeEnd = check parseCronFieldValue(rangeParts[1].trim(), min, max, allowSevenAsSunday, segment); - if rangeEnd < rangeStart { - return error(string `Invalid descending cron range: ${segment}`); - } - - return value >= rangeStart && value <= rangeEnd && (value - rangeStart) % step == 0; - } - int exactValue = check parseCronFieldValue(base, min, max, allowSevenAsSunday, segment); - if step != 1 { - return value == exactValue; - } - return value == exactValue; -} - -function parseCronFieldValue(string token, int min, int max, boolean allowSevenAsSunday, string segment) returns int|error { - int value = check parseCronNumber(token, min, max, string `value in ${segment}`); - if allowSevenAsSunday && value == 7 { - return 0; - } - return value; -} - -function parseCronNumber(string token, int min, int max, string fieldName) returns int|error { - int|error parsed = int:fromString(token); - if parsed is error { - return error(string `Invalid cron ${fieldName}: ${token}`); - } - if parsed < min || parsed > max { - return error(string `Cron ${fieldName} must be between ${min.toString()} and ${max.toString()}: ${token}`); - } - return parsed; -} - -function getDayOfWeek(time:Civil candidate) returns int { - int year = candidate.year; - int month = candidate.month; - if month < 3 { - year -= 1; - } - - return (year + year / 4 - year / 100 + year / 400 + MONTH_OFFSETS[candidate.month - 1] + candidate.day) % 7; -} - -function formatScheduledTime(time:Civil scheduledTime) returns string { - return string `${scheduledTime.year}-${scheduledTime.month.toString().padZero(2)}-${scheduledTime.day.toString().padZero(2)} ${scheduledTime.hour.toString().padZero(2)}:${scheduledTime.minute.toString().padZero(2)} UTC`; -} diff --git a/ballerina-integrator/trello-summary-email/config.bal b/ballerina-integrator/trello-summary-email/config.bal index 1d96752c..180cd721 100644 --- a/ballerina-integrator/trello-summary-email/config.bal +++ b/ballerina-integrator/trello-summary-email/config.bal @@ -15,9 +15,7 @@ configurable record { boolean includeDateInSubject = true; } mailchimpConfig = ?; -configurable record { - string cron = "0 9 * * 1"; -} scheduleConfig = {}; + configurable record { string[] labels = []; diff --git a/ballerina-integrator/trello-summary-email/main.bal b/ballerina-integrator/trello-summary-email/main.bal index ea0a227c..fca4a603 100644 --- a/ballerina-integrator/trello-summary-email/main.bal +++ b/ballerina-integrator/trello-summary-email/main.bal @@ -1,21 +1,9 @@ import ballerina/log; -import ballerina/lang.runtime; -import ballerina/task; public function main() returns error? { log:printInfo("Starting Trello Card Summary Automation"); - log:printInfo(string `Schedule: ${scheduleConfig.cron}`); log:printInfo(string `Grouping: ${summaryConfig.grouping.toString()}`); log:printInfo(string `Mailchimp List: ${mailchimpConfig.listId}`); - - - task:JobId jobId = check scheduleNextTrelloSummary(); - - log:printInfo(string `Job scheduled with ID: ${jobId.id.toString()}`); - log:printInfo("Automation is running. Press Ctrl+C to stop."); - - while true { - runtime:sleep(60); - } + check sendTrelloSummary(); } From cf9fa2cf81ff2b04064263fda17450b527f4b7ee Mon Sep 17 00:00:00 2001 From: Anjana Edirisinghe Date: Thu, 12 Mar 2026 17:02:27 +0530 Subject: [PATCH 09/18] add project path --- .github/workflows/projects.json | 3 +++ .../samples}/trello-summary-email/.choreo/config-schema.json | 0 .../samples}/trello-summary-email/.choreo/diagram.md | 0 .../samples}/trello-summary-email/.choreo/instructions.md | 0 .../samples}/trello-summary-email/.gitignore | 0 .../samples}/trello-summary-email/Ballerina.toml | 0 .../samples}/trello-summary-email/README.md | 0 .../samples}/trello-summary-email/agents.bal | 0 .../samples}/trello-summary-email/automation.bal | 0 .../samples}/trello-summary-email/config.bal | 0 .../samples}/trello-summary-email/connections.bal | 0 .../samples}/trello-summary-email/data_mappings.bal | 0 .../samples}/trello-summary-email/functions.bal | 0 .../samples}/trello-summary-email/main.bal | 0 .../samples}/trello-summary-email/types.bal | 0 15 files changed, 3 insertions(+) rename {ballerina-integrator => integrator-default-profile/samples}/trello-summary-email/.choreo/config-schema.json (100%) rename {ballerina-integrator => integrator-default-profile/samples}/trello-summary-email/.choreo/diagram.md (100%) rename {ballerina-integrator => integrator-default-profile/samples}/trello-summary-email/.choreo/instructions.md (100%) rename {ballerina-integrator => integrator-default-profile/samples}/trello-summary-email/.gitignore (100%) rename {ballerina-integrator => integrator-default-profile/samples}/trello-summary-email/Ballerina.toml (100%) rename {ballerina-integrator => integrator-default-profile/samples}/trello-summary-email/README.md (100%) rename {ballerina-integrator => integrator-default-profile/samples}/trello-summary-email/agents.bal (100%) rename {ballerina-integrator => integrator-default-profile/samples}/trello-summary-email/automation.bal (100%) rename {ballerina-integrator => integrator-default-profile/samples}/trello-summary-email/config.bal (100%) rename {ballerina-integrator => integrator-default-profile/samples}/trello-summary-email/connections.bal (100%) rename {ballerina-integrator => integrator-default-profile/samples}/trello-summary-email/data_mappings.bal (100%) rename {ballerina-integrator => integrator-default-profile/samples}/trello-summary-email/functions.bal (100%) rename {ballerina-integrator => integrator-default-profile/samples}/trello-summary-email/main.bal (100%) rename {ballerina-integrator => integrator-default-profile/samples}/trello-summary-email/types.bal (100%) diff --git a/.github/workflows/projects.json b/.github/workflows/projects.json index d5fe1a49..a89579fa 100644 --- a/.github/workflows/projects.json +++ b/.github/workflows/projects.json @@ -34,5 +34,8 @@ }, { "path": "integrator-default-profile/samples/github-issue-to-google-chat" + }, + { + "path": "integrator-default-profile/samples/trello-summary-email" } ] diff --git a/ballerina-integrator/trello-summary-email/.choreo/config-schema.json b/integrator-default-profile/samples/trello-summary-email/.choreo/config-schema.json similarity index 100% rename from ballerina-integrator/trello-summary-email/.choreo/config-schema.json rename to integrator-default-profile/samples/trello-summary-email/.choreo/config-schema.json diff --git a/ballerina-integrator/trello-summary-email/.choreo/diagram.md b/integrator-default-profile/samples/trello-summary-email/.choreo/diagram.md similarity index 100% rename from ballerina-integrator/trello-summary-email/.choreo/diagram.md rename to integrator-default-profile/samples/trello-summary-email/.choreo/diagram.md diff --git a/ballerina-integrator/trello-summary-email/.choreo/instructions.md b/integrator-default-profile/samples/trello-summary-email/.choreo/instructions.md similarity index 100% rename from ballerina-integrator/trello-summary-email/.choreo/instructions.md rename to integrator-default-profile/samples/trello-summary-email/.choreo/instructions.md diff --git a/ballerina-integrator/trello-summary-email/.gitignore b/integrator-default-profile/samples/trello-summary-email/.gitignore similarity index 100% rename from ballerina-integrator/trello-summary-email/.gitignore rename to integrator-default-profile/samples/trello-summary-email/.gitignore diff --git a/ballerina-integrator/trello-summary-email/Ballerina.toml b/integrator-default-profile/samples/trello-summary-email/Ballerina.toml similarity index 100% rename from ballerina-integrator/trello-summary-email/Ballerina.toml rename to integrator-default-profile/samples/trello-summary-email/Ballerina.toml diff --git a/ballerina-integrator/trello-summary-email/README.md b/integrator-default-profile/samples/trello-summary-email/README.md similarity index 100% rename from ballerina-integrator/trello-summary-email/README.md rename to integrator-default-profile/samples/trello-summary-email/README.md diff --git a/ballerina-integrator/trello-summary-email/agents.bal b/integrator-default-profile/samples/trello-summary-email/agents.bal similarity index 100% rename from ballerina-integrator/trello-summary-email/agents.bal rename to integrator-default-profile/samples/trello-summary-email/agents.bal diff --git a/ballerina-integrator/trello-summary-email/automation.bal b/integrator-default-profile/samples/trello-summary-email/automation.bal similarity index 100% rename from ballerina-integrator/trello-summary-email/automation.bal rename to integrator-default-profile/samples/trello-summary-email/automation.bal diff --git a/ballerina-integrator/trello-summary-email/config.bal b/integrator-default-profile/samples/trello-summary-email/config.bal similarity index 100% rename from ballerina-integrator/trello-summary-email/config.bal rename to integrator-default-profile/samples/trello-summary-email/config.bal diff --git a/ballerina-integrator/trello-summary-email/connections.bal b/integrator-default-profile/samples/trello-summary-email/connections.bal similarity index 100% rename from ballerina-integrator/trello-summary-email/connections.bal rename to integrator-default-profile/samples/trello-summary-email/connections.bal diff --git a/ballerina-integrator/trello-summary-email/data_mappings.bal b/integrator-default-profile/samples/trello-summary-email/data_mappings.bal similarity index 100% rename from ballerina-integrator/trello-summary-email/data_mappings.bal rename to integrator-default-profile/samples/trello-summary-email/data_mappings.bal diff --git a/ballerina-integrator/trello-summary-email/functions.bal b/integrator-default-profile/samples/trello-summary-email/functions.bal similarity index 100% rename from ballerina-integrator/trello-summary-email/functions.bal rename to integrator-default-profile/samples/trello-summary-email/functions.bal diff --git a/ballerina-integrator/trello-summary-email/main.bal b/integrator-default-profile/samples/trello-summary-email/main.bal similarity index 100% rename from ballerina-integrator/trello-summary-email/main.bal rename to integrator-default-profile/samples/trello-summary-email/main.bal diff --git a/ballerina-integrator/trello-summary-email/types.bal b/integrator-default-profile/samples/trello-summary-email/types.bal similarity index 100% rename from ballerina-integrator/trello-summary-email/types.bal rename to integrator-default-profile/samples/trello-summary-email/types.bal From dff75b015cf27927d766a8235b9f5f38ff65d017 Mon Sep 17 00:00:00 2001 From: Anjana Edirisinghe Date: Fri, 13 Mar 2026 12:10:39 +0530 Subject: [PATCH 10/18] added simplified diagram --- .../trello-summary-email/.choreo/diagram.md | 21 +++++++------------ .../trello-summary-email/Ballerina.toml | 2 +- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/integrator-default-profile/samples/trello-summary-email/.choreo/diagram.md b/integrator-default-profile/samples/trello-summary-email/.choreo/diagram.md index 73952b57..fd3faae8 100644 --- a/integrator-default-profile/samples/trello-summary-email/.choreo/diagram.md +++ b/integrator-default-profile/samples/trello-summary-email/.choreo/diagram.md @@ -1,17 +1,10 @@ A(["Begin"]):::startNode - B["Fetch Cards from
Trello Boards & Lists"]:::processNode - C{"Are there Cards?"}:::decisionNode - D["Apply Filters
(Labels / Members / Due Date)"]:::processNode - E{"Cards remaining
after filtering?"}:::decisionNode - F["Group Cards
(by List / Member / Label)"]:::processNode - G["Generate HTML
Email Content"]:::processNode - H["Create Mailchimp
Email Campaign"]:::processNode - I["Send Campaign to
Mailchimp Audience"]:::processNode - J(["Complete"]):::endNode - K(["Skip - No Cards"]):::endNode + B["Fetch & Filter
Trello Cards"]:::processNode + C{"Matching
Cards Found?"}:::decisionNode + D["Group Cards &
Generate HTML Email"]:::processNode + E["Create & Send
Mailchimp Campaign"]:::processNode + F(["End"]):::endNode A --> B --> C - C -- Yes --> D --> E - C -- No --> K - E -- Yes --> F --> G --> H --> I --> J - E -- No --> K + C -- Yes --> D --> E --> F + C -- No --> F \ No newline at end of file diff --git a/integrator-default-profile/samples/trello-summary-email/Ballerina.toml b/integrator-default-profile/samples/trello-summary-email/Ballerina.toml index 11ddd34f..6bd22aa9 100644 --- a/integrator-default-profile/samples/trello-summary-email/Ballerina.toml +++ b/integrator-default-profile/samples/trello-summary-email/Ballerina.toml @@ -6,4 +6,4 @@ distribution = "2201.13.1" title = "trello-summary-email" [build-options] -sticky = true \ No newline at end of file +sticky = true From 8d7cf566dc800a414f86734c885a1aefeb402914 Mon Sep 17 00:00:00 2001 From: Anjana Edirisinghe <91928786+anjanaed@users.noreply.github.com> Date: Fri, 13 Mar 2026 12:08:03 +0530 Subject: [PATCH 11/18] Update ballerina-integrator/trello-summary-email/.choreo/config-schema.json Co-authored-by: Haritha Hasathcharu <42366642+hasathcharu@users.noreply.github.com> --- .../samples/trello-summary-email/.choreo/config-schema.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/integrator-default-profile/samples/trello-summary-email/.choreo/config-schema.json b/integrator-default-profile/samples/trello-summary-email/.choreo/config-schema.json index 3d67ec4e..7d6f19a4 100644 --- a/integrator-default-profile/samples/trello-summary-email/.choreo/config-schema.json +++ b/integrator-default-profile/samples/trello-summary-email/.choreo/config-schema.json @@ -668,5 +668,6 @@ "additionalProperties": false } }, - "additionalProperties": false + "additionalProperties": false, + "requiredLevel": 3 } From 38ce13c37ff9ba3e888c9e7ac94e009c0ade2274 Mon Sep 17 00:00:00 2001 From: Anjana Edirisinghe Date: Fri, 13 Mar 2026 14:01:26 +0530 Subject: [PATCH 12/18] update config.bal --- .../samples/trello-summary-email/config.bal | 2 -- 1 file changed, 2 deletions(-) diff --git a/integrator-default-profile/samples/trello-summary-email/config.bal b/integrator-default-profile/samples/trello-summary-email/config.bal index 180cd721..2f4104e5 100644 --- a/integrator-default-profile/samples/trello-summary-email/config.bal +++ b/integrator-default-profile/samples/trello-summary-email/config.bal @@ -15,8 +15,6 @@ configurable record { boolean includeDateInSubject = true; } mailchimpConfig = ?; - - configurable record { string[] labels = []; string[] members = []; From dee44f11baab2fc72777d89dbfde3ae2ee788466 Mon Sep 17 00:00:00 2001 From: Anjana Edirisinghe <91928786+anjanaed@users.noreply.github.com> Date: Fri, 13 Mar 2026 13:49:51 +0530 Subject: [PATCH 13/18] Update ballerina-integrator/trello-summary-email/.gitignore Co-authored-by: Haritha Hasathcharu <42366642+hasathcharu@users.noreply.github.com> --- .../samples/trello-summary-email/.gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integrator-default-profile/samples/trello-summary-email/.gitignore b/integrator-default-profile/samples/trello-summary-email/.gitignore index 31794861..fa4d3d61 100644 --- a/integrator-default-profile/samples/trello-summary-email/.gitignore +++ b/integrator-default-profile/samples/trello-summary-email/.gitignore @@ -9,4 +9,4 @@ generated/ # Contains configuration values used during development time. # See https://ballerina.io/learn/provide-values-to-configurable-variables/ for more details. Config.toml -Dependencies.toml \ No newline at end of file +Dependencies.toml From a38a000fa03c35eaa19d975d52e8357699db2bc9 Mon Sep 17 00:00:00 2001 From: Anjana Edirisinghe Date: Fri, 13 Mar 2026 16:59:44 +0530 Subject: [PATCH 14/18] Refactor Trello API integration and enhance card processing with member mapping --- .../trello-summary-email/connections.bal | 2 +- .../trello-summary-email/functions.bal | 78 +++++++++++++++---- 2 files changed, 63 insertions(+), 17 deletions(-) diff --git a/integrator-default-profile/samples/trello-summary-email/connections.bal b/integrator-default-profile/samples/trello-summary-email/connections.bal index b84b9b40..7a4e846c 100644 --- a/integrator-default-profile/samples/trello-summary-email/connections.bal +++ b/integrator-default-profile/samples/trello-summary-email/connections.bal @@ -8,7 +8,7 @@ final trello:Client trelloClient = check new ({ }); final http:Client trelloHttpClient = check new ("https://api.trello.com/1"); -#Had to use a separate client for Trello API calls as the trello:Client does not support all endpoints needed for fetching card details and attachments. +//Had to use a separate client for Trello API calls as the trello:Client does not support all endpoints needed for fetching card details and attachments. final mailchimp:Client mailchimpClient = check new ( config = { diff --git a/integrator-default-profile/samples/trello-summary-email/functions.bal b/integrator-default-profile/samples/trello-summary-email/functions.bal index 2b464156..cdfebc46 100644 --- a/integrator-default-profile/samples/trello-summary-email/functions.bal +++ b/integrator-default-profile/samples/trello-summary-email/functions.bal @@ -12,6 +12,26 @@ function fetchListCardsAsJson(string listId) returns json[]|error { return check cardsJson.ensureType(); } +// Fetches all members of a board upfront and builds a memberId -> fullName lookup map. +// Should switch back to trello:Client once issues fixed. +function fetchBoardMembersMap(string boardId) returns map|error { + json membersJson = check trelloHttpClient->/boards/[boardId]/members.get( + 'key = trelloConfig.key, + token = trelloConfig.token, + fields = "id,fullName" + ); + json[] membersArray = check membersJson.ensureType(); + map memberMap = {}; + foreach json memberJson in membersArray { + string? memberId = check memberJson.id; + string? fullName = check memberJson.fullName; + if memberId is string && fullName is string { + memberMap[memberId] = fullName; + } + } + return memberMap; +} + function fetchTrelloCards() returns CardSummary[]|error { CardSummary[] allCards = []; @@ -36,6 +56,8 @@ function fetchTrelloCards() returns CardSummary[]|error { ); string boardName = board.name ?: "Unknown Board"; + map memberMap = check fetchBoardMembersMap(boardId); + json boardJson = board.toJson(); json listsJson = check boardJson.lists; json[] listsArray = check listsJson.ensureType(); @@ -51,7 +73,7 @@ function fetchTrelloCards() returns CardSummary[]|error { json[] cardsArray = check fetchListCardsAsJson(listId); foreach json cardJson in cardsArray { - CardSummary? cardSummary = check processCardFromJson(cardJson, listName, boardName); + CardSummary? cardSummary = check processCardFromJson(cardJson, listName, boardName, memberMap); if cardSummary is CardSummary { allCards.push(cardSummary); } @@ -62,7 +84,7 @@ function fetchTrelloCards() returns CardSummary[]|error { return allCards; } -function processCardFromJson(json cardJson, string listName, string boardName) returns CardSummary?|error { +function processCardFromJson(json cardJson, string listName, string boardName, map memberMap) returns CardSummary?|error { string cardId = check cardJson.id; string cardName = check cardJson.name; string cardUrl = check cardJson.url; @@ -128,15 +150,9 @@ function processCardFromJson(json cardJson, string listName, string boardName) r json? membersJson = check cardJson.idMembers; if membersJson is json[] { foreach json memberIdJson in membersJson { - string? memberIdStr = memberIdJson.toString(); - if memberIdStr is string { - trello:InlineResponse2001|error memberInfo = trelloClient->/members/[memberIdStr].get(); - if memberInfo is trello:InlineResponse2001 { - string? fullName = memberInfo?.fullName; - if fullName is string { - memberNames.push(fullName); - } - } + string memberIdStr = memberIdJson.toString(); + if memberMap.hasKey(memberIdStr) { + memberNames.push(memberMap.get(memberIdStr)); } } } @@ -248,9 +264,39 @@ function getFormattedTimestamp(time:Utc utcTime) returns string { return string `${civil.year}-${civil.month.toString().padZero(2)}-${civil.day.toString().padZero(2)} ${civil.hour.toString().padZero(2)}:${civil.minute.toString().padZero(2)} UTC`; } +function escapeHtml(string value) returns string { + string escaped = ""; + foreach int i in 0 ..< value.length() { + string ch = value.substring(i, i + 1); + match ch { + "&" => { + escaped += "&"; + } + "<" => { + escaped += "<"; + } + ">" => { + escaped += ">"; + } + "\"" => { + escaped += """; + } + "'" => { + escaped += "'"; + } + _ => { + escaped += ch; + } + } + } + return escaped; +} + function getHtmlFormattedGroup(GroupedSummary group) returns string { string cardsHtml = ""; foreach CardSummary card in group.cards { + string cardNameEscaped = escapeHtml(card.name); + string cardUrlEscaped = escapeHtml(card.url); string leftBorder = card.isOverdue ? " style=\"border-left: 3px solid #de350b; padding: 16px 30px; border-bottom: 1px solid #e1e4e8;\"" : (card.isStale @@ -267,12 +313,12 @@ function getHtmlFormattedGroup(GroupedSummary group) returns string { string labelsHtml = ""; foreach string label in card.labels { - labelsHtml += string `${label}`; + labelsHtml += string `${escapeHtml(label)}`; } string membersHtml = ""; foreach string member in card.members { - membersHtml += member + " "; + membersHtml += escapeHtml(member) + " "; } string cardAgeHtml = summaryConfig.showCardAge @@ -291,7 +337,7 @@ function getHtmlFormattedGroup(GroupedSummary group) returns string { if card.description.trim().length() > 0 { string truncatedDesc = card.description.length() > 200 ? card.description.substring(0, 200) + "..." : card.description; - descHtml = string `
${truncatedDesc}
`; + descHtml = string `
${escapeHtml(truncatedDesc)}
`; } string overdueTag = summaryConfig.highlightOverdueCards && card.isOverdue @@ -306,7 +352,7 @@ function getHtmlFormattedGroup(GroupedSummary group) returns string { cardsHtml += string ` - ${card.name}${overdueTag} + ${cardNameEscaped}${overdueTag}
${metaHtml}
${labelsRow} ${membersRow} @@ -318,7 +364,7 @@ function getHtmlFormattedGroup(GroupedSummary group) returns string { return string ` - ${group.groupName} + ${escapeHtml(group.groupName)} (${group.cards.length().toString()} cards) From c5d25177f3bcd54dc9167337fbcf5be9b18f7e38 Mon Sep 17 00:00:00 2001 From: Anjana Edirisinghe Date: Thu, 26 Mar 2026 12:59:01 +0530 Subject: [PATCH 15/18] minor bug fixes --- .../.choreo/instructions.md | 10 ++--- .../trello-summary-email/automation.bal | 3 +- .../trello-summary-email/functions.bal | 43 ++++++++++--------- 3 files changed, 29 insertions(+), 27 deletions(-) diff --git a/integrator-default-profile/samples/trello-summary-email/.choreo/instructions.md b/integrator-default-profile/samples/trello-summary-email/.choreo/instructions.md index 27ac576e..fe042080 100644 --- a/integrator-default-profile/samples/trello-summary-email/.choreo/instructions.md +++ b/integrator-default-profile/samples/trello-summary-email/.choreo/instructions.md @@ -39,18 +39,18 @@ 1. `filterConfig.labels` - Filter cards by label name. Leave empty to include all labels. -3. `filterConfig.members` +2. `filterConfig.members` - Filter cards by member full name. Leave empty to include all members. -4. `filterConfig.includeDueDateFilter` +3. `filterConfig.includeDueDateFilter` - Set to `true` to only include cards due within the next `dueDateDaysAhead` days. -5. `mailchimpConfig.includeDateInSubject` +4. `mailchimpConfig.includeDateInSubject` - If `true` (default), today's date is included in the email subject (e.g., `Trello Cards Summary - 2026-03-12`). -6. `summaryConfig.grouping` +5. `summaryConfig.grouping` - How to group cards in the email. Possible values: - `LIST` (default) - `MEMBER` - `LABEL` -7. `summaryConfig.staleCardDays` +6. `summaryConfig.staleCardDays` - Cards with no activity for this many days are considered stale (default: `30`).
diff --git a/integrator-default-profile/samples/trello-summary-email/automation.bal b/integrator-default-profile/samples/trello-summary-email/automation.bal index 0fcba442..efb4cab2 100644 --- a/integrator-default-profile/samples/trello-summary-email/automation.bal +++ b/integrator-default-profile/samples/trello-summary-email/automation.bal @@ -13,11 +13,12 @@ function sendTrelloSummary() returns error? { } int overdueCount = countOverdueCards(cards); + int staleCount = countStaleCards(cards); GroupedSummary[] groupedSummaries = groupCards(cards); log:printInfo(string `Grouped cards into ${groupedSummaries.length().toString()} groups`); - string emailContent = generateEmailContent(groupedSummaries, cards.length(), overdueCount); + string emailContent = generateEmailContent(groupedSummaries, cards.length(), overdueCount, staleCount); check sendEmailSummary(emailContent); log:printInfo("Email sent successfully"); diff --git a/integrator-default-profile/samples/trello-summary-email/functions.bal b/integrator-default-profile/samples/trello-summary-email/functions.bal index cdfebc46..59834c2f 100644 --- a/integrator-default-profile/samples/trello-summary-email/functions.bal +++ b/integrator-default-profile/samples/trello-summary-email/functions.bal @@ -150,9 +150,9 @@ function processCardFromJson(json cardJson, string listName, string boardName, m json? membersJson = check cardJson.idMembers; if membersJson is json[] { foreach json memberIdJson in membersJson { - string memberIdStr = memberIdJson.toString(); - if memberMap.hasKey(memberIdStr) { - memberNames.push(memberMap.get(memberIdStr)); + string? memberId = check memberIdJson; + if memberId is string && memberMap.hasKey(memberId) { + memberNames.push(memberMap.get(memberId)); } } } @@ -265,31 +265,31 @@ function getFormattedTimestamp(time:Utc utcTime) returns string { } function escapeHtml(string value) returns string { - string escaped = ""; + string[] parts = []; foreach int i in 0 ..< value.length() { string ch = value.substring(i, i + 1); match ch { "&" => { - escaped += "&"; + parts.push("&"); } "<" => { - escaped += "<"; + parts.push("<"); } ">" => { - escaped += ">"; + parts.push(">"); } "\"" => { - escaped += """; + parts.push("""); } "'" => { - escaped += "'"; + parts.push("'"); } _ => { - escaped += ch; + parts.push(ch); } } } - return escaped; + return string:'join("", ...parts); } function getHtmlFormattedGroup(GroupedSummary group) returns string { @@ -385,16 +385,7 @@ function formatPercentageTwoDecimals(decimal value) returns string { return string `${wholePart.toString()}.${decimalPart.toString().padZero(2)}`; } -function generateEmailContent(GroupedSummary[] groupedSummaries, int totalCards, int overdueCount) returns string { - int staleCount = 0; - foreach GroupedSummary group in groupedSummaries { - foreach CardSummary card in group.cards { - if card.isStale { - staleCount += 1; - } - } - } - +function generateEmailContent(GroupedSummary[] groupedSummaries, int totalCards, int overdueCount, int staleCount) returns string { string groupedByStr = summaryConfig.grouping.toString(); string generatedAt = getFormattedTimestamp(time:utcNow()); @@ -533,3 +524,13 @@ function countOverdueCards(CardSummary[] cards) returns int { } return count; } + +function countStaleCards(CardSummary[] cards) returns int { + int count = 0; + foreach CardSummary card in cards { + if card.isStale { + count += 1; + } + } + return count; +} From 1c882d095f32f0145b892fc6e72e92df7d38cf64 Mon Sep 17 00:00:00 2001 From: Anjana Edirisinghe Date: Thu, 2 Apr 2026 10:52:34 +0530 Subject: [PATCH 16/18] requested changes applied --- .../trello-summary-email/.choreo/diagram.md | 18 +++++++++--------- .../trello-summary-email/automation.bal | 2 -- .../samples/trello-summary-email/config.bal | 3 +-- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/integrator-default-profile/samples/trello-summary-email/.choreo/diagram.md b/integrator-default-profile/samples/trello-summary-email/.choreo/diagram.md index fd3faae8..b9ef7060 100644 --- a/integrator-default-profile/samples/trello-summary-email/.choreo/diagram.md +++ b/integrator-default-profile/samples/trello-summary-email/.choreo/diagram.md @@ -1,10 +1,10 @@ - A(["Begin"]):::startNode - B["Fetch & Filter
Trello Cards"]:::processNode - C{"Matching
Cards Found?"}:::decisionNode - D["Group Cards &
Generate HTML Email"]:::processNode - E["Create & Send
Mailchimp Campaign"]:::processNode - F(["End"]):::endNode +A(["Begin"]):::startNode +B["Fetch & Filter
Trello Cards"]:::processNode +C{"Matching
Cards Found?"}:::decisionNode +D["Group Cards &
Generate HTML Email"]:::processNode +E["Create & Send
Mailchimp Campaign"]:::processNode +F(["End"]):::endNode - A --> B --> C - C -- Yes --> D --> E --> F - C -- No --> F \ No newline at end of file +A --> B --> C +C -- Yes --> D --> E --> F +C -- No --> F \ No newline at end of file diff --git a/integrator-default-profile/samples/trello-summary-email/automation.bal b/integrator-default-profile/samples/trello-summary-email/automation.bal index efb4cab2..ca06faf7 100644 --- a/integrator-default-profile/samples/trello-summary-email/automation.bal +++ b/integrator-default-profile/samples/trello-summary-email/automation.bal @@ -23,5 +23,3 @@ function sendTrelloSummary() returns error? { check sendEmailSummary(emailContent); log:printInfo("Email sent successfully"); } - - diff --git a/integrator-default-profile/samples/trello-summary-email/config.bal b/integrator-default-profile/samples/trello-summary-email/config.bal index 2f4104e5..b00e2f55 100644 --- a/integrator-default-profile/samples/trello-summary-email/config.bal +++ b/integrator-default-profile/samples/trello-summary-email/config.bal @@ -29,5 +29,4 @@ configurable record { int staleCardDays = 30; boolean showAttachmentCount = true; boolean showChecklistProgress = true; -} summaryConfig = {}; - +} summaryConfig = {}; \ No newline at end of file From 81e356082c9f57c6c027e1b72fa0476cbb5831b4 Mon Sep 17 00:00:00 2001 From: Anjana Edirisinghe <91928786+anjanaed@users.noreply.github.com> Date: Thu, 2 Apr 2026 12:54:33 +0530 Subject: [PATCH 17/18] Update ballerina-integrator/trello-summary-email/config.bal Co-authored-by: Haritha Hasathcharu <42366642+hasathcharu@users.noreply.github.com> --- .../samples/trello-summary-email/config.bal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integrator-default-profile/samples/trello-summary-email/config.bal b/integrator-default-profile/samples/trello-summary-email/config.bal index b00e2f55..66397ebe 100644 --- a/integrator-default-profile/samples/trello-summary-email/config.bal +++ b/integrator-default-profile/samples/trello-summary-email/config.bal @@ -29,4 +29,4 @@ configurable record { int staleCardDays = 30; boolean showAttachmentCount = true; boolean showChecklistProgress = true; -} summaryConfig = {}; \ No newline at end of file +} summaryConfig = {}; From 7c0e8fa4dfda2c85ffbf6071b5530e06e27b6530 Mon Sep 17 00:00:00 2001 From: Anjana Edirisinghe <91928786+anjanaed@users.noreply.github.com> Date: Tue, 28 Apr 2026 15:00:48 +0530 Subject: [PATCH 18/18] minor change --- .../samples/trello-summary-email/functions.bal | 1 - 1 file changed, 1 deletion(-) diff --git a/integrator-default-profile/samples/trello-summary-email/functions.bal b/integrator-default-profile/samples/trello-summary-email/functions.bal index 59834c2f..a35a0f19 100644 --- a/integrator-default-profile/samples/trello-summary-email/functions.bal +++ b/integrator-default-profile/samples/trello-summary-email/functions.bal @@ -473,7 +473,6 @@ function generateEmailContent(GroupedSummary[] groupedSummaries, int totalCards,   - `;