From 55aee11d4c87d46ffc03aca31c99b0c36a6a1c83 Mon Sep 17 00:00:00 2001 From: Akshat Date: Wed, 28 Jan 2026 21:17:04 +0530 Subject: [PATCH] News Loader app initial commit. --- .../appian-component-plugin.xml | 75 +++++ .../componentPlugin/newsLoader/v1/app.js | 266 ++++++++++++++++ .../componentPlugin/newsLoader/v1/image.svg | 13 + .../componentPlugin/newsLoader/v1/index.html | 14 + .../newsLoader/v1/newsLoader_en_US.properties | 16 + .../componentPlugin/newsLoader/v1/styles.css | 287 ++++++++++++++++++ .../connectedSystemPlugin/README.md | 42 +++ .../connectedSystemPlugin/build.gradle | 24 ++ .../connectedSystemPlugin/settings.gradle | 2 + .../LoadNewsFromFolderClientAPI.java | 84 +++++ .../NewsLoaderConnectedSystemTemplate.java | 39 +++ .../templates/StoreNewsToFolderClientAPI.java | 79 +++++ .../src/main/resources/appian-plugin.xml | 14 + .../main/resources/resources_en_US.properties | 2 + 14 files changed, 957 insertions(+) create mode 100644 Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/componentPlugin/appian-component-plugin.xml create mode 100644 Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/componentPlugin/newsLoader/v1/app.js create mode 100644 Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/componentPlugin/newsLoader/v1/image.svg create mode 100644 Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/componentPlugin/newsLoader/v1/index.html create mode 100644 Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/componentPlugin/newsLoader/v1/newsLoader_en_US.properties create mode 100644 Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/componentPlugin/newsLoader/v1/styles.css create mode 100644 Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/connectedSystemPlugin/README.md create mode 100644 Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/connectedSystemPlugin/build.gradle create mode 100644 Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/connectedSystemPlugin/settings.gradle create mode 100644 Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/connectedSystemPlugin/src/main/java/com/mycorp/newsloader/templates/LoadNewsFromFolderClientAPI.java create mode 100644 Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/connectedSystemPlugin/src/main/java/com/mycorp/newsloader/templates/NewsLoaderConnectedSystemTemplate.java create mode 100644 Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/connectedSystemPlugin/src/main/java/com/mycorp/newsloader/templates/StoreNewsToFolderClientAPI.java create mode 100644 Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/connectedSystemPlugin/src/main/resources/appian-plugin.xml create mode 100644 Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/connectedSystemPlugin/src/main/resources/resources_en_US.properties diff --git a/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/componentPlugin/appian-component-plugin.xml b/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/componentPlugin/appian-component-plugin.xml new file mode 100644 index 0000000..bb9c850 --- /dev/null +++ b/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/componentPlugin/appian-component-plugin.xml @@ -0,0 +1,75 @@ + + + + Loads latest News + + 1.0.0 + + + + 2.0.0 + chrome firefox ie11 edge safari mobile + image.svg + index.html + + connectedSystem + input-only + ConnectedSystem + connectedSystem field is required. + + + saveNewsToFolder + input-only + TypedValue + hidden + saveNewsToFolder field is required. + + + userDetails + input-only + Dictionary + + a!map( + "name": user(loggedInUser(), "firstName"), + "email": user(loggedInUser(), "email"), + "country": user(loggedInUser(), "country") + ) + + + { + "name": "Dummy Name", + "email": "dummy@email.com", + "country": "dummyCountry" + } + + + + canAddNews + input-only + Boolean + + a!isUserMemberOfGroup(loggedInUser(), "editors") + + visible + + + newsFreshness + input-only + + + latest + oneWeek + twoWeeks + oneMonth + + + "latest" + + + sortBy + input-only + Text + "latest-first" + + + diff --git a/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/componentPlugin/newsLoader/v1/app.js b/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/componentPlugin/newsLoader/v1/app.js new file mode 100644 index 0000000..bfc25ce --- /dev/null +++ b/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/componentPlugin/newsLoader/v1/app.js @@ -0,0 +1,266 @@ +let NewsLoadFriendlyName = "LoadNewsClientApi"; +let NewsStoreFriendlyName = "StoreNewsClientApi"; +const newsStructure = ["id", "title", "source", "date", "region", "content"]; + +state = { + userDetails: {}, + news: [], + region: null, + sortBy: null, + newsFreshness: null, + canAddNews: null, + connectedSystem: null +}; + +Appian.Component.onNewValue(async (newValues) => { + state.userDetails = newValues.userDetails; + state.region = newValues.region; + state.sortBy = newValues.sortBy; + state.newsFreshness = newValues.newsFreshness; + state.canAddNews = newValues.canAddNews; + state.connectedSystem = newValues.connectedSystem; + + state.news = await fetchNews(newValues.connectedSystem); + render(); + attachEventListeners(); +}) + +// Render app +function render() { + const app = document.getElementById('app'); + app.innerHTML = ` +
+
+ +
+ +
+

+ Fetching News for you +

+ + ${state.canAddNews != null && state.canAddNews === true ? '' : ''} +
+ + ${ + state.news?.length == 0 ? + `
+ no news to show +
` : + `
+ ${ + getFilteredNews().map(news =>` +
+
+ ${news.region} + ${formatDate(news.date)} +
+

${news.title}

+

${news.content}

+ +
+ `).join('') + } +
` + } +
+ + + `; +} + +// Filter and sort news +function getFilteredNews() { + let filtered = []; + for (let news of state.news) { + try { + let parsedNews = JSON.parse(news); + + if ( + parsedNews && + typeof parsedNews === "object" && + !Array.isArray(parsedNews) && + isValidNews(parsedNews) + ) { + filtered.push(parsedNews); + } + } catch (e) { + console.error("Invalid JSON:", e); + } + } + + // Filter by freshness + const now = new Date('2026-01-28'); + filtered = filtered.filter(news => { + const newsDate = new Date(news.date); + const daysDiff = Math.floor((now - newsDate) / (1000 * 60 * 60 * 24)); + + switch(state.newsFreshness) { + case 'latest': return daysDiff <= 3; + case 'oneWeek': return daysDiff <= 7; + case 'twoWeeks': return daysDiff <= 14; + case 'oneMonth': return daysDiff <= 30; + default: return true; + } + }); + + // Sort + filtered.sort((a, b) => { + const dateA = new Date(a.date); + const dateB = new Date(b.date); + return state.sortBy === 'latest-first' ? dateB - dateA : dateA - dateB; + }); + + return filtered; +} + +// Format date +function formatDate(dateStr) { + const date = new Date(dateStr); + const now = new Date(); + const daysDiff = Math.floor((now - date) / (1000 * 60 * 60 * 24)); + + if (daysDiff === 0) return 'Today'; + if (daysDiff === 1) return 'Yesterday'; + if (daysDiff < 7) return `${daysDiff} days ago`; + return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); +} + +// Event listeners +function attachEventListeners() { + const addBtn = document.getElementById('addNewsBtn'); + const modal = document.getElementById('modal'); + const closeModal = document.getElementById('closeModal'); + const cancelBtn = document.getElementById('cancelBtn'); + const newsForm = document.getElementById('newsForm'); + const newsFetch = document.getElementById('fetchNews'); + + if (addBtn) { + addBtn.addEventListener('click', () => { + modal.style.display = 'flex'; + }); + } + + if (closeModal) { + closeModal.addEventListener('click', () => { + modal.style.display = 'none'; + newsForm.reset(); + }); + } + + if (cancelBtn) { + cancelBtn.addEventListener('click', () => { + modal.style.display = 'none'; + newsForm.reset(); + }); + } + + if (newsForm) { + newsForm.addEventListener('submit', async (e) => { + e.preventDefault(); + + const newArticle = { + id: state.news?.length + 1, + title: document.getElementById('newsTitle').value, + content: document.getElementById('newsContent').value, + source: document.getElementById('newsSource').value, + region: document.getElementById('newsRegion').value, + date: new Date().toISOString().split('T')[0] + }; + + await storeNews(newArticle); + window.location.reload(); + }); + } + + if(newsFetch){ + newsFetch.addEventListener('click', () => { + fetchNews(state.connectedSystem); + }) + } + + modal?.addEventListener('click', (e) => { + if (e.target === modal) { + modal.style.display = 'none'; + newsForm.reset(); + } + }); +} + +function isValidNews(news) { + return newsStructure.every(key => + Object.prototype.hasOwnProperty.call(news, key) + ); +} + +async function storeNews(params) { + var connectedSystem = state.connectedSystem; + var payload = { + "news": JSON.stringify(params) + }; + if(connectedSystem){ + const resp = await Appian.Component.invokeClientApi(connectedSystem, NewsStoreFriendlyName, payload, ["canAddNews", "saveNewsToFolder"]); + return resp; + } +} + +async function fetchNews() { + var connectedSystem = state.connectedSystem; + if(connectedSystem){ + const resp = await Appian.Component.invokeClientApi(connectedSystem, NewsLoadFriendlyName, {}, ["saveNewsToFolder"]); + + if(resp.type === "INVOCATION_SUCCESS" && Object.prototype.hasOwnProperty.call(resp.payload, "news")){ + return resp.payload.news; + } else { + return []; + } + + } else { + console.error("Connected system not provided") + return []; + } +} diff --git a/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/componentPlugin/newsLoader/v1/image.svg b/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/componentPlugin/newsLoader/v1/image.svg new file mode 100644 index 0000000..c9716c9 --- /dev/null +++ b/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/componentPlugin/newsLoader/v1/image.svg @@ -0,0 +1,13 @@ + + + + + + + Appian Logo + + + \ No newline at end of file diff --git a/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/componentPlugin/newsLoader/v1/index.html b/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/componentPlugin/newsLoader/v1/index.html new file mode 100644 index 0000000..2d0d2a1 --- /dev/null +++ b/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/componentPlugin/newsLoader/v1/index.html @@ -0,0 +1,14 @@ + + + + + + NewsLoader + + + +
+ + + + diff --git a/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/componentPlugin/newsLoader/v1/newsLoader_en_US.properties b/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/componentPlugin/newsLoader/v1/newsLoader_en_US.properties new file mode 100644 index 0000000..c709a27 --- /dev/null +++ b/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/componentPlugin/newsLoader/v1/newsLoader_en_US.properties @@ -0,0 +1,16 @@ +name=newsLoader +description=A mock news loader component that will load news. +parameter.userDetails.name=userDetails +parameter.userDetails.description=User details as map that contains user_email, user_name, user_country keys. +parameter.canAddNews.name=addNewNews +parameter.canAddNews.description=Allow to add new news. +parameter.newsFreshness.name=newsFreshness +parameter.newsFreshness.description=Determines freshness of News. +parameter.sortBy.name=sortBy +parameter.sortBy.description=Order in which news will be shown. +parameter.region.name=region +parameter.region.description=News of whole region. +parameter.connectedSystem.name=connectedSystem +parameter.connectedSystem.description=Connected system constant for client API calls. +parameter.saveNewsToFolder.name=saveNewsToFolder +parameter.saveNewsToFolder.description=folder location where news would be saved. \ No newline at end of file diff --git a/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/componentPlugin/newsLoader/v1/styles.css b/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/componentPlugin/newsLoader/v1/styles.css new file mode 100644 index 0000000..8d209a7 --- /dev/null +++ b/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/componentPlugin/newsLoader/v1/styles.css @@ -0,0 +1,287 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #f8fafc; + min-height: 600px; + padding: 20px; +} + +.container { + max-width: 1200px; + margin: 0 auto; +} + +.header { + background: white; + padding: 30px; + border-radius: 12px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + margin-bottom: 30px; + border: 1px solid #e2e8f0; +} + +.user-info h1 { + font-size: 28px; + color: #0f172a; + margin-bottom: 8px; +} + +.user-details { + color: #64748b; + font-size: 14px; +} + +.controls { + background: white; + padding: 20px; + border-radius: 12px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + margin-bottom: 30px; + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 15px; + border: 1px solid #e2e8f0; +} + + +.btn-primary { + background: #3b82f6; + color: white; + border: none; + padding: 12px 24px; + border-radius: 8px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; +} + +.btn-primary:hover { + background: #2563eb; + box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); +} + +.news-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); + gap: 24px; +} + +.news-card { + background: white; + border-radius: 12px; + padding: 24px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + transition: all 0.2s; + display: flex; + flex-direction: column; + border: 1px solid #e2e8f0; +} + +.news-card:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + border-color: #cbd5e0; +} + +.news-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; +} + +.news-region { + background: #3b82f6; + color: white; + padding: 4px 12px; + border-radius: 20px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.news-date { + color: #94a3b8; + font-size: 12px; + font-weight: 500; +} + +.news-title { + font-size: 18px; + color: #0f172a; + margin-bottom: 12px; + line-height: 1.4; +} + +.news-content { + color: #475569; + font-size: 14px; + line-height: 1.6; + margin-bottom: 16px; + flex-grow: 1; +} + +.news-footer { + padding-top: 12px; + border-top: 1px solid #e2e8f0; +} + +.news-source { + color: #64748b; + font-size: 13px; + font-weight: 600; +} + +@media (max-width: 768px) { + .news-grid { + grid-template-columns: 1fr; + } + + .controls { + flex-direction: column; + align-items: stretch; + } + + .filters { + flex-direction: column; + } + + .filter-group select { + width: 100%; + } +} + +.modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + align-items: center; + justify-content: center; + z-index: 1000; +} + +.modal-content { + background: white; + border-radius: 12px; + width: 90%; + max-width: 600px; + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1); +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 24px; + border-bottom: 1px solid #e2e8f0; +} + +.modal-header h2 { + font-size: 20px; + color: #0f172a; +} + +.close-btn { + background: none; + border: none; + font-size: 28px; + color: #94a3b8; + cursor: pointer; + line-height: 1; +} + +.close-btn:hover { + color: #475569; +} + +#newsForm { + padding: 24px; +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + font-size: 14px; + font-weight: 600; + color: #475569; + margin-bottom: 8px; +} + +.form-group input, +.form-group textarea, +.form-group select { + width: 100%; + padding: 10px 14px; + border: 1px solid #cbd5e0; + border-radius: 8px; + font-size: 14px; + color: #0f172a; + font-family: inherit; +} + +.form-group input:focus, +.form-group textarea:focus, +.form-group select:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} + +.form-actions { + display: flex; + justify-content: flex-end; + gap: 12px; + margin-top: 24px; +} + +.btn-secondary { + background: white; + color: #475569; + border: 1px solid #cbd5e0; + padding: 12px 24px; + border-radius: 8px; + font-size: 14px; + font-weight: 600; + cursor: pointer; +} + +.btn-secondary:hover { + background: #f8fafc; +} + +.empty-state { + background: white; + border-radius: 12px; + padding: 60px 24px; + text-align: center; + color: #64748b; + font-size: 16px; + border: 1px solid #e2e8f0; +} + +.no-news{ + width: 100%; + text-align: center; + color: #475569; +} \ No newline at end of file diff --git a/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/connectedSystemPlugin/README.md b/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/connectedSystemPlugin/README.md new file mode 100644 index 0000000..1206298 --- /dev/null +++ b/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/connectedSystemPlugin/README.md @@ -0,0 +1,42 @@ +## How to install +The Component Plug-in and the Connected System Plug-in are packaged in two separate bundles. Installing both requires creating both bundles and then placing both into the plug-in directory separately. + +#### Installing the Connected System Plug-in +* Enter the `connectedSystemPlugin` directory +* Make sure you have `lib/appian-plug-in-sdk.jar` +* Run the gradle JAR task (typically `./gradlew build`) +* Drop the generated jar (which will be located in `build/libs/`) into the plugin directory of your Appian install `/_admin/plugins` + +#### Installing the Component Plug-in +* See https://github.com/appian/integration-sdk-examples/blob/master/Component%20Plug-in%20(CP)%20Examples/README.md for installation instructions for the Component Plug-in + +## Sample interface +``` +newsLoader( + label: "newsLoader", + labelPosition: "ABOVE", + validations: {}, + height: "AUTO", + connectedSystem: null, + saveNewsToFolder: null, + userDetails: a!map( + "name": user( + loggedInUser(), + "firstName" + ), + "email": user( + loggedInUser(), + "email" + ), + "country": user( + loggedInUser(), + "country" + ) + ), + canAddNews: a!isUserMemberOfGroup( + loggedInUser(), + "editors" + ), + newsFreshness: "latest" +) +``` diff --git a/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/connectedSystemPlugin/build.gradle b/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/connectedSystemPlugin/build.gradle new file mode 100644 index 0000000..a5b9952 --- /dev/null +++ b/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/connectedSystemPlugin/build.gradle @@ -0,0 +1,24 @@ +plugins { + id 'java' +} + +group = 'org.mycorp' +version = '1.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +dependencies { + compileOnly 'com.appian:connected-systems-core:1.10.0' + implementation 'com.appian:connected-systems-client:1.4.0' + testImplementation platform('org.junit:junit-bom:5.10.0') + testImplementation 'org.junit.jupiter:junit-jupiter' + + // Appian's suite API jar + compileOnly files('lib/appian-plug-in-sdk.jar') +} + +test { + useJUnitPlatform() +} diff --git a/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/connectedSystemPlugin/settings.gradle b/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/connectedSystemPlugin/settings.gradle new file mode 100644 index 0000000..a83ef93 --- /dev/null +++ b/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/connectedSystemPlugin/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'news-loader' + diff --git a/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/connectedSystemPlugin/src/main/java/com/mycorp/newsloader/templates/LoadNewsFromFolderClientAPI.java b/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/connectedSystemPlugin/src/main/java/com/mycorp/newsloader/templates/LoadNewsFromFolderClientAPI.java new file mode 100644 index 0000000..7774068 --- /dev/null +++ b/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/connectedSystemPlugin/src/main/java/com/mycorp/newsloader/templates/LoadNewsFromFolderClientAPI.java @@ -0,0 +1,84 @@ +package com.mycorp.newsloader.templates; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.appian.connectedsystems.simplified.sdk.SimpleClientApi; +import com.appian.connectedsystems.simplified.sdk.SimpleClientApiRequest; +import com.appian.connectedsystems.templateframework.sdk.ClientApiResponse; +import com.appian.connectedsystems.templateframework.sdk.ExecutionContext; +import com.appian.connectedsystems.templateframework.sdk.TemplateId; +import com.appiancorp.suiteapi.common.exceptions.AppianStorageException; +import com.appiancorp.suiteapi.content.ContentFilter; +import com.appiancorp.suiteapi.content.ContentService; +import com.appiancorp.suiteapi.content.DocumentInputStream; +import com.appiancorp.suiteapi.content.exceptions.InvalidContentException; +import com.appiancorp.suiteapi.content.exceptions.InvalidTypeMaskException; +import com.appiancorp.suiteapi.type.TypedValue; +import com.appiancorp.type.AppianTypeLong; + +/** + * This is an example of a Client API that performs an operation when executed from a + * Component Plug-in (CP). The Client API accepts a data structure which contains + * the CP request payload as well as the data stored inside the Connected System object. + * It uses both pieces to perform the operation. + * + * In this example, Client API gets folder reference (TypedValue) for the request payload + * and return all the news stored in the files in that folder. + */ + +@TemplateId(name = "LoadNewsClientApi") +public class LoadNewsFromFolderClientAPI extends SimpleClientApi { + + private final ContentService contentService; + + public LoadNewsFromFolderClientAPI(ContentService contentService){ + this.contentService = contentService; + } + + @Override + protected ClientApiResponse execute(SimpleClientApiRequest simpleClientApiRequest, ExecutionContext executionContext){ + + // Folder reference provided in the request's secured payload. + TypedValue folderCons = (TypedValue)simpleClientApiRequest.getSecuredPayload().getOrDefault("saveNewsToFolder", new TypedValue()); + if(folderCons.getValue() == null){ + throw new IllegalStateException("saveNewsToFolder should be null."); + } + + if(!AppianTypeLong.FOLDER.equals(folderCons.getTypeRef().getId())){ + throw new IllegalStateException("saveNewsToFolder should be set to a constant pointing to a folder."); + } + + Long folderId = (Long) folderCons.getValue(); + List newsJson = new ArrayList<>(); + + try { + // Getting all the files IDs stored in that folder + Long[] docIds = contentService.getAllChildrenIds(folderId, ContentFilter.DOCUMENTS, 0); + + for(Long id: docIds){ + // Reading file content via file ID. + DocumentInputStream docStream = contentService.getDocumentInputStream(id); + + try(BufferedReader reader = new BufferedReader(new InputStreamReader(docStream, StandardCharsets.UTF_8))) { + String news = reader.lines().collect(Collectors.joining("\n")); + newsJson.add(news); + } catch ( IOException e) { + throw new IllegalStateException("Cannot read files in saveNewsToFolder's folder."); + } + } + + } catch (InvalidContentException | InvalidTypeMaskException | AppianStorageException e) { + throw new RuntimeException(e); + } + return new ClientApiResponse(Map.of( + "news", newsJson + )); + } +} diff --git a/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/connectedSystemPlugin/src/main/java/com/mycorp/newsloader/templates/NewsLoaderConnectedSystemTemplate.java b/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/connectedSystemPlugin/src/main/java/com/mycorp/newsloader/templates/NewsLoaderConnectedSystemTemplate.java new file mode 100644 index 0000000..2b2f1c2 --- /dev/null +++ b/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/connectedSystemPlugin/src/main/java/com/mycorp/newsloader/templates/NewsLoaderConnectedSystemTemplate.java @@ -0,0 +1,39 @@ +package com.mycorp.newsloader.templates; + +import com.appian.connectedsystems.simplified.sdk.configuration.SimpleConfiguration; +import com.appian.connectedsystems.simplified.sdk.connectiontesting.SimpleTestableConnectedSystemTemplate; +import com.appian.connectedsystems.templateframework.sdk.ExecutionContext; +import com.appian.connectedsystems.templateframework.sdk.TemplateId; +import com.appian.connectedsystems.templateframework.sdk.connectiontesting.TestConnectionResult; + +/** + * This Connected System template serves as a secure vault for mock credentials + * associated with a hypothetical third-party News API. + * + * NOTE: This configuration is for demonstration purposes only. It does not + * influence active Client API transactions, but rather illustrates best + * practices for centralized credential management within Connected Systems. + */ + +@TemplateId(name = "NewsLoaderConnectedSystemTemplate") +public class NewsLoaderConnectedSystemTemplate extends SimpleTestableConnectedSystemTemplate { + public static final String MOCK_NEWS_CORP_PASSWORD= "mockNewsCorpPassword"; + + @Override + protected SimpleConfiguration getConfiguration( + SimpleConfiguration simpleConfiguration, ExecutionContext executionContext) { + return simpleConfiguration.setProperties( + textProperty(MOCK_NEWS_CORP_PASSWORD) + .label("Password for Mock_News_Corp authentication") + .instructionText("Just a mock simulation of 3rd party API call.") + .isRequired(true) + .isImportCustomizable(true) + .build() + ); + } + + @Override + protected TestConnectionResult testConnection(SimpleConfiguration configuration, ExecutionContext executionContext) { + return TestConnectionResult.success(); + } +} diff --git a/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/connectedSystemPlugin/src/main/java/com/mycorp/newsloader/templates/StoreNewsToFolderClientAPI.java b/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/connectedSystemPlugin/src/main/java/com/mycorp/newsloader/templates/StoreNewsToFolderClientAPI.java new file mode 100644 index 0000000..213ee8a --- /dev/null +++ b/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/connectedSystemPlugin/src/main/java/com/mycorp/newsloader/templates/StoreNewsToFolderClientAPI.java @@ -0,0 +1,79 @@ +package com.mycorp.newsloader.templates; + +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.UUID; + +import com.appian.connectedsystems.simplified.sdk.SimpleClientApi; +import com.appian.connectedsystems.simplified.sdk.SimpleClientApiRequest; +import com.appian.connectedsystems.templateframework.sdk.ClientApiResponse; +import com.appian.connectedsystems.templateframework.sdk.ExecutionContext; +import com.appian.connectedsystems.templateframework.sdk.TemplateId; +import com.appiancorp.suiteapi.content.ContentConstants; +import com.appiancorp.suiteapi.content.ContentService; +import com.appiancorp.suiteapi.content.ContentUploadOutputStream; +import com.appiancorp.suiteapi.knowledge.Document; +import com.appiancorp.suiteapi.type.TypedValue; +import com.appiancorp.type.AppianTypeLong; + +/** + * This is an example of a Client API that performs an operation when executed from a + * Component Plug-in (CP). The Client API accepts a data structure which contains + * the CP request payload as well as the data stored inside the Connected System object. + * It uses both pieces to perform the operation. + * + * In this example, Client API will reviece news and store it as file in the defined folder + * Returns ID of newly stored news file + */ + +@TemplateId(name="StoreNewsClientApi") +public class StoreNewsToFolderClientAPI extends SimpleClientApi { + private final ContentService contentService; + + public StoreNewsToFolderClientAPI(ContentService contentService){ + this.contentService = contentService; + } + @Override + protected ClientApiResponse execute(SimpleClientApiRequest simpleClientApiRequest, ExecutionContext executionContext) { + + // News payload + String newsPayload = (String)simpleClientApiRequest.getPayload().getOrDefault("news", "{}"); + + // permission to add news (secured) + Boolean canAddNewNews = (Boolean)simpleClientApiRequest.getSecuredPayload().getOrDefault("canAddNews", false); + + // folder to store news file (TypedValue) + TypedValue folderCons = (TypedValue)simpleClientApiRequest.getSecuredPayload().getOrDefault("saveNewsToFolder", new TypedValue()); + + if(folderCons.getValue() == null){ + throw new IllegalStateException("saveNewsToFolder should be null."); + } + + if(!AppianTypeLong.FOLDER.equals(folderCons.getTypeRef().getId())){ + throw new IllegalStateException("saveNewsToFolder should be set to a constant pointing to a folder."); + } + + if(canAddNewNews){ + Document newsDoc = new Document(); + newsDoc.setName(UUID.randomUUID().toString()); + newsDoc.setExtension("json"); + newsDoc.setParent((Long)folderCons.getValue()); + + // uploading news document to the defined folder + try (ContentUploadOutputStream cos = contentService.uploadDocument(newsDoc, ContentConstants.UNIQUE_NONE)) { + cos.write(newsPayload.getBytes(StandardCharsets.UTF_8)); + Long newsDocId = cos.getContentId(); + + return new ClientApiResponse( + Map.of( + "newsDocId", newsDocId + ) + ); + } catch (Exception e) { + throw new RuntimeException("Failed to store news to folder."); + } + + } + throw new IllegalStateException("You don't have permission to add news."); + } +} diff --git a/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/connectedSystemPlugin/src/main/resources/appian-plugin.xml b/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/connectedSystemPlugin/src/main/resources/appian-plugin.xml new file mode 100644 index 0000000..c7dc300 --- /dev/null +++ b/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/connectedSystemPlugin/src/main/resources/appian-plugin.xml @@ -0,0 +1,14 @@ + + + + My Corp Connected System - News Loader + + 1.0.0.0 + + + + + + + + diff --git a/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/connectedSystemPlugin/src/main/resources/resources_en_US.properties b/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/connectedSystemPlugin/src/main/resources/resources_en_US.properties new file mode 100644 index 0000000..d005f81 --- /dev/null +++ b/Component Plug-in (CP) Examples/Client API-Enabled Examples/newsLoader/connectedSystemPlugin/src/main/resources/resources_en_US.properties @@ -0,0 +1,2 @@ +NewsLoaderConnectedSystemTemplate.name=News Loader Connected System Plug-in +NewsLoaderConnectedSystemTemplate.description=News Loader CS for learning how to build Client APIs for CPs!