From c81e6c0e904cc71e2221105f0f4de47c656d9ddb Mon Sep 17 00:00:00 2001 From: anikitenko Date: Wed, 8 Apr 2026 10:07:02 +0300 Subject: [PATCH] fix(examples): stabilize learning examples for fdo runtime and docs --- .gitignore | 2 + README.md | 11 +- docs/EXAMPLES_AND_FIXTURES.md | 81 +++ docs/INJECTED_LIBRARIES.md | 23 +- docs/OPERATOR_PLUGIN_PATTERNS.md | 2 +- docs/PRODUCTION_GRADE_TODO.md | 160 ------ docs/QUICK_REFERENCE.md | 7 +- examples/01-basic-plugin.ts | 90 ++-- examples/02-interactive-plugin.ts | 374 ++++++------- examples/03-persistence-plugin.ts | 509 +++++++++--------- examples/04-ui-extensions-plugin.ts | 496 ++++++----------- examples/05-advanced-dom-plugin.ts | 159 +++--- examples/07-injected-libraries-demo.ts | 32 +- examples/08-privileged-actions-plugin.ts | 4 +- examples/09-operator-plugin.ts | 4 +- examples/README.md | 8 +- examples/dom_elements_plugin.ts | 24 +- .../fixtures/advanced-ui-plugin.fixture.ts | 78 +-- package.json | 5 +- scripts/verify-docs.mjs | 84 +++ src/Logger.ts | 1 + tests/Logger.test.ts | 6 + tests/examples.advanced-dom.test.ts | 25 + 23 files changed, 1031 insertions(+), 1154 deletions(-) create mode 100644 docs/EXAMPLES_AND_FIXTURES.md delete mode 100644 docs/PRODUCTION_GRADE_TODO.md create mode 100644 scripts/verify-docs.mjs create mode 100644 tests/examples.advanced-dom.test.ts diff --git a/.gitignore b/.gitignore index 4c8b2f8..cb075a7 100644 --- a/.gitignore +++ b/.gitignore @@ -212,3 +212,5 @@ dist # npm audit files .*-audit.json +# Local-only working docs +docs-local/ diff --git a/README.md b/README.md index 4c42d84..49177db 100644 --- a/README.md +++ b/README.md @@ -29,11 +29,12 @@ Plugin metadata is also part of the host contract. In particular, `metadata.icon For the detailed render/runtime contract, see [docs/RENDER_RUNTIME_CONTRACT.md](./docs/RENDER_RUNTIME_CONTRACT.md). -Roadmaps: +Further documentation: -- production hardening and completed durability work: [docs/PRODUCTION_GRADE_TODO.md](./docs/PRODUCTION_GRADE_TODO.md) -- longer-term platform vision for DevOps/SRE/operator workflows: [docs/REVOLUTIONARY_PLUGIN_SYSTEM_TODO.md](./docs/REVOLUTIONARY_PLUGIN_SYSTEM_TODO.md) -- concrete FDO host/editor/AI handoff for the current Phase 1 golden path: [docs/PHASE_1_FDO_ALIGNMENT_PROMPT.md](./docs/PHASE_1_FDO_ALIGNMENT_PROMPT.md) +- safe authoring guide: [docs/SAFE_PLUGIN_AUTHORING.md](./docs/SAFE_PLUGIN_AUTHORING.md) +- operator plugin guidance: [docs/OPERATOR_PLUGIN_PATTERNS.md](./docs/OPERATOR_PLUGIN_PATTERNS.md) +- examples and fixtures guide: [docs/EXAMPLES_AND_FIXTURES.md](./docs/EXAMPLES_AND_FIXTURES.md) +- API stability policy: [docs/API_STABILITY.md](./docs/API_STABILITY.md) ## Features @@ -138,7 +139,7 @@ See `examples/08-privileged-actions-plugin.ts` for the low-level host privileged See `examples/09-operator-plugin.ts` for a curated operator helper example for a known tool family built on scoped host process execution. -For FDO-side Monaco/editor/AI alignment on these example priorities, use `docs/PHASE_1_FDO_ALIGNMENT_PROMPT.md`. +For public SDK guidance, follow the fixture-first and curated-helper-first recommendation order documented in [docs/OPERATOR_PLUGIN_PATTERNS.md](./docs/OPERATOR_PLUGIN_PATTERNS.md). The SDK also provides curated operator presets for common DevOps/SRE tooling such as Docker, kubectl, Helm, Terraform, Ansible, AWS CLI, gcloud, Azure CLI, Podman, Kustomize, GitHub CLI, Git, Vault, and Nomad, while still supporting generic custom scopes for host-specific tools. diff --git a/docs/EXAMPLES_AND_FIXTURES.md b/docs/EXAMPLES_AND_FIXTURES.md new file mode 100644 index 0000000..c2aac2d --- /dev/null +++ b/docs/EXAMPLES_AND_FIXTURES.md @@ -0,0 +1,81 @@ +# Examples And Fixtures + +This guide defines the public, stable example surface for `@anikitenko/fdo-sdk`. + +If you are starting a new plugin, prefer the fixture set under `examples/fixtures/` before the numbered learning examples. + +## Recommended Starting Order + +1. `examples/fixtures/minimal-plugin.fixture.ts` + Use when you need the smallest valid plugin scaffold. +2. `examples/fixtures/error-handling-plugin.fixture.ts` + Use when you need deterministic render fallback behavior. +3. `examples/fixtures/storage-plugin.fixture.ts` + Use when you need plugin-scoped storage with graceful JSON-store handling. +4. `examples/fixtures/advanced-ui-plugin.fixture.ts` + Use when you need richer UI composition with DOM helpers. +5. `examples/fixtures/operator-kubernetes-plugin.fixture.ts` + Use when building a `kubectl`-style operator plugin. +6. `examples/fixtures/operator-terraform-plugin.fixture.ts` + Use when building a Terraform preview/apply plugin. +7. `examples/fixtures/operator-custom-tool-plugin.fixture.ts` + Use when you need a host-specific scoped tool that is not covered by a curated operator preset. + +The numbered examples under `examples/01-...` to `examples/09-...` are learning references. They are not the default production starting point. + +## Production-Grade Rules + +Use these rules for examples, fixtures, and AI-generated plugin scaffolds: + +- Keep backend orchestration in plugin methods and registered handlers. +- Keep `renderOnLoad()` thin and UI-focused. +- For UI-to-backend calls, use the real bridge contract: + +```ts +const result = await window.createBackendReq("UI_MESSAGE", { + handler: "plugin.handlerName", + content: {}, +}); +``` + +- For known operator tool families, prefer: + - `createOperatorToolCapabilityPreset(...)` + - `requestOperatorTool(...)` +- For multi-step operator flows, prefer: + - `requestScopedWorkflow(...)` +- For host-specific or non-curated tools, prefer: + - `requestScopedProcessExec(...)` +- For privileged or operator plugins, declare expected capabilities in code via `declareCapabilities()`. + +## Validation Expectations + +The examples surface is considered stable only when all of the following stay true: + +- `npm run test:examples` passes +- public example docs do not reference local-only or internal planning files +- public example docs do not reference stale docs domains +- `createBackendReq(...)` examples reflect the real `UI_MESSAGE` handler pattern +- the canonical fixtures remain the primary recommended starting point + +## Canonical Operator Patterns + +For operator plugins, the recommended order is: + +1. start from the closest fixture +2. use curated capability presets for known tool families +3. use `requestOperatorTool(...)` for single-action known-tool execution +4. use `requestScopedWorkflow(...)` for preview/apply or inspect/act flows +5. use `requestScopedProcessExec(...)` for host-specific internal tools +6. use lower-level transport helpers only when transport-level control is explicitly required + +For more detail, see [OPERATOR_PLUGIN_PATTERNS.md](./OPERATOR_PLUGIN_PATTERNS.md). + +## Public Documentation Links + +These are the public guides that should stay stable for generated docs sites: + +- [SAFE_PLUGIN_AUTHORING.md](./SAFE_PLUGIN_AUTHORING.md) +- [OPERATOR_PLUGIN_PATTERNS.md](./OPERATOR_PLUGIN_PATTERNS.md) +- [RENDER_RUNTIME_CONTRACT.md](./RENDER_RUNTIME_CONTRACT.md) +- [INJECTED_LIBRARIES.md](./INJECTED_LIBRARIES.md) +- [API_STABILITY.md](./API_STABILITY.md) diff --git a/docs/INJECTED_LIBRARIES.md b/docs/INJECTED_LIBRARIES.md index 84b2c9d..905be62 100644 --- a/docs/INJECTED_LIBRARIES.md +++ b/docs/INJECTED_LIBRARIES.md @@ -147,20 +147,25 @@ These helper functions are injected into the iframe UI runtime `window` object. ### `createBackendReq(type, data)` -Creates a request to your plugin's backend handler. +Creates a request to your plugin's backend transport. **Parameters:** -- `type` (string): The function name to call on the backend +- `type` (string): The backend transport type - `data` (any, optional): The data to send to the backend **Returns:** `Promise` - The response from the backend **Example:** ```javascript -const result = await window.createBackendReq('getUserData', { userId: 123 }); +const result = await window.createBackendReq("UI_MESSAGE", { + handler: "plugin.getUserData", + content: { userId: 123 } +}); console.log(result); ``` +For production plugins, register handlers in `init()` and call them through `UI_MESSAGE`. Do not assume arbitrary handler names are transport types. + ### `waitForElement(selector, callback, timeout)` Waits for an element to appear in the DOM. @@ -317,11 +322,10 @@ export default class GridLayoutPlugin extends FDO_SDK { ```javascript export default class DataPlugin extends FDO_SDK { - async fetchData() { - const data = await window.createBackendReq('getData', { - filter: 'active' + init() { + PluginRegistry.registerHandler("data.getActive", async (content) => { + return { filter: content?.filter ?? "active", items: [] }; }); - return data; } render() { @@ -330,8 +334,9 @@ export default class DataPlugin extends FDO_SDK { `, - - // Key Concepts - domNested.createBlockDiv([ - domText.createHText(3, 'Key Concepts'), - domNested.createList([ - domNested.createListItem([ - domText.createStrongText('Message Handlers:'), - ' Functions registered in init() that respond to UI events' - ]), - domNested.createListItem([ - domText.createStrongText('Handler Registration:'), - ' Use PluginRegistry.registerHandler(name, function)' - ]), - domNested.createListItem([ - domText.createStrongText('Async Patterns:'), - ' Handlers can be async for long-running operations' - ]), - domNested.createListItem([ - domText.createStrongText('Error Handling:'), - ' Always wrap handler logic in try-catch blocks' - ]), - domNested.createListItem([ - domText.createStrongText('State Management:'), - ' Use class properties for temporary state, storage for persistence' - ]) - ]) - ], { - style: { - marginTop: '20px', - padding: '15px', - backgroundColor: '#fff3cd', - borderRadius: '5px' - } - }) - ], { - style: { - padding: '20px', - fontFamily: 'Arial, sans-serif' - } - }); - - return mainContent; - +
+

Counter Example

+

Current count: ${this.counter}

+
+ + +
+

+ Click the buttons to increment or decrement the counter. The counter value is maintained in plugin state. +

+
+
+ +
+

Form Example

+
+ + +
+ +
+
+
+

+ Enter your name and submit the form to see async handler processing. +

+
+ +
+

Key Concepts

+
    +
  • Message Handlers: functions registered in init() that respond to UI events
  • +
  • Handler Registration: use PluginRegistry.registerHandler(name, fn)
  • +
  • Async Patterns: handlers can be async for long-running operations
  • +
  • Error Handling: wrap handler logic in try/catch blocks
  • +
  • State Management: use class properties for temporary state and storage for persistence
  • +
+
+ + `; } catch (error) { this.error(error as Error); + return `

Error rendering plugin

@@ -397,6 +274,87 @@ export default class InteractivePlugin extends FDO_SDK implements FDOInterface { `; } } + + renderOnLoad(): string { + return ` + () => { + const callHandler = (handler, content = {}) => + window.createBackendReq("UI_MESSAGE", { handler, content }); + + const incrementBtn = document.getElementById("increment-counter-btn"); + const decrementBtn = document.getElementById("decrement-counter-btn"); + const counterResult = document.getElementById("counter-result"); + const formContainer = document.getElementById("interactive-example-form"); + const submitFormButton = document.getElementById("submit-form-btn"); + const formResult = document.getElementById("form-result"); + + const setCounterResult = (text, color = "#666") => { + if (!counterResult) return; + counterResult.textContent = text; + counterResult.style.color = color; + }; + + incrementBtn?.addEventListener('click', async () => { + try { + const result = await callHandler('incrementCounter', {}); + if (result?.success) { + setCounterResult(result.message || ('Counter is now ' + result.counter), 'green'); + } else { + setCounterResult(result?.error || 'Failed to increment counter', 'red'); + } + } catch (error) { + setCounterResult('An error occurred while incrementing counter.', 'red'); + } + }); + + decrementBtn?.addEventListener('click', async () => { + try { + const result = await callHandler('decrementCounter', {}); + if (result?.success) { + setCounterResult(result.message || ('Counter is now ' + result.counter), 'green'); + } else { + setCounterResult(result?.error || 'Failed to decrement counter', 'red'); + } + } catch (error) { + setCounterResult('An error occurred while decrementing counter.', 'red'); + } + }); + + const handleFormSubmit = async () => { + const userNameInput = document.getElementById("userName"); + if (!formResult || !userNameInput) return; + const userName = userNameInput.value; + + if (!userName || userName.trim() === '') { + formResult.textContent = 'Please enter your name'; + formResult.style.color = 'red'; + return; + } + + formResult.textContent = 'Processing...'; + formResult.style.color = '#666'; + + try { + const result = await callHandler('submitForm', { userName }); + if (result?.success) { + formResult.textContent = 'Form submitted successfully.'; + formResult.style.color = 'green'; + } else { + formResult.textContent = 'Form submission failed.'; + formResult.style.color = 'red'; + } + } catch (error) { + formResult.textContent = 'An error occurred while submitting the form.'; + formResult.style.color = 'red'; + } + }; + + submitFormButton?.addEventListener("click", () => { + void handleFormSubmit(); + }); + } + `; + } } /** @@ -420,3 +378,5 @@ export default class InteractivePlugin extends FDO_SDK implements FDOInterface { * - See example 04 for UI extensions (quick actions and side panels) * - See example 05 for advanced DOM generation with styling */ + +new InteractivePlugin(); diff --git a/examples/03-persistence-plugin.ts b/examples/03-persistence-plugin.ts index d7c8903..b2e007a 100644 --- a/examples/03-persistence-plugin.ts +++ b/examples/03-persistence-plugin.ts @@ -10,26 +10,26 @@ * Learning Objectives: * - Use StoreDefault for temporary data (in-memory) * - Use StoreJson for persistent data (file-based) - * - Implement proper key naming conventions - * - Handle storage errors gracefully - * - Save and retrieve different data types + * - Initialize storage safely in `init()` + * - Handle storage errors and StoreJson unavailability gracefully + * - Trigger persistence handlers from iframe UI via `UI_MESSAGE` + * - Save and retrieve different data types with namespaced keys * * Expected Output: * When this plugin runs in the FDO application, it will: * 1. Display saved user preferences (name, theme, notifications) - * 2. Provide forms to update preferences + * 2. Provide UI controls to update preferences through backend handlers * 3. Show temporary session data (visit count, last action) - * 4. Persist preference changes across application restarts - * 5. Clear temporary data on each session + * 4. Persist preference changes across application restarts when JSON storage is available + * 5. Clear and record session data through explicit UI actions */ -import { FDO_SDK, FDOInterface, PluginMetadata, PluginRegistry, DOMText, DOMNested, DOMButton, DOMInput } from "@anikitenko/fdo-sdk"; +import { FDO_SDK, FDOInterface, PluginMetadata, PluginRegistry } from "@anikitenko/fdo-sdk"; declare global { interface Window { - fdoSDK: { - sendMessage: (handler: string, data: any) => Promise; - }; + createBackendReq: (type: string, data?: any) => Promise; + callHandler: (handler: string, content?: unknown) => Promise; } } @@ -111,8 +111,8 @@ export default class PersistencePlugin extends FDO_SDK implements FDOInterface { return this.handleSavePreferences(data); }); - PluginRegistry.registerHandler("clearPreferences", (data: any) => { - return this.handleClearPreferences(data); + PluginRegistry.registerHandler("clearPreferences", (_data: unknown) => { + return this.handleClearPreferences(); }); PluginRegistry.registerHandler("recordAction", (data: any) => { @@ -193,7 +193,7 @@ export default class PersistencePlugin extends FDO_SDK implements FDOInterface { * @param data - Optional data * @returns Result object */ - private handleClearPreferences(data: any): any { + private handleClearPreferences(): any { try { this.persistentStore.remove(this.KEYS.USER_NAME); this.persistentStore.remove(this.KEYS.USER_THEME); @@ -265,261 +265,109 @@ export default class PersistencePlugin extends FDO_SDK implements FDOInterface { const lastAction = this.tempStore.get(this.KEYS.LAST_ACTION); const sessionStart = this.tempStore.get(this.KEYS.SESSION_START); - const domText = new DOMText(); - const domNested = new DOMNested(); - const domButton = new DOMButton(); - const domInput = new DOMInput("", {}); - - // Create main container - const mainContent = domNested.createBlockDiv([ - // Header section - domText.createHText(1, this._metadata.name), - domText.createPText(this._metadata.description), - - // Persistent Data Section - domNested.createBlockDiv([ - domText.createHText(3, "Persistent Preferences (StoreJson)"), - domText.createPText("These preferences are saved to a file and persist across application restarts.", { - style: { - fontSize: '12px', - color: '#666', - marginBottom: '15px' - } - }), - - // Current values display - domNested.createBlockDiv([ - domText.createPText([ - domText.createStrongText("User Name:"), - ` ${userName}` - ].join(''), { style: { marginBottom: '10px' } }), - - domText.createPText([ - domText.createStrongText("Theme:"), - ` ${theme}` - ].join(''), { style: { marginBottom: '10px' } }), - - domText.createPText([ - domText.createStrongText("Notifications:"), - ` ${notificationsEnabled ? "Enabled" : "Disabled"}` - ].join(''), { style: { marginBottom: '10px' } }) - ]), - - // Preferences form - domNested.createForm([ - // Username input - domNested.createBlockDiv([ - domText.createLabelText("User Name:", "userName", { - style: { display: 'block', marginBottom: '5px' } - }), - new DOMInput("userName", { - style: { padding: '8px', width: '300px' } - }).createInput("text") - ], { style: { marginBottom: '10px' } }), - - // Theme select - domNested.createBlockDiv([ - domText.createLabelText("Theme:", "theme", { - style: { display: 'block', marginBottom: '5px' } - }), - new DOMInput("theme", { - style: { padding: '8px', width: '316px' } - }).createSelect([ - domInput.createOption("Light", "light", theme === 'light'), - domInput.createOption("Dark", "dark", theme === 'dark'), - domInput.createOption("Auto", "auto", theme === 'auto') - ]) - ], { style: { marginBottom: '10px' } }), - - // Notifications checkbox - domNested.createBlockDiv([ - domText.createLabelText([ - new DOMInput("notifications", {}).createInput("checkbox"), - " Enable Notifications" - ].join(''), "notifications") - ], { style: { marginBottom: '15px' } }), - - // Form buttons - domNested.createBlockDiv([ - domButton.createButton("Save Preferences", - () => { - const form = document.querySelector('form'); - if (form) form.dispatchEvent(new Event('submit')); - }, - { - style: { - padding: '10px 20px', - marginRight: '10px', - cursor: 'pointer', - backgroundColor: '#007bff', - color: 'white', - border: 'none', - borderRadius: '3px' - } - } - ), - - domButton.createButton("Clear All", - () => window.fdoSDK.sendMessage('clearPreferences', {}), - { - style: { - padding: '10px 20px', - cursor: 'pointer', - backgroundColor: '#dc3545', - color: 'white', - border: 'none', - borderRadius: '3px' - } - } - ) - ]) - ], { - customAttributes: { - onsubmit: 'event.preventDefault(); savePreferences();' - }, - style: { marginTop: '15px' } - }), - - // Result area - domNested.createBlockDiv([], { - customAttributes: { id: 'prefs-result' }, - style: { marginTop: '10px' } - }) - ], { - style: { - marginTop: '20px', - padding: '15px', - backgroundColor: '#e8f4f8', - borderRadius: '5px' - } - }), - - // Temporary Data Section - domNested.createBlockDiv([ - domText.createHText(3, "Session Data (StoreDefault)"), - domText.createPText("This data is stored in memory and cleared when the application restarts.", { - style: { - fontSize: '12px', - color: '#666', - marginBottom: '15px' - } - }), - - domNested.createBlockDiv([ - domText.createPText([ - domText.createStrongText("Visit Count:"), - ` ${visitCount}` - ].join(''), { style: { marginBottom: '10px' } }), - - domText.createPText([ - domText.createStrongText("Session Started:"), - ` ${sessionStart ? new Date(sessionStart).toLocaleString() : 'N/A'}` - ].join(''), { style: { marginBottom: '10px' } }), - - domText.createPText([ - domText.createStrongText("Last Action:"), - ` ${lastAction ? `${lastAction.action} at ${new Date(lastAction.timestamp).toLocaleTimeString()}` : 'None'}` - ].join(''), { style: { marginBottom: '15px' } }) - ]), - - domButton.createButton("Record Action", - () => window.fdoSDK.sendMessage('recordAction', { action: 'Button Click' }), - { - style: { - padding: '10px 20px', - cursor: 'pointer' - } - } - ) - ], { - style: { - marginTop: '20px', - padding: '15px', - backgroundColor: '#f0f0f0', - borderRadius: '5px' - } - }), - - // Key Concepts Section - domNested.createBlockDiv([ - domText.createHText(3, "Storage Concepts"), - domNested.createList([ - domNested.createListItem([ - domText.createStrongText("StoreDefault:"), - " In-memory storage, data cleared on restart" - ]), - domNested.createListItem([ - domText.createStrongText("StoreJson:"), - " File-based storage, data persists across restarts" - ]), - domNested.createListItem([ - domText.createStrongText("Key Naming:"), - " Use namespaced keys (pluginName:category:key)" - ]), - domNested.createListItem([ - domText.createStrongText("Error Handling:"), - " Always wrap storage operations in try-catch" - ]), - domNested.createListItem([ - domText.createStrongText("Data Types:"), - " Stores support strings, numbers, booleans, objects, arrays" - ]) - ]) - ], { - style: { - marginTop: '20px', - padding: '15px', - backgroundColor: '#fff3cd', - borderRadius: '5px' - } - }), - - // JavaScript for handling preferences - domText.createText(` - - `) - ], { - style: { - padding: '20px', - fontFamily: 'Arial, sans-serif' - } - }); - - return mainContent; + return ` +
+

${this._metadata.name}

+

${this._metadata.description}

+ +
+

Persistent Preferences (StoreJson)

+

+ These preferences are saved to a file and persist across application restarts. +

+ +
+

User Name: ${userName}

+

Theme: ${theme}

+

Notifications: ${notificationsEnabled ? "Enabled" : "Disabled"}

+
+ +
+
+ + +
+ +
+ + +
+ +
+ +
+ +
+ + + +
+
+ +
+
+ +
+

Session Data (StoreDefault)

+

+ This data is stored in memory and cleared when the application restarts. +

+ +
+

Visit Count: ${visitCount}

+

Session Started: ${sessionStart ? new Date(sessionStart).toLocaleString() : "N/A"}

+

Last Action: ${lastAction ? `${lastAction.action} at ${new Date(lastAction.timestamp).toLocaleTimeString()}` : "None"}

+
+ + +
+ +
+

Storage Concepts

+
    +
  • StoreDefault: in-memory storage, data cleared on restart
  • +
  • StoreJson: file-based storage, data persists across restarts
  • +
  • Key Naming: use namespaced keys (pluginName:category:key)
  • +
  • Error Handling: always wrap storage operations in try/catch
  • +
  • Data Types: stores support strings, numbers, booleans, objects, and arrays
  • +
+
+
+ `; } catch (error) { this.error(error as Error); @@ -531,6 +379,129 @@ export default class PersistencePlugin extends FDO_SDK implements FDOInterface { `; } } + + renderOnLoad(): string { + return ` + () => { + const callHandler = (handler, content = {}) => + window.createBackendReq("UI_MESSAGE", { handler, content }); + + const saveBtn = document.getElementById("save-preferences-btn"); + const clearBtn = document.getElementById("clear-preferences-btn"); + const recordBtn = document.getElementById("record-action-btn"); + const resultDiv = document.getElementById("prefs-result"); + const currentUserName = document.getElementById("current-user-name"); + const currentTheme = document.getElementById("current-theme"); + const currentNotifications = document.getElementById("current-notifications"); + const currentVisitCount = document.getElementById("current-visit-count"); + const currentLastAction = document.getElementById("current-last-action"); + + const setResult = (message, color = "#666") => { + if (!resultDiv) return; + resultDiv.textContent = message; + resultDiv.style.color = color; + }; + + const savePreferences = async () => { + const userNameInput = document.getElementById("userName"); + const themeInput = document.getElementById("theme"); + const notificationsInput = document.getElementById("notifications"); + if (!userNameInput || !themeInput || !notificationsInput) return; + + const userName = userNameInput.value; + const theme = themeInput.value; + const notificationsEnabled = notificationsInput.checked; + + setResult("Saving..."); + + try { + const result = await callHandler("savePreferences", { + userName, + theme, + notificationsEnabled, + }); + + if (result?.success) { + setResult(result.message || "Preferences saved successfully.", "green"); + if (currentUserName) { + currentUserName.textContent = userName || "Not set"; + } + if (currentTheme) { + currentTheme.textContent = theme; + } + if (currentNotifications) { + currentNotifications.textContent = notificationsEnabled ? "Enabled" : "Disabled"; + } + } else { + setResult(result?.error || "Failed to save preferences.", "red"); + } + } catch (error) { + setResult("An error occurred while saving preferences.", "red"); + } + }; + + saveBtn?.addEventListener("click", () => { + void savePreferences(); + }); + + clearBtn?.addEventListener("click", async () => { + try { + const result = await callHandler("clearPreferences", {}); + if (result?.success) { + setResult(result.message || "Preferences cleared.", "green"); + const userNameInput = document.getElementById("userName"); + const themeInput = document.getElementById("theme"); + const notificationsInput = document.getElementById("notifications"); + if (currentUserName) { + currentUserName.textContent = "Not set"; + } + if (currentTheme) { + currentTheme.textContent = "light"; + } + if (currentNotifications) { + currentNotifications.textContent = "Enabled"; + } + if (userNameInput) { + userNameInput.value = ""; + } + if (themeInput) { + themeInput.value = "light"; + } + if (notificationsInput) { + notificationsInput.checked = true; + } + } else { + setResult(result?.error || "Failed to clear preferences.", "red"); + } + } catch (error) { + setResult("An error occurred while clearing preferences.", "red"); + } + }); + + recordBtn?.addEventListener("click", async () => { + try { + const result = await callHandler("recordAction", { action: "Button Click" }); + if (result?.success) { + setResult("Action recorded.", "green"); + if (currentLastAction) { + currentLastAction.textContent = result.timestamp + ? "Button Click at " + new Date(result.timestamp).toLocaleTimeString() + : "Button Click"; + } + if (currentVisitCount) { + const parsedCount = Number(currentVisitCount.textContent || "0"); + currentVisitCount.textContent = Number.isFinite(parsedCount) ? String(parsedCount) : currentVisitCount.textContent; + } + } else { + setResult(result?.error || "Failed to record action.", "red"); + } + } catch (error) { + setResult("An error occurred while recording action.", "red"); + } + }); + } + `; + } } /** @@ -553,3 +524,5 @@ export default class PersistencePlugin extends FDO_SDK implements FDOInterface { * - See example 04 for UI extensions (quick actions and side panels) * - See example 05 for advanced DOM generation with styling */ + +new PersistencePlugin(); diff --git a/examples/04-ui-extensions-plugin.ts b/examples/04-ui-extensions-plugin.ts index 5d84699..ec1a49d 100644 --- a/examples/04-ui-extensions-plugin.ts +++ b/examples/04-ui-extensions-plugin.ts @@ -31,12 +31,7 @@ import { QuickActionMixin, SidePanelMixin, QuickAction, - SidePanelConfig, - DOMText, - DOMNested, - DOMButton, - DOMInput, - DOMLink + SidePanelConfig } from "@anikitenko/fdo-sdk"; /** @@ -88,28 +83,28 @@ export default class UIExtensionsPlugin extends UIExtensionsPluginBase implement try { this.log("UIExtensionsPlugin initialized!"); - PluginRegistry.registerHandler("quickSearch", (data: any) => { - return this.handleQuickSearch(data); + PluginRegistry.registerHandler("quickSearch", (_data: unknown) => { + return this.handleQuickSearch(); }); - PluginRegistry.registerHandler("quickCreate", (data: any) => { - return this.handleQuickCreate(data); + PluginRegistry.registerHandler("quickCreate", (_data: unknown) => { + return this.handleQuickCreate(); }); - PluginRegistry.registerHandler("quickSettings", (data: any) => { - return this.handleQuickSettings(data); + PluginRegistry.registerHandler("quickSettings", (_data: unknown) => { + return this.handleQuickSettings(); }); - PluginRegistry.registerHandler("showDashboard", (data: any) => { - return this.handleShowDashboard(data); + PluginRegistry.registerHandler("showDashboard", (_data: unknown) => { + return this.handleShowDashboard(); }); - PluginRegistry.registerHandler("showReports", (data: any) => { - return this.handleShowReports(data); + PluginRegistry.registerHandler("showReports", (_data: unknown) => { + return this.handleShowReports(); }); - PluginRegistry.registerHandler("showSettings", (data: any) => { - return this.handleShowSettings(data); + PluginRegistry.registerHandler("showSettings", (_data: unknown) => { + return this.handleShowSettings(); }); @@ -135,18 +130,18 @@ export default class UIExtensionsPlugin extends UIExtensionsPluginBase implement name: "Search Plugin Data", message_type: "quickSearch", subtitle: "Search through plugin data", - icon: "search.png" + icon: "search" }, { name: "Create New Item", message_type: "quickCreate", subtitle: "Create a new item in the plugin", - icon: "create.png" + icon: "add" }, { name: "Plugin Settings", message_type: "quickSettings", - icon: "settings.png" + icon: "cog" } ]; } catch (error) { @@ -168,7 +163,7 @@ export default class UIExtensionsPlugin extends UIExtensionsPluginBase implement defineSidePanel(): SidePanelConfig { try { return { - icon: "panel.png", + icon: "panel-table", label: "UI Extensions", submenu_list: [ { @@ -191,7 +186,7 @@ export default class UIExtensionsPlugin extends UIExtensionsPluginBase implement } catch (error) { this.error(error as Error); return { - icon: "panel.png", + icon: "panel-table", label: "UI Extensions", submenu_list: [] }; @@ -204,7 +199,7 @@ export default class UIExtensionsPlugin extends UIExtensionsPluginBase implement * @param data - Data from the quick action * @returns Result object */ - private handleQuickSearch(data: any): any { + private handleQuickSearch(): any { try { this.currentView = "search"; this.log("Quick search triggered"); @@ -229,7 +224,7 @@ export default class UIExtensionsPlugin extends UIExtensionsPluginBase implement * @param data - Data from the quick action * @returns Result object */ - private handleQuickCreate(data: any): any { + private handleQuickCreate(): any { try { this.currentView = "create"; this.log("Quick create triggered"); @@ -254,7 +249,7 @@ export default class UIExtensionsPlugin extends UIExtensionsPluginBase implement * @param data - Data from the quick action * @returns Result object */ - private handleQuickSettings(data: any): any { + private handleQuickSettings(): any { try { this.currentView = "settings"; this.log("Quick settings triggered"); @@ -279,7 +274,7 @@ export default class UIExtensionsPlugin extends UIExtensionsPluginBase implement * @param data - Data from the side panel * @returns Result object */ - private handleShowDashboard(data: any): any { + private handleShowDashboard(): any { try { this.currentView = "dashboard"; this.log("Dashboard view triggered from side panel"); @@ -304,7 +299,7 @@ export default class UIExtensionsPlugin extends UIExtensionsPluginBase implement * @param data - Data from the side panel * @returns Result object */ - private handleShowReports(data: any): any { + private handleShowReports(): any { try { this.currentView = "reports"; this.log("Reports view triggered from side panel"); @@ -329,7 +324,7 @@ export default class UIExtensionsPlugin extends UIExtensionsPluginBase implement * @param data - Data from the side panel * @returns Result object */ - private handleShowSettings(data: any): any { + private handleShowSettings(): any { try { this.currentView = "settings"; this.log("Settings view triggered from side panel"); @@ -357,69 +352,40 @@ export default class UIExtensionsPlugin extends UIExtensionsPluginBase implement */ render(): string { try { - const domText = new DOMText(); - const domNested = new DOMNested(); - - // Main container - const mainContent = domNested.createBlockDiv([ - domText.createHText(1, this._metadata.name), - domText.createPText(this._metadata.description), - - // Dynamic content based on current view - this.renderCurrentView(), - - // UI Extensions Info - domNested.createBlockDiv([ - domText.createHText(3, "UI Extensions"), - domText.createPText([ - domText.createStrongText("Quick Actions:"), - " Access quick actions from the FDO application's quick action menu" - ].join('')), - domNested.createList([ - "Search Plugin Data", - "Create New Item", - "Plugin Settings" - ]), - - domText.createPText([ - domText.createStrongText("Side Panel:"), - " Access features from the side panel menu" - ].join(''), { style: { marginTop: '15px' } }), - domNested.createList([ - "Dashboard", - "Reports", - "Settings" - ]) - ], { - style: { - marginTop: '20px', - padding: '15px', - backgroundColor: '#fff3cd', - borderRadius: '5px' - } - }) - ], { - style: { - padding: '20px', - fontFamily: 'Arial, sans-serif' - } - }); - - return mainContent; + return ` +
+

${this._metadata.name}

+

${this._metadata.description}

+ + ${this.renderCurrentView()} + +
+

UI Extensions

+

Quick Actions: Access quick actions from the FDO application's quick action menu.

+
    +
  • Search Plugin Data
  • +
  • Create New Item
  • +
  • Plugin Settings
  • +
+ +

Side Panel: Access features from the side panel menu.

+
    +
  • Dashboard
  • +
  • Reports
  • +
  • Settings
  • +
+
+
+ `; } catch (error) { this.error(error as Error); - const errorDomText = new DOMText(); - const errorDomNested = new DOMNested(); - return errorDomNested.createBlockDiv([ - errorDomText.createHText(2, "Error rendering plugin"), - errorDomText.createPText("An error occurred while rendering the plugin UI. Check the console for details.") - ], { - style: { - padding: '20px', - color: 'red' - } - }); + return ` +
+

Error rendering plugin

+

An error occurred while rendering the plugin UI. Check plugin logs for details.

+
+ `; } } @@ -447,265 +413,157 @@ export default class UIExtensionsPlugin extends UIExtensionsPluginBase implement * Render the default view. */ private renderDefaultView(): string { - const domText = new DOMText(); - const domNested = new DOMNested(); - - return domNested.createBlockDiv([ - domText.createHText(3, "Welcome to UI Extensions Example"), - domText.createPText("This plugin demonstrates quick actions and side panel integration."), - domText.createPText("Try the following:"), - domNested.createList([ - "Use the quick action menu to trigger quick actions", - "Use the side panel to navigate between views", - "See how different views are rendered based on UI extension triggers" - ]) - ], { - style: { - marginTop: '20px', - padding: '15px', - backgroundColor: '#f0f0f0', - borderRadius: '5px' - } - }); + return ` +
+

Welcome to UI Extensions Example

+

This plugin demonstrates quick actions and side panel integration.

+

Try the following:

+
    +
  • Use the quick action menu to trigger quick actions
  • +
  • Use the side panel to navigate between views
  • +
  • See how different views are rendered based on UI extension triggers
  • +
+
+ `; } /** * Render the search view. */ private renderSearchView(): string { - const domText = new DOMText(); - const domNested = new DOMNested(); - const domInput = new DOMInput("search", { - style: { - padding: '10px', - width: '400px', - marginTop: '10px' - } - }); - const domButton = new DOMButton(); - - return domNested.createBlockDiv([ - domText.createHText(3, "Search View"), - domText.createPText("This view was triggered by the \"Search Plugin Data\" quick action."), - domNested.createBlockDiv([ - domInput.createInput("text"), - domButton.createButton("Search", () => {}, { - style: { - padding: '10px 20px', - marginLeft: '10px', - cursor: 'pointer' - } - }) - ], { - style: { - display: 'flex', - alignItems: 'center' - } - }) - ], { - style: { - marginTop: '20px', - padding: '15px', - backgroundColor: '#e8f4f8', - borderRadius: '5px' - } - }); + return ` +
+

Search View

+

This view was triggered by the "Search Plugin Data" quick action.

+
+ + +
+
+ `; } /** * Render the create view. */ private renderCreateView(): string { - const domText = new DOMText(); - const domNested = new DOMNested(); - const domInput = new DOMInput("create", { - style: { - padding: '10px', - width: '400px', - marginBottom: '10px' - } - }); - const domButton = new DOMButton(); - - return domNested.createBlockDiv([ - domText.createHText(3, "Create View"), - domText.createPText("This view was triggered by the \"Create New Item\" quick action."), - domNested.createForm([ - domNested.createBlockDiv([ - domInput.createInput("text") - ], { - style: { - marginBottom: '10px' - } - }), - domNested.createBlockDiv([ - domInput.createTextarea() - ], { - style: { - marginBottom: '10px' - } - }), - domButton.createButton("Create", () => {}, { - style: { - padding: '10px 20px', - cursor: 'pointer' - } - }) - ], { - style: { marginTop: '10px' } - }) - ], { - style: { - marginTop: '20px', - padding: '15px', - backgroundColor: '#d4edda', - borderRadius: '5px' - } - }); + return ` +
+

Create View

+

This view was triggered by the "Create New Item" quick action.

+
+
+ +
+
+