### Simple GET Handler Example Source: https://docs.blueplm.io/extensions/server-api An example of a server-side handler that retrieves user information and storage data to return a status response. It checks for user authentication and fetches `lastSync` and `syncCount` from storage, returning them in a JSON format. ```typescript // server/status.ts import type { ExtensionServerAPI } from '@blueplm/extension-api' export default async function handler(api: ExtensionServerAPI) { const { user, storage, response } = api if (!user) { return response.error('Authentication required', 401) } const lastSync = await storage.get('lastSync') const syncCount = await storage.get('syncCount') ?? 0 return response.json({ connected: true, lastSync: lastSync ? new Date(lastSync).toISOString() : null, totalSyncs: syncCount }) } ``` -------------------------------- ### Publish Extension via CLI (Bash) Source: https://docs.blueplm.io/extensions/publishing This snippet demonstrates how to use the BluePLM CLI to log in and publish an extension. It requires the 'blueplm-ext' tool to be installed and configured. ```bash # Login blueplm-ext login # Publish blueplm-ext publish my-extension-1.0.0.bpx ``` -------------------------------- ### Build and Package Commands (Bash) Source: https://docs.blueplm.io/extensions/getting-started These bash commands are used to install dependencies and run the build and package scripts defined in package.json. They prepare the extension for installation. ```bash npm install npm run package ``` -------------------------------- ### API Route URL Examples Source: https://docs.blueplm.io/extensions/contributions Illustrates how API routes are mounted under the `/extensions/{extensionId}/` base URL. Examples show different HTTP methods and paths, including dynamic segments. ```json { "method": "POST", "path": "sync", "handler": "server/sync.js" } // → POST /extensions/myext/sync { "method": "GET", "path": "status", "handler": "server/status.js" } // → GET /extensions/myext/status { "method": "GET", "path": "files/:id", "handler": "server/file.js" } // → GET /extensions/myext/files/123 ``` -------------------------------- ### Optimizing Activation Events in JSON Source: https://docs.blueplm.io/extensions/best-practices Shows how to configure `activationEvents` in `package.json` for efficient extension startup. The 'good' example uses specific events like `onCommand` and `onNavigate` for targeted loading. The 'bad' example demonstrates the less efficient `onStartup` event, which should only be used for extensions requiring constant background operation or global event interception. ```json // ✓ Good: Specific events "activationEvents": [ "onCommand:myext.sync", "onNavigate:settings/extensions/myext" ] // ❌ Avoid: Loads on every startup "activationEvents": [ "onStartup" ] ``` -------------------------------- ### README.md for File Sync Extension Source: https://docs.blueplm.io/extensions/getting-started The README.md file for the File Sync Extension, detailing its features, setup instructions, usage, and requirements. It serves as an introductory guide for users and developers. ```markdown # File Sync Extension for BluePLM Sync your engineering files with your custom service. ## Features - 🔄 One-click sync with keyboard shortcut (Ctrl+Shift+S) - ⏰ Automatic background sync - 📁 Watch for file changes - 🔒 Secure API key storage ## Setup 1. Install from the BluePLM Extension Store 2. Open Settings → Extensions → File Sync 3. Enter your API endpoint and key 4. Enable automatic sync (optional) ## Usage - Press `Ctrl+Shift+S` to sync immediately - Or use Command Palette → "Sync Files Now" ## Requirements - BluePLM 1.0.0 or later - Active subscription to your sync service ``` -------------------------------- ### Complete BluePLM Extension Example Source: https://docs.blueplm.io/extensions/client-api A comprehensive example demonstrating extension activation, command registration, file change watching, event subscription, and UI interactions like progress indicators and toasts. It also shows how to use telemetry and make API calls. ```typescript import type { ExtensionContext, ExtensionClientAPI } from '@blueplm/extension-api' export async function activate( context: ExtensionContext, api: ExtensionClientAPI ): Promise { context.log.info('Extension activating...') // Register commands context.subscriptions.push( api.commands.registerCommand('myext.sync', async () => { await syncWithProgress(api) }), api.commands.registerCommand('myext.configure', async () => { const endpoint = await api.ui.showInputBox({ title: 'API Endpoint', placeholder: 'https://api.example.com' }) if (endpoint) { await api.storage.set('apiEndpoint', endpoint) api.ui.showToast('Configuration saved', 'success') } }) ) // Watch for file changes context.subscriptions.push( api.workspace.onFileChanged((events) => { const modified = events.filter(e => e.type !== 'deleted') if (modified.length > 0) { context.log.debug(`${modified.length} files changed`) } }) ) // Subscribe to events context.subscriptions.push( api.events.on('vault:changed', () => { context.log.info('Vault changed, refreshing...') }) ) // Set initial status api.ui.setStatus('online') api.ui.showToast('Extension ready!', 'success') } async function syncWithProgress(api: ExtensionClientAPI): Promise { await api.ui.showProgress( { title: 'Syncing...', cancellable: true }, async (progress, token) => { progress.report({ message: 'Connecting...' }) const vault = await api.workspace.getCurrentVault() if (!vault) { api.ui.showToast('No vault selected', 'error') return } progress.report({ message: 'Syncing...', increment: 50 }) const response = await api.callOrgApi('/extensions/myext/sync', { method: 'POST', body: { vaultId: vault.id } }) if (response.ok) { api.ui.showToast('Sync complete!', 'success') api.telemetry.trackEvent('sync_completed') } else { api.ui.showToast('Sync failed', 'error') } } ) } export function deactivate(): void { // Cleanup (subscriptions are auto-disposed) } ``` -------------------------------- ### HTTP POST Request Example Source: https://docs.blueplm.io/extensions/server-api Demonstrates how to make a POST request using the `http.fetch` API. This example includes setting the method, authorization headers, content type, and sending a JSON payload in the request body. ```typescript const result = await api.http.fetch('https://api.example.com/sync', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ vault: vaultName, files: fileList }) }) ``` -------------------------------- ### Automated Release Workflow with GitHub Actions Source: https://docs.blueplm.io/extensions/publishing This GitHub Actions workflow automates the build, packaging, and release process when a new Git tag is pushed. It checks out code, sets up Node.js, installs dependencies, runs type checks and builds, packages the extension, extracts version and changelog information from Git tags and CHANGELOG.md, and finally creates a GitHub release with the packaged file. It also handles pre-release tagging based on tag names. ```yaml name: Build and Release on: push: tags: - 'v*' jobs: build: runs-on: ubuntu-latest permissions: contents: write steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' - name: Install dependencies run: npm ci - name: Type check run: npm run typecheck - name: Build extension run: npm run build - name: Package extension (.bpx) run: npm run package - name: Get version from tag id: get_version run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT - name: Extract changelog for version id: changelog run: | VERSION="${GITHUB_REF#refs/tags/v}" CHANGELOG=$(awk "/^## [${VERSION}]/{flag=1; next} /^## [/{flag=0} flag" CHANGELOG.md) echo "CHANGELOG<> $GITHUB_OUTPUT echo "$CHANGELOG" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT - name: Create Release uses: softprops/action-gh-release@v2 with: name: ${{ steps.get_version.outputs.VERSION }} body: ${{ steps.changelog.outputs.CHANGELOG }} draft: false prerelease: ${{ contains(github.ref, '-beta') || contains(github.ref, '-alpha') || contains(github.ref, '-rc') }} files: | *.bpx env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ``` -------------------------------- ### Extension Keywords Examples (JSON) Source: https://docs.blueplm.io/extensions/manifest An example of an array of keywords used for searching extensions in the store. These keywords help users discover relevant extensions based on their needs. ```json "keywords": ["sync", "cloud", "backup", "google"] ``` -------------------------------- ### Server Handler Logic Example Source: https://docs.blueplm.io/extensions/server-api An example of a server handler's core logic, demonstrating how to access request details, user information, and build a JSON response using the ExtensionServerAPI. ```typescript export default async function handler(api: ExtensionServerAPI) { const { request, user, response, storage, secrets, http } = api // Your handler logic here return response.json({ success: true }) } ``` -------------------------------- ### Lazy Initialization for Fast Activation in TypeScript Source: https://docs.blueplm.io/extensions/best-practices Illustrates how to achieve fast extension activation by deferring heavy operations until they are actually needed. The 'good' example registers commands quickly and performs intensive tasks only when a command is invoked. The 'bad' example shows a slow activation by performing heavy work during the initial `activate` call. ```typescript // ✓ Good: Fast activation, lazy loading export async function activate(context, api) { // Register commands immediately (fast) context.subscriptions.push( api.commands.registerCommand('myext.sync', async () => { // Heavy work only when command is invoked const data = await loadLargeDataset() await performSync(data) }) ) } // ❌ Bad: Slow activation export async function activate(context, api) { // This blocks activation const data = await loadLargeDataset() context.subscriptions.push( api.commands.registerCommand('myext.sync', () => performSync(data)) ) } ``` -------------------------------- ### Extension Icon Path Examples (JSON) Source: https://docs.blueplm.io/extensions/manifest Examples showing the path to the extension's icon file, relative to the package root. The icon should be a PNG format, ideally 128x128 pixels with a square aspect ratio. ```json "icon": "icon.png" "icon": "assets/icon.png" ``` -------------------------------- ### Build and Package Extension (Bash) Source: https://docs.blueplm.io/extensions/package-format Commands to install dependencies, build the extension using the configured npm scripts, and package it into a distributable format (.bpx). This process involves running 'npm install' followed by 'npm run package'. ```bash npm install npm run package # Output: my-extension-1.0.0.bpx ``` -------------------------------- ### Extension Description Example (JSON) Source: https://docs.blueplm.io/extensions/manifest An example of a short description for an extension, intended for display in the extension store. It should concisely explain what the extension does and its key features, with a maximum length of 500 characters. ```json "description": "Sync your engineering files with Google Drive. Supports automatic sync, conflict resolution, and shared drive access." ``` -------------------------------- ### Set Up Google OAuth Redirect URI Source: https://docs.blueplm.io/admin-setup This step involves configuring the authorized redirect URI in Google Cloud Console for web applications. It specifies the callback URL that Google will redirect users to after successful authentication. The URI must include your Supabase project reference. ```text https://YOUR-PROJECT-REF.supabase.co/auth/v1/callback ``` -------------------------------- ### Publisher ID Examples (JSON) Source: https://docs.blueplm.io/extensions/manifest Examples of publisher identifiers. This string must match the prefix of the extension's ID and is used to identify the developer or organization behind the extension. ```json "publisher": "blueplm" "publisher": "mycompany" ``` -------------------------------- ### MIT License Example Source: https://docs.blueplm.io/extensions/getting-started An example of the MIT License, commonly used for open-source software. It grants broad permissions for use, modification, and distribution, requiring only the inclusion of the copyright notice and permission statement. ```text MIT License Copyright (c) 2024 My Company Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. ``` -------------------------------- ### Extension Name Examples (JSON) Source: https://docs.blueplm.io/extensions/manifest Examples of human-readable display names for extensions. This name is shown in the BluePLM UI and store, and must be between 1 and 100 characters. ```json "name": "Google Drive" "name": "ERP Connector" ``` -------------------------------- ### Complete BluePLM Extension Manifest Example Source: https://docs.blueplm.io/extensions/manifest A comprehensive example of an extension manifest file (package.json) for a Google Drive integration. It includes metadata, activation events, contributions to the UI and API, configuration options, and required permissions. ```json { "$schema": "https://blueplm.io/schemas/extension-v1.schema.json", "id": "blueplm.google-drive", "name": "Google Drive", "version": "1.2.0", "publisher": "blueplm", "description": "Sync engineering files with Google Drive. Supports automatic sync, shared drives, and conflict resolution.", "icon": "icon.png", "repository": "https://github.com/bluerobotics/blueplm-google-drive", "license": "MIT", "keywords": ["sync", "cloud", "google", "drive", "backup"], "categories": ["sync", "integration"], "category": "sandboxed", "engines": { "blueplm": "^1.0.0" }, "main": "client/index.js", "serverMain": "server/index.js", "activationEvents": [ "onExtensionEnabled", "onCommand:google-drive.sync", "onNavigate:settings/extensions/google-drive" ], "contributes": { "views": [ { "id": "google-drive.panel", "name": "Google Drive", "icon": "cloud", "location": "panel", "component": "client/components/DrivePanel.js" } ], "commands": [ { "id": "google-drive.sync", "title": "Sync with Google Drive", "icon": "refresh-cw", "keybinding": "Ctrl+Shift+G", "category": "Google Drive" }, { "id": "google-drive.disconnect", "title": "Disconnect Google Account", "category": "Google Drive" } ], "settings": [ { "id": "google-drive.settings", "name": "Google Drive", "description": "Configure Google Drive sync", "icon": "cloud", "component": "client/components/Settings.js", "category": "extensions" } ], "apiRoutes": [ { "method": "POST", "path": "connect", "handler": "server/connect.js" }, { "method": "POST", "path": "sync", "handler": "server/sync.js" }, { "method": "GET", "path": "oauth-callback", "handler": "server/oauth-callback.js", "public": true } ], "configuration": { "title": "Google Drive", "properties": { "syncInterval": { "type": "number", "default": 300, "minimum": 60, "maximum": 3600, "description": "Automatic sync interval in seconds" }, "autoSync": { "type": "boolean", "default": true, "description": "Enable automatic synchronization" }, "conflictResolution": { "type": "string", "enum": ["ask", "local", "remote", "newest"], "enumDescriptions": [ "Ask for each conflict", "Keep local version", "Keep remote version", "Keep newest version" ], "default": "ask", "description": "How to handle sync conflicts" } } } }, "permissions": { "client": [ "ui:toast", "ui:dialog", "ui:status", "ui:progress", "storage:local", "network:orgApi", "commands:register", "workspace:files" ], "server": [ "storage:database", "secrets:read", "secrets:write", "http:domain:googleapis.com", "http:domain:accounts.google.com" ] } } ``` -------------------------------- ### Extension Changelog Example (JSON) Source: https://docs.blueplm.io/extensions/manifest An example of an inline changelog or release notes for an extension, supporting Markdown formatting. This field provides users with information about changes in different versions. ```json "changelog": "## 1.1.0\n- Added batch sync\n- Fixed memory leak\n\n## 1.0.0\n- Initial release" ``` -------------------------------- ### Extension Categories Examples (JSON) Source: https://docs.blueplm.io/extensions/manifest An example of an array of store categories for filtering extensions. These categories help organize extensions and allow users to browse by functionality. ```json "categories": ["sync", "integration", "productivity"] ``` -------------------------------- ### Extension Version Examples (JSON) Source: https://docs.blueplm.io/extensions/manifest Examples of extension versions adhering to semantic versioning (Semver). This field is crucial for managing updates and compatibility, with guidelines for MAJOR, MINOR, and PATCH version bumps. ```json "version": "1.0.0" "version": "2.1.0-beta.1" "version": "0.9.0+build.123" ``` -------------------------------- ### Extension Repository URL Example (JSON) Source: https://docs.blueplm.io/extensions/manifest Example of a source code repository URL for the extension. This field is required for submitting extensions to the store and helps users find the extension's source code. ```json "repository": "https://github.com/mycompany/blueplm-extension" ``` -------------------------------- ### Extension License Examples (JSON) Source: https://docs.blueplm.io/extensions/manifest Examples of OSI-approved license identifiers for extensions. BluePLM requires all extensions to be open source, and common licenses like MIT, Apache-2.0, and GPL-3.0 are supported. ```json "license": "MIT" "license": "Apache-2.0" "license": "GPL-3.0" ``` -------------------------------- ### Extension ID Format Examples (JSON) Source: https://docs.blueplm.io/extensions/manifest Examples demonstrating the required format for the extension's unique identifier. The ID must follow the pattern 'publisher.name', using only lowercase letters, numbers, and hyphens, and cannot be changed after publishing. ```json "id": "blueplm.google-drive" "id": "mycompany.erp-connector" "id": "johndoe.file-watcher" ``` -------------------------------- ### Extension Category Type Example (JSON) Source: https://docs.blueplm.io/extensions/manifest Example of setting the extension's category, which determines its execution environment and trust level. Options are 'sandboxed' for most extensions or 'native' for verified extensions requiring direct access to the main process. ```json "category": "sandboxed" ``` -------------------------------- ### Verify Storage Key Names Source: https://docs.blueplm.io/extensions/troubleshooting Ensures that the same key name is used when setting and getting values from storage. Mismatched keys will result in `undefined` being returned. ```typescript await api.storage.set('myKey', value) const result = await api.storage.get('myKey') // Same key ``` -------------------------------- ### Measuring and Optimizing Startup Time in TypeScript Source: https://docs.blueplm.io/extensions/best-practices Focuses on ensuring extensions activate quickly, adhering to a 200ms budget. The example measures the duration of the `activate` function, logging a warning if it exceeds the threshold. It emphasizes registering handlers like commands and event listeners as fast operations. ```typescript export async function activate(context, api) { const start = performance.now() // Register handlers (fast) registerCommands(context, api) registerEventListeners(context, api) const duration = performance.now() - start if (duration > 200) { context.log.warn(`Slow activation: ${duration}ms`) } } ``` -------------------------------- ### Initialize Extension with ExtensionClientAPI (TypeScript) Source: https://docs.blueplm.io/extensions/client-api The `activate` function is the entry point for extensions. It receives the `ExtensionContext` and `ExtensionClientAPI` to access various functionalities like UI, storage, commands, and more. All operations are sandboxed and permission-gated. ```typescript export async function activate( context: ExtensionContext, api: ExtensionClientAPI ): Promise { // api contains all available APIs api.ui // UI operations api.storage // Local storage api.commands // Command registration api.workspace // Workspace info api.events // Event subscriptions api.telemetry // Analytics api.context // Read-only context api.callOrgApi // Org API calls api.callStoreApi // Store API calls api.fetch // External HTTP } ``` -------------------------------- ### Fix TypeScript Module Not Found Errors Source: https://docs.blueplm.io/extensions/troubleshooting Resolves 'Cannot find module' errors for '@blueplm/extension-api' by either installing the types package or creating a local type definition file. ```typescript // Error: Cannot find module '@blueplm/extension-api' // Fix: Install types package or use local types npm install @blueplm/extension-api --save-dev // Or create local type file: // types/extension-api.d.ts declare module '@blueplm/extension-api' { export interface ExtensionContext { ... } export interface ExtensionClientAPI { ... } } ``` -------------------------------- ### Scope Custom CSS for Extensions Source: https://docs.blueplm.io/extensions/troubleshooting Provides an example of how to scope custom CSS to a specific extension using a unique class name. This prevents styles from leaking and conflicting with other extensions or the host application. ```css /* Scope to your extension */ .myext-panel { /* your styles */ } ``` -------------------------------- ### Configure Supabase Site and Redirect URLs Source: https://docs.blueplm.io/admin-setup This configuration in the Supabase Dashboard sets the allowed URLs for your application's authentication flow. It includes localhost and specific development server ports to ensure smooth local development and testing. ```text http://localhost http://localhost:5173 http://127.0.0.1 ``` -------------------------------- ### Deploy BluePLM REST API to Railway Source: https://docs.blueplm.io/admin-setup This describes the process of deploying the BluePLM REST API server to Railway. It involves selecting 'Deploy from Docker Image' and providing the image URL and necessary environment variables obtained from the Supabase Dashboard. The service role key should be kept secret. ```docker ghcr.io/bluerobotics/blueplm-api:latest ``` -------------------------------- ### Test Handlers Locally with BluePLM CLI and cURL (Bash) Source: https://docs.blueplm.io/extensions/server-api Provides instructions for testing extension handlers locally using the BluePLM CLI and the cURL command-line tool. It shows how to start a local server and send POST requests with JSON payloads. ```bash # Start local handler server blueplm-ext serve # Test with curl curl -X POST http://localhost:3000/extensions/myext/sync \ -H "Content-Type: application/json" \ -d '{"vaultId": "test-vault"}' ``` -------------------------------- ### Create BluePLM Default Workflow via SQL Source: https://docs.blueplm.io/admin-setup This SQL function call creates a default workflow for BluePLM, essential for managing file lifecycles. It requires your organization ID and user ID, which can be obtained via separate SELECT statements. Replace 'ORG-UUID' and 'USER-UUID' with the respective IDs. ```sql SELECT create_default_workflow_v2('ORG-UUID', 'USER-UUID'); ``` -------------------------------- ### Create BluePLM Vault via SQL Source: https://docs.blueplm.io/admin-setup This SQL script creates a new vault within BluePLM. It requires the organization ID, which can be retrieved using a preceding query. Ensure the storage bucket name matches the configuration from Step 3. Replace 'ORG-UUID-HERE' with your actual organization ID. ```sql INSERT INTO vaults (org_id, name, slug, storage_bucket, is_default) VALUES ( 'ORG-UUID-HERE', -- Your organization ID from above 'Main Vault', -- Display name 'main-vault', -- URL-safe slug 'vault', -- Storage bucket name (must match step 3) true -- Make this the default vault ); ``` -------------------------------- ### Configure BluePLM Admin User via SQL Source: https://docs.blueplm.io/admin-setup This SQL script configures a user as an administrator within BluePLM. It links the user to the organization and assigns the 'admin' role. It also adds the user to the 'Administrators' team. Replace placeholder values with your actual organization slug and user email. ```sql -- 1. Link yourself to the org and set as admin UPDATE users SET org_id = (SELECT id FROM organizations WHERE slug = 'your-company'), role = 'admin' WHERE email = 'your.email@yourcompany.com'; -- 2. Add yourself to the Administrators team INSERT INTO team_members (team_id, user_id, is_team_admin, added_by) SELECT t.id, u.id, TRUE, u.id FROM teams t, users u WHERE t.org_id = (SELECT id FROM organizations WHERE slug = 'your-company') AND t.name = 'Administrators' AND u.email = 'your.email@yourcompany.com' ON CONFLICT (team_id, user_id) DO NOTHING; ``` -------------------------------- ### BluePLM Extension Build Configuration (package.json) Source: https://docs.blueplm.io/extensions/package-format A sample package.json file demonstrating scripts for building BluePLM extensions. It includes commands for building client and server code using esbuild, packaging the extension, and cleaning build artifacts. Prerequisites include Node.js 18+, TypeScript, and a bundler. ```json { "name": "my-extension", "version": "1.0.0", "private": true, "scripts": { "build": "npm run build:client && npm run build:server", "build:client": "esbuild client/index.ts --bundle --outfile=dist/client/index.js --format=esm --platform=browser --external:react --external:react-dom", "build:server": "esbuild server/*.ts --bundle --outdir=dist/server --format=esm --platform=node", "package": "npm run build && node scripts/package.js", "clean": "rimraf dist *.bpx" }, "devDependencies": { "esbuild": "^0.19.0", "typescript": "^5.0.0", "rimraf": "^5.0.0" } } ``` -------------------------------- ### Add Windows Defender Exclusion via PowerShell Source: https://docs.blueplm.io/admin-setup This snippet demonstrates how to add a folder exclusion to Windows Defender using PowerShell. It's useful for preventing antivirus software from slowing down file operations on critical directories like BluePLM vaults. Ensure PowerShell is run as an administrator. ```powershell Add-MpPreference -ExclusionPath "C:\BluePLM" ``` ```powershell Get-MpPreference | Select-Object -ExpandProperty ExclusionPath ``` -------------------------------- ### Continuous Integration Workflow with GitHub Actions Source: https://docs.blueplm.io/extensions/publishing This GitHub Actions workflow is designed for continuous integration on pull requests and pushes to the main branch. It checks out code, sets up Node.js with caching, installs dependencies, and runs type checks, builds, and packaging steps. It also lists the generated package files. ```yaml name: CI on: push: branches: [main] pull_request: branches: [main] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' - run: npm ci - run: npm run typecheck - run: npm run build - run: npm run package - run: ls -la *.bpx ``` -------------------------------- ### Create BluePLM Organization in Supabase Source: https://docs.blueplm.io/admin-setup This SQL statement inserts a new organization into the 'organizations' table in Supabase. It requires specifying the organization's display name, a URL-safe slug, and an array of email domains for automatic user association. This action also provisions default teams and job titles. ```sql INSERT INTO organizations (name, slug, email_domains) VALUES ( 'Your Company Name', 'your-company', ARRAY['yourcompany.com'] ); ``` -------------------------------- ### Configure Supabase Data API Max Rows Source: https://docs.blueplm.io/admin-setup This configuration step in Supabase increases the maximum number of rows returned per query. It is crucial for handling large vaults to prevent incomplete query results. The default is 1,000 rows, and it should be set to a value significantly higher than the expected vault size. ```text 1000000 ``` -------------------------------- ### Project Setup: package.json - BluePLM Extension Development Source: https://docs.blueplm.io/extensions/structure Configuration file for npm, defining project metadata, scripts for building, watching, packaging, linting, type checking, and testing. It lists development dependencies including the BluePLM extension API, esbuild, eslint, and TypeScript. ```json { "name": "my-extension", "version": "1.0.0", "private": true, "type": "module", "scripts": { "build": "npm run build:client && npm run build:server", "build:client": "esbuild client/index.ts --bundle --outfile=dist/client/index.js --format=esm --platform=browser --external:react --external:react-dom --minify", "build:server": "esbuild server/*.ts --bundle --outdir=dist/server --format=esm --minify", "watch": "npm run build -- --watch", "package": "npm run build && node scripts/package.js", "lint": "eslint client server", "typecheck": "tsc --noEmit", "test": "jest" }, "devDependencies": { "@blueplm/extension-api": "^1.0.0", "@types/react": "^18.0.0", "archiver": "^6.0.0", "esbuild": "^0.19.0", "eslint": "^8.0.0", "jest": "^29.0.0", "typescript": "^5.0.0" } } ``` -------------------------------- ### Build and Package Scripts (package.json) Source: https://docs.blueplm.io/extensions/getting-started The package.json file defines the build and packaging scripts for the File Sync Extension. It uses esbuild for bundling TypeScript files and zip for creating the final package. ```json { "name": "blueplm-file-sync", "version": "1.0.0", "scripts": { "build": "esbuild client/index.ts --bundle --outfile=client/index.js --format=esm --platform=browser && esbuild server/*.ts --bundle --outdir=server --format=esm", "package": "npm run build && zip -r file-sync-1.0.0.bpx extension.json README.md LICENSE icon.png client/ server/" }, "devDependencies": { "esbuild": "^0.19.0", "typescript": "^5.0.0" } } ``` -------------------------------- ### Check Extension Entry Point and Build Source: https://docs.blueplm.io/extensions/troubleshooting Verifies that the main entry point specified in the extension's manifest exists and checks for syntax errors in the code. Running type checking and build commands helps identify code-level issues. ```json // extension.json "main": "client/index.js" // This file must exist ``` ```bash npm run typecheck npm run build ``` -------------------------------- ### Never Expose Secrets with TypeScript Source: https://docs.blueplm.io/extensions/best-practices This example contrasts a bad practice of leaking secrets with a good practice of only exposing configuration status. The 'bad' example directly returns an API key, which is a security risk. The 'good' example only indicates whether a secret is configured, without revealing the secret itself. ```typescript // ❌ Bad: Leaking secrets return api.response.json({ apiKey: await api.secrets.get('api_key') }) // ✓ Good: Only expose status return api.response.json({ configured: !!(await api.secrets.get('api_key')) }) ``` -------------------------------- ### Bundle Multiple Extensions as a Pack Source: https://docs.blueplm.io/extensions/manifest Groups multiple extensions together into a single pack. Installing the pack will automatically install all the listed extensions. ```json { "extensionPack": [ "blueplm.google-drive", "blueplm.dropbox", "blueplm.onedrive" ] } ``` -------------------------------- ### Activate BluePLM File Sync Extension Client Source: https://docs.blueplm.io/extensions/getting-started Handles the activation of the File Sync extension client. It registers a command for manual sync, sets up a file change listener, configures automatic sync based on storage settings, and displays a success toast. Dependencies include the '@blueplm/extension-api' types. ```typescript import type { ExtensionContext, ExtensionClientAPI } from '@blueplm/extension-api' // Sync interval handle let syncInterval: ReturnType | undefined /** * Called when extension is activated. * This is your extension's entry point. */ export async function activate( context: ExtensionContext, api: ExtensionClientAPI ): Promise { context.log.info('File Sync extension activating...') // Register the sync command context.subscriptions.push( api.commands.registerCommand('file-sync.syncNow', async () => { await performSync(api, context) }) ) // Watch for file changes context.subscriptions.push( api.workspace.onFileChanged(async (events) => { const createdOrChanged = events.filter( e => e.type === 'created' || e.type === 'changed' ) if (createdOrChanged.length > 0) { context.log.debug(`${createdOrChanged.length} files changed`) // Could trigger auto-sync here } }) ) // Set up automatic sync if enabled const autoSync = await api.storage.get('autoSync') const interval = await api.storage.get('syncInterval') ?? 300 if (autoSync !== false) { syncInterval = setInterval(() => { performSync(api, context).catch(err => { context.log.error('Auto-sync failed:', err) }) }, interval * 1000) } context.log.info('File Sync extension activated!') api.ui.showToast('File Sync ready', 'success') } /** * Called when extension is deactivated. * Clean up any resources here. */ export function deactivate(): void { if (syncInterval) { clearInterval(syncInterval) syncInterval = undefined } } /** * Perform the sync operation with progress UI. */ async function performSync( api: ExtensionClientAPI, context: ExtensionContext ): Promise { await api.ui.showProgress( { title: 'Syncing files...', cancellable: true }, async (progress, token) => { // Check if user cancelled token.onCancellationRequested(() => { context.log.info('Sync cancelled by user') }) progress.report({ message: 'Preparing sync...' }) try { // Get current vault const vault = await api.workspace.getCurrentVault() if (!vault) { api.ui.showToast('No vault selected', 'error') return } progress.report({ message: 'Connecting to server...', increment: 20 }) // Call our server handler const response = await api.callOrgApi( `/extensions/file-sync/sync`, { method: 'POST', body: { vaultId: vault.id, vaultName: vault.name } } ) if (response.ok) { progress.report({ message: 'Sync complete!', increment: 80 }) api.ui.showToast( `Synced ${response.data.fileCount} files`, 'success' ) // Track telemetry api.telemetry.trackEvent('sync_completed', { fileCount: response.data.fileCount, duration: response.data.duration }) } else { throw new Error(response.data.error || 'Sync failed') } } catch (error) { const err = error as Error context.log.error('Sync error:', err) api.ui.showToast(`Sync failed: ${err.message}`, 'error') api.telemetry.trackError(err, { operation: 'sync' }) } } ) } // Type for sync response interface SyncResult { fileCount: number duration: number error?: string } ``` -------------------------------- ### Retrieving Data from Storage Source: https://docs.blueplm.io/extensions/server-api An example of using the `storage.get` method to retrieve data from the extension's key-value storage. It demonstrates retrieving a number and a configuration object, and handling cases where the data might be undefined. ```typescript const lastSync = await api.storage.get('lastSync:vault-123') const config = await api.storage.get('settings') if (lastSync) { console.log('Last sync:', new Date(lastSync)) } ``` -------------------------------- ### Configure esbuild for Client and Server Builds (package.json) Source: https://docs.blueplm.io/extensions/package-format Defines npm scripts for building the client and server components of the extension using esbuild. The client build bundles React components into an ES module for browser platforms, while the server build bundles TypeScript files into an ES module for Node.js. ```json { "name": "my-extension", "version": "1.0.0", "scripts": { "build": "npm run build:client && npm run build:server", "build:client": "esbuild client/index.tsx --bundle --outfile=dist/client/index.js --format=esm --platform=browser --external:react --external:react-dom --minify", "build:server": "esbuild server/*.ts --bundle --outdir=dist/server --format=esm --minify", "package": "npm run build && node scripts/package.js", "dev": "npm run build -- --watch", "lint": "eslint client server", "typecheck": "tsc --noEmit" }, "devDependencies": { "@types/react": "^18.0.0", "archiver": "^6.0.0", "esbuild": "^0.19.0", "eslint": "^8.0.0", "typescript": "^5.0.0" } } ``` -------------------------------- ### Respect User Preferences Source: https://docs.blueplm.io/extensions/best-practices Illustrates how to respect user preferences, such as whether to show a hint or tip again, by storing this information. ```APIDOC ## Respect User Preferences ### Description This example shows how to check and respect user preferences stored using `api.storage.get` and `api.storage.set`. It prevents repeatedly showing non-essential information like hints. ### Method N/A (Illustrative Code) ### Endpoint N/A ### Parameters N/A ### Request Example ```typescript async function maybeShowHint(api: ExtensionClientAPI) { const dismissed = await api.storage.get('hintDismissed') if (!dismissed) { const result = await api.ui.showDialog({ title: 'Tip', message: 'Press Ctrl+Shift+S to sync quickly!', type: 'info', confirmText: 'Got it', cancelText: "Don't show again" }) if (!result.confirmed && !result.dismissed) { await api.storage.set('hintDismissed', true) } } } ``` ### Response N/A ``` -------------------------------- ### Loading and Saving Extension Configuration (TypeScript) Source: https://docs.blueplm.io/extensions/best-practices Demonstrates how to load and save extension configuration, including default values and event emission upon changes. It uses the storage API to persist settings and notifies other parts of the application when the configuration is updated. ```typescript interface ExtensionConfig { autoSync: boolean syncInterval: number excludePatterns: string[] } const DEFAULT_CONFIG: ExtensionConfig = { autoSync: true, syncInterval: 300, excludePatterns: ['*.tmp', '.git/**'] } async function loadConfig(api: ExtensionClientAPI): Promise { const saved = await api.storage.get>('config') return { ...DEFAULT_CONFIG, ...saved } } async function saveConfig( api: ExtensionClientAPI, config: ExtensionConfig ): Promise { await api.storage.set('config', config) api.events.emit('myext.configChanged', config) } ``` -------------------------------- ### Handling Async Errors Gracefully in TypeScript Source: https://docs.blueplm.io/extensions/best-practices Provides examples of robust asynchronous error handling in BluePLM extensions. The 'good' example uses a `try...catch` block to handle potential errors during command execution, logging the error, showing a user-friendly message, and tracking telemetry. The 'bad' example omits error handling, potentially leading to unhandled rejections. ```typescript // ✓ Good: Errors handled context.subscriptions.push( api.commands.registerCommand('myext.sync', async () => { try { await performSync(api) } catch (error) { context.log.error('Sync failed:', error) api.ui.showToast('Sync failed. Check logs for details.', 'error') api.telemetry.trackError(error as Error, { operation: 'sync' }) } }) ) // ❌ Bad: Unhandled rejection api.commands.registerCommand('myext.sync', async () => { await performSync(api) // Might throw! }) ``` -------------------------------- ### Provide Clear Feedback Source: https://docs.blueplm.io/extensions/best-practices Shows how to provide clear feedback to the user during long-running operations using progress indicators and toasts. ```APIDOC ## Provide Clear Feedback ### Description This example demonstrates how to provide clear user feedback during long operations by using `api.ui.showProgress` for detailed progress and `api.ui.showToast` for concise status updates. ### Method N/A (Illustrative Code) ### Endpoint N/A ### Parameters N/A ### Request Example ```typescript async function handleSync(api: ExtensionClientAPI) { // Show progress for long operations await api.ui.showProgress( { title: 'Syncing files...', cancellable: true }, async (progress, token) => { progress.report({ message: 'Preparing...' }) const files = await getFiles() for (let i = 0; i < files.length; i++) { if (token.isCancellationRequested) { api.ui.showToast('Sync cancelled', 'info') return } progress.report({ message: `Syncing ${files[i].name}`, increment: 100 / files.length }) await syncFile(files[i]) } api.ui.showToast('Sync complete!', 'success') } ) } ``` ### Response N/A ``` -------------------------------- ### Checking User Authentication and Role Source: https://docs.blueplm.io/extensions/server-api An example illustrating how to check for user authentication and specific roles within a server handler. It demonstrates returning error responses with appropriate status codes if the user is not authenticated or lacks the required role. ```typescript export default async function handler(api: ExtensionServerAPI) { const { user, response } = api // Check authentication if (!user) { return response.error('Authentication required', 401) } // Check role if (user.role !== 'admin') { return response.error('Admin access required', 403) } // Use user info console.log(`Request from ${user.email} in org ${user.orgId}`) return response.json({ userId: user.id }) } ``` -------------------------------- ### Optimize Bundle Size with esbuild Source: https://docs.blueplm.io/extensions/troubleshooting Reduces extension package size and improves load times by enabling minification, externalizing libraries like React, and analyzing bundle contents. ```bash esbuild ... --minify ``` ```bash esbuild ... --external:react --external:react-dom ``` ```bash esbuild ... --metafile=meta.json # Analyze meta.json to find large dependencies ``` -------------------------------- ### Resource Management with Disposable Pattern in TypeScript Source: https://docs.blueplm.io/extensions/best-practices Demonstrates the correct way to manage resources by pushing subscriptions to `context.subscriptions` for automatic cleanup, preventing memory leaks. The incorrect example shows a common mistake of not cleaning up resources, leading to potential issues upon extension deactivation. ```typescript // ✓ Good: Resources are auto-disposed export async function activate(context, api) { context.subscriptions.push( api.commands.registerCommand('myext.cmd', handler), api.workspace.onFileChanged(fileHandler), api.events.on('vault:changed', vaultHandler) ) } // ❌ Bad: Memory leak on deactivate export async function activate(context, api) { api.commands.registerCommand('myext.cmd', handler) // No cleanup! } ``` -------------------------------- ### Build and Minify Client Code with esbuild (Bash) Source: https://docs.blueplm.io/extensions/package-format This bash command uses esbuild to bundle and minify the client-side code for a BluePLM extension. It bundles the entry point 'client/index.ts', applies minification, and excludes 'react' and 'react-dom' as they are expected to be provided by the host environment. The output is written to 'dist/client/index.js'. ```bash esbuild client/index.ts \ --bundle \ --minify \ --external:react \ --external:react-dom \ --outfile=dist/client/index.js ``` -------------------------------- ### Execute Registered Commands (Client-Side) Source: https://docs.blueplm.io/extensions/permissions This TypeScript example demonstrates how to programmatically execute registered commands using the `commands:execute` permission. It allows triggering extension functionality from other parts of the code. ```typescript await api.commands.executeCommand('myext.doThing', arg1, arg2) ```